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 @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] - 2026-09-28

### Added
- Background sub-agents (LOU-Y4): the `task` tool takes `background: true`, which starts the sub-agent and returns `{ taskId, status: 'running', agent }` at once. Agents with `subagents` also get `agent_status({ taskId? })` (`queued`/`running`/`done`/`failed`/`cancelled`/`awaiting-approval`, plus `elapsedMs`), `agent_await({ taskId | taskIds, timeoutMs? })` (the synchronous `task` result, or `status: 'timeout'`) and `agent_cancel({ taskId })`. `withSubagentOptions(subagents, { maxConcurrent })` bounds how many run at once per lead run (default 3; the rest queue). Background sub-agents inherit the lead run's runtime and `maxSubagentDepth`, and aborting the lead cancels them. Not yet (LOU-Y4.2): cancelling or awaiting them when the lead run finishes, and resuming one that paused for approval (it reports `awaiting-approval` with its `approvalId`). A user tool named `agent_status`, `agent_await` or `agent_cancel` now conflicts with `subagents`, like `task`. See docs/sub-agents.md.
- React hook (LOU-D15): new `@loushy/build-ai-agent/react` subpath with `useLoushyAgent(source, options?)`. `source` is `{ agent, sessionId? }` (in process, via `agent.stream()` or `agent.session({ id }).stream()`) or `{ url, headers?, fetch? }` (POSTs `{ input }` and reads the SSE or NDJSON event stream). It returns `messages` (text and tool calls with status, args and result), `status` (`idle`/`streaming`/`awaiting-approval`/`error`), `pendingApproval`, `error`, `usage`, `lastEvent`, `send()`, `stop()` (aborts; unmount aborts too), `approve(note?)` and `reject(note?)` (via `agent.approvals.resolve()` in process, or `POST ${approvalsUrl}/${id}` remotely). The framework-neutral `reduceAgentEvents()` reducer and `parseEventStream()` parser are exported from the same subpath. `react` (`^18 || ^19`) is an optional peer dependency. See docs/react.md.
- Summarizing compaction (LOU-W3): `summarizeStrategy({ model, protectedTokens?, prompt?, maxSummaryTokens? })` replaces the turns before the protected tail with one `[Conversation summary]` user message written by `model` (an `LLMProvider` or a `"provider/model"` spec), keeping system and pinned messages and each assistant turn together with its tool results; if the summary call fails it falls back to pruning and reports `error`. `twoPhaseStrategy()` (the recommended strategy) prunes first and summarizes only when still above the threshold. `pinMessage()` / `isPinned()` and the new optional `Message.metadata` (never sent to providers) mark messages no built-in strategy compacts. `CompactionStrategy.compact()` may now be async, so `compactMessages()` returns a Promise; `CompactionResult` / `CompactionInfo` gain `summary` and `error`, and `CompactionInput` gains `thresholdTokens` and `signal`. A strategy that throws no longer fails the run: the hook reports the `error` to `onCompaction`. See docs/compaction.md.
- `createAgent({ retry, fallbackModels })` (LOU-V7.2): `retry` (`withRetry()` options, or `false`) retries failed model calls and defaults to `{ maxRetries: 2 }` for models given as `provider/model` strings (or picked from the environment); a `provider` instance is wrapped only when `retry` is set. `fallbackModels` are `provider/model` strings tried in order once the primary model's retries are used up (`withFallback([withRetry(primary), withRetry(fallback), ...])`). `agent.stream()`, `session.stream()` and `AgentExecutor.stream()` report them as two new typed events, `provider.retry` (`attempt`, `maxRetries`, `delayMs`, `error: { message, category? }`, `provider`) and `provider.fallback` (`from`, `to`, `error: { message }`), for any `withRetry()` / `withFallback()` provider. Additive: the event schema version stays 1.
Expand Down
56 changes: 54 additions & 2 deletions docs/sub-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ With `subagents`, the lead agent gets:
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 }`
`{ agent: <one of the names>, prompt: string, description: string, background?: boolean }`
(`description` is a 3-5 word label used in events and hooks).
3. Three tools for background tasks: `agent_status`, `agent_await` and
`agent_cancel` (see [Background sub-agents](#background-sub-agents)).

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
Expand All @@ -76,7 +78,7 @@ 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`.
own already named `task` (or `agent_status`, `agent_await`, `agent_cancel`).

## Parallel tasks

Expand All @@ -85,6 +87,56 @@ 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).

## Background sub-agents

With `background: true`, `task` starts the sub-agent and returns at once with
`{ taskId, status: 'running', agent }`, so the lead can keep working (or start
more tasks) while it runs. The lead then uses:

| Tool | Input | Result |
| --- | --- | --- |
| `agent_status` | `{ taskId? }` | `{ tasks: [{ taskId, agent, status, elapsedMs, approvalId?, toolName?, error? }] }` for one task or all of them. `status` is `queued`, `running`, `done`, `failed`, `cancelled` or `awaiting-approval`. |
| `agent_await` | `{ taskId }` or `{ taskIds }`, optional `timeoutMs` | Waits until the task(s) end. A `done` task carries `result`: the same text (with footer) a synchronous `task` returns. A `failed` one carries `error`. A task still running after `timeoutMs` reports `status: 'timeout'`. With `taskIds` the result is `{ tasks: [...] }`. |
| `agent_cancel` | `{ taskId }` | Cancels a queued or running task (its run is aborted) and returns its status. |

