Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ async function createSession(conversationId: string, codebaseId: string) {
2. **Workflows** (YAML-based):
- Stored in `.archon/workflows/` (searched recursively)
- Multi-step AI execution chains, discovered at runtime
- **`nodes:` (DAG format)**: Nodes with explicit `depends_on` edges; independent nodes in the same topological layer run concurrently. Node types: `command:` (named command file), `prompt:` (inline prompt), `bash:` (shell script, stdout captured as `$nodeId.output`, no AI, receives managed per-project env vars in its subprocess environment when configured), `loop:` (iterative AI prompt until completion signal), `approval:` (human gate; pauses until user approves or rejects; `capture_response: true` stores the user's comment as `$<node-id>.output` for downstream nodes, default false), `script:` (inline TypeScript/Python or named script from `.archon/scripts/`, runs via `bun` or `uv`, stdout captured as `$nodeId.output`, no AI, receives managed per-project env vars in its subprocess environment when configured, supports `deps:` for dependency installation and `timeout:` in ms, requires `runtime: bun` or `runtime: uv`) . Supports `when:` conditions, `trigger_rule` join semantics, `$nodeId.output` substitution, `output_format` for structured JSON output (Claude and Codex), `allowed_tools`/`denied_tools` for per-node tool restrictions (Claude only), `hooks` for per-node SDK hook callbacks (Claude only), `mcp` for per-node MCP server config files (Claude only, env vars expanded at execution time), and `skills` for per-node skill preloading via AgentDefinition wrapping (Claude only), and `effort`/`thinking`/`maxBudgetUsd`/`systemPrompt`/`fallbackModel`/`betas`/`sandbox` for Claude SDK advanced options (Claude only, also settable at workflow level)
- **`nodes:` (DAG format)**: Nodes with explicit `depends_on` edges; independent nodes in the same topological layer run concurrently. Node types: `command:` (named command file), `prompt:` (inline prompt), `bash:` (shell script, stdout captured as `$nodeId.output`, no AI, receives managed per-project env vars in its subprocess environment when configured), `loop:` (iterative AI prompt until completion signal), `approval:` (human gate; pauses until user approves or rejects; `capture_response: true` stores the user's comment as `$<node-id>.output` for downstream nodes, default false), `script:` (inline TypeScript/Python or named script from `.archon/scripts/`, runs via `bun` or `uv`, stdout captured as `$nodeId.output`, no AI, receives managed per-project env vars in its subprocess environment when configured, supports `deps:` for dependency installation and `timeout:` in ms, requires `runtime: bun` or `runtime: uv`) . Supports `when:` conditions, `trigger_rule` join semantics, `$nodeId.output` substitution, `output_format` for structured JSON output (Claude and Codex), `allowed_tools`/`denied_tools` for per-node tool restrictions (Claude only), `hooks` for per-node SDK hook callbacks (Claude only), `mcp` for per-node MCP server config files (Claude only, env vars expanded at execution time), and `skills` for per-node skill preloading via AgentDefinition wrapping (Claude only), `agents` for inline sub-agent definitions invokable via the Task tool (Claude only), and `effort`/`thinking`/`maxBudgetUsd`/`systemPrompt`/`fallbackModel`/`betas`/`sandbox` for Claude SDK advanced options (Claude only, also settable at workflow level)
- Provider inherited from `.archon/config.yaml` unless explicitly set; per-node `provider` and `model` overrides supported
- Model and options can be set per workflow or inherited from config defaults
- `interactive: true` at the workflow level forces foreground execution on web (required for approval-gate workflows in the web UI)
Expand Down
32 changes: 32 additions & 0 deletions packages/docs-web/src/content/docs/guides/authoring-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ nodes:
| `hooks` | object | — | Per-node SDK hook callbacks. Claude only. See [Hooks](/guides/hooks/) |
| `mcp` | string | — | Path to MCP server config JSON file. Claude only. See [MCP Servers](/guides/mcp-servers/) |
| `skills` | string[] | — | Skills to preload. Claude only. See [Skills](/guides/skills/) |
| `agents` | object | — | Inline sub-agent definitions keyed by kebab-case ID. Claude only. See [Inline sub-agents](#inline-sub-agents) |
| `effort` | `'low'`\|`'medium'`\|`'high'`\|`'max'` | — | Reasoning depth. Claude only. Also settable at workflow level |
| `thinking` | string \| object | — | Thinking mode: `'adaptive'`, `'disabled'`, or `{type:'enabled', budgetTokens:N}`. Claude only. Also settable at workflow level |
| `maxBudgetUsd` | number | — | USD cost cap; node fails if exceeded. Claude only. Per-node only |
Expand Down Expand Up @@ -404,6 +405,36 @@ nodes:
- `undefined` (field absent) and `[]` have different semantics — absent means use default tool set, `[]` means no tools
- Claude only — Codex nodes/steps emit a warning and continue (Codex doesn't support per-call tool restrictions)

### Inline sub-agents

Define Claude sub-agents directly in the workflow YAML, without authoring `.claude/agents/*.md` files. The main agent can spawn them in parallel via the `Task` tool — useful for map-reduce patterns where a cheap model (e.g. Haiku) briefs items and a stronger model reduces.

```yaml
nodes:
- id: triage
prompt: |
Fetch open issues via `gh issue list ...`. For each issue, spawn the
brief-gen sub-agent in parallel (one message, multiple Task tool calls)
to produce a 2-3 sentence brief. Then cluster briefs for duplicates.
model: sonnet
allowed_tools: [Bash, Read, Write, Task]
agents:
brief-gen:
description: Summarises a single GitHub issue in 2-3 sentences
prompt: |
You are concise. Read the issue provided in the caller's prompt.
Return JSON { summary, primarySymptom, affectedArea }.
model: haiku
tools: [Bash, Read]
```

Keys:

- Agent IDs must be **kebab-case** (`^[a-z0-9][a-z0-9-]*$`)
- Each definition requires `description` and `prompt`; `model`, `tools`, `disallowedTools`, `skills`, and `maxTurns` are optional
- Map is merged with any SDK-level agents and with the internal `dag-node-skills` wrapper created by `skills:` — user-defined agents win on ID collision
- Claude only. Codex and community providers that don't support inline agents emit a warning and ignore the field

---

## Retry Configuration
Expand Down Expand Up @@ -1126,6 +1157,7 @@ Before deploying a workflow:
10. **`hooks`** — attach SDK hook callbacks to Claude nodes for tool control and context injection
11. **`mcp:`** — attach per-node MCP servers via JSON config (Claude only)
12. **`skills:`** — preload skills into Claude nodes for domain expertise
12b. **`agents:`** — inline Claude sub-agent definitions invokable via the `Task` tool
13. **`effort` / `thinking`** — control reasoning depth and thinking mode per node or workflow (Claude only)
14. **`maxBudgetUsd`** — set a USD cost cap per node; fails with error if exceeded (Claude only)
15. **`systemPrompt`** — override the default system prompt per node (Claude only)
Expand Down
1 change: 1 addition & 0 deletions packages/providers/src/claude/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const CLAUDE_CAPABILITIES: ProviderCapabilities = {
mcp: true,
hooks: true,
skills: true,
agents: true,
toolRestrictions: true,
structuredOutput: true,
envInjection: true,
Expand Down
81 changes: 81 additions & 0 deletions packages/providers/src/claude/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe('ClaudeProvider', () => {
mcp: true,
hooks: true,
skills: true,
agents: true,
toolRestrictions: true,
structuredOutput: true,
envInjection: true,
Expand Down Expand Up @@ -1165,4 +1166,84 @@ describe('sendQuery decomposition behaviors', () => {
'claude.result_is_error'
);
});

describe('inline agents (nodeConfig.agents)', () => {
test('passes inline agents map through to SDK options.agents', async () => {
mockQuery.mockImplementation(async function* () {
yield { type: 'result', session_id: 'sid' };
});

const agents = {
'brief-gen': {
description: 'Summarises issues',
prompt: 'Be concise.',
model: 'haiku',
tools: ['Bash', 'Read'],
},
};

for await (const _ of client.sendQuery('test', '/workspace', undefined, {
nodeConfig: { agents },
})) {
// consume
}

expect(mockQuery).toHaveBeenCalledTimes(1);
const callArgs = mockQuery.mock.calls[0][0] as { options: Record<string, unknown> };
expect(callArgs.options.agents).toMatchObject(agents);
});

test('does not set options.agent when only inline agents are present', async () => {
mockQuery.mockImplementation(async function* () {
yield { type: 'result', session_id: 'sid' };
});

for await (const _ of client.sendQuery('test', '/workspace', undefined, {
nodeConfig: {
agents: {
'sub-a': { description: 'd', prompt: 'p' },
},
},
})) {
// consume
}

const callArgs = mockQuery.mock.calls[0][0] as { options: Record<string, unknown> };
// agent (singular) is set by skills wrapper; inline-only must leave it unset
expect(callArgs.options.agent).toBeUndefined();
});

test('merges inline agents with skills wrapper; user wins on ID collision', async () => {
mockQuery.mockImplementation(async function* () {
yield { type: 'result', session_id: 'sid' };
});

for await (const _ of client.sendQuery('test', '/workspace', undefined, {
nodeConfig: {
skills: ['my-skill'],
agents: {
// Intentionally collides with the internal 'dag-node-skills' wrapper ID
'dag-node-skills': {
description: 'user override',
prompt: 'user-defined prompt',
},
'extra-sub': { description: 'd', prompt: 'p' },
},
},
})) {
// consume
}

const callArgs = mockQuery.mock.calls[0][0] as { options: Record<string, unknown> };
const outAgents = callArgs.options.agents as Record<
string,
{ description: string; prompt: string }
>;
// Both entries present
expect(Object.keys(outAgents).sort()).toEqual(['dag-node-skills', 'extra-sub']);
// User's definition wins the collision
expect(outAgents['dag-node-skills'].description).toBe('user override');
expect(outAgents['dag-node-skills'].prompt).toBe('user-defined prompt');
});
});
});
13 changes: 13 additions & 0 deletions packages/providers/src/claude/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,19 @@ async function applyNodeConfig(
getLog().info({ skills, agentId }, 'claude.skills_agent_created');
}

// agents → inline AgentDefinition pass-through.
// Runs AFTER skills: so user-defined agents win on ID collision with
// the internal 'dag-node-skills' wrapper.
// options.agent is intentionally left alone — inline agents are sub-agents
// invokable via the Task tool, not the primary agent for the query.
if (nodeConfig.agents) {
options.agents = {
...(options.agents ?? {}),
...(nodeConfig.agents as NonNullable<Options['agents']>),
};
getLog().info({ agentIds: Object.keys(nodeConfig.agents) }, 'claude.inline_agents_registered');
}

// effort
if (nodeConfig.effort !== undefined) {
options.effort = nodeConfig.effort as Options['effort'];
Expand Down
1 change: 1 addition & 0 deletions packages/providers/src/codex/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const CODEX_CAPABILITIES: ProviderCapabilities = {
mcp: false,
hooks: false,
skills: false,
agents: false,
toolRestrictions: false,
structuredOutput: true,
envInjection: true,
Expand Down
1 change: 1 addition & 0 deletions packages/providers/src/codex/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe('CodexProvider', () => {
mcp: false,
hooks: false,
skills: false,
agents: false,
toolRestrictions: false,
structuredOutput: true,
envInjection: true,
Expand Down
1 change: 1 addition & 0 deletions packages/providers/src/community/pi/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const PI_CAPABILITIES: ProviderCapabilities = {
mcp: false,
hooks: false,
skills: true,
agents: false,
toolRestrictions: true,
structuredOutput: false,
envInjection: true,
Expand Down
1 change: 1 addition & 0 deletions packages/providers/src/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function makeMockProvider(id: string): IAgentProvider {
mcp: false,
hooks: false,
skills: false,
agents: false,
toolRestrictions: false,
structuredOutput: false,
envInjection: false,
Expand Down
19 changes: 19 additions & 0 deletions packages/providers/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,23 @@ export interface NodeConfig {
mcp?: string;
hooks?: unknown;
skills?: string[];
/**
* Inline sub-agent definitions (keyed by kebab-case agent ID).
* Shape mirrors Claude Agent SDK's AgentDefinition, kept structural so this
* contract module stays SDK-dep-free.
*/
agents?: Record<
string,
{
description: string;
prompt: string;
model?: string;
tools?: string[];
disallowedTools?: string[];
skills?: string[];
maxTurns?: number;
}
>;
allowed_tools?: string[];
denied_tools?: string[];
effort?: string;
Expand Down Expand Up @@ -158,6 +175,8 @@ export interface ProviderCapabilities {
mcp: boolean;
hooks: boolean;
skills: boolean;
/** Whether the provider supports inline sub-agent definitions (Claude SDK's options.agents). */
agents: boolean;
toolRestrictions: boolean;
structuredOutput: boolean;
envInjection: boolean;
Expand Down
11 changes: 11 additions & 0 deletions packages/web/src/lib/api.generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2199,6 +2199,17 @@ export interface components {
};
mcp?: string;
skills?: string[];
agents?: {
[key: string]: {
description: string;
prompt: string;
model?: string;
tools?: string[];
disallowedTools?: string[];
skills?: string[];
maxTurns?: number;
};
};
/** @enum {string} */
effort?: 'low' | 'medium' | 'high' | 'max';
thinking?:
Expand Down
Loading
Loading