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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased] - 2026-09-28

### Added
- `createAgent()` agents can pause for approval (LOU-D21): a `needsApproval` tool no longer fails the run with "requires approval but no approvalStore". The run pauses (`finishReason: 'awaiting-approval'`) in a per-agent `InMemoryApprovalStore` (or the new `approvalStore` option), and `agent.approvals.list()` / `agent.approvals.resolve({ id, approved, note? })` continue it, in its session if it paused in one. The `approve` option decides calls in code without pausing.

### Changed
- BREAKING: `encrypt()` now generates a random salt per call instead of a hardcoded one. Ciphertext produced before this change cannot be decrypted with the new code and must be re-encrypted.
- BREAKING: The mock repositories (`MockAgentRepository`, `MockSessionRepository`, `MockResultRepository`, `MockMemoryRepository`, `MockAttachmentRepository` — previously re-exported from `src/data/mocks.ts` via the package root/`./data` subpath; this package has no `createMockRepositories` factory) are no longer exported from the package root. Import them from `@loushy/build-ai-agent/testing` instead.
Expand Down
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,43 @@ fallback model.

### Human-in-the-loop approval gates

Flag a tool `needsApproval`; `AgentExecutor` pauses before calling it and
persists an `ExecutionSnapshot` instead of invoking the tool. Resume later —
after a real restart if you like — with `resumeAfterApproval()`:
Flag a tool `needsApproval` and the run pauses before calling it. A
`createAgent()` agent saves the pause in its own `InMemoryApprovalStore`
(or the `approvalStore` you pass): `send()` resolves with
`finishReason: 'awaiting-approval'` and an `approvalId`,
`agent.approvals.list()` shows what is waiting, and
`agent.approvals.resolve({ id, approved, note? })` runs (or rejects) the call
and continues the run — in the same session, if it paused inside
`agent.session()`. Pass `approve` to decide each call in code instead of
pausing (`stream()` still ends at the pause):

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

const sendEmail = defineTool({
name: 'send_email',
description: 'Send an email',
input: z.object({ to: z.string() }),
needsApproval: true,
execute: async ({ to }) => `sent to ${to}`,
});
const agent = createAgent({ provider, instructions: 'You send emails.', tools: [sendEmail] });

const paused = await agent.send('Email the report to sam@example.com');
if (paused.finishReason === 'awaiting-approval') {
console.log(await agent.approvals.list()); // [{ id, toolName: 'send_email', args: { to: '...' }, ... }]
const result = await agent.approvals.resolve({ id: paused.approvalId!, approved: true }); // or approved: false, note: 'why'
console.log(result.text);
}