At most `maxConcurrent` background sub-agents of one lead run run at once
(default 3); further `background: true` calls get `status: 'queued'` and start
as slots free. Set it with `withSubagentOptions()`, which returns the same
`subagents` value:

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

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

const lead = createAgent({
provider,
instructions: 'You coordinate research.',
subagents: withSubagentOptions({ researcher }, { maxConcurrent: 2 }),
});
```

A background sub-agent inherits from the lead run like a synchronous one
(hooks, tracing, `onEvent`, usage roll-up), and `maxSubagentDepth` applies the
same way. Aborting the lead's signal cancels its background sub-agents, queued
or running.

Limits (follow-up LOU-Y4.2, which needs a run-end hook in the executor):

- Background tasks belong to one `execute()` call. When the lead's run
finishes, sub-agents still running are **not** cancelled or awaited yet:
they run to completion on their own, and their events still reach
`onEvent`. Have the lead collect them with `agent_await` before its final
answer (the system prompt tells it to), or abort the signal you passed to
the run to stop them.
- A background sub-agent that needs approval stops with
`status: 'awaiting-approval'` (with the child's `approvalId` and
`toolName`); its call does not run, and it cannot be resumed through the
approval store. Run that task synchronously (`background: false`) when it
may need approval.
- A lead that pauses for approval and is resumed starts with no background
tasks: those started before the pause cannot be observed afterwards.

## Dynamic catalogs

Instead of a fixed record, pass a catalog with `list()` and `resolve(name)`.
Expand Down
56 changes: 54 additions & 2 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4486,8 +4486,10 @@ With `subagents`, the lead agent gets:
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 }`
`{ agent: <one of the names>, prompt: string, description: string, background?: boolean }`
(`description` is a 3-5 word label used in events and hooks).
3. Three tools for background tasks: `agent_status`, `agent_await` and
`agent_cancel` (see [Background sub-agents](#background-sub-agents)).

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
Expand All @@ -4507,7 +4509,7 @@ 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`.
own already named `task` (or `agent_status`, `agent_await`, `agent_cancel`).

## Parallel tasks

Expand All @@ -4516,6 +4518,56 @@ 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).

## Background sub-agents

With `background: true`, `task` starts the sub-agent and returns at once with
`{ taskId, status: 'running', agent }`, so the lead can keep working (or start
more tasks) while it runs. The lead then uses:

| Tool | Input | Result |
| --- | --- | --- |
| `agent_status` | `{ taskId? }` | `{ tasks: [{ taskId, agent, status, elapsedMs, approvalId?, toolName?, error? }] }` for one task or all of them. `status` is `queued`, `running`, `done`, `failed`, `cancelled` or `awaiting-approval`. |
| `agent_await` | `{ taskId }` or `{ taskIds }`, optional `timeoutMs` | Waits until the task(s) end. A `done` task carries `result`: the same text (with footer) a synchronous `task` returns. A `failed` one carries `error`. A task still running after `timeoutMs` reports `status: 'timeout'`. With `taskIds` the result is `{ tasks: [...] }`. |
| `agent_cancel` | `{ taskId }` | Cancels a queued or running task (its run is aborted) and returns its status. |

At most `maxConcurrent` background sub-agents of one lead run run at once
(default 3); further `background: true` calls get `status: 'queued'` and start
as slots free. Set it with `withSubagentOptions()`, which returns the same
`subagents` value:

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

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

const lead = createAgent({
provider,
instructions: 'You coordinate research.',
subagents: withSubagentOptions({ researcher }, { maxConcurrent: 2 }),
});
```

A background sub-agent inherits from the lead run like a synchronous one
(hooks, tracing, `onEvent`, usage roll-up), and `maxSubagentDepth` applies the
same way. Aborting the lead's signal cancels its background sub-agents, queued
or running.

Limits (follow-up LOU-Y4.2, which needs a run-end hook in the executor):

- Background tasks belong to one `execute()` call. When the lead's run
finishes, sub-agents still running are **not** cancelled or awaited yet:
they run to completion on their own, and their events still reach
`onEvent`. Have the lead collect them with `agent_await` before its final
answer (the system prompt tells it to), or abort the signal you passed to
the run to stop them.
- A background sub-agent that needs approval stops with
`status: 'awaiting-approval'` (with the child's `approvalId` and
`toolName`); its call does not run, and it cannot be resumed through the
approval store. Run that task synchronously (`background: false`) when it
may need approval.
- A lead that pauses for approval and is resumed starts with no background
tasks: those started before the pause cannot be observed afterwards.

## Dynamic catalogs

Instead of a fixed record, pass a catalog with `list()` and `resolve(name)`.
Expand Down
Loading
Loading