diff --git a/apps/cli/package-lock.json b/apps/cli/package-lock.json index 5a7c8a43..8ae1f922 100644 --- a/apps/cli/package-lock.json +++ b/apps/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rowboatlabs/rowboatx", - "version": "0.10.0", + "version": "0.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rowboatlabs/rowboatx", - "version": "0.10.0", + "version": "0.13.0", "license": "Apache-2.0", "dependencies": { "@ai-sdk/anthropic": "^2.0.44", diff --git a/apps/cli/src/application/assistant/skills/workflow-authoring/skill.ts b/apps/cli/src/application/assistant/skills/workflow-authoring/skill.ts index dd6dfc0e..4739d47f 100644 --- a/apps/cli/src/application/assistant/skills/workflow-authoring/skill.ts +++ b/apps/cli/src/application/assistant/skills/workflow-authoring/skill.ts @@ -7,7 +7,7 @@ Load this skill whenever a user wants to inspect, create, or update agents insid **IMPORTANT**: In the CLI, there are NO separate "workflow" files. Everything is an agent. -- **All definitions live in \`agents/*.json\`** - there is no separate workflows folder +- **All definitions live in \`agents//\`** with separate files for config, tools, and instructions - Agents configure a model, instructions, and the tools they can use - Tools can be: builtin (like \`executeCommand\`), MCP integrations, or **other agents** - **"Workflows" are just agents that orchestrate other agents** by having them as tools @@ -20,23 +20,39 @@ Load this skill whenever a user wants to inspect, create, or update agents insid 4. Data flows through tool call parameters and responses ## Agent format +\`\`\` +agents/ + agent_name/ + config.json # description + optional model/provider + instructions.md # agent instructions + tools.json # tool definitions +\`\`\` + +**config.json** \`\`\`json { - "name": "agent_name", "description": "Description of the agent", - "model": "gpt-5.1", - "instructions": "Instructions for the agent", - "tools": { - "descriptive_tool_key": { - "type": "mcp", - "name": "actual_mcp_tool_name", - "description": "What the tool does", - "mcpServerName": "server_name_from_config", - "inputSchema": { - "type": "object", - "properties": { - "param1": {"type": "string", "description": "What the parameter means"} - } + "model": "gpt-5.1" +} +\`\`\` + +**instructions.md** +\`\`\`md +Instructions for the agent +\`\`\` + +**tools.json** +\`\`\`json +{ + "descriptive_tool_key": { + "type": "mcp", + "name": "actual_mcp_tool_name", + "description": "What the tool does", + "mcpServerName": "server_name_from_config", + "inputSchema": { + "type": "object", + "properties": { + "param1": {"type": "string", "description": "What the parameter means"} } } } @@ -88,53 +104,51 @@ Load this skill whenever a user wants to inspect, create, or update agents insid **1. Task-specific agent** (does one thing): \`\`\`json -{ - "name": "summariser_agent", - "description": "Summarises an arxiv paper", - "model": "gpt-5.1", - "instructions": "Download and summarise an arxiv paper. Use curl to fetch the PDF. Output just the GIST in two lines. Don't ask for human input.", - "tools": { - "bash": {"type": "builtin", "name": "executeCommand"} - } -} +// agents/summariser_agent/config.json +{ "description": "Summarises an arxiv paper", "model": "gpt-5.1" } + +// agents/summariser_agent/instructions.md +Download and summarise an arxiv paper. Use curl to fetch the PDF. Output just the GIST in two lines. Don't ask for human input. + +// agents/summariser_agent/tools.json +{ "bash": { "type": "builtin", "name": "executeCommand" } } \`\`\` **2. Agent that delegates to other agents**: \`\`\`json -{ - "name": "summarise-a-few", - "description": "Summarises multiple arxiv papers", - "model": "gpt-5.1", - "instructions": "Pick 2 interesting papers and summarise each using the summariser tool. Pass the paper URL to the tool. Don't ask for human input.", - "tools": { - "summariser": { - "type": "agent", - "name": "summariser_agent" - } - } -} +// agents/summarise-a-few/config.json +{ "description": "Summarises multiple arxiv papers", "model": "gpt-5.1" } + +// agents/summarise-a-few/instructions.md +Pick 2 interesting papers and summarise each using the summariser tool. Pass the paper URL to the tool. Don't ask for human input. + +// agents/summarise-a-few/tools.json +{ "summariser": { "type": "agent", "name": "summariser_agent" } } \`\`\` **3. Orchestrator agent** (coordinates the whole workflow): \`\`\`json +// agents/podcast_workflow/config.json +{ "description": "Create a podcast from arXiv papers", "model": "gpt-5.1" } + +// agents/podcast_workflow/instructions.md +1. Fetch arXiv papers about agents using bash +2. Pick papers and summarise them using summarise_papers +3. Create a podcast transcript +4. Generate audio using text_to_speech + +Execute these steps in sequence. + +// agents/podcast_workflow/tools.json { - "name": "podcast_workflow", - "description": "Create a podcast from arXiv papers", - "model": "gpt-5.1", - "instructions": "1. Fetch arXiv papers about agents using bash\n2. Pick papers and summarise them using summarise_papers\n3. Create a podcast transcript\n4. Generate audio using text_to_speech\n\nExecute these steps in sequence.", - "tools": { - "bash": {"type": "builtin", "name": "executeCommand"}, - "summarise_papers": { - "type": "agent", - "name": "summarise-a-few" - }, - "text_to_speech": { - "type": "mcp", - "name": "text_to_speech", - "mcpServerName": "elevenLabs", - "description": "Generate audio", - "inputSchema": { "type": "object", "properties": {...}} - } + "bash": { "type": "builtin", "name": "executeCommand" }, + "summarise_papers": { "type": "agent", "name": "summarise-a-few" }, + "text_to_speech": { + "type": "mcp", + "name": "text_to_speech", + "mcpServerName": "elevenLabs", + "description": "Generate audio", + "inputSchema": { "type": "object", "properties": { "...": "..." } } } } \`\`\` @@ -142,10 +156,10 @@ Load this skill whenever a user wants to inspect, create, or update agents insid **To run this workflow**: \`rowboatx --agent podcast_workflow\` ## Naming and organization rules -- **All agents live in \`agents/*.json\`** - no other location -- Agent filenames must match the \`"name"\` field exactly -- When referencing an agent as a tool, use its \`"name"\` value -- Always keep filenames and \`"name"\` fields perfectly aligned +- **All agents live in \`agents//\`** with \`config.json\`, \`instructions.md\`, and \`tools.json\` +- Directory name must match the agent name exactly +- When referencing an agent as a tool, use its directory/agent name +- Keep directory names aligned with any references inside tools.json - Use relative paths (no \${BASE_DIR} prefixes) when giving examples to users ## Best practices for multi-agent design diff --git a/apps/cli/src/application/entities/agent-config.ts b/apps/cli/src/application/entities/agent-config.ts new file mode 100644 index 00000000..688f3e05 --- /dev/null +++ b/apps/cli/src/application/entities/agent-config.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const AgentConfig = z.object({ + description: z.string(), + model: z.string().optional(), + provider: z.string().optional(), +}); diff --git a/apps/cli/src/application/lib/agent.ts b/apps/cli/src/application/lib/agent.ts index 09528f85..6bc24e7f 100644 --- a/apps/cli/src/application/lib/agent.ts +++ b/apps/cli/src/application/lib/agent.ts @@ -14,6 +14,7 @@ import { AskHumanRequestEvent, RunEvent, ToolPermissionRequestEvent, ToolPermiss import { BuiltinTools } from "./builtin-tools.js"; import { CopilotAgent } from "../assistant/agent.js"; import { isBlocked } from "./command-executor.js"; +import { AgentConfig } from "../entities/agent-config.js"; export async function mapAgentTool(t: z.infer): Promise { switch (t.type) { @@ -165,9 +166,36 @@ export async function loadAgent(id: string): Promise> { if (id === "copilot") { return CopilotAgent; } - const agentPath = path.join(WorkDir, "agents", `${id}.json`); - const agent = fs.readFileSync(agentPath, "utf8"); - return Agent.parse(JSON.parse(agent)); + const agentDir = path.join(WorkDir, "agents", id); + if (!fs.existsSync(agentDir)) { + throw new Error(`Agent directory not found: ${agentDir}`); + } + + const instructionsPath = path.join(agentDir, "instructions.md"); + if (!fs.existsSync(instructionsPath)) { + throw new Error(`Missing instructions.md for agent '${id}'`); + } + const instructions = fs.readFileSync(instructionsPath, "utf8"); + + const toolsPath = path.join(agentDir, "tools.json"); + const tools = fs.existsSync(toolsPath) + ? JSON.parse(fs.readFileSync(toolsPath, "utf8")) + : undefined; + + const configPath = path.join(agentDir, "config.json"); + if (!fs.existsSync(configPath)) { + throw new Error(`Missing config.json for agent '${id}'`); + } + const config = AgentConfig.parse(JSON.parse(fs.readFileSync(configPath, "utf8"))); + + return Agent.parse({ + name: id, + provider: config.provider, + model: config.model, + description: config.description, + instructions, + tools, + }); } export function convertFromMessages(messages: z.infer[]): ModelMessage[] {