// Or decide in code, with no pause: true runs the tool, false sends the model a rejection.
const trusted = createAgent({ provider, tools: [sendEmail], approve: ({ args }) => String(args.to).endsWith('@example.com') });
```

With `AgentExecutor` directly, pass an `approvalStore`: it persists an
`ExecutionSnapshot` instead of invoking the tool. Resume later — after a real
restart if you like — with `resumeAfterApproval()`:

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

### Core

- **`createAgent()`** - zero-config `{ send, stream }` agent
- **`createAgent()`** - zero-config `{ send, stream, session, approvals }` agent
- **`AgentBuilder`** - fluent `AgentConfig` builder
- **`AgentExecutor`** - static executor (`execute()`, approvals, checkpoints, tracing)
- **`ToolRegistry`** - manage available tools
Expand All @@ -680,7 +714,7 @@ npm run pipeline:demo:trigger # POSTs a synthetic error to kick it off

### Safety & ops

- **`resumeAfterApproval()`**, **`StorageServiceApprovalStore`** - human-in-the-loop
- **`resumeAfterApproval()`**, **`StorageServiceApprovalStore`**, **`InMemoryApprovalStore`** - human-in-the-loop
- **`LocalStorageCheckpointStore`** - durable execution
- **`createDelegateTool()`** - delegate to one child agent through a tool (see `subagents` for named sub-agents)
- **`runGuardrails()`**, **`secretScanGuardrail`**, **`createDiffSizeGuardrail()`**, **`createCommandGuardrail()`** - guardrails
Expand Down
13 changes: 13 additions & 0 deletions docs/api-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ npm run docs:build # writes docs/api/index.html
| `AgentExecutor.execute(opts)` | Static executor: runs an agent (LLM + tool-calling loop) and resolves to an `ExecutionResult`. |
| `AgentType` | Agent type enum (e.g. `AgentType.SmartAssistant`). |
| `resumeAfterApproval()` | Resume an execution paused for human approval. |
| `InMemoryApprovalStore` | Process-local `ApprovalStore`; the default store of `createAgent()` agents. |
| `SessionAwaitingApprovalError` | Thrown by `execute()` when its `sessionId` is paused on an approval (see [Durable execution](./durable-execution.md)). |
| `createDelegateTool()` | Wrap a child agent as a tool for multi-agent delegation. |

Expand All @@ -30,6 +31,18 @@ signal, hooks (`ctx.subagent`), tracing, approval store and `onEvent`
(`event.subagent`). `maxSubagentDepth` (default 1) bounds nesting. See
[Sub-agents](./sub-agents.md).

### Approvals

A `createAgent()` agent pauses on a `needsApproval` tool instead of failing:
`send()` (and `session.send()`) resolves with `finishReason: 'awaiting-approval'`
and an `approvalId`. `agent.approvals.list()` returns the pending calls and
`agent.approvals.resolve({ id, approved, note? })` runs or rejects the call and
resolves with the continued run's result (continuing the session it paused
in). Pauses are kept in a per-agent `InMemoryApprovalStore` unless you pass
`approvalStore` (e.g. `SqliteStore.approvals`); `approve: (call) => boolean`
decides each call in code without pausing (`stream()` still ends at the
pause). See [Human-in-the-loop approval gates](../README.md#human-in-the-loop-approval-gates).

### Skills

Pass `skills: [defineSkill({ name, description, content }), ...(await loadSkills(dir))]` to
Expand Down
8 changes: 7 additions & 1 deletion docs/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ exactly as it was before that call. An aborted `send()` resolves with
`finishReason: 'aborted'`, as `agent.send()` does. The stored transcript never
contains an assistant tool-call turn without the matching tool results.

A `send()` that pauses on a `needsApproval` tool resolves with
`finishReason: 'awaiting-approval'`. `agent.approvals.resolve({ id, approved })`
then continues the run as the session's next turn, so the tool call, its result
and the final answer join the transcript (see
[approval gates](../README.md#human-in-the-loop-approval-gates)).

Sessions are a thin layer: the session owns the transcript and hands it to the
executor on each turn. They do not use the checkpoint/resume mechanism
(`sessionId` + `checkpointStore` on `AgentExecutor.execute()`). That lower-level
Expand Down Expand Up @@ -87,7 +93,7 @@ implementation by where the process runs:

| Store | Sessions | Checkpoints | Approvals | Use it when |
| --- | --- | --- | --- | --- |
| In memory | `MemorySessionStore` | (supply your own) | (supply your own) | Tests, scripts, one process that never restarts |
| In memory | `MemorySessionStore` | (supply your own) | `InMemoryApprovalStore` | Tests, scripts, one process that never restarts |
| Files | `FileSessionStore(dir)` | `LocalStorageCheckpointStore` | `StorageServiceApprovalStore` | One machine, you want plain inspectable files |
| SQLite | `store.sessions` | `store.checkpoints` | `store.approvals` | A Node server: one durable, transactional file, shared safely by several processes |
| Cloudflare KV | - | `KVCheckpointStore` | - | Workers deployments (see [Deployment](deployment.md)) |
Expand Down
9 changes: 7 additions & 2 deletions docs/sub-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ const { text } = await lead.send('Write a short article about the history of the
```

