# Multi-Model Skill Pipelines ## Overview Didactyl's skill system already supports executing a series of tasks where each task uses a different LLM model — even from different providers. This document describes what works today, what the limitations are, and what improvements would unlock more powerful multi-step workflows. --- ## What Works Today ### Per-Skill LLM Override Every skill can declare its own `llm` tag with a provider/model spec and fallback chain: ``` ["llm", "anthropic/claude-sonnet-4-20250514, cheap"] ["llm", "openai/gpt-4o-mini"] ["llm", "best"] ``` When a triggered skill fires, the runtime applies the skill's execution parameters (model, temperature, max_tokens) before calling the LLM, then restores the agent defaults afterward. This happens in `apply_trigger_runtime_to_llm_config()` in `src/trigger_manager.c`. The `llm` tag supports the `provider/model` format. If the tag contains a slash, the runtime parses the provider name and model name separately and overrides both in the LLM config for that execution. ### Chain Triggers Connect Skills Sequentially The `chain` trigger type fires when another skill completes execution. The `filter` field specifies the source skill's `d` tag: ```json { "trigger": "chain", "filter": "source-skill-d-tag" } ``` After a triggered skill completes, `trigger_manager_fire_chains()` in `src/trigger_manager.c` looks for all adopted skills with `trigger=chain` whose `filter` matches the completed skill's d-tag, and fires them. ### Combined: Multi-Model Pipeline By combining per-skill LLM overrides with chain triggers, you can build a pipeline where each step uses a different model: ``` DM arrives │ ├─ Skill: triage │ llm: openai/gpt-4o-mini (fast/cheap) │ trigger: dm │ → Classifies the request │ ├─ chain fires ──→ Skill: deep-analysis │ llm: anthropic/claude-sonnet-4-20250514 (powerful) │ trigger: chain, filter: triage │ → Performs thorough analysis │ └─ chain fires ──→ Skill: summarize llm: openai/gpt-4o-mini (cheap) trigger: chain, filter: deep-analysis → Summarizes and DMs admin ``` Each skill gets its own model, temperature, and max_tokens applied independently. ### Execution Parameter Resolution Per Step For each triggered skill execution: 1. Start with agent/app defaults 2. Apply the skill's `llm` tag (parsed as `provider/model` if slash present) 3. Apply the skill's `temperature` tag if present 4. Apply the skill's `max_tokens` tag if present 5. Execute with those effective settings 6. Restore defaults after the run --- ## Concrete Example ### Skill 1: triage (cheap fast model) ```json { "kind": 31123, "content": "## Triage\n\nClassify the incoming message:\n- If it needs deep research, use the memory_save tool to store the classification and key details.\n- If trivial, respond directly.\n\n{{message}}", "tags": [ ["d", "triage"], ["description", "Fast triage of incoming messages"], ["trigger", "dm"], ["filter", "{\"from\":\"admin\"}"], ["llm", "openai/gpt-4o-mini, cheap"], ["temperature", "0"], ["max_tokens", "200"] ] } ``` ### Skill 2: deep-analysis (powerful model, chains from triage) ```json { "kind": 31123, "content": "## Deep Analysis\n\n{{identity}}\n\nRecall the triage classification from memory. Perform thorough analysis using available tools. Save your findings to memory for the next step.\n\nOriginal request context:\n{{message}}", "tags": [ ["d", "deep-analysis"], ["description", "Thorough analysis with powerful model"], ["trigger", "chain"], ["filter", "triage"], ["llm", "anthropic/claude-sonnet-4-20250514, best"], ["max_tokens", "2000"], ["requires_tool", "memory_recall"], ["requires_tool", "memory_save"], ["requires_tool", "nostr_query"], ["requires_skill", "identity"] ] } ``` ### Skill 3: summarize (cheap model, chains from deep-analysis) ```json { "kind": 31123, "content": "## Summarize\n\nRecall the analysis findings from memory. Write a concise summary and DM it to admin.", "tags": [ ["d", "summarize"], ["description", "Summarize analysis and notify admin"], ["trigger", "chain"], ["filter", "deep-analysis"], ["llm", "openai/gpt-4o-mini, cheap"], ["max_tokens", "500"], ["requires_tool", "memory_recall"], ["requires_tool", "nostr_dm_send"] ] } ``` --- ## Current Capabilities | Capability | Status | Notes | |---|---|---| | Different model per skill | ✅ Works | Via `llm` tag on each skill | | Different provider per skill | ✅ Works | `provider/model` format in `llm` tag | | Sequential multi-step pipelines | ✅ Works | Via `chain` trigger type | | Per-step temperature | ✅ Works | Via `temperature` tag | | Per-step max_tokens | ✅ Works | Via `max_tokens` tag | | Fallback chains per skill | ✅ Works | `provider/model, provider/model, cheap` | | LLM config restore after each step | ✅ Works | Runtime saves/restores global config | --- ## Current Limitations ### 1. No Direct Data Passing Between Chain Steps **Problem:** Chain triggers fire with the *original* triggering event, not the output of the previous skill. Skill B doesn't automatically receive Skill A's output. **Current workaround:** Use `memory_save` at the end of each step and `memory_recall` at the start of the next. This works but is fragile — memory is a shared scratchpad, not a structured pipeline bus. **Potential improvement:** Extend the chain trigger event to include the previous skill's final LLM response text. In `trigger_manager_fire_chains()`, the chain event could carry a `"previous_output"` field that the next skill accesses via `{{triggering_event}}`. ### 2. No Conditional Branching **Problem:** All chain skills matching a source d-tag fire unconditionally. You can't say "if triage classifies as X, run skill A; if Y, run skill B." **Current workaround:** The chained skill can check the triggering event or memory and decide to do nothing if the condition doesn't match. But it still fires and consumes an LLM call. **Potential improvement:** Add an optional `chain_condition` tag that the runtime evaluates before firing. Could be a simple JSON match against the previous output, or a keyword presence check. ### 3. No Parallel Fan-Out **Problem:** Multiple chain skills matching the same source fire sequentially, not in parallel. **Current workaround:** This is fine for most use cases. True parallelism would require thread-safe LLM config management. ### 4. Chain Depth Limit of 5 **Problem:** `s_chain_depth` in `trigger_manager_fire_chains()` caps at 5 levels to prevent runaway chains. **Current workaround:** 5 steps is usually sufficient. For longer pipelines, the last step could use a tool to trigger a new chain externally. **Potential improvement:** Make the depth limit configurable via genesis config. ### 5. Provider Credentials Are Global **Problem:** The runtime has one set of API keys per provider. If Skill A uses `anthropic/claude-sonnet-4-20250514` and Skill B uses `openai/gpt-4o`, both providers must be configured in the agent's LLM config. There's no per-skill credential storage. **Current workaround:** Configure all needed providers in the agent's genesis config or via `model_set` tool. The runtime already supports provider switching via the `provider` field in `llm_config_t`. **Potential improvement:** None needed for most cases — agents typically have a small number of providers configured globally. --- ## Architecture: How It Works in Code ### Trigger Execution Flow ``` trigger_manager fires skill │ ├─ Save current llm_config (old_cfg) │ ├─ apply_trigger_runtime_to_llm_config(trigger, &next_cfg) │ ├─ Parse llm tag: "anthropic/claude-sonnet-4-20250514" │ │ ├─ Set cfg->provider = "anthropic" │ │ └─ Set cfg->model = "claude-sonnet-4-20250514" │ ├─ Apply max_tokens if present │ └─ Apply temperature if present │ ├─ llm_set_config(&next_cfg) │ ├─ Execute skill (agent_on_trigger) │ ├─ Build context from triggered skills │ ├─ Call llm_chat_with_tools_messages() │ └─ Tool loop until completion │ ├─ Restore llm_set_config(&old_cfg) │ └─ trigger_manager_fire_chains(source_d_tag) ├─ Find chain skills where filter == source_d_tag ├─ For each matching chain skill: │ ├─ Save config again │ ├─ Apply chain skill's llm override │ ├─ Execute chain skill │ ├─ Restore config │ └─ Recursively fire chains (depth < 5) └─ Done ``` ### Key Source Files | File | Role | |---|---| | `src/trigger_manager.c` | Trigger matching, chain firing, LLM config override/restore | | `src/agent.c` | `agent_on_trigger()` — builds context and runs LLM loop | | `src/llm.c` | `llm_chat_with_tools_messages()` — actual LLM API call | | `docs/SKILLS.md` | Skill spec including `llm` tag format and chain triggers | --- ## Future Enhancements (Not Yet Implemented) ### Priority 1: Chain Output Forwarding Pass the previous skill's output to the next chain step via the triggering event: ```c // In trigger_manager_fire_chains(): cJSON_AddStringToObject(event, "previous_output", last_response_text); ``` The chained skill would access this via `{{triggering_event}}` in its template, seeing: ```json { "type": "chain", "source_d_tag": "triage", "previous_output": "Classification: needs deep analysis. Key topics: ..." } ``` ### Priority 2: Conditional Chain Firing Add an optional `chain_condition` tag: ```json ["chain_condition", "{\"previous_output_contains\":\"needs deep analysis\"}"] ``` The runtime would check this before firing the chain skill. ### Priority 3: Configurable Chain Depth ```json // In genesis.jsonc: "trigger_chain_max_depth": 10 ``` --- ## Summary Multi-model skill pipelines work today using per-skill `llm` tags and `chain` triggers. The main gap is data flow between steps (currently requires memory_save/recall workaround). The system is designed for this use case — each skill execution gets its own model config applied and restored — it just needs better inter-step communication to be truly seamless.