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 @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `http_request` is no longer open to DNS rebinding (N13a). It resolved a host name once to check it against the private-address list and then let `fetch` resolve it again to connect, so a name that answered a public address to the check and `127.0.0.1` (or a cloud metadata or intranet address) to the connection reached the private address; the docs and a comment in the Worker runtime described the check as rebinding-safe. Now the one resolution is the check: the in-process path connects through an undici `Agent` whose `connect.lookup` checks every address and hands the socket the address it checked (each redirect hop included), and the sandboxed path (`sandboxExecute`, which agents use) resolves and checks the host in the agent's process and has the sandboxed process connect to that address (node:http/https with a fixed lookup; the host name still goes in `Host` and TLS SNI). The private-address list is now the shared one (below), which adds `0.0.0.0/8`, `100.64.0.0/10`, `192.0.0.0/24`, `198.18.0.0/15`, multicast and reserved `224.0.0.0/3`, `::`, `ff00::/8`, NAT64 `64:ff9b::/96` and 6to4 `2002::/16` to what `http_request` refused. Behavior changes: a host name that cannot be resolved now fails with the resolver's error instead of being passed to `fetch`; `undici` is loaded on the first `http_request` request (not only with `validateSSL: false`). New option `createHttpTool({ allowPrivate })` for hosts that may resolve to private addresses. Cloudflare Workers have no DNS hook, so neither tool exists in the Worker build; docs/deployment.md now says what a Worker can and cannot check.

