# Tool Orchestration Plan ## Overview This plan covers four related features: 1. **Slash commands** — direct tool execution bypassing the LLM 2. **Skill-forward execution (`/run` + `skill_run`)** — one-shot skill invocation with LLM, including external skill sharing 3. **Skill-tool maturity levels** — skills that register as callable tools with promotion workflow 4. **Deterministic step executor** — hardened skills that run without LLM involvement ## 1. Slash Commands (Direct Tool Execution) ### Behavior When a message starts with `/`, bypass the LLM entirely and execute the tool directly. ``` /local_shell_exec {"command": "ls -la"} /nostr_query {"filter": {"kinds": [1], "limit": 5}} /nostr_nip05_lookup {"identifier": "jack@cash.app"} ``` ### Parsing Rules - `/tool_name` — call tool with empty args `{}` - `/tool_name {"key": "value"}` — call tool with JSON args - `/tool_name plain text` — wrap as `{"input": "plain text"}` (convenience) - `/help` — list available tools - `/help tool_name` — show tool schema ### Implementation 1. In [`agent_on_message()`](../src/agent.c:1453), check if `message[0] == '/'` 2. Parse tool name (everything between `/` and first space or end) 3. Parse args (everything after tool name, try JSON first, fall back to string wrapper) 4. Call [`tools_execute()`](../src/tools.c) directly 5. Send result as DM — no LLM round-trip 6. Still log to context.log.md with `phase=direct_tool_exec` ### Special Slash Commands - `/help` — list available tools - `/help tool_name` — show tool schema - `/run` — skill-forward execution (see section 2) ### Security - Only admin tier can use slash commands (same as current tool policy) - Slash commands respect the same `tools.enabled` and `security.admin.tools_enabled` config flags ## 2. Skill-Forward Execution ### The Problem Today, skills are passive — they're injected into the LLM context via [`append_adopted_skills_context()`](../src/agent.c:1418) and the LLM decides when they're relevant. There's no way to say "run this specific skill right now" and there's no way to try someone else's skill without permanently adopting it. ### Three Invocation Layers | Layer | How it works | LLM? | Persists? | |-------|-------------|------|-----------| | **Passive/adopted** | Skill instructions injected into every conversation context | Yes, LLM decides relevance | Yes — in kind 10123 adoption list | | **Skill-forward** | Skill instructions become the primary system prompt; LLM executes them | Yes, but constrained | No — one-shot execution | | **Hardened** | Deterministic step executor, no LLM | No | Yes — adopted skill with `execution: hardened` | ### Entry Points #### A. `/run` slash command (admin direct invocation) ``` /run deploy-website staging # run own adopted skill by d_tag /run 31123::deploy-website staging # run anyone's skill by address /run deploy-website {"target": "production"} # JSON args ``` Parsing: 1. First token after `/run` is the skill identifier (d_tag or `kind:pubkey:d_tag` address) 2. Everything after is args (try JSON first, fall back to `{"input": "plain text"}`) #### B. `skill_run` tool (LLM-mediated invocation) The LLM can invoke skills on behalf of the admin during conversation: ```json { "name": "skill_run", "description": "Fetch and execute a skill one-shot without adopting it. Works with own adopted skills by d_tag or any public skill by address.", "parameters": { "type": "object", "properties": { "d_tag": { "type": "string", "description": "Skill d_tag for own adopted skills" }, "address": { "type": "string", "description": "Full skill address: kind:pubkey:d_tag" }, "pubkey": { "type": "string", "description": "Author pubkey, used with d_tag to form address" }, "args": { "type": "string", "description": "Arguments or context to pass to the skill" }, "sandbox": { "type": "boolean", "description": "Override sandbox setting. Default: true for external, false for own skills" } } } } ``` This enables natural conversation like: > "My friend @jack just published a skill called summarize-thread. Try it on the latest thread in my feed." The agent would `skill_search` to find it, then `skill_run` to execute it. ### Execution Flow Both `/run` and `skill_run` use the same underlying executor: ```mermaid graph TD A[/run or skill_run called] --> B{Skill identifier type?} B -->|d_tag only| C[Look up in adopted skills cache] B -->|address or pubkey+d_tag| D[Fetch skill event from Nostr] C --> E{Found?} D --> E E -->|No| F[Return error: skill not found] E -->|Yes| G{External skill?} G -->|Yes| H[Apply sandbox - restrict tools] G -->|No| I[Full tool access] H --> J[Build skill-forward prompt] I --> J J --> K[System: base context + skill instructions] K --> L[User: args as user message] L --> M[LLM call with tools] M --> N[Return result to caller] ``` ### Skill-Forward Prompt Construction Reuses the pattern from [`agent_on_trigger()`](../src/agent.c:1837): ``` [base system context / soul] Skill execution context: - You are executing a specific skill on demand. - Follow the skill instructions below precisely. - The user's arguments provide the context for this execution. - Keep output concise and actionable. Skill d_tag: deploy-website Skill address: 31123::deploy-website Skill source: [own | external:] Skill instructions: [skill content here] ``` User message: ``` [args provided by caller] ``` The prompt is so skill-forward that the LLM has no real option but to execute the skill instructions against the provided args. ### Sandbox for External Skills When executing a skill from another author (not own pubkey), a **tool sandbox** is applied by default: **Allowed tools (safe/read-only):** - `nostr_query` — read Nostr events - `nostr_nip05_lookup` — NIP-05 lookups - `nostr_post` — publish events (the agent signs, so this is safe) - `nostr_list_manage` — manage lists - `skill_list`, `skill_search` — read skill metadata **Blocked tools (destructive/dangerous):** - `local_shell_exec` — arbitrary command execution - `local_file_read`, `local_file_write` — filesystem access - Any future tools marked as `destructive: true` **Override:** The admin can explicitly opt in to full tool access: - `/run --unsafe 31123::risky-skill args` - `skill_run` with `sandbox: false` ### Skill Sharing on Nostr ```mermaid graph TD A[Friend creates skill] -->|kind 31123| B[Published on Nostr relays] B --> C{How do you find it?} C -->|skill_search popular:true| D[Discovery via WoT adoption lists] C -->|Friend tells you the d_tag| E[Direct reference] C -->|skill_search pubkey:friend| F[Browse friends skills] D --> G[skill_run - try it once, sandboxed] E --> G F --> G G -->|Liked it?| H{Adopt?} H -->|Yes| I[skill_adopt - permanent] H -->|No| J[Done - nothing persisted] I --> K[Shows in adopted skills context] K --> L[Agent uses it automatically] L -->|Or invoke directly| M[/run skill-d_tag args] ``` ## 3. Skill-Tool Maturity Levels Skills can declare an execution maturity level that determines how they run: | Level | Execution | LLM? | Use case | |-------|-----------|-------|----------| | `draft` | LLM interprets procedure text from skill instructions | Yes | Exploring and iterating on a workflow | | `guided` | LLM with forced tool_choice + parameter defaults | Yes, constrained | Workflow is stable but needs LLM judgment | | `hardened` | Deterministic step executor, no LLM | No | Workflow is proven and should run exactly as defined | ### Skill Definition Extensions ```yaml kind: 31123 d: deploy_website execution: hardened tool_schema: name: deploy_website description: Build and deploy the static website parameters: target: type: string enum: [staging, production] default: staging steps: - tool: local_shell_exec args: command: "make build TARGET={{target}}" - tool: local_shell_exec args: command: "rsync -av dist/ server:/var/www/{{target}}/" - return: "Deployed to {{target}}" ``` ### How Each Level Works **draft:** Current behavior. Skill instructions are injected into context. LLM reads them and decides which tools to call. No special handling needed. **guided:** Agent sets `tool_choice` to the skill's preferred tool. Parameter defaults from the skill are merged with the model's generated arguments (skill defaults win on conflict). Reduces LLM freedom while still allowing it to fill in dynamic values. **hardened:** Agent executes the `steps` array directly using the deterministic step executor. No LLM call at all. The skill becomes equivalent to a slash command. ### Promotion Workflow 1. Admin iterates with LLM on a task (draft) 2. Admin saves working procedure as a skill: `skill_create` with `execution: draft` 3. Admin tests, refines, promotes: update skill to `execution: guided` 4. Once proven reliable, promote to `execution: hardened` with explicit `steps` 5. Hardened skills become available as slash commands: `/deploy_website {"target": "production"}` ## 4. Deterministic Step Executor A simple sequential executor for hardened skills. ### Step Types ```yaml steps: # Execute a tool - tool: local_shell_exec args: {command: "ls -la"} save_as: listing # optional: save result to variable # Execute a tool with variable substitution - tool: nostr_post args: kind: 30023 content: "{{file_content}}" tags: [["d", "{{d_tag}}"]] # Conditional - simple - if: "{{listing.success}}" then: - tool: local_shell_exec args: {command: "echo done"} else: - return: "Failed: {{listing.error}}" # Return final result - return: "Published to {{d_tag}}" ``` ### Variable Substitution - `{{param_name}}` — from tool parameters provided by caller - `{{step_name.field}}` — from a previous step's result (requires `save_as`) - Simple string replacement, no expression evaluation ### Implementation in C - Parse `steps` array from skill content (JSON or YAML) - Iterate steps sequentially - For each `tool` step: call [`tools_execute()`](../src/tools.c), optionally save result - For each `return` step: substitute variables and return string - For each `if` step: evaluate truthiness of variable, branch accordingly - Total implementation: ~200-400 lines of C ### Error Handling - If any tool step fails (returns `success: false`), abort and return the error - Optional `on_error` field per step for custom error messages - Timeout inherited from tool config ## 5. Tool Registration for Skill-Tools Hardened and guided skills with a `tool_schema` field get registered in the tools array at runtime. ### At Skill Refresh Time 1. Parse `tool_schema` from skill content 2. Generate OpenAI function schema from it 3. Append to the tools array returned by [`tools_build_openai_schema_json()`](../src/tools.c:919) 4. When model calls the skill-tool, route to skill executor instead of hardcoded C function ### In [`tools_execute()`](../src/tools.c) 1. Check if tool_name matches a hardcoded tool — execute normally 2. If not, check if it matches a registered skill-tool 3. If guided: run sub-LLM call with skill procedure + forced tool_choice 4. If hardened: run deterministic step executor ## 6. Security Model ### Tool Classification Tools are classified for sandbox purposes: ```c typedef enum { TOOL_SAFETY_SAFE, // read-only or agent-signed actions TOOL_SAFETY_DESTRUCTIVE // filesystem, shell, or external system mutations } tool_safety_t; ``` | Tool | Safety | Reason | |------|--------|--------| | `nostr_query` | safe | Read-only | | `nostr_nip05_lookup` | safe | Read-only | | `nostr_post` | safe | Agent signs, admin controls keys | | `nostr_list_manage` | safe | Agent signs | | `skill_list` | safe | Read-only | | `skill_search` | safe | Read-only | | `skill_run` | safe | Recursive execution uses its own sandbox | | `local_shell_exec` | destructive | Arbitrary command execution | | `local_file_read` | destructive | Filesystem access | | `local_file_write` | destructive | Filesystem mutation | ### Sandbox Rules | Scenario | Default sandbox | Override | |----------|----------------|---------| | Own adopted skill via `/run d_tag` | No sandbox | N/A | | External skill via `/run address` | Sandbox ON | `/run --unsafe address` | | `skill_run` tool, own skill | No sandbox | `sandbox: true` | | `skill_run` tool, external skill | Sandbox ON | `sandbox: false` | | Hardened skill steps | No sandbox (steps are explicit) | N/A | ## Future Considerations - **Skill sharing:** Hardened skill-tools could be shared between agents via Nostr (kind 31123 events). Another agent adopts the skill and gets the tool automatically. - **Versioning:** Skills already use addressable events (d-tag). Updating a skill automatically updates the tool. - **Permissions:** Skill-tools could have their own permission model (e.g., some skill-tools available to WoT contacts). - **Composability:** Skill-tools calling other skill-tools (nested execution with sandbox inheritance). - **Dry-run mode:** A future `/run --dry` flag that shows what tools would be called without executing them. - **Skill ratings:** Agents could publish ratings/reviews of skills they've tried, building a WoT-based skill marketplace. ## Implementation Priority 1. **Slash commands** (direct tool execution) — simplest, highest immediate value 2. **`/run` for own adopted skills** — skill-forward execution of already-adopted skills 3. **`skill_run` tool + external skill fetching** — enables "try my friend's skill" flow 4. **External skill sandbox** — tool safety classification and sandbox enforcement 5. **Hardened skill-tool step executor** — enables deterministic workflows 6. **Skill-tool registration in tools array** — makes skill-tools visible to LLM 7. **Guided execution with forced tool_choice** — bridges draft and hardened 8. **Promotion workflow UX** — admin commands to change skill maturity level