`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)).
options. Use it when you need hooks or tracing on the lead run - sub-agents
inherit those (see [Inheritance](#what-a-sub-agent-inherits)). Approvals work
from `createAgent()` too (see [Approvals](#approvals-inside-a-sub-agent)).

## How it works

Expand Down Expand Up @@ -207,6 +208,10 @@ again with a new `approvalId`. This works at any depth and for
`createDelegateTool()` children (pass the registry that holds the delegate
tool).

With a `createAgent()` lead, `lead.send()` pauses the same way and
`lead.approvals.resolve({ id, approved })` resumes it (the lead already knows
its `subagents`); the lead's `approve` option decides its sub-agents' calls too.

Guarantees and limits:

- No call runs twice and none is lost: the sub-agent's paused state is stored
Expand Down
74 changes: 66 additions & 8 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,43 @@ fallback model.

### Human-in-the-loop approval gates

Flag a tool `needsApproval`; `AgentExecutor` pauses before calling it and
persists an `ExecutionSnapshot` instead of invoking the tool. Resume later —
after a real restart if you like — with `resumeAfterApproval()`:
Flag a tool `needsApproval` and the run pauses before calling it. A
`createAgent()` agent saves the pause in its own `InMemoryApprovalStore`
(or the `approvalStore` you pass): `send()` resolves with
`finishReason: 'awaiting-approval'` and an `approvalId`,
`agent.approvals.list()` shows what is waiting, and
`agent.approvals.resolve({ id, approved, note? })` runs (or rejects) the call
and continues the run — in the same session, if it paused inside
`agent.session()`. Pass `approve` to decide each call in code instead of
pausing (`stream()` still ends at the pause):

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

const sendEmail = defineTool({
name: 'send_email',
description: 'Send an email',
input: z.object({ to: z.string() }),
needsApproval: true,
execute: async ({ to }) => `sent to ${to}`,
});
const agent = createAgent({ provider, instructions: 'You send emails.', tools: [sendEmail] });

const paused = await agent.send('Email the report to sam@example.com');
if (paused.finishReason === 'awaiting-approval') {
console.log(await agent.approvals.list()); // [{ id, toolName: 'send_email', args: { to: '...' }, ... }]
const result = await agent.approvals.resolve({ id: paused.approvalId!, approved: true }); // or approved: false, note: 'why'
console.log(result.text);
}

// Or decide in code, with no pause: true runs the tool, false sends the model a rejection.
const trusted = createAgent({ provider, tools: [sendEmail], approve: ({ args }) => String(args.to).endsWith('@example.com') });
```

With `AgentExecutor` directly, pass an `approvalStore`: it persists an
`ExecutionSnapshot` instead of invoking the tool. Resume later — after a real
restart if you like — with `resumeAfterApproval()`:

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

### Core

- **`createAgent()`** - zero-config `{ send, stream }` agent
- **`createAgent()`** - zero-config `{ send, stream, session, approvals }` agent
- **`AgentBuilder`** - fluent `AgentConfig` builder
- **`AgentExecutor`** - static executor (`execute()`, approvals, checkpoints, tracing)
- **`ToolRegistry`** - manage available tools
Expand All @@ -678,7 +712,7 @@ npm run pipeline:demo:trigger # POSTs a synthetic error to kick it off

### Safety & ops

