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
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ dashboard — any provider, any deploy target, no lock-in.
- **Parallel tool calls** — when the model asks for several tools in one turn they run concurrently (cap it with `toolConcurrency`, or `1` for sequential), and results still reach the transcript in the model's call order
- **Usage & cost** — every result carries `usage` (tokens, USD cost, per-model breakdown, delegated children included, continuing across resumes) and `formatUsage(result.usage)` for logs
- **Skills** — `defineSkill()` / `loadSkills('./skills')`: only each skill's name and description sit in the system prompt; the model loads the full markdown on demand through an auto-registered `load_skill` tool
- **Sub-agents** — `createAgent({ subagents: { researcher, writer } })` gives the lead one `task` tool: each sub-agent runs on the task prompt alone (isolated context) with its own instructions, model and tools, several tasks in one turn run in parallel, and sub-agents inherit the lead run's abort signal, hooks, tracing, approvals and events (`maxSubagentDepth`, dynamic `{ list, resolve }` catalogs; `createDelegateTool()` for a single hand-wired child) — see [Sub-agents](docs/sub-agents.md)
- **Agents as directories** — `loadAgentDir('./my-agent')` turns `instructions.md`, `tools/*.ts`, `skills/` and `subagents/<name>/` into the same object `createAgent()` returns, so you can start with files and drop to code later — see [Agent directories](docs/agent-directories.md)
- **Multi-agent delegation** — wrap a child agent as a tool with `createDelegateTool()`, with a `maxDepth` guard against delegation loops
- **Pre/post hooks** — a `HookRegistry` of `AgentHook`s that can inspect or mutate a tool call/LLM generate step, or throw to abort it, run sandboxed by Agent Forge's hook editor
- **Guardrails** — fail-closed, concurrently-run checks (secret scan, diff size, test/lint commands) that gate a fixer agent's patch before it's used
- **Workspace tools** — `createFsTools()` (`read_file`, `write_file`, `edit_file`, `list_dir`, `glob`, `grep`) and `createShellTool()` over pluggable `FsProvider`/`ShellProvider` backends (`NodeWorkspace`, `MemoryWorkspace`, Docker-backed `SandboxShell`), with paths confined to the workspace root (symlinks included) and shell commands approval-gated by default — see [Workspace tools](docs/workspace-tools.md)
Expand Down Expand Up @@ -257,6 +257,7 @@ console.log(session.id, session.messages.length);
- [Evals](docs/evals.md) - trajectory evals with `defineEval()`, datasets, `mockModel`, judge evals, `loushy eval` with JUnit/JSON reports
- [Sessions](docs/sessions.md) - multi-turn conversations: `agent.session()`, `MemorySessionStore`, `FileSessionStore`
- [Skills](docs/skills.md) - on-demand instructions: `defineSkill()`, `loadSkills()`, how they save context
- [Sub-agents](docs/sub-agents.md) - the `subagents` option and its `task` tool, what sub-agents inherit, approvals inside sub-agents
- [Agent directories](docs/agent-directories.md) - define an agent as a folder: layout, mapping to `createAgent()`, moving to code, security
- [Agent Forge](docs/agent-forge.md) - the visual dashboard (`loushy studio`): quickstart, first-agent walkthrough, hook authoring
- Full guides site: [linuxdevil.github.io/agent-sdk-docs](https://linuxdevil.github.io/agent-sdk-docs/)
Expand Down Expand Up @@ -412,10 +413,27 @@ await AgentExecutor.execute({ agent, input, provider, sessionId: 'session-123',
await AgentExecutor.execute({ agent, input: 'continue', provider, sessionId: 'session-123', checkpointStore });
```

### Multi-agent delegation
### Sub-agents and delegation

Wrap a child `AgentConfig` as a tool so a parent agent can delegate a task to
it, running the child through `AgentExecutor.execute()` under the hood:
Give a lead agent named sub-agents; it delegates with one auto-registered
`task` tool, and each sub-agent sees only the task prompt:

```typescript
import { createAgent } from '@loushy/build-ai-agent';

const researcher = createAgent({ provider, instructions: 'You research.', description: 'Finds and summarizes sources' });
const writer = createAgent({ provider, instructions: 'You write.', description: 'Turns notes into an article' });

const lead = createAgent({ provider, instructions: 'You coordinate.', subagents: { researcher, writer } });
const { text } = await lead.send('Write a short article about bicycles.');
```

See [Sub-agents](docs/sub-agents.md) for catalogs, depth limits and how hooks,
tracing and approvals carry into sub-agents.

To wire one child `AgentConfig` as a tool yourself, use `createDelegateTool()`
(it runs the child through `AgentExecutor.execute()` and inherits the parent
run the same way):

```typescript
import { AgentExecutor, createDelegateTool, ToolRegistry, AgentType } from '@loushy/build-ai-agent';
Expand Down Expand Up @@ -658,7 +676,7 @@ npm run pipeline:demo:trigger # POSTs a synthetic error to kick it off

- **`resumeAfterApproval()`**, **`StorageServiceApprovalStore`** - human-in-the-loop
- **`LocalStorageCheckpointStore`** - durable execution
- **`createDelegateTool()`** - multi-agent delegation
- **`createDelegateTool()`** - delegate to one child agent through a tool (see `subagents` for named sub-agents)
- **`runGuardrails()`**, **`secretScanGuardrail`**, **`createDiffSizeGuardrail()`**, **`createCommandGuardrail()`** - guardrails
- **`withSpan()`**, **`TraceExporter`** - tracing
- **`defineEval()`**, **`exactMatch`**, **`toolCallOrder`**, **`budget`**, **`llmJudge()`** - evals
Expand Down
10 changes: 10 additions & 0 deletions docs/api-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ npm run docs:build # writes docs/api/index.html
| `resumeAfterApproval()` | Resume an execution paused for human approval. |
| `createDelegateTool()` | Wrap a child agent as a tool for multi-agent delegation. |

### Sub-agents

Pass `subagents: { researcher, writer }` (agents from `createAgent()` with a
`description`, or a `{ list, resolve }` catalog) to `createAgent()` or
`AgentExecutor.execute()`: the lead gets one `task` tool and a prompt listing,
each sub-agent runs on the task prompt alone, and it inherits the lead run's
signal, hooks (`ctx.subagent`), tracing, approval store and `onEvent`
(`event.subagent`). `maxSubagentDepth` (default 1) bounds nesting. See
[Sub-agents](./sub-agents.md).

### Skills

Pass `skills: [defineSkill({ name, description, content }), ...(await loadSkills(dir))]` to
Expand Down
34 changes: 34 additions & 0 deletions docs/streaming.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Every event has these fields:
| `seq` | number | `0` for the first event, then `+1` per event, with no gaps. |
| `timestamp` | string | When the event was emitted, ISO 8601 (`2026-10-01T09:30:00.000Z`). |
| `v` | `1` | Schema version, exported as `AGENT_EVENT_SCHEMA_VERSION`. |
| `subagent` | object, optional | Only on events of a sub-agent's run - see [Sub-agents](#sub-agents). |

The event types and their extra fields:

Expand Down Expand Up @@ -158,6 +159,39 @@ if (isAgentEvent(received)) console.log(render(received), toolNames([received]))
removed, renamed or retyped). New event types and new optional fields can be
added without changing `v`, so ignore event types you do not know.

## Sub-agents

When the agent delegates with the `task` tool or a `createDelegateTool()`
tool (see [Sub-agents](./sub-agents.md)), the sub-agent's run is streamed
inside the same stream: its steps, `text.delta`s, `text.done`s, tool events
and errors appear between the lead's `tool.start` and `tool.done` (or
`tool.error`) for that call, each with a `subagent` field:

```json
{ "name": "researcher", "depth": 1, "toolCallId": "call_1", "description": "find sources" }
```

`toolCallId` is the lead's tool call that started the sub-agent; a sub-agent
of a sub-agent has `depth: 2` and the enclosing one as `parent`. Events
without `subagent` are the top-level run's: the ordering guarantees above hold
for them, and separately for each sub-agent's steps (several sub-agents
running in parallel interleave). `run.start` and `run.done` belong to the
top-level run only, so they still come exactly once. A sub-agent that pauses
for approval is reported once, by the top-level `approval.requested` (which
carries the sub-agent's call).

```ts
import { createAgent } from '@loushy/build-ai-agent';

const researcher = createAgent({ provider, instructions: 'You research.', description: 'Finds sources' });
const lead = createAgent({ provider, instructions: 'You coordinate.', subagents: { researcher } });

for await (const event of lead.stream('Write about bicycles')) {
const who = event.subagent ? `[${event.subagent.name}] ` : '';
if (event.type === 'text.delta') process.stdout.write(who + event.text);
}
```

## Token streaming and providers

When the provider implements `stream()` (all built-in providers and
Expand Down
237 changes: 237 additions & 0 deletions docs/sub-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# Sub-agents

A sub-agent is a separate agent - its own instructions, model, tools and
skills - that a lead agent hands a self-contained task to. The sub-agent
starts with a clean context (it sees only the task prompt, never the lead's
conversation), works through as many steps as it needs, and its final answer
comes back to the lead as one tool result.

## Sub-agents, skills or flows?

| Use | When |
| --- | --- |
| **Skills** ([docs](./skills.md)) | The *same* agent needs extra instructions for some tasks. Cheap: a skill is text loaded into the current conversation. |
| **Sub-agents** | A task needs different tools, a different model, or a long tool-heavy exploration you do not want in the lead's context. The lead decides at run time whether and what to delegate; independent tasks run in parallel. |
| **Flows** | The steps and their order are known up front (a pipeline), so they should not be left to a model's judgment. |

## Quick start

Give each sub-agent a `description` (the lead model reads it to choose), then
pass them to the lead as `subagents`:

```ts
import { createAgent } from '@loushy/build-ai-agent';

const researcher = createAgent({
provider,
instructions: 'You research a question and report your sources.',
description: 'Finds and summarizes sources',
});
const writer = createAgent({
provider,
instructions: 'You write clear, short articles.',
description: 'Turns notes into a polished article',
});

const lead = createAgent({
provider,
instructions: 'You coordinate research and writing.',
subagents: { researcher, writer },
});

const { text } = await lead.send('Write a short article about the history of the bicycle.');
```

`AgentExecutor.execute()` takes the same `subagents` and `maxSubagentDepth`
options. Use it when you need hooks, tracing or approvals on the lead run -
sub-agents inherit those (see [Inheritance](#what-a-sub-agent-inherits)).

## How it works

With `subagents`, the lead agent gets:

1. An **Available sub-agents** block in its system prompt: one line per
sub-agent (`- name: description`), plus a sentence saying that a sub-agent
sees only the prompt it is given.
2. ONE tool, `task`, with the input
`{ agent: <one of the names>, prompt: string, description: string }`
(`description` is a 3-5 word label used in events and hooks).

When the lead calls `task`, the named sub-agent runs on `prompt` alone. The
tool result is the sub-agent's final text plus a small footer the lead can
use:

```text
The first bicycles appeared in the 1810s ...

[sub-agent 'researcher': 3 step(s), finish reason 'stop']
```

If the sub-agent does not finish - it throws, runs out of `maxSteps`, or is
aborted - the lead gets an error result (`isError: true`) that says why, for
example `Sub-agent 'researcher' used all 10 of its steps (maxSteps) without
giving a final answer.` An unknown agent name is also an error result listing
the valid names. Sub-agent token usage is added to the lead's `result.usage`.

Errors at setup time are thrown with a fix: a sub-agent without a
`description`, a value that is not a `createAgent()` agent, or a tool of your
own already named `task`.

## Parallel tasks

Tool calls of one model turn run concurrently (see `toolConcurrency`), so when
the lead calls `task` several times in one turn, those sub-agents run in
parallel. Results reach the lead's transcript in the order the model made the
calls. Cap it with `toolConcurrency` on the lead (`1` runs them one at a time).

## Dynamic catalogs

Instead of a fixed record, pass a catalog with `list()` and `resolve(name)`.
`list()` is called at the start of every run that offers the `task` tool, so
the set can change between runs; `resolve()` is called when the lead picks a
name (return `undefined` for an unknown one).

```ts
import { createAgent, type SubagentCatalog } from '@loushy/build-ai-agent';

const experts = new Map([
['researcher', createAgent({ provider, instructions: 'You research.' })],
]);

const catalog: SubagentCatalog = {
list: async () => [{ name: 'researcher', description: 'Finds and summarizes sources' }],
resolve: async (name) => experts.get(name),
};

const lead = createAgent({ provider, instructions: 'You coordinate.', subagents: catalog });
```

## Depth

`maxSubagentDepth` (default `1`) bounds how deep sub-agents nest. With the
default, the lead can call sub-agents but they cannot call sub-agents of their
own: a run at the limit is simply not offered the `task` tool, even if it was
created with `subagents`. `maxSubagentDepth: 2` lets the lead's sub-agents
delegate once more. The top-level run's value applies to the whole tree; a
sub-agent's own `maxSubagentDepth` only matters when it runs as a top-level
agent.

## What a sub-agent inherits

The sub-agent keeps its own instructions, model/provider, tools, skills and
`maxSteps`, and its context is isolated. From the run that called it, it
inherits:

| Runtime setting | Inherited? | Notes |
| --- | --- | --- |
| Abort `signal` | Yes | Aborting the lead aborts its sub-agents. |
| Tracing (`exporter`) | Yes | The sub-agent's `invoke_agent` span is a child of the lead's `execute_tool task` span. `captureContent` and `redactContent` are inherited too. |
| Hooks (`hooks`) | Yes | They run on the sub-agent's model calls and tool calls, with `ctx.subagent` set (see below). A hook that throws inside a sub-agent halts the whole run. |
| Approval store (`approvalStore`) | Yes | See [Approvals](#approvals-inside-a-sub-agent). |
| `onEvent` and `stream()` | Yes | The sub-agent's events are forwarded with a `subagent` field. |
| `toolConcurrency` | Yes, unless the sub-agent sets its own | |
| `sandbox` | Yes | |
| Token usage | Rolls up | Added to the lead's `result.usage` (totals, `byModel`, and `usage.delegated`). |
| `maxSubagentDepth` | The remaining budget | See [Depth](#depth). |
| `onLLMRequest`, `onToolCall` and the other single-run callbacks | No | They describe one run; use hooks or `onEvent` to observe sub-agents. |
| `sessionId` / `checkpointStore` | No | A sub-agent is not checkpointed on its own. If the process dies while a sub-agent runs, the resumed lead runs that `task` call again. |
| Conversation history | No | The sub-agent sees only the task prompt. |

`createDelegateTool()` children inherit the same way.

### Hooks inside sub-agents

A parent's hooks apply to its sub-agents by default. `ctx.subagent` tells a
hook it is running inside one - the sub-agent's `name`, its `depth` (1 for a
sub-agent of the top-level run), the lead's `toolCallId` that started it, and
the enclosing sub-agent as `parent` when nested deeper. A hook that should only
see the top-level run returns early:

```ts
import { HookRegistry } from '@loushy/build-ai-agent';

const hooks = new HookRegistry();
hooks.register({
name: 'audit',
preToolCall(ctx) {
if (ctx.subagent) return; // top-level tool calls only
console.log('tool call', ctx.toolName, ctx.args);
},
});
```

### Events

`agent.stream()` / `AgentExecutor.stream()` on the lead stream each
sub-agent's run inside the same stream - steps, text deltas, tool events and
errors - with a `subagent` field on every one of its events, so a UI can nest
sub-agent activity under the lead's `task` call (`subagent.toolCallId`). See
[Streaming: sub-agents](./streaming.md#sub-agents).

With `onEvent` on the lead, every event a sub-agent emits (`start`,
`tool-call`, `tool-result`, `text-complete`, `finish`, ...) reaches the same
listener with `event.subagent` set. The lead's own events have no `subagent`
field.

## Approvals inside a sub-agent

When a sub-agent calls a tool that `needsApproval`, the **whole run pauses**:
the lead's `execute()` resolves with `finishReason: 'awaiting-approval'` and
an `approvalId`, and the approval store holds ONE record whose pending call is
the sub-agent's call (`toolName`, `args`) with `subagentPath` naming the
sub-agents it runs inside (e.g. `['researcher']`). Approve or reject it with
`resumeAfterApproval()`, passing the same `subagents` option as the paused run:

```ts
import { AgentExecutor, resumeAfterApproval, ToolRegistry, createAgent } from '@loushy/build-ai-agent';

const subagents = { researcher: createAgent({ provider, instructions: 'You research.', description: 'Researches' }) };

const paused = await AgentExecutor.execute({ agent, input: 'Go', provider, subagents, approvalStore });
if (paused.finishReason === 'awaiting-approval' && paused.approvalId) {
const result = await resumeAfterApproval(
{ id: paused.approvalId, approved: true },
approvalStore,
new ToolRegistry(),
provider,
{ subagents }
);
console.log(result.text);
}
```

Resuming runs (or rejects) the sub-agent's pending call, lets the sub-agent
finish, returns its final answer to the lead as the `task` result, and then
continues the lead. If the sub-agent needs another approval, the run pauses
again with a new `approvalId`. This works at any depth and for
`createDelegateTool()` children (pass the registry that holds the delegate
tool).

Guarantees and limits:

- No call runs twice and none is lost: the sub-agent's paused state is stored
inside the lead's approval record (plain JSON, so any `ApprovalStore`
works), and the approved call runs exactly once, on resume.
- Other tool calls of the same lead turn that finished keep their results and
are not run again.
- One approval is pending per run. If two sub-agents pause in the same turn,
the run pauses on the first (in call order); the second sub-agent stops and
the lead gets an error result for that `task` call saying its call was not
run, so it can ask again after the approval.
- A `task` call's pre/post tool hooks fire again on resume, like the hooks of
any approved tool.
- Approvals need an `approvalStore` on the lead run, which `createAgent()`
does not take yet; use `AgentExecutor.execute()` for the lead. Without one,
the sub-agent's call becomes an error result and nothing runs.
- Token usage keeps adding up across the pause: the sub-agent's usage before
the pause is in the paused result, and what it spends after the resume is
added to the resumed lead's `result.usage`.

## `createDelegateTool()`

`createDelegateTool({ agent, provider, toolRegistry })` wraps one child agent as
a tool you name and register yourself, with its own `maxDepth` guard. It runs
on the same delegation core as `task`, so it inherits the parent runtime the
same way, and its result shape (`{ text, usage }`) is unchanged. Prefer
`subagents` for new code: one tool, a prompt listing, parallel tasks and depth
limits come for free.
Loading
Loading