Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Inline sub-agent definitions on DAG nodes (`agents:`).** Define Claude Agent SDK `AgentDefinition`s directly in workflow YAML, keyed by kebab-case agent ID. 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. Removes the need to author `.claude/agents/*.md` files for workflow-scoped helpers. Claude only; Codex and community providers that don't support inline agents emit a capability warning and ignore the field. Merges with the internal `dag-node-skills` wrapper set by `skills:` on the same node — user-defined agents win on ID collision (a warning is logged). (#1276)
- **Pi community provider (`@mariozechner/pi-coding-agent`).** First community provider under the Phase 2 registry (`builtIn: false`). One adapter exposes ~20 LLM backends (Anthropic, OpenAI, Google, Groq, Mistral, Cerebras, xAI, OpenRouter, Hugging Face, and more) via a `<pi-provider-id>/<model-id>` model format. Reads credentials from `~/.pi/agent/auth.json` (populated by running `pi /login` for OAuth subscriptions like Claude Pro/Max, ChatGPT Plus, GitHub Copilot) AND from env vars (env vars take priority per-request). Per-node workflow options supported: `effort`/`thinking` → Pi `thinkingLevel`; `allowed_tools`/`denied_tools` → filter Pi's 7 built-in coding tools; `skills` → resolved against `.agents/skills`, `.claude/skills` (project + user-global); `systemPrompt`; codebase env vars; session resume via `sessionId` round-trip. Unsupported fields (MCP, hooks, structured output, cost limits, fallback model, sandbox) trigger an explicit dag-executor warning rather than silently dropping. Use in workflow YAML: `provider: pi` + `model: anthropic/claude-haiku-4-5`. (#1270)
- **`registerCommunityProviders()` aggregator** in `@archon/providers`. Process entrypoints (CLI, server, config-loader) now call one function to register every bundled community provider. Adding a new community provider is a single-line edit to this aggregator rather than touching each entrypoint — makes the Phase 2 "community providers are a localized addition" promise real.
- **`contributing/adding-a-community-provider.md` guide** — contributor-facing walkthrough of the Phase 2 registry pattern using Pi as the reference implementation.
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ nodes:
| Tool restrictions | ✅ | `allowed_tools` / `denied_tools` (read, bash, edit, write, grep, find, ls) |
| Thinking level | ✅ | `effort: low\|medium\|high\|max` (max → xhigh) |
| Skills | ✅ | `skills: [name]` (searches `.agents/skills`, `.claude/skills`, user-global) |
| Inline sub-agents | ❌ | `agents:` is Claude-only; ignored with a warning on Pi |
| System prompt override | ✅ | `systemPrompt:` |
| Codebase env vars (`envInjection`) | ✅ | `.archon/config.yaml` `env:` section |
| MCP servers | ❌ | Pi rejects MCP by design |
Expand Down
53 changes: 46 additions & 7 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,43 @@ 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 (a warning is logged when this happens)
- Claude only. Codex and community providers that don't support inline agents emit a warning and ignore the field

**When to use `agents:` vs `.claude/agents/*.md` files:**

- **`agents:` (inline)** — use when the sub-agent is specific to ONE workflow's needs. Keeps the workflow self-contained in a single YAML file; travels cleanly in PRs and forks.
- **`.claude/agents/*.md` (on-disk)** — use when the sub-agent is shared across multiple workflows OR the whole project (for example, a `triage-agent` used by several maintenance workflows). On-disk agents live outside workflow YAMLs and are picked up automatically by the Claude Agent SDK.

Both sources coexist — inline agents and on-disk agents are both available to `Task(subagent_type=...)` at runtime.

---

## Retry Configuration
Expand Down Expand Up @@ -1126,10 +1164,11 @@ 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
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)
16. **`sandbox`** — OS-level filesystem/network restrictions per node or workflow (Claude only)
17. **Loop nodes** — use `loop:` within a DAG node for iterative execution until completion signal
18. **Defaults as templates** — browse `.archon/workflows/defaults/` for real examples to copy and modify
19. **Test thoroughly** — each command, the artifact flow, and edge cases
13. **`agents:`** — inline Claude sub-agent definitions invokable via the `Task` tool
14. **`effort` / `thinking`** — control reasoning depth and thinking mode per node or workflow (Claude only)
15. **`maxBudgetUsd`** — set a USD cost cap per node; fails with error if exceeded (Claude only)
16. **`systemPrompt`** — override the default system prompt per node (Claude only)
17. **`sandbox`** — OS-level filesystem/network restrictions per node or workflow (Claude only)
18. **Loop nodes** — use `loop:` within a DAG node for iterative execution until completion signal
19. **Defaults as templates** — browse `.archon/workflows/defaults/` for real examples to copy and modify
20. **Test thoroughly** — each command, the artifact flow, and edge cases
1 change: 1 addition & 0 deletions packages/docs-web/src/content/docs/guides/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ To use skills, ensure the node uses Claude (the default provider, or set

## Related

- [Inline sub-agents](/guides/authoring-workflows/#inline-sub-agents) — `agents:` field for workflow-scoped sub-agents (composes with `skills:` on the same node; user-defined agents win on ID collision with the internal `dag-node-skills` wrapper)
- [Per-Node MCP Servers](/guides/mcp-servers/) — `mcp:` field for external tool access
- [Hooks](/guides/hooks/) — `hooks:` field for tool permission control
- [skills.sh](https://skills.sh) — marketplace for discovering skills
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
125 changes: 125 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,128 @@ 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');
});

test('logs a warning when user-defined dag-node-skills overrides the skills wrapper', 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: {
'dag-node-skills': { description: 'user override', prompt: 'p' },
},
},
})) {
// consume
}

expect(mockLogger.warn).toHaveBeenCalledWith(
expect.objectContaining({ nodeSkills: ['my-skill'] }),
'claude.inline_agents_override_skills_wrapper'
);
});

test('does NOT warn when inline agents do not collide with the skills wrapper', 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: {
'brief-gen': { description: 'd', prompt: 'p' },
},
},
})) {
// consume
}

const warnCalls = mockLogger.warn.mock.calls.filter(
(args: unknown[]) => args[1] === 'claude.inline_agents_override_skills_wrapper'
);
expect(warnCalls).toHaveLength(0);
});
});
});
26 changes: 26 additions & 0 deletions packages/providers/src/claude/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,32 @@ 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) {
// Warn loudly when a user-defined agent overrides the internal
// 'dag-node-skills' wrapper set by the skills: block above. The
// merge is by design (user wins) but silent capability removal
// is the exact failure mode we want to avoid.
if (
Object.hasOwn(nodeConfig.agents, 'dag-node-skills') &&
options.agents?.['dag-node-skills'] !== undefined
) {
getLog().warn(
{ nodeSkills: nodeConfig.skills ?? [] },
'claude.inline_agents_override_skills_wrapper'
);
}
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
Loading
Loading