- **`resumeAfterApproval()`**, **`StorageServiceApprovalStore`** - human-in-the-loop
- **`resumeAfterApproval()`**, **`StorageServiceApprovalStore`**, **`InMemoryApprovalStore`** - human-in-the-loop
- **`LocalStorageCheckpointStore`** - durable execution
- **`createDelegateTool()`** - delegate to one child agent through a tool (see `subagents` for named sub-agents)
- **`runGuardrails()`**, **`secretScanGuardrail`**, **`createDiffSizeGuardrail()`**, **`createCommandGuardrail()`** - guardrails
Expand Down Expand Up @@ -1187,6 +1221,7 @@ npm run docs:build # writes docs/api/index.html
| `AgentExecutor.execute(opts)` | Static executor: runs an agent (LLM + tool-calling loop) and resolves to an `ExecutionResult`. |
| `AgentType` | Agent type enum (e.g. `AgentType.SmartAssistant`). |
| `resumeAfterApproval()` | Resume an execution paused for human approval. |
| `InMemoryApprovalStore` | Process-local `ApprovalStore`; the default store of `createAgent()` agents. |
| `SessionAwaitingApprovalError` | Thrown by `execute()` when its `sessionId` is paused on an approval (see [Durable execution](https://github.com/LinuxDevil/agent-sdk/blob/main/docs/durable-execution.md)). |
| `createDelegateTool()` | Wrap a child agent as a tool for multi-agent delegation. |

Expand All @@ -1200,6 +1235,18 @@ signal, hooks (`ctx.subagent`), tracing, approval store and `onEvent`
(`event.subagent`). `maxSubagentDepth` (default 1) bounds nesting. See
[Sub-agents](https://github.com/LinuxDevil/agent-sdk/blob/main/docs/sub-agents.md).

### Approvals

A `createAgent()` agent pauses on a `needsApproval` tool instead of failing:
`send()` (and `session.send()`) resolves with `finishReason: 'awaiting-approval'`
and an `approvalId`. `agent.approvals.list()` returns the pending calls and
`agent.approvals.resolve({ id, approved, note? })` runs or rejects the call and
resolves with the continued run's result (continuing the session it paused
in). Pauses are kept in a per-agent `InMemoryApprovalStore` unless you pass
`approvalStore` (e.g. `SqliteStore.approvals`); `approve: (call) => boolean`
decides each call in code without pausing (`stream()` still ends at the
pause). See [Human-in-the-loop approval gates](https://github.com/LinuxDevil/agent-sdk/blob/main/README.md#human-in-the-loop-approval-gates).

### Skills

Pass `skills: [defineSkill({ name, description, content }), ...(await loadSkills(dir))]` to
Expand Down Expand Up @@ -3226,6 +3273,12 @@ exactly as it was before that call. An aborted `send()` resolves with
`finishReason: 'aborted'`, as `agent.send()` does. The stored transcript never
contains an assistant tool-call turn without the matching tool results.

A `send()` that pauses on a `needsApproval` tool resolves with
`finishReason: 'awaiting-approval'`. `agent.approvals.resolve({ id, approved })`
then continues the run as the session's next turn, so the tool call, its result
and the final answer join the transcript (see
[approval gates](https://github.com/LinuxDevil/agent-sdk/blob/main/README.md#human-in-the-loop-approval-gates)).

Sessions are a thin layer: the session owns the transcript and hands it to the
executor on each turn. They do not use the checkpoint/resume mechanism
(`sessionId` + `checkpointStore` on `AgentExecutor.execute()`). That lower-level
Expand Down Expand Up @@ -3278,7 +3331,7 @@ implementation by where the process runs:

| Store | Sessions | Checkpoints | Approvals | Use it when |
| --- | --- | --- | --- | --- |
| In memory | `MemorySessionStore` | (supply your own) | (supply your own) | Tests, scripts, one process that never restarts |
| In memory | `MemorySessionStore` | (supply your own) | `InMemoryApprovalStore` | Tests, scripts, one process that never restarts |
| Files | `FileSessionStore(dir)` | `LocalStorageCheckpointStore` | `StorageServiceApprovalStore` | One machine, you want plain inspectable files |
| SQLite | `store.sessions` | `store.checkpoints` | `store.approvals` | A Node server: one durable, transactional file, shared safely by several processes |
| Cloudflare KV | - | `KVCheckpointStore` | - | Workers deployments (see [Deployment](https://github.com/LinuxDevil/agent-sdk/blob/main/docs/deployment.md)) |
Expand Down Expand Up @@ -3799,8 +3852,9 @@ const { text } = await lead.send('Write a short article about the history of the
```

`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)).
options. Use it when you need hooks or tracing on the lead run - sub-agents
inherit those (see [Inheritance](#what-a-sub-agent-inherits)). Approvals work
from `createAgent()` too (see [Approvals](#approvals-inside-a-sub-agent)).

## How it works

Expand Down Expand Up @@ -3963,6 +4017,10 @@ again with a new `approvalId`. This works at any depth and for
`createDelegateTool()` children (pass the registry that holds the delegate
tool).

With a `createAgent()` lead, `lead.send()` pauses the same way and
`lead.approvals.resolve({ id, approved })` resumes it (the lead already knows
its `subagents`); the lead's `approve` option decides its sub-agents' calls too.

Guarantees and limits:

- No call runs twice and none is lost: the sub-agent's paused state is stored
Expand Down
Loading
Loading