### Added
- Two ready-made durable stores (R2). `@lousho/build-ai-agent/kv` is a new subpath exporting `KVStore`, `KVCheckpointStore`, `CHECKPOINT_KV_BINDING` and the types `KVStoreOptions`, `KVBinding`, `KVPutOptions`, so a hand-written Cloudflare Worker can use `createAgent({ provider, store: new KVStore(env.AGENT_KV) })`; nothing in its import graph touches `node:*`, so it bundles without shims (`assertSessionId` moved to the Node-free `src/session/sessionId.ts` and is still exported where it was). `fileStore(dir, { historyLimit? })` (root export) is an `AgentStore` of plain JSON files: `sessions/<id>.json`, `checkpoints/<id>.json`, `checkpoint-history/<id>.json` and `approvals/<id>.json`, each written to a temp file and renamed into place, with no lock files and no `StorageService`; resolving an approval claims it with an exclusive create, so of two processes resolving one approval only one gets it. It replaces combining `FileSessionStore`, `LocalStorageCheckpointStore` and `StorageServiceApprovalStore` by hand. See docs/sessions.md#choosing-a-store and docs/deployment.md.
- `web_fetch` built-in tool (N13a): `webFetchTool` / `createWebFetchTool(options)`, and `web-fetch` in spec files. It `GET`s one public web page and returns `{ url, finalUrl, status, contentType, content, truncated }`: HTML converted to text by a small built-in converter (scripts and styles dropped, link URLs kept, entities decoded), JSON and `text/*` as sent, a note for other types; a 4xx/5xx is returned with its body. Loopback, private and link-local destinations are refused on every hop with the connection pinned to the checked address; redirects (10), bytes read (2 MiB), characters returned (50 000) and time (30 s) are capped; `allowedHosts` / `blockedHosts` are checked before DNS and `allowPrivate` exempts named hosts. Node only. See docs/tools.md#built-in-tools.
- `src/security/privateAddress.ts` (internal): `isPrivateAddress()` and `pinnedLookup()`, shared by the credential broker, `http_request` and `web_fetch`. The credential broker uses it instead of its own list, so it also refuses `192.0.0.0/24`, `198.18.0.0/15`, NAT64 and 6to4 addresses.
- Slack and Discord: a pending `ask_question` survives a restart (M10a). Given durable stores for sessions, checkpoints and approvals, the next message in the Slack thread (or the next `/ask` in the Discord channel) after a restart is still the answer: the turn continues, the reply is posted, and the question, the answer and the reply are appended to the session transcript (before, the answer became a new turn and the question was orphaned). A message in a conversation that waits on a tool approval still does not decide it. New `ChannelContext.pendingQuestion(sessionKey)` for custom channels: the id of the `ask_question` that key's session waits on, also one asked before a restart (it binds the approval to the session, so the continuation is recorded); handed out once until that answer has run. A channel's answer and click continuations now run one at a time with the session's turns. Supporting additions: `Checkpoint.approvalKind`, `PendingTurn.approvalKind` (`session.pending()`) and `SessionAwaitingApprovalError.approvalKind` are `'question'` for a turn paused on an `ask_question`. Slack thread replies that answer a pending question no longer need a saved transcript first (a first turn paused on a question with checkpoints has none yet). See docs/channels.md.
Expand Down
2 changes: 2 additions & 0 deletions docs/api-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ How the pieces fit:
| `InMemoryApprovalStore` | Process-local `ApprovalStore`; the default store of `createAgent()` agents. |
| `StorageServiceApprovalStore`, `LocalStorageCheckpointStore` | File-backed approval and checkpoint stores over a `StorageService` (see [Approvals](./approvals.md), [Durable execution](./durable-execution.md)). |
| `SqliteStore` (from `/sqlite`) | Sessions, checkpoints and approvals in one SQLite file (see [Sessions](./sessions.md#choosing-a-store)). |
| `fileStore(dir)` | Sessions, checkpoints and approvals as plain JSON files under `dir` (see [Sessions](./sessions.md#choosing-a-store)). |
| `KVStore`, `KVCheckpointStore` (from `/kv`) | Stores on a Cloudflare Workers KV binding, for a hand-written Worker (see [Deployment](./deployment.md)). |
| `AgentStore`, `memoryStore()` | The `createAgent({ store })` option: `{ sessions?, checkpoints?, approvals? }`, and an in-memory one (see [Sessions](./sessions.md#choosing-a-store)). |
| `SessionAwaitingApprovalError` | Thrown by `execute()` when its `sessionId` is paused on an approval (see [Durable execution](./durable-execution.md)). |
| `SDKError`, `ERROR_CODES` | Base class of the SDK's errors: a stable `code`, a `hint` and a `docs` link (see [Errors](./errors.md)). |
Expand Down
36 changes: 29 additions & 7 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,33 @@ curl -N https://<your-worker>.workers.dev/chat \
-d '{ "sessionId": "alice", "input": "Hello" }'
```

`KVStore(kvBinding, { prefix?, ttl? })` (`src/deploy/kvStore.ts`) is the
`AgentStore` the generated Worker builds from the binding. It is not exported
from any entry point of the package, so importing it in a hand-written Worker
is not supported yet. Its keys, with an optional `prefix` before each:
`KVStore(kvBinding, { prefix?, ttl?, historyLimit? })` is the `AgentStore` the
generated Worker builds from the binding. A hand-written Worker imports it from
the `/kv` subpath, which has no `node:*` import anywhere in its graph, with the
binding typed as `KVBinding` (the `get`/`put`/`delete` part of Cloudflare's
`KVNamespace`, so `@cloudflare/workers-types` is not needed):

```ts
import { createAgent } from '@lousho/build-ai-agent';
import { KVStore, type KVBinding } from '@lousho/build-ai-agent/kv';

interface Env {
AGENT_KV: KVBinding;
}

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const agent = createAgent({ provider, store: new KVStore(env.AGENT_KV) });
const { sessionId, input } = (await request.json()) as { sessionId: string; input: string };
const { text } = await agent.session({ id: sessionId }).send(input);
return Response.json({ text });
},
};
```

`/kv` also exports `KVCheckpointStore` (checkpoints only) and
`CHECKPOINT_KV_BINDING` (`'AGENT_CHECKPOINTS'`, the binding name the generated
Worker reads). `KVStore`'s keys, with an optional `prefix` before each:

| Key | Value |
| --- | ----- |
Expand Down Expand Up @@ -306,9 +329,8 @@ one session at the same moment can overwrite each other's turn, since a KV
read-modify-write is not atomic.

The KV-backed stores (`KVStore`, `KVCheckpointStore` and `CHECKPOINT_KV_BINDING`,
in `src/deploy/kvStore.ts`, `src/deploy/kvCheckpointStore.ts` and
`src/deploy/checkpointBinding.ts`) have no `node:*` references anywhere in their
dependency graph. The Worker runs the spec as a `createAgent()` agent, whose
exported from `@lousho/build-ai-agent/kv`) have no `node:*` references anywhere
in their dependency graph. The Worker runs the spec as a `createAgent()` agent, whose
Node-only imports (project instructions, the file session store, guardrail
patches, MCP over stdio) the build points at a shim that fails when used
(`src/deploy/shims/node.worker.ts`). The built `dist/worker.js` bundle is then
Expand Down
3 changes: 2 additions & 1 deletion docs/durable-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ Which stores keep one:
| `memoryStore()` | yes | `memoryStore({ historyLimit })` |
| `SqliteStore` | yes (`checkpoint_history` table) | `new SqliteStore(path, { historyLimit })` |
| `LocalStorageCheckpointStore` | yes | `new LocalStorageCheckpointStore(storage, { historyLimit })` |
| `KVCheckpointStore` / `KVStore` (Cloudflare Workers KV) | yes (see below) | `new KVStore(kv, { historyLimit })` |
| `fileStore(dir)` (JSON files) | yes (`checkpoint-history/<id>.json`) | `fileStore(dir, { historyLimit })` |
| `KVCheckpointStore` / `KVStore` (Cloudflare Workers KV, from `@lousho/build-ai-agent/kv`) | yes (see below) | `new KVStore(kv, { historyLimit })` |
| Agent Forge's `FileCheckpointStore` | yes | `new FileCheckpointStore(dir, { historyLimit })` |
| a custom `CheckpointStore` | only if it implements `history()` | - |

Expand Down
37 changes: 17 additions & 20 deletions docs/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,33 +227,30 @@ implementation by where the process runs:
| Store | Sessions | Checkpoints | Approvals | Use it when |
| --- | --- | --- | --- | --- |
| In memory (`memoryStore()`) | `MemorySessionStore` | in memory | `InMemoryApprovalStore` | Tests, scripts, one process that never restarts |
| Files | `FileSessionStore(dir)` | `LocalStorageCheckpointStore` | `StorageServiceApprovalStore` | One machine, you want plain inspectable files |
| Files (`fileStore(dir)`) | `<dir>/sessions/<id>.json` | `<dir>/checkpoints/`, `<dir>/checkpoint-history/` | `<dir>/approvals/<id>.json` | 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)) |
| Cloudflare KV (`KVStore` from `@lousho/build-ai-agent/kv`) | `store.sessions` | `store.checkpoints` | `store.approvals` | Workers deployments (see [Deployment](deployment.md)) |

Each one is an `AgentStore` part: pass them together as
`createAgent({ store: { sessions, checkpoints, approvals } })`. `memoryStore()`
and `SqliteStore` are ready-made `AgentStore`s; for plain files, combine the
file stores:
`createAgent({ store: { sessions, checkpoints, approvals } })`. `memoryStore()`,
`fileStore(dir)`, `SqliteStore` and `KVStore` are ready-made `AgentStore`s. For
plain files, `fileStore(dir)` writes one JSON file per session, checkpoint and
pending approval under `dir`, each written to a temp file and renamed into
place, with no lock files:

```ts
import {
createAgent,
FileSessionStore,
LocalStorageCheckpointStore,
StorageServiceApprovalStore,
type AgentStore,
} from '@lousho/build-ai-agent';

// `storage` is a StorageService rooted where the files should go.
const store: AgentStore = {
sessions: new FileSessionStore('./.lousho/sessions'),
checkpoints: new LocalStorageCheckpointStore(storage),
approvals: new StorageServiceApprovalStore(storage),
};
const agent = createAgent({ provider, store });
import { createAgent, fileStore } from '@lousho/build-ai-agent';

const agent = createAgent({ provider, store: fileStore('./.lousho') });
// ./.lousho/sessions/user-42.json, ./.lousho/checkpoints/..., ./.lousho/approvals/...
await agent.session({ id: 'user-42' }).send('Hello');
```

Resolving an approval from `fileStore` is safe across processes (only one
caller gets the record); two processes writing one session at the same moment
are not coordinated, so the last write wins. For several processes sharing a
store, use `SqliteStore`.

Any object with the three methods of a part works there too: a Redis
`SessionStore`, or a KV-backed `CheckpointStore` on Cloudflare Workers (the
generated Worker uses `KVCheckpointStore`, see [Deployment](deployment.md)).
Expand Down
Loading
Loading