diff --git a/CHANGELOG.md b/CHANGELOG.md index e79321b0..ed590a10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ All notable changes to @lousho/build-ai-agent will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - 2026-09-28 +## [Unreleased] + +This section lists what is on `main` and not yet on npm. ### Security - `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. @@ -18,6 +20,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `lousho init --provider ollama` now scaffolds `ai@^7.0.0` with `ollama-ai-provider-v2@^4.0.0` and `zod@^4.0.0` (it was `ai@^4.3.19` with `ollama-ai-provider@^1.2.0` and zod 3). Existing projects are not touched; to stay on the old pairing keep `ai@^4.3.19` and `ollama-ai-provider@^1.2.0`. - A run paused inside a sub-agent compares the sub-agent with its current definition on resume, under the lead's `onAgentDrift` (M10c). Behaviour change: with `onAgentDrift: 'error'`, `agent.approvals.resolve()` (and `resumeAfterApproval()`) now also rejects with `LOUSHO_AGENT_DRIFT` when a paused sub-agent's instructions, model or tools changed; before, only tool and provider default-model changes of a sub-agent were seen, and always as a warning. This holds at any depth (a sub-agent of a sub-agent uses the lead's mode). With `'warn'` (still the default) the `agent.drift` event carries `subagent`. A rejected resume puts the lead's approval record and its `'awaiting-approval'` checkpoint back, so fixing the sub-agent and resolving again finishes the run; before, a sub-agent's missing-tool error became an error result of the `task` call and the run continued. Migration: none for `'warn'` / `'ignore'`; with `'error'`, resolve approvals paused before a sub-agent changed with the old definition, or use `'warn'`. See docs/durable-execution.md#resuming-with-a-changed-agent. - `lousho add` enforces the permission manifest at install (M7a). Behaviour change: an item whose code does not match its manifest is now refused with the new code `LOUSHO_REGISTRY_MANIFEST_MISMATCH`, and nothing is written. Before writing, every JavaScript / TypeScript file of the item is scanned (comments stripped) for `child_process` / `execa` / `shelljs` / `createShellTool` / `SubprocessSandbox` (needs `exec: true`), `fs` imports and `createFsTools` (needs `filesystem: "read"`), file-writing calls (needs `"write"`), `fetch(` and network modules (needs a non-empty `network`), `http(s)://` hosts in strings (each must match a `network` entry) and `process.env.NAME` reads (each must be in `env`); `eval(`, `new Function(`, a computed `process.env[...]` and a non-literal `import()` / `require()` are always refused. Each finding is listed as `:: ()`. `network` entries must now be host patterns and `env` entries variable names (`LOUSHO_REGISTRY_INVALID` otherwise). With `--yes`, an item asking for `exec: true`, `filesystem: "write"`, a `network` or an `env` is refused unless each is named in the new `--allow exec,fs-write,network,env` flag; the interactive prompt lists them, and the printed manifest marks them `[elevated: ...]`. After writing, `lousho add` records the item (type, registry, time, permissions, each file's sha256) in `/lousho-registry.json`. Migration: registry authors declare what their code uses; scripts that run `lousho add --yes` add `--allow` with the permissions they accept. It is a static check, not a sandbox; see docs/registry.md#permission-manifest. The example item in docs/registry.md now uses `input` (not `inputSchema`). + +### Added +- `session.history()` and `session.fork({ fromStep, id?, patch? })` on `agent.session()` sessions (N3a, #214). `history()` lists the steps of the committed transcript (one per model response: `step` numbered from 1 across the session, `turn`, `messageIndex`, `text`, and `toolCalls` with `args` and `result`). `fork()` saves the transcript up to and including a step (`0` keeps nothing), optionally with one tool result replaced (`patch.toolResult`), under a new id in the same store (default `-fork-`) and returns a session of the same agent and options (with the permission mode the session has at that moment); the original is not changed. The spend recorded for `limits` up to that point carries over. A turn waiting on an approval or a question, or an interrupted checkpointed turn, stays with the source session, so it is resolved once; workspace files are not rewound. Works on every shipped store (memory, `fileStore()`, `SqliteStore`, `KVStore`), with one contract suite run against each. New types `SessionHistoryStep`, `SessionForkOptions` and `SessionSpawner`; new error codes `LOUSHO_SESSION_STEP_NOT_FOUND`, `LOUSHO_SESSION_EXISTS` and `LOUSHO_SESSION_FORK_UNSUPPORTED`; `AgentSession`'s constructor takes an optional fourth argument, the function that creates its forks. See [Forking a session](docs/sessions.md#forking-a-session). +- Permission modes (N4, #215): `createAgent({ permissionMode })`, `send()` / `stream()` `{ permissionMode }`, `agent.session({ permissionMode })` and `session.setPermissionMode()` put an agent in `'plan'` (only read-only tools run; everything else is refused with `kind: 'denied'`, also when an `allow` rule matched, and a run that starts in plan mode gets one system-prompt paragraph saying so), `'acceptEdits'` (file edits that would ask run without asking) or `'dontAsk'` (a call that would ask is refused; nothing pauses). The modes are presets applied after hooks, permission rules, tool guardrails and `needsApproval`, so no mode turns a deny into a run. A function mode, and a session's mode, are read at every tool call, so a switch applies from the next call, also mid-turn and in a paused turn continued by `agent.approvals.resolve()`. Sub-agents run under the lead's mode unless it is `'default'`; a remote sub-agent is refused in plan mode. Audit: under a mode other than `'default'` every call is audited, `PermissionDecisionEntry.mode` says when the mode changed the outcome, and the new `onPermissionModeChange` records each switch. New: `PermissionMode` and `PermissionModeChange` types, `defineTool({ editsFiles })` (`metadata.editsFiles`, set on `write_file` and `edit_file`), `readOnlyHint: true` on `load_skill`, `recall_`, `agent_status` and `agent_await`. The mode is not saved in checkpoints. Example: `examples/plan-mode`. See [Permission modes](docs/permission-modes.md). +- Semantic recall (N15, #258): a memory slot can recall by meaning. New `EmbeddingProvider` (`{ id, embed(texts) }`) and `aiSdkEmbedder(model, { id?, maxBatch? })`, which wraps any AI SDK embedding model through `embedMany` from the installed `ai` (4, 6 or 7; `ai` is loaded on first use, no new dependency), and two providers that rank by cosine similarity: `inMemoryVectorMemory({ embedder, maxItems?, minScore? })` from the root and `sqliteVectorMemory(store, options)` from `@lousho/build-ai-agent/sqlite` (a new `memory_vectors` table, added to an existing database file on open; `memory_items` and `sqliteMemory()` are unchanged). With a query, `list()` returns the scope key's items by score (floor `minScore`, default 0.2; score in `metadata.score`); without one it lists the newest first. Items embedded by another embedder id are skipped when ranking until `provider.reindex(scopeKey?)` re-embeds them. A `MemoryProvider` may set `ranking: 'relevance'`, which makes the `recall_` tool say "by meaning, most relevant first". `hashEmbedder({ dimensions? })` from `@lousho/build-ai-agent/testing` is a deterministic offline embedder for tests. `describeMemoryProviderContract` takes `{ query: 'filter' | 'rank' }`. docs/memory.md has a new `## Semantic recall` section at the end. +- `teamsChannel({ appId, appPassword, tenantId?, name?, fetch?, approvers?, onError? })` (N11c, #253): an agent behind an Azure Bot in Microsoft Teams, mounted like the other channels (`POST /teams`). The inbound Bot Framework token is verified with the auth module (`oidc()`: RS256, the key set of the fixed Bot Framework OpenID metadata, issuer `https://api.botframework.com`, audience `appId`, 5-minute tolerance) before the body is parsed, and its `serviceurl` claim must equal the activity's `serviceUrl`; replies go only to that service URL (https). Personal chats always reach the agent, group chats and channels only on an `@mention` (stripped from the text); one session per conversation; Markdown replies split at 25,000 characters; tool approvals are Adaptive Cards with Approve and Deny buttons (`approvers` as on the other channels; the card updates to "Approved by ..." and names its conversation, so a copied or replayed reference decides nothing); `ask_question` is answered by the next message, also after a restart with durable stores. No `node:*` import. Telegram and GitHub now set the sender as the run's `principal` too (`authenticator: 'telegram'` with the user id, `'github'` with the login), as Slack and Discord do. See docs/channels.md#microsoft-teams. +- Cloudflare Worker target builds agent directories (config, instructions, TypeScript tools, skills) (M3b): `npx lousho build ./my-agent --target=cloudflare-worker`. The build reads the directory on Node and generates `agent.module.ts` (static imports of `tools/*` and of an `agent.ts` config; `instructions.md`, a JSON/YAML config and the skills embedded as JSON), and the Worker builds the agent with the rules of `resolveAgentDir()` (the config check and the tool-export rules now live in Node-free modules both use). The config's `model` must name a Worker provider, or `agent.ts` sets a `provider` instance. `subagents/`, `schedules/`, `channels/`, `memory/`, `projectInstructions` and an unsupported provider are rejected with `LOUSHO_DEPLOY_FAILED`, naming what to remove. In a Worker build, an agent file's `@lousho/build-ai-agent` import resolves to a Worker-safe subset (`defineTool`, `defineSkill`, the Worker providers, `fromAiSdk`, errors, ...); another name fails the build with the list. The leak check now names the file that imported a Node builtin (`"node:fs" imported by my-agent/tools/files.ts`) instead of failing with esbuild's "Could not resolve". Tools run in the Worker's isolate with its rights: see docs/cloudflare-workers.md#build-and-deploy. +- Workspace rewind (N7, #219): `createFsTools(fs, { checkpoints })` with a new `WorkspaceCheckpoints` backs up every file `write_file` and `edit_file` change (content and, where the provider has the new optional `FsProvider.getMode` / `chmod`, permission bits), grouped by session and turn. `checkpoints.list({ sessionId })` shows the changed paths per turn; `checkpoints.rewind(toTurn, { sessionId, dryRun, force })` restores every file changed in that turn or later, deletes files the agent created, and skips files changed since by something else (`'changed-since'`), files over `maxFileBytes` (`'too-large'`) and paths that are no longer files. Every path is checked before anything is written, and restores go through the provider, so `NodeWorkspace` keeps them inside its root. Retention: the newest `maxTurns` turns per session (default 20), the old content only for a turn's first write of a path, rewound turns dropped, `clear({ sessionId })`. Stores: `MemoryWorkspaceCheckpointStore` (default) and `FileWorkspaceCheckpointStore(dir)` (one JSON file per session, atomic writes). `NodeWorkspace` gains `getMode()` and `chmod()`. Without `checkpoints` the tools behave as before. See [Workspace tools](docs/workspace-tools.md#the-tools). +- Route auth and principals (N10a, #249): a new subpath, `@lousho/build-ai-agent/auth`, with `jwt()`, `oidc()`, `basic()`, `apiToken()`, `anonymous()` and `routeAuth()`. A route takes an ordered list of entries: the first one that returns a `Principal` accepts the request, `null` skips to the next, `AuthError(401 | 403)` stops; when every entry skips the answer is a generic `401` with one `WWW-Authenticate` header per challenge (`Bearer`, `Basic realm="..."`), and the body never says which check failed. JWTs are verified with Web Crypto only (no new dependency, no `node:` import, so the helpers run on Workers and edge routes): `alg: none`, algorithms outside the configured list, a `crit` header and algorithm confusion (an HS token checked against a public key) are refused; `exp` is required, `nbf` / `iat` honored, `iss` / `aud` checked, with a clock tolerance of 60 s (at most 300); JWKS key sets are cached for 10 minutes and refetched at most once per 30 s, and a key-set URL is never taken from a token. `oidc()` uses the configured issuer's discovery document and requires it to name the same issuer. `basic()` and `apiToken()` compare in constant time. `createRouteHandler({ auth })` and `createDeployedServer(agent, { auth })` take the list (a token string and a boolean function work as before); the node server appends `apiToken(LOUSHO_API_TOKEN)` when that variable is set; an agent directory's `auth.ts` is bundled by `lousho build --target=node-server` / `docker` and guards the built server (`resolveAgentDir()` returns it as `auth`, the manifest has `auth: true`). The accepted principal reaches the run: `send()` / `stream()` / session turns take `principal`, and `RunConfigContext` (the `model` / `instructions` / `tools` functions) and `MemoryScopeContext` (memory scopes) carry it; Slack and Discord channels set the sender as the principal. `Principal` is also exported from the root. Bad helper options throw the new error code `LOUSHO_AUTH_CONFIG_INVALID` when the helper is created. `createRouteHandler()` without `auth` logs one warning when `NODE_ENV` is `production`. Route auth does not check session ownership, the principal does not reach tools or approval policies yet (N10b), and the Cloudflare Worker target keeps the `LOUSHO_API_TOKEN` token only. See [Route auth and principals](docs/auth.md). +- OAuth token storage (N9a, #246): `AgentStore` gains a fourth optional part, `tokens` (`OAuthTokenStore`: `get` / `set` / `delete` / `list` per provider and credential owner, single-use pending sign-ins with `putPending` / `takePending`, and `getClient` / `setClient` for dynamically registered clients). Owners are `{ owner: 'app' }` or `{ owner: 'user', principalId, issuer? }`; `tokenStoreKey(provider, owner)` is the stable, percent-encoded record key (`|app`, `|user||`). `memoryStore()` keeps tokens in process memory; `fileStore(dir)`, `SqliteStore` and `KVStore` encrypt every record with AES-256-GCM (Web Crypto, a fresh 12-byte IV per write, the record key as additional authenticated data) under a 32-byte key the application supplies as `tokenKey` or `LOUSHO_TOKEN_KEY`; there is no default key. Several keys (newest first, or comma-separated in the variable) rotate: writes use the first, reads try each. New error codes `LOUSHO_TOKEN_KEY_MISSING` (first write, or a read of an existing record, without a key) and `LOUSHO_TOKEN_DECRYPT_FAILED` (wrong key or a changed record; names the provider, never the token). `list()` returns metadata only. `generateTokenKey()` makes a key. SQLite: schema migration 4 adds the `oauth_tokens` and `oauth_pending` tables, and `prune()` also deletes expired pending sign-ins (its result gains `oauthPending`). KV: keys under `oauth/`, pending sign-ins written with an `expirationTtl`; `KVBinding` gains an optional `list()` (used only by `tokens.list()`), and `KVListOptions` / `KVListResult` are exported from `/kv`. The generated Cloudflare Worker passes its `LOUSHO_TOKEN_KEY` secret to `KVStore`. Nothing signs in yet; the sign-in flow follows. New page [OAuth](docs/oauth.md); new `## OAuth` section at the end of docs/errors.md. +- `githubChannel({ webhookSecret, botName, token?, app?, botLogin?, name?, apiUrl?, fetch?, triggers?, approvers?, onError? })` (N11b): an agent that answers GitHub issue, pull-request and review comments. `@` in a comment starts a turn and the reply is a new comment in the same thread (a review thread gets a reply under it; text over 60,000 characters is split); one session per issue or pull request and one per review thread, and later comments in a thread with a session are follow-ups. `X-Hub-Signature-256` is verified over the raw body in constant time before the body is parsed (401 otherwise); the webhook is acknowledged at once. Comments by bots, by the channel's own account and every comment the channel posts (a hidden marker) are ignored, and so are edits, deletions and other events. Replies are posted with `token` (a personal access token, or a function) or as a GitHub App (`app: { appId, privateKey }`: a PKCS#1 or PKCS#8 key signs an RS256 JWT with Web Crypto, exchanged for an installation token that is cached per installation until 5 minutes before it expires; `src/channels/githubAppAuth.ts`, not exported). Approvals are comments: the prompt asks for `/approve ` or `/deny ` (first line only, notes below it), and by default only a commenter whose `author_association` is `OWNER`, `MEMBER` or `COLLABORATOR` may decide, read from the command comment itself; `approvers` (logins, or a function that sees the association in `user.roles`) overrides it, and `triggers` (logins, or a function) restricts who may start a turn or answer a question (default: everyone who can comment, so restrict it on a public repository). A comment is untrusted input to the agent; see the security note in docs/channels.md. An `ask_question` is answered by the next comment in the thread and survives a restart given durable stores. Errors name the call and the HTTP status, never the token. The `LOUSHO_CHANNEL_INVALID` hint, docs/agent-directories.md and docs/errors.md now list `githubChannel()`. New section `## GitHub` at the end of docs/channels.md. +- Agent Forge keeps the traces of its runs (M5b, #227): every run is written with `fileTraceExporter()` to `.lousho/agents//traces` (the files `lousho traces` reads), and the Trace tab gets a list of the agent's past runs (time, duration, model calls, tokens, cost, status) with a "Live" entry while a run is active; choosing one opens its spans in the waterfall, with span kind and error status. New server routes `GET /agents/:id/traces?limit=N` and `GET /agents/:id/traces/:traceId`; the server reads only inside the agent's trace folder. The live `span` WebSocket messages now carry `kind` and `status` (optional fields of `SpanEvent`). See [Agent Forge](docs/agent-forge.md). +- A background sub-agent that needs approval pauses the lead when the lead awaits it (`agent_await`), and resumes with `approvals.resolve()` / `resume()` (M4, #225). The lead's result is `awaiting-approval` with the child's call (`subagentPath: ['']`) in `agent.approvals.list()`; the decision runs or rejects the child's call, the child finishes, and the continued lead gets its answer as the `agent_await` result. A child that pauses again, or several awaited tasks that are all paused, pause the lead one approval at a time. The child's usage is added to the lead's, and its conversation is saved under its `taskId`. The paused tasks are kept on the lead's approval record (`SubagentSuspension.background`, plain JSON), never in the tool arguments the model sees. Without `agent_await` nothing changes: the task reports `awaiting-approval` and is not resumable. The `task` and `agent_status` tool descriptions tell the model that a task awaiting approval continues only when it calls `agent_await` on it. Docs: docs/sub-agents.md ("When the lead run ends" and "Continuing a task"). +- Guardrail starter set (N5a): `piiGuardrail()` (email, phone, credit card with a Luhn check, IBAN with its mod-97 check, US SSN, IPv4; blocks, or rewrites each match to `[]`), `secretsGuardrail()` (private keys; Anthropic, OpenRouter, OpenAI, AWS, GitHub, Slack, Google and Stripe live keys; JWTs; `Authorization: Bearer` tokens; `extraPatterns`; rewrites to `[secret]` by default), `promptInjectionGuardrail()` (phrase, role-marker and Unicode-tag heuristics, plus an optional model check that must answer `SAFE` or `INJECTION: `) and `moderationGuardrail({ model })` (one model call that answers `NONE` or the categories that apply: `hate`, `harassment`, `self-harm`, `sexual`, `sexual-minors`, `violence`, `illicit`). The PII, secret and injection checks are heuristic pattern checks: they catch common cases and miss others, and are not a security boundary. Model-backed checks fail closed on a reply they cannot read. A guardrail result can now carry `info?: GuardrailTripInfo` (`pii`, `secret`, `prompt-injection`, `moderation` or `custom`), copied to `result.guardrail`, `guardrail.tripped` and `guardrail.rewrote`; it holds types, labels and offsets, never the matched text. Agent spec files can name them in `policy.guardrails` as `pii`, `secrets`, `prompt-injection` and `moderation`. New exported types `GuardrailTripInfo`, `PiiType`, `ModerationCategory`. Docs: [Guardrails](docs/guardrails.md#input-and-output-guardrails), [Configuration](docs/configuration.md#policy-policy), [Stream events](docs/stream-events.md). +- `telegramChannel({ botToken, secretToken, botUsername?, name?, fetch?, approvers?, onError? })` (N11a): an agent behind a Telegram bot, mounted like the Slack and Discord channels (`POST &channels&telegram`). The webhook secret token header (`X-Telegram-Bot-Api-Secret-Token`) is compared in constant time (401 otherwise); updates are acknowledged with `200` and the turn runs after. A private chat is one session per chat; in a group only `&ask`, `&ask@`, an `@` mention or a reply to the bot wakes it, and a forum topic is its own session. Replies are plain text, split at 4096 characters. Tool approvals are an inline keyboard (Approve & Deny) that only `approvers` (default: the user who started the turn) can tap; the tap names its chat, so it works after a restart. An `ask_question` is sent with `force_reply`; a pending one survives a restart given durable stores, like Slack and Discord. The bot token is never put in an error message or log line. `src&channels&channelSupport.ts` gains the internal `secretsEqual()` (constant-time compare with Web Crypto). The `LOUSHO_CHANNEL_INVALID` hint, docs&agent-directories.md and docs&errors.md now list `discordChannel()` and `telegramChannel()`. New section `## Telegram` at the end of docs&channels.md (attachments are not read yet).&r +- `todo.updated` stream event and `useTodos()` (React, Vue) / `loushoTodos()` (Svelte) (N12): every successful `todo_write` emits `todo.updated { todos, counts, toolCallId }` right after its `tool.done` (sub-agents too, with `subagent`). The shared UI reducer keeps the list in a new `AgentUIState.todos` (carried across turns, cleared by `reset()`), `useLoushoAgent()` / `loushoAgent()` expose it, `useTodos(agent)` / `loushoTodos(agent)` add counts, the current item and progress (`todoView()` computes the same view framework-free), and `toUIMessageStream()` sends a `data-lousho-todos` part for the AI SDK UI. See [Todos](docs/react.md#todos). +- Cloudflare Worker target: the `openrouter` provider and the `http` tool (with a required `LOUSHO_HTTP_ALLOW` host allowlist) (M3a). `openrouter` reads its key from the `OPENROUTER_API_KEY` binding and is `@ai-sdk/openai` pointed at OpenRouter, so its bundle passes the `node:` leak check like `openai`. The Worker's `http` is `http_request` with the Node tool's input, over the platform `fetch()`: since a Worker cannot see or pin where a host name resolves, it reaches only the host names (`api.github.com`) and `*.` wildcards (subdomains only) listed in the comma-separated `LOUSHO_HTTP_ALLOW` binding, refuses IP-address hosts even when listed and any scheme but `http:`/`https:`, checks every redirect hop the same way, and refuses every request when the binding is unset or empty. An invalid entry fails every request with an error naming it. `wrangler.toml` gets a commented `[vars]` `LOUSHO_HTTP_ALLOW` line when the spec lists `http`. `ollama` and `web-fetch` stay unsupported on Workers. Internally, the request, redirect and response logic of `http_request` moved to the Node-free `src/tools/built-in/httpCore.ts`, shared by both tools; the Node tool's behaviour is unchanged. See docs/cloudflare-workers.md#providers-and-tools. +- `fromAiSdk(model)`: any AI SDK `LanguageModel` (Google, Bedrock, Azure, Mistral, Gateway, ...) as `createAgent({ provider })`. Options `name` (default: the model's `provider` field), `fileMediaTypes`, `replaysReasoning` and `maxRetries` (default 0); exported with `FromAiSdkOptions` from the package root. Throws `LOUSHO_CONFIG_INVALID` for a model id string, for a model built for another `ai` major than the installed one, and for a call with another model id than the wrapped one. See docs/providers.md#any-ai-sdk-model-fromaisdk. +- 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/.json`, `checkpoints/.json`, `checkpoint-history/.json` and `approvals/.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. +- Local traces and `lousho traces` (M5a): `createAgent({ exporter, captureContent })` traces every run of the agent (`send()`, `stream()`, session turns, `agent.resume()` and runs continued by `agent.approvals.resolve()`; sub-agents join the lead's trace). The new subpath `@lousho/build-ai-agent/traces` exports `fileTraceExporter({ dir })`, which writes each run as JSON Lines to `//.jsonl` (default `.lousho/traces`; one finished span per line; a write error warns once and never fails the run), and `listTraces()` / `readTrace()` to read them. `npx lousho traces [--dir D] [--limit N] [--json]` lists recent runs with duration, model and tool calls, tokens and cost; `npx lousho traces [--json] [--content]` prints one run as a span tree with duration bars. Projects made by `lousho init` now ignore `.lousho/`. See docs/observability.md#local-traces. +- `openApiTools(document, options)` (N8): an OpenAPI 3.0 / 3.1 document (object, JSON or YAML string, or https URL) becomes one typed tool per operation, exported from the root and `./tools`. Names come from `operationId` (or `_`); the input has path, query, header and cookie parameters plus an `application/json` `body`, with `$ref`s resolved; every HTTP response is returned as `{ status, statusText, body }`. GET, HEAD and OPTIONS run and other methods ask for approval by default (`approval`), with read-only / destructive annotations. Options: `baseUrl`, `include`, `exclude`, `prefix`, `headers`, `bearerToken`, `providedArguments` (removed from the model's schema), `timeoutMs`, `maxResponseChars`, `fetch`. The base URL must be https (http only for loopback), credentials never reach events or the transcript, and redirects are followed only on the base origin. Swagger 2.0, multipart bodies and remote `$ref`s are refused. `jsonSchemaToZod(schema, root?)` takes an optional root for `$ref` resolution. New page docs/openapi-tools.md. +- Built-in OpenAI, Anthropic and OpenRouter providers send PDF file parts on `ai` 6 and 7 (Anthropic also `text/plain`); `ai` 4 and Ollama keep the text note. A provider subclass opts more media types in with `fileMediaTypes()` (`acceptsFileParts = true` still sends every type). The text-note warning is now once per provider and media type and names the type and the `ai` major. Added `npm run test:live` (`vitest.live.config.ts`) for `*.live.test.ts` files, which the default suite excludes. See docs/providers.md#multimodal-input. +- `telegramChannel({ botToken, secretToken, botUsername?, name?, fetch?, approvers?, onError? })` (N11a): an agent behind a Telegram bot, mounted like the Slack and Discord channels (`POST &channels&telegram`). The webhook secret token header (`X-Telegram-Bot-Api-Secret-Token`) is compared in constant time (401 otherwise); updates are acknowledged with `200` and the turn runs after. A private chat is one session per chat; in a group only `&ask`, `&ask@`, an `@` mention or a reply to the bot wakes it, and a forum topic is its own session. Replies are plain text, split at 4096 characters. Tool approvals are an inline keyboard (Approve & Deny) that only `approvers` (default: the user who started the turn) can tap; the tap names its chat, so it works after a restart. An `ask_question` is sent with `force_reply`; a pending one survives a restart given durable stores, like Slack and Discord. The bot token is never put in an error message or log line. `src&channels&channelSupport.ts` gains the internal `secretsEqual()` (constant-time compare with Web Crypto). The `LOUSHO_CHANNEL_INVALID` hint, docs&agent-directories.md and docs&errors.md now list `discordChannel()` and `telegramChannel()`. New section `## Telegram` at the end of docs&channels.md (attachments are not read yet).&r + +### Fixed +- Parallel `task` calls now get their `taskId`s, and background tasks start, in call order: allocating an id awaited a SHA-256 digest whose completion order is not fixed, so with `maxConcurrent: 1` the second task could take the slot first (#296, the flaky "queues tasks beyond maxConcurrent" test). +- The `cloudflare-worker` build's Node-builtin leak check now accepts the runtime-guarded `loadNodeModule("node:module" | "node:dns")` probes that `@ai-sdk/provider-utils` 4 (installed with `ai` 6) uses, as it already did for `ai` 7's `loadBuiltinModule`; without it a Worker bundle built on `ai` 6 failed with "Node builtins leaked" (found by the new `ai6` matrix entries). +- `isAgentEvent()` is now exhaustive (the runtime type list is a `Record`, so the compiler rejects a missing type). It had no `agent.drift`, so remote UI clients (`parseEventStream()`, the React, Vue and Svelte bindings) silently dropped that event. +- A remote sub-agent's token usage is added to the lead's `result.usage` (M10b). It was dropped, so budgets and cost reports undercounted delegation to a `remoteAgent()`. The lead adds what the remote agent reports on its `run.done` event to its totals, to `usage.delegated` and to `byModel['remote:']`, which keeps the remote's own `costUsd` (the lead's `costUsd` is `undefined` when the remote sends none). A remote run that pauses for an approval adds what it spent up to the pause, and the continuation adds only what it spent after it; background remote tasks roll up the same way. A deployment that sends no usage adds nothing. `RemoteRunOptions` has a new optional `onUsage(usage)` callback; `RemoteSubagent.run()` still returns `Promise`, so hand-written ones compile unchanged. See docs/sub-agents.md#remote-sub-agents. +- `lousho studio` finds the Agent Forge build that ships inside the installed package when it is run from a project that does not have `apps/agent-forge` (it used to fail with "could not find apps/agent-forge" everywhere except the SDK repo). A project's own `apps/agent-forge` still wins, and `.lousho/` is still created in the directory you ran it from. `--dev` from an installed package still needs the TypeScript source and says so. +- Documentation corrections: install commands, CLI command lists, optional peers, durable execution and compaction descriptions, the Status section, reasoning and file-part notes in the providers guide, and the `KVStore` note in the deployment guide now match the code. Ticket ids are gone from user-facing prose. + +### Tests +- `pack-smoke` allows a 16 MiB unpacked tarball (was 14 MiB): `main` was 45 KB under the old cap and permission modes (N4) went over it; see #316 for shrinking the package instead. +- Sandbox egress and the credential broker are tested against a real Docker Engine (M6): a new CI workflow, "Docker Engine" (`.github/workflows/docker.yml`, on pull requests touching the sandbox, broker or docker deploy adapter, on demand and weekly), runs `npm run test:docker` on GitHub's `ubuntu-latest` runner (rootful Engine 28.0.4). `src/security/sandboxEgress.docker.test.ts` checks nine cases of `SubprocessSandbox({ network: { allow }, broker })` with `curlimages/curl`: an allowed HTTPS host answers through the broker, another host gets a 403, bypassing the proxy and raw IPs have no route, outside names do not resolve, the broker injects a credential the container never sees, a container outside the internal network cannot use the gateway listener, an aborted run and `close()` leave no container or network, and a reused non-internal network is refused. The docker deploy image test (`/health` and `/chat`) moved to `src/deploy/adapters/docker.docker.test.ts` and runs in the same job; it was skipped in CI before and passes there. No product defect was found. `*.docker.test.ts` files are excluded from `npm test` and `npm run test:coverage`; they skip without a daemon unless `LOUSHO_DOCKER_TESTS=1` is set (the workflow sets it), which makes a missing daemon fail. docs/workspace-tools.md says what the job checks; CONTRIBUTING.md mentions `npm run test:docker`. + +### Docs +- New page, [Permission modes](docs/permission-modes.md) (N4): the modes and their table, which tools are read-only, the `editsFiles` marker, the order of evaluation, switching, the audit log, sub-agents and resume. [Approvals](docs/approvals.md) "Permission policies" links to it; [Tools](docs/tools.md), [Stream events](docs/stream-events.md) and [Sub-agents](docs/sub-agents.md) ("What a sub-agent inherits") gained a row each. +- New page, [Route auth and principals](docs/auth.md): the ordered list and its three outcomes, each helper, 401 / 403 / `WWW-Authenticate`, which helpers run where (Node, Workers, edge), `createRouteHandler`, the node server and `auth.ts`, reading the principal in the run, and security notes (no session ownership check; `basic()` only over HTTPS). docs/errors.md gains a `## Auth` section at the end (`LOUSHO_AUTH_CONFIG_INVALID`). One-paragraph links from deployment.md (`### Auth`), nextjs.md (`## Routes`), memory.md (`## Scopes`), agent-directories.md (layout and deploy), api-overview.md and a "Route auth" row in the cloudflare-workers.md limits table; no heading of an existing page changed. +- New page, [Cloudflare Workers](docs/cloudflare-workers.md): a table of what the Worker target supports and what it does not comes first, then build and deploy, bindings, sessions, checkpoints and approvals in KV, scheduled runs, consistency, and bundle size and Node builtins. The Worker sections moved out of docs/deployment.md unchanged (`Bindings, sessions and the API on Workers`, `Cron triggers and handleScheduled` and `Durable execution (pause/resume) on Workers` are gone from it); deployment.md keeps a short `cloudflare-worker` summary that links to the page. Links to `deployment.md#cron-triggers-and-handlescheduled` moved to `cloudflare-workers.md#scheduled-runs`. +- Two dead heading links fixed: docs/executor-api.md now links to `tools.md#advanced-toolregistry`, and docs/troubleshooting.md links to `mcp.md#use-mcp-servers-in-an-agent` (the configuration anchor moved with the MCP section). No other link in `docs/` or the README points to a missing heading. docs/approvals.md lists `fileStore(dir)` among the approval stores. +- Long pages split; text moved unchanged. New pages: [Runs](docs/runs.md), [Models and cost](docs/models-and-cost.md), [Stream events](docs/stream-events.md), [Queued input and steering](docs/queue-and-steer.md). Moved anchors (old to new): `api-overview.md#finish-reasons`, `#cancellation` and `#parallel-tool-calls` to `runs.md` (same anchors); `api-overview.md#models-tokens-and-cost` to `models-and-cost.md#models-and-the-price-table`; `api-overview.md#usage-and-cost-of-a-run` to `models-and-cost.md#usage-and-cost-of-a-run`; `api-overview.md#flow-expressions` to `flows.md#flow-expressions`; `api-overview.md#todo-tools` to `tools.md#todo-tools`; `api-overview.md#tool-errors` to `tools.md#errors`; `api-overview.md#context-compaction` to `compaction.md`; `streaming.md#event-schema-version-1`, `#ordering-guarantees`, `#typescript` and `#versioning` to `stream-events.md` (same anchors); `streaming.md#queued-input` and `#steering` to `queue-and-steer.md` (same anchors); the `AgentExecutor.execute()` options table of `configuration.md` to `executor-api.md#options-of-agentexecutorexecute`. The error codes page gained a "Find a code by area" table; its headings are unchanged. +- The API overview, providers, structured output, tools, tracing, guardrails and testing guides teach `createAgent()` first: usage and multimodal examples use it, the testing tool example uses `defineTool()`, and every executor, `AgentBuilder` or `ToolRegistry` snippet sits under an "Advanced" heading that links to [the executor API](docs/executor-api.md). They say that `createAgent()` does not take `exporter`, `captureContent` or `sandbox` yet. +- New page, [Triggers](docs/triggers.md): the `@lousho/build-ai-agent/triggers` adapters, a table that says when to use triggers, channels or schedules, the `TriggerAdapter` interface (with a custom adapter and `TriggerRegistry`), and the webhook, Slack and cron sections that moved from the API overview, unchanged. The API overview now points to it; links to `api-overview.md#triggers`, `#webhook-authentication` and `#slack-request-signatures` moved to `triggers.md`. The missing-`auth` warning of `WebhookTriggerAdapter` and the missing-`signingSecret` warning of `SlackTriggerAdapter` now link to `docs/triggers.md`. +- New page docs/migrating-to-create-agent.md: from `AgentBuilder`, `AgentExecutor`, `ToolRegistry` and `resumeAfterApproval()` to `createAgent()`, with a before-and-after example, a mapping table, the `ExecuteOptions` that `createAgent()` does not take yet (with alternatives), the behavior differences and a step-by-step checklist. Linked from docs/executor-api.md and the README docs table. +- New page, [Hooks](docs/hooks.md): the four hook points, the outcomes of a `preToolCall` / `postToolCall` hook, four type-checked recipes (deny a tool, add a default argument, redact a result, inject context), ordering, and `HookRegistry`. The `HookRegistry` bullet and the `### Hook outcomes` section moved there from the API overview, which now points to it; links to `api-overview.md#hook-outcomes` moved to `hooks.md#hook-outcomes`. +- Approvals, sessions, streaming, sub-agents, compaction and skills guides show `createAgent()` first. Executor snippets (`AgentExecutor`, `resumeAfterApproval`, `streamResumeAfterApproval`, `InputQueue`, `HookRegistry`) moved under headings named "Advanced: the executor API" (links to docs/executor-api.md); `approvals` and `streaming` renamed a section to that name. `createAgent({ hooks: [createCompactionHook(...)] })` is the compaction example. Removed a statement from the sub-agents guide that `createAgent()` takes no `approvalStore` (it does, and `agent.approvals` resolves sub-agent pauses). +- Stale statements removed: ticket ids in the Agent Forge page, the "removed next minor" promise on `AgentType` in the API overview, and the installation note that read as if every user needs Node 22.19 for the `http` tool (it comes from the `undici@8` dependency, which is loaded only for `validateSSL: false` requests). The `ProposedAction` doc comment no longer refers to an unlanded ticket. +- New page docs/mcp.md, "MCP (Model Context Protocol)": using MCP servers in an agent, `connectMcp()`, approval for MCP tools, `loadMcpTools()`, serving an agent with `serveMcp()` and `lousho mcp`, and a Limits list. These sections moved out of docs/configuration.md, which keeps the `mcpServers` spec field and links to the new page; links from approvals, CLI, tools, API overview and the README now point to `mcp.md`. +- `docs/durable-execution.md` and `docs/workspace-tools.md` lead with `createAgent()` (`send()` with a `sessionId`, `agent.resume()`, `agent.approvals.resolve()`, `agent.fork()`) instead of `AgentExecutor`. The executor snippets (`execute()` with a `checkpointStore`, `resumeAfterApproval()`, `AgentExecutor.fork()`, the `ToolRegistry` setup) moved under "Advanced: the executor API". In durable-execution the heading "What happens when you call `execute()` again with the same `sessionId`" is now "What happens when you send again with the same `sessionId`". +- New page docs/troubleshooting.md: start from the symptom (setup, runs that end without an answer, tools, providers, sandbox and MCP), with cause, fix and a link for each. README docs table, `docs/installation.md` and `docs/errors.md` link to it. +- New guide, [Build a coding agent](docs/build-a-coding-agent.md): a terminal coding agent in six steps (workspace tools confined to the project, approval before writes and risky commands, a streamed UI that handles the approval pause, a session for follow-up questions, offline tests with `MemoryWorkspace` and `mockModel`), with an offline test of the same agent (`docs/buildACodingAgent.test.ts`; its replay of a recorded real-model run is skipped until the cassette `docs/__cassettes__/build-a-coding-agent.json` is recorded). +- The Quick Start is rewritten around `createAgent()`: hello world, a tool, streaming, a session, an approval, an offline test with `mockModel`, a custom provider and spec files, each runnable with no API key (the first excepted). The `AgentBuilder` + `AgentExecutor` section moved to a new page, docs/executor-api.md (with the `ToolRegistry` note); the Quick Start no longer shows either API. +- `create-lousho-agent` has a README (its npm page was empty) and `license`, `homepage` and `repository` fields; they appear on npm with its next release. `lousho init --help` no longer says `--sdk-path` is needed until the package is on npm. + +## [1.0.0-alpha.8] - 2026-10-02 + +### Changed - No `any` left in the SDK's types (LOU-D16): `npm run lint` now fails on any ESLint warning, and `no-explicit-any`, `no-unused-vars` and `ban-ts-comment` are errors. Runtime behavior is unchanged; some exported types are stricter, which can be a compile error where code read an `any` value without checking it. What changed, and how to migrate: - Values the SDK cannot know are `unknown` (narrow them, or cast to the shape you expect): `FlowExecutionResult.output` and `.variables`, `FlowExecutionContext.variables` / `session` / `memory`, `GenerateResult.rawResponse`, `StreamChunk.toolResult.result`, extra keys of `LLMProviderConfig`, `AgentConfig.expectedResult` / `events` / `metadata`, `AgentExecutionResult.result`, `ResultData.result`, `SessionData.data`, `JiraTicket.customFields`, the extra keys of `formatZodError()`'s result, `ToolConfiguration.options`, `ToolSetting.options`, `FlowToolSetting.options`, `ToolNode.toolOptions`, `UIComponentNode.componentProps`, `FlowChunkEvent.issues` / `input` / `toolResults[].args` / `componentProps`, and `ToolDefinition.function.parameters`. - `FlowExecutionEvent` is a union discriminated on `type`, with `data` typed per event type (new `FlowExecutionEventOf` and `FlowExecutionEventDataMap`): check `event.type` before reading `event.data` (`if (event.type === 'tool-call') event.data?.tool`). @@ -59,29 +122,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - One session-API client behind `remoteAgent()` and `remoteTarget()` (LOU-D53): both now share an internal client (`src/server/sessionClient.ts`) for `POST /chat`, the SSE read, bearer-token scrubbing and the error mapping, and use one pair of codes, `LOUSHO_REMOTE_REQUEST_FAILED` and `LOUSHO_REMOTE_UNAUTHORIZED`. The unreleased `LOUSHO_REMOTE_AGENT_FAILED` (LOU-Y7) is removed: a `remoteAgent()` failure now carries `LOUSHO_REMOTE_REQUEST_FAILED` (network, non-2xx, truncated stream, or a remote run that ends in an error, with "the remote run ended in an error" in the message) or `LOUSHO_REMOTE_UNAUTHORIZED` (401). No option or behavior of either function changed. ### Added -- `session.history()` and `session.fork({ fromStep, id?, patch? })` on `agent.session()` sessions (N3a, #214). `history()` lists the steps of the committed transcript (one per model response: `step` numbered from 1 across the session, `turn`, `messageIndex`, `text`, and `toolCalls` with `args` and `result`). `fork()` saves the transcript up to and including a step (`0` keeps nothing), optionally with one tool result replaced (`patch.toolResult`), under a new id in the same store (default `-fork-`) and returns a session of the same agent and options (with the permission mode the session has at that moment); the original is not changed. The spend recorded for `limits` up to that point carries over. A turn waiting on an approval or a question, or an interrupted checkpointed turn, stays with the source session, so it is resolved once; workspace files are not rewound. Works on every shipped store (memory, `fileStore()`, `SqliteStore`, `KVStore`), with one contract suite run against each. New types `SessionHistoryStep`, `SessionForkOptions` and `SessionSpawner`; new error codes `LOUSHO_SESSION_STEP_NOT_FOUND`, `LOUSHO_SESSION_EXISTS` and `LOUSHO_SESSION_FORK_UNSUPPORTED`; `AgentSession`'s constructor takes an optional fourth argument, the function that creates its forks. See [Forking a session](docs/sessions.md#forking-a-session). -- Permission modes (N4, #215): `createAgent({ permissionMode })`, `send()` / `stream()` `{ permissionMode }`, `agent.session({ permissionMode })` and `session.setPermissionMode()` put an agent in `'plan'` (only read-only tools run; everything else is refused with `kind: 'denied'`, also when an `allow` rule matched, and a run that starts in plan mode gets one system-prompt paragraph saying so), `'acceptEdits'` (file edits that would ask run without asking) or `'dontAsk'` (a call that would ask is refused; nothing pauses). The modes are presets applied after hooks, permission rules, tool guardrails and `needsApproval`, so no mode turns a deny into a run. A function mode, and a session's mode, are read at every tool call, so a switch applies from the next call, also mid-turn and in a paused turn continued by `agent.approvals.resolve()`. Sub-agents run under the lead's mode unless it is `'default'`; a remote sub-agent is refused in plan mode. Audit: under a mode other than `'default'` every call is audited, `PermissionDecisionEntry.mode` says when the mode changed the outcome, and the new `onPermissionModeChange` records each switch. New: `PermissionMode` and `PermissionModeChange` types, `defineTool({ editsFiles })` (`metadata.editsFiles`, set on `write_file` and `edit_file`), `readOnlyHint: true` on `load_skill`, `recall_`, `agent_status` and `agent_await`. The mode is not saved in checkpoints. Example: `examples/plan-mode`. See [Permission modes](docs/permission-modes.md). -- Semantic recall (N15, #258): a memory slot can recall by meaning. New `EmbeddingProvider` (`{ id, embed(texts) }`) and `aiSdkEmbedder(model, { id?, maxBatch? })`, which wraps any AI SDK embedding model through `embedMany` from the installed `ai` (4, 6 or 7; `ai` is loaded on first use, no new dependency), and two providers that rank by cosine similarity: `inMemoryVectorMemory({ embedder, maxItems?, minScore? })` from the root and `sqliteVectorMemory(store, options)` from `@lousho/build-ai-agent/sqlite` (a new `memory_vectors` table, added to an existing database file on open; `memory_items` and `sqliteMemory()` are unchanged). With a query, `list()` returns the scope key's items by score (floor `minScore`, default 0.2; score in `metadata.score`); without one it lists the newest first. Items embedded by another embedder id are skipped when ranking until `provider.reindex(scopeKey?)` re-embeds them. A `MemoryProvider` may set `ranking: 'relevance'`, which makes the `recall_` tool say "by meaning, most relevant first". `hashEmbedder({ dimensions? })` from `@lousho/build-ai-agent/testing` is a deterministic offline embedder for tests. `describeMemoryProviderContract` takes `{ query: 'filter' | 'rank' }`. docs/memory.md has a new `## Semantic recall` section at the end. -- `teamsChannel({ appId, appPassword, tenantId?, name?, fetch?, approvers?, onError? })` (N11c, #253): an agent behind an Azure Bot in Microsoft Teams, mounted like the other channels (`POST /teams`). The inbound Bot Framework token is verified with the auth module (`oidc()`: RS256, the key set of the fixed Bot Framework OpenID metadata, issuer `https://api.botframework.com`, audience `appId`, 5-minute tolerance) before the body is parsed, and its `serviceurl` claim must equal the activity's `serviceUrl`; replies go only to that service URL (https). Personal chats always reach the agent, group chats and channels only on an `@mention` (stripped from the text); one session per conversation; Markdown replies split at 25,000 characters; tool approvals are Adaptive Cards with Approve and Deny buttons (`approvers` as on the other channels; the card updates to "Approved by ..." and names its conversation, so a copied or replayed reference decides nothing); `ask_question` is answered by the next message, also after a restart with durable stores. No `node:*` import. Telegram and GitHub now set the sender as the run's `principal` too (`authenticator: 'telegram'` with the user id, `'github'` with the login), as Slack and Discord do. See docs/channels.md#microsoft-teams. -- Cloudflare Worker target builds agent directories (config, instructions, TypeScript tools, skills) (M3b): `npx lousho build ./my-agent --target=cloudflare-worker`. The build reads the directory on Node and generates `agent.module.ts` (static imports of `tools/*` and of an `agent.ts` config; `instructions.md`, a JSON/YAML config and the skills embedded as JSON), and the Worker builds the agent with the rules of `resolveAgentDir()` (the config check and the tool-export rules now live in Node-free modules both use). The config's `model` must name a Worker provider, or `agent.ts` sets a `provider` instance. `subagents/`, `schedules/`, `channels/`, `memory/`, `projectInstructions` and an unsupported provider are rejected with `LOUSHO_DEPLOY_FAILED`, naming what to remove. In a Worker build, an agent file's `@lousho/build-ai-agent` import resolves to a Worker-safe subset (`defineTool`, `defineSkill`, the Worker providers, `fromAiSdk`, errors, ...); another name fails the build with the list. The leak check now names the file that imported a Node builtin (`"node:fs" imported by my-agent/tools/files.ts`) instead of failing with esbuild's "Could not resolve". Tools run in the Worker's isolate with its rights: see docs/cloudflare-workers.md#build-and-deploy. -- Workspace rewind (N7, #219): `createFsTools(fs, { checkpoints })` with a new `WorkspaceCheckpoints` backs up every file `write_file` and `edit_file` change (content and, where the provider has the new optional `FsProvider.getMode` / `chmod`, permission bits), grouped by session and turn. `checkpoints.list({ sessionId })` shows the changed paths per turn; `checkpoints.rewind(toTurn, { sessionId, dryRun, force })` restores every file changed in that turn or later, deletes files the agent created, and skips files changed since by something else (`'changed-since'`), files over `maxFileBytes` (`'too-large'`) and paths that are no longer files. Every path is checked before anything is written, and restores go through the provider, so `NodeWorkspace` keeps them inside its root. Retention: the newest `maxTurns` turns per session (default 20), the old content only for a turn's first write of a path, rewound turns dropped, `clear({ sessionId })`. Stores: `MemoryWorkspaceCheckpointStore` (default) and `FileWorkspaceCheckpointStore(dir)` (one JSON file per session, atomic writes). `NodeWorkspace` gains `getMode()` and `chmod()`. Without `checkpoints` the tools behave as before. See [Workspace tools](docs/workspace-tools.md#the-tools). -- Route auth and principals (N10a, #249): a new subpath, `@lousho/build-ai-agent/auth`, with `jwt()`, `oidc()`, `basic()`, `apiToken()`, `anonymous()` and `routeAuth()`. A route takes an ordered list of entries: the first one that returns a `Principal` accepts the request, `null` skips to the next, `AuthError(401 | 403)` stops; when every entry skips the answer is a generic `401` with one `WWW-Authenticate` header per challenge (`Bearer`, `Basic realm="..."`), and the body never says which check failed. JWTs are verified with Web Crypto only (no new dependency, no `node:` import, so the helpers run on Workers and edge routes): `alg: none`, algorithms outside the configured list, a `crit` header and algorithm confusion (an HS token checked against a public key) are refused; `exp` is required, `nbf` / `iat` honored, `iss` / `aud` checked, with a clock tolerance of 60 s (at most 300); JWKS key sets are cached for 10 minutes and refetched at most once per 30 s, and a key-set URL is never taken from a token. `oidc()` uses the configured issuer's discovery document and requires it to name the same issuer. `basic()` and `apiToken()` compare in constant time. `createRouteHandler({ auth })` and `createDeployedServer(agent, { auth })` take the list (a token string and a boolean function work as before); the node server appends `apiToken(LOUSHO_API_TOKEN)` when that variable is set; an agent directory's `auth.ts` is bundled by `lousho build --target=node-server` / `docker` and guards the built server (`resolveAgentDir()` returns it as `auth`, the manifest has `auth: true`). The accepted principal reaches the run: `send()` / `stream()` / session turns take `principal`, and `RunConfigContext` (the `model` / `instructions` / `tools` functions) and `MemoryScopeContext` (memory scopes) carry it; Slack and Discord channels set the sender as the principal. `Principal` is also exported from the root. Bad helper options throw the new error code `LOUSHO_AUTH_CONFIG_INVALID` when the helper is created. `createRouteHandler()` without `auth` logs one warning when `NODE_ENV` is `production`. Route auth does not check session ownership, the principal does not reach tools or approval policies yet (N10b), and the Cloudflare Worker target keeps the `LOUSHO_API_TOKEN` token only. See [Route auth and principals](docs/auth.md). -- OAuth token storage (N9a, #246): `AgentStore` gains a fourth optional part, `tokens` (`OAuthTokenStore`: `get` / `set` / `delete` / `list` per provider and credential owner, single-use pending sign-ins with `putPending` / `takePending`, and `getClient` / `setClient` for dynamically registered clients). Owners are `{ owner: 'app' }` or `{ owner: 'user', principalId, issuer? }`; `tokenStoreKey(provider, owner)` is the stable, percent-encoded record key (`|app`, `|user||`). `memoryStore()` keeps tokens in process memory; `fileStore(dir)`, `SqliteStore` and `KVStore` encrypt every record with AES-256-GCM (Web Crypto, a fresh 12-byte IV per write, the record key as additional authenticated data) under a 32-byte key the application supplies as `tokenKey` or `LOUSHO_TOKEN_KEY`; there is no default key. Several keys (newest first, or comma-separated in the variable) rotate: writes use the first, reads try each. New error codes `LOUSHO_TOKEN_KEY_MISSING` (first write, or a read of an existing record, without a key) and `LOUSHO_TOKEN_DECRYPT_FAILED` (wrong key or a changed record; names the provider, never the token). `list()` returns metadata only. `generateTokenKey()` makes a key. SQLite: schema migration 4 adds the `oauth_tokens` and `oauth_pending` tables, and `prune()` also deletes expired pending sign-ins (its result gains `oauthPending`). KV: keys under `oauth/`, pending sign-ins written with an `expirationTtl`; `KVBinding` gains an optional `list()` (used only by `tokens.list()`), and `KVListOptions` / `KVListResult` are exported from `/kv`. The generated Cloudflare Worker passes its `LOUSHO_TOKEN_KEY` secret to `KVStore`. Nothing signs in yet; the sign-in flow follows. New page [OAuth](docs/oauth.md); new `## OAuth` section at the end of docs/errors.md. -- `githubChannel({ webhookSecret, botName, token?, app?, botLogin?, name?, apiUrl?, fetch?, triggers?, approvers?, onError? })` (N11b): an agent that answers GitHub issue, pull-request and review comments. `@` in a comment starts a turn and the reply is a new comment in the same thread (a review thread gets a reply under it; text over 60,000 characters is split); one session per issue or pull request and one per review thread, and later comments in a thread with a session are follow-ups. `X-Hub-Signature-256` is verified over the raw body in constant time before the body is parsed (401 otherwise); the webhook is acknowledged at once. Comments by bots, by the channel's own account and every comment the channel posts (a hidden marker) are ignored, and so are edits, deletions and other events. Replies are posted with `token` (a personal access token, or a function) or as a GitHub App (`app: { appId, privateKey }`: a PKCS#1 or PKCS#8 key signs an RS256 JWT with Web Crypto, exchanged for an installation token that is cached per installation until 5 minutes before it expires; `src/channels/githubAppAuth.ts`, not exported). Approvals are comments: the prompt asks for `/approve ` or `/deny ` (first line only, notes below it), and by default only a commenter whose `author_association` is `OWNER`, `MEMBER` or `COLLABORATOR` may decide, read from the command comment itself; `approvers` (logins, or a function that sees the association in `user.roles`) overrides it, and `triggers` (logins, or a function) restricts who may start a turn or answer a question (default: everyone who can comment, so restrict it on a public repository). A comment is untrusted input to the agent; see the security note in docs/channels.md. An `ask_question` is answered by the next comment in the thread and survives a restart given durable stores. Errors name the call and the HTTP status, never the token. The `LOUSHO_CHANNEL_INVALID` hint, docs/agent-directories.md and docs/errors.md now list `githubChannel()`. New section `## GitHub` at the end of docs/channels.md. -- Agent Forge keeps the traces of its runs (M5b, #227): every run is written with `fileTraceExporter()` to `.lousho/agents//traces` (the files `lousho traces` reads), and the Trace tab gets a list of the agent's past runs (time, duration, model calls, tokens, cost, status) with a "Live" entry while a run is active; choosing one opens its spans in the waterfall, with span kind and error status. New server routes `GET /agents/:id/traces?limit=N` and `GET /agents/:id/traces/:traceId`; the server reads only inside the agent's trace folder. The live `span` WebSocket messages now carry `kind` and `status` (optional fields of `SpanEvent`). See [Agent Forge](docs/agent-forge.md). -- A background sub-agent that needs approval pauses the lead when the lead awaits it (`agent_await`), and resumes with `approvals.resolve()` / `resume()` (M4, #225). The lead's result is `awaiting-approval` with the child's call (`subagentPath: ['']`) in `agent.approvals.list()`; the decision runs or rejects the child's call, the child finishes, and the continued lead gets its answer as the `agent_await` result. A child that pauses again, or several awaited tasks that are all paused, pause the lead one approval at a time. The child's usage is added to the lead's, and its conversation is saved under its `taskId`. The paused tasks are kept on the lead's approval record (`SubagentSuspension.background`, plain JSON), never in the tool arguments the model sees. Without `agent_await` nothing changes: the task reports `awaiting-approval` and is not resumable. The `task` and `agent_status` tool descriptions tell the model that a task awaiting approval continues only when it calls `agent_await` on it. Docs: docs/sub-agents.md ("When the lead run ends" and "Continuing a task"). -- Guardrail starter set (N5a): `piiGuardrail()` (email, phone, credit card with a Luhn check, IBAN with its mod-97 check, US SSN, IPv4; blocks, or rewrites each match to `[]`), `secretsGuardrail()` (private keys; Anthropic, OpenRouter, OpenAI, AWS, GitHub, Slack, Google and Stripe live keys; JWTs; `Authorization: Bearer` tokens; `extraPatterns`; rewrites to `[secret]` by default), `promptInjectionGuardrail()` (phrase, role-marker and Unicode-tag heuristics, plus an optional model check that must answer `SAFE` or `INJECTION: `) and `moderationGuardrail({ model })` (one model call that answers `NONE` or the categories that apply: `hate`, `harassment`, `self-harm`, `sexual`, `sexual-minors`, `violence`, `illicit`). The PII, secret and injection checks are heuristic pattern checks: they catch common cases and miss others, and are not a security boundary. Model-backed checks fail closed on a reply they cannot read. A guardrail result can now carry `info?: GuardrailTripInfo` (`pii`, `secret`, `prompt-injection`, `moderation` or `custom`), copied to `result.guardrail`, `guardrail.tripped` and `guardrail.rewrote`; it holds types, labels and offsets, never the matched text. Agent spec files can name them in `policy.guardrails` as `pii`, `secrets`, `prompt-injection` and `moderation`. New exported types `GuardrailTripInfo`, `PiiType`, `ModerationCategory`. Docs: [Guardrails](docs/guardrails.md#input-and-output-guardrails), [Configuration](docs/configuration.md#policy-policy), [Stream events](docs/stream-events.md). -- `telegramChannel({ botToken, secretToken, botUsername?, name?, fetch?, approvers?, onError? })` (N11a): an agent behind a Telegram bot, mounted like the Slack and Discord channels (`POST &channels&telegram`). The webhook secret token header (`X-Telegram-Bot-Api-Secret-Token`) is compared in constant time (401 otherwise); updates are acknowledged with `200` and the turn runs after. A private chat is one session per chat; in a group only `&ask`, `&ask@`, an `@` mention or a reply to the bot wakes it, and a forum topic is its own session. Replies are plain text, split at 4096 characters. Tool approvals are an inline keyboard (Approve & Deny) that only `approvers` (default: the user who started the turn) can tap; the tap names its chat, so it works after a restart. An `ask_question` is sent with `force_reply`; a pending one survives a restart given durable stores, like Slack and Discord. The bot token is never put in an error message or log line. `src&channels&channelSupport.ts` gains the internal `secretsEqual()` (constant-time compare with Web Crypto). The `LOUSHO_CHANNEL_INVALID` hint, docs&agent-directories.md and docs&errors.md now list `discordChannel()` and `telegramChannel()`. New section `## Telegram` at the end of docs&channels.md (attachments are not read yet).&r -- `todo.updated` stream event and `useTodos()` (React, Vue) / `loushoTodos()` (Svelte) (N12): every successful `todo_write` emits `todo.updated { todos, counts, toolCallId }` right after its `tool.done` (sub-agents too, with `subagent`). The shared UI reducer keeps the list in a new `AgentUIState.todos` (carried across turns, cleared by `reset()`), `useLoushoAgent()` / `loushoAgent()` expose it, `useTodos(agent)` / `loushoTodos(agent)` add counts, the current item and progress (`todoView()` computes the same view framework-free), and `toUIMessageStream()` sends a `data-lousho-todos` part for the AI SDK UI. See [Todos](docs/react.md#todos). -- Cloudflare Worker target: the `openrouter` provider and the `http` tool (with a required `LOUSHO_HTTP_ALLOW` host allowlist) (M3a). `openrouter` reads its key from the `OPENROUTER_API_KEY` binding and is `@ai-sdk/openai` pointed at OpenRouter, so its bundle passes the `node:` leak check like `openai`. The Worker's `http` is `http_request` with the Node tool's input, over the platform `fetch()`: since a Worker cannot see or pin where a host name resolves, it reaches only the host names (`api.github.com`) and `*.` wildcards (subdomains only) listed in the comma-separated `LOUSHO_HTTP_ALLOW` binding, refuses IP-address hosts even when listed and any scheme but `http:`/`https:`, checks every redirect hop the same way, and refuses every request when the binding is unset or empty. An invalid entry fails every request with an error naming it. `wrangler.toml` gets a commented `[vars]` `LOUSHO_HTTP_ALLOW` line when the spec lists `http`. `ollama` and `web-fetch` stay unsupported on Workers. Internally, the request, redirect and response logic of `http_request` moved to the Node-free `src/tools/built-in/httpCore.ts`, shared by both tools; the Node tool's behaviour is unchanged. See docs/cloudflare-workers.md#providers-and-tools. -- `fromAiSdk(model)`: any AI SDK `LanguageModel` (Google, Bedrock, Azure, Mistral, Gateway, ...) as `createAgent({ provider })`. Options `name` (default: the model's `provider` field), `fileMediaTypes`, `replaysReasoning` and `maxRetries` (default 0); exported with `FromAiSdkOptions` from the package root. Throws `LOUSHO_CONFIG_INVALID` for a model id string, for a model built for another `ai` major than the installed one, and for a call with another model id than the wrapped one. See docs/providers.md#any-ai-sdk-model-fromaisdk. -- 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/.json`, `checkpoints/.json`, `checkpoint-history/.json` and `approvals/.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. -- Local traces and `lousho traces` (M5a): `createAgent({ exporter, captureContent })` traces every run of the agent (`send()`, `stream()`, session turns, `agent.resume()` and runs continued by `agent.approvals.resolve()`; sub-agents join the lead's trace). The new subpath `@lousho/build-ai-agent/traces` exports `fileTraceExporter({ dir })`, which writes each run as JSON Lines to `//.jsonl` (default `.lousho/traces`; one finished span per line; a write error warns once and never fails the run), and `listTraces()` / `readTrace()` to read them. `npx lousho traces [--dir D] [--limit N] [--json]` lists recent runs with duration, model and tool calls, tokens and cost; `npx lousho traces [--json] [--content]` prints one run as a span tree with duration bars. Projects made by `lousho init` now ignore `.lousho/`. See docs/observability.md#local-traces. -- `openApiTools(document, options)` (N8): an OpenAPI 3.0 / 3.1 document (object, JSON or YAML string, or https URL) becomes one typed tool per operation, exported from the root and `./tools`. Names come from `operationId` (or `_`); the input has path, query, header and cookie parameters plus an `application/json` `body`, with `$ref`s resolved; every HTTP response is returned as `{ status, statusText, body }`. GET, HEAD and OPTIONS run and other methods ask for approval by default (`approval`), with read-only / destructive annotations. Options: `baseUrl`, `include`, `exclude`, `prefix`, `headers`, `bearerToken`, `providedArguments` (removed from the model's schema), `timeoutMs`, `maxResponseChars`, `fetch`. The base URL must be https (http only for loopback), credentials never reach events or the transcript, and redirects are followed only on the base origin. Swagger 2.0, multipart bodies and remote `$ref`s are refused. `jsonSchemaToZod(schema, root?)` takes an optional root for `$ref` resolution. New page docs/openapi-tools.md. -- Built-in OpenAI, Anthropic and OpenRouter providers send PDF file parts on `ai` 6 and 7 (Anthropic also `text/plain`); `ai` 4 and Ollama keep the text note. A provider subclass opts more media types in with `fileMediaTypes()` (`acceptsFileParts = true` still sends every type). The text-note warning is now once per provider and media type and names the type and the `ai` major. Added `npm run test:live` (`vitest.live.config.ts`) for `*.live.test.ts` files, which the default suite excludes. See docs/providers.md#multimodal-input. - Remote sub-agent approvals go through the lead run (LOU-Y7.3): a `remoteAgent()` task whose remote run pauses for a tool approval now pauses the lead run the way a local sub-agent does (it used to fail with `LOUSHO_SESSION_AWAITING_APPROVAL`): the lead's pending approval (`agent.approvals.list()`, channel buttons, the dev chat, ACP permission requests) has the remote tool's name and input and `subagentPath: []`, and a remote `ask_question` arrives as a question. Deciding it on the lead (`resolve`, `streamResolve`, `answer`) posts the decision to the remote `POST /chat/:sessionId/approvals/:id` and the continuation's final answer is the `task` result; a further pause pauses the lead again. The lead's approval snapshot stores the remote session id, the remote approval id, the `taskId` and the agent name (never the token), so a fresh lead process on the same store can decide it. Failures while deciding (401, other non-2xx including the remote's 404 for an approval no longer pending, network) are the `task` call's structured tool error with `LOUSHO_REMOTE_UNAUTHORIZED` / `LOUSHO_REMOTE_REQUEST_FAILED`. A remote agent with an `output` schema now returns its object as JSON with the footer (the V4.2 shape), read from `run.done`'s `object`. `RemoteSubagent.run()` takes `pausable` and `decision` (type `RemoteRunOptions`); the internal session client gains `resolveRemoteApproval()` and `SessionTurnSummary.object`. Only a lead run without an approval store keeps the old `LOUSHO_SESSION_AWAITING_APPROVAL` error. See docs/sub-agents.md#remote-approvals. - Structured output for sessions and sub-agents, typed for either zod major (LOU-V4.2): `createAgent({ output })` and `ExecuteOptions.output` accept a zod 3 schema, a zod 4 schema (`zod/v4` on zod 3.25, or zod 4) or a Standard Schema that can produce JSON Schema, and `result.object` is inferred from each without a cast (`InferSchemaOutput`, as `defineTool`). `AgentSession` is generic (`AgentSession`): `agent.session().send()` and `.stream()` results carry the typed `object`. A sub-agent with its own `output` returns its validated object as JSON (then the `taskId` footer) as the `task` and `agent_await` result; its `output-invalid` finish is a structured tool error. Sub-agents do not inherit the lead's `output`; `remoteAgent()` returns text only. The exported spec schemas (`agentSpecSchema`, ...) are typed as `SpecSchema` / `SpecObjectSchema`, so the published `schema-*.d.ts` no longer depends on zod 3 generics (`skipLibCheck: false` projects on zod 4). The Ollama missing-peer note now says `ollama-ai-provider-v2` needs zod 4 (`npm install zod@^4.0.0`). See docs/structured-output.md. - Publish readiness (LOU-D49): `npm run pack-smoke` (scripts/pack-smoke.ts, a CI job) packs the SDK and `create-lousho-agent`, checks the tarball (no `.env`, tests or secret-looking strings; entry count and size caps), runs `npm publish --dry-run` for both (nothing is published), installs the tarballs plus peers from the registry into a fresh project and verifies ESM and CJS loads of every `exports` entry, a mock-model agent turn, the `lousho` bin (`--help`, `doctor`) and `tsc` with `moduleResolution` bundler and node16. @@ -161,14 +201,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `AgentSpec.mcpServers` (LOU-D20): a validated map of MCP servers (stdio `command`/`args`/`env` or HTTP `url`/`headers`) in agent spec files. `loadSpec()` reports a bad entry with its name, `lousho doctor` reads the validated field instead of the raw file, and `specToAgent()` exposes the parsed servers as `agent.mcpServers` (connecting them is TODO(LOU-D20.2)). Existing specs are unaffected. - `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. - Tools own their contract (LOU-D22): `ToolDescriptor` gains optional `inputSchema` (zod) and `execute(args, ctx)`, which are now the canonical fields. `defineTool()` sets both and no longer calls `ai`'s `tool()`. Argument validation, the schema sent to the model, and tool execution read them first and fall back to `tool.parameters` / `tool.execute`. `ToolDescriptor.tool` (the `ai` v4 `Tool`) is now legacy: it is still built by `defineTool()` and still accepted on hand-written descriptors this release, but new code should set `inputSchema` and `execute`. +- ✅ Complete Phase 5 migration: Security, Storage, Templates, and Utils modules +- ✅ Security module with encryption, hashing, and quota validation +- ✅ Storage service with file locking mechanism +- ✅ Template rendering engine with Jinja2-like syntax +- ✅ Comprehensive utility functions (errors, formatters, validators) +- ✅ File extraction and processing utilities +- ✅ JSON path navigation utilities +- ✅ Framework-agnostic architecture (zero framework dependencies) ### Fixed -- Parallel `task` calls now get their `taskId`s, and background tasks start, in call order: allocating an id awaited a SHA-256 digest whose completion order is not fixed, so with `maxConcurrent: 1` the second task could take the slot first (#296, the flaky "queues tasks beyond maxConcurrent" test). -- The `cloudflare-worker` build's Node-builtin leak check now accepts the runtime-guarded `loadNodeModule("node:module" | "node:dns")` probes that `@ai-sdk/provider-utils` 4 (installed with `ai` 6) uses, as it already did for `ai` 7's `loadBuiltinModule`; without it a Worker bundle built on `ai` 6 failed with "Node builtins leaked" (found by the new `ai6` matrix entries). -- `isAgentEvent()` is now exhaustive (the runtime type list is a `Record`, so the compiler rejects a missing type). It had no `agent.drift`, so remote UI clients (`parseEventStream()`, the React, Vue and Svelte bindings) silently dropped that event. -- A remote sub-agent's token usage is added to the lead's `result.usage` (M10b). It was dropped, so budgets and cost reports undercounted delegation to a `remoteAgent()`. The lead adds what the remote agent reports on its `run.done` event to its totals, to `usage.delegated` and to `byModel['remote:']`, which keeps the remote's own `costUsd` (the lead's `costUsd` is `undefined` when the remote sends none). A remote run that pauses for an approval adds what it spent up to the pause, and the continuation adds only what it spent after it; background remote tasks roll up the same way. A deployment that sends no usage adds nothing. `RemoteRunOptions` has a new optional `onUsage(usage)` callback; `RemoteSubagent.run()` still returns `Promise`, so hand-written ones compile unchanged. See docs/sub-agents.md#remote-sub-agents. -- `lousho studio` finds the Agent Forge build that ships inside the installed package when it is run from a project that does not have `apps/agent-forge` (it used to fail with "could not find apps/agent-forge" everywhere except the SDK repo). A project's own `apps/agent-forge` still wins, and `.lousho/` is still created in the directory you ran it from. `--dev` from an installed package still needs the TypeScript source and says so. -- Documentation corrections: install commands, CLI command lists, optional peers, durable execution and compaction descriptions, the Status section, reasoning and file-part notes in the providers guide, and the `KVStore` note in the deployment guide now match the code. Ticket ids are gone from user-facing prose. - `lousho --help`, `-h` and `help` print the usage and exit 0 (they were "unknown command" with exit 1) (LOU-D49). - Install truth (LOU-U20): the README, installation, CLI, quick start and Agent Forge docs, `lousho init --help` and the message after a failed `lousho init` install now say the package is not on npm yet and point to one section, "Installing before the first release" (docs/installation.md); `npm install github:LinuxDevil/agent-sdk` is documented as not working. Stale "planned / not yet" statements about agent-directory channels, `toolCallId` in sandboxed tools and the Agent Forge Settings tab are corrected. - On `ai` 5+ the providers send images as `file` parts with an image `mediaType` (`image/*` when unknown) instead of the deprecated `image` part, which made `ai` 7 log a deprecation warning for every image. `ai` 4 still gets `image` parts. @@ -196,28 +238,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `nanoid` is no longer a dependency (LOU-D35). Generated ids (agent, run, trace, memory ids) now come from `globalThis.crypto.randomUUID()` via an internal `newId()`, so they are UUIDs instead of 21-character nanoids. Nothing public depended on the old format. - `finishReason: 'max-steps'` (LOU-U19): a run that exhausts `maxSteps` while the model still wanted to continue (last turn ended in tool calls) now resolves with `finishReason: 'max-steps'` instead of the stale last-turn reason (usually `'tool_calls'`), on `ExecutionResult`, the `finish` event and `run.done`. Resumed runs count `initialSteps` toward the budget. A run that finishes naturally within the budget is unchanged. Code that treated `'tool_calls'` as "hit maxSteps" should check `'max-steps'` instead (the eval `completed()` message and the MCP server's agent tool do). -### Tests -- `pack-smoke` allows a 16 MiB unpacked tarball (was 14 MiB): `main` was 45 KB under the old cap and permission modes (N4) went over it; see #316 for shrinking the package instead. -- Sandbox egress and the credential broker are tested against a real Docker Engine (M6): a new CI workflow, "Docker Engine" (`.github/workflows/docker.yml`, on pull requests touching the sandbox, broker or docker deploy adapter, on demand and weekly), runs `npm run test:docker` on GitHub's `ubuntu-latest` runner (rootful Engine 28.0.4). `src/security/sandboxEgress.docker.test.ts` checks nine cases of `SubprocessSandbox({ network: { allow }, broker })` with `curlimages/curl`: an allowed HTTPS host answers through the broker, another host gets a 403, bypassing the proxy and raw IPs have no route, outside names do not resolve, the broker injects a credential the container never sees, a container outside the internal network cannot use the gateway listener, an aborted run and `close()` leave no container or network, and a reused non-internal network is refused. The docker deploy image test (`/health` and `/chat`) moved to `src/deploy/adapters/docker.docker.test.ts` and runs in the same job; it was skipped in CI before and passes there. No product defect was found. `*.docker.test.ts` files are excluded from `npm test` and `npm run test:coverage`; they skip without a daemon unless `LOUSHO_DOCKER_TESTS=1` is set (the workflow sets it), which makes a missing daemon fail. docs/workspace-tools.md says what the job checks; CONTRIBUTING.md mentions `npm run test:docker`. - ### Docs -- New page, [Permission modes](docs/permission-modes.md) (N4): the modes and their table, which tools are read-only, the `editsFiles` marker, the order of evaluation, switching, the audit log, sub-agents and resume. [Approvals](docs/approvals.md) "Permission policies" links to it; [Tools](docs/tools.md), [Stream events](docs/stream-events.md) and [Sub-agents](docs/sub-agents.md) ("What a sub-agent inherits") gained a row each. -- New page, [Route auth and principals](docs/auth.md): the ordered list and its three outcomes, each helper, 401 / 403 / `WWW-Authenticate`, which helpers run where (Node, Workers, edge), `createRouteHandler`, the node server and `auth.ts`, reading the principal in the run, and security notes (no session ownership check; `basic()` only over HTTPS). docs/errors.md gains a `## Auth` section at the end (`LOUSHO_AUTH_CONFIG_INVALID`). One-paragraph links from deployment.md (`### Auth`), nextjs.md (`## Routes`), memory.md (`## Scopes`), agent-directories.md (layout and deploy), api-overview.md and a "Route auth" row in the cloudflare-workers.md limits table; no heading of an existing page changed. -- New page, [Cloudflare Workers](docs/cloudflare-workers.md): a table of what the Worker target supports and what it does not comes first, then build and deploy, bindings, sessions, checkpoints and approvals in KV, scheduled runs, consistency, and bundle size and Node builtins. The Worker sections moved out of docs/deployment.md unchanged (`Bindings, sessions and the API on Workers`, `Cron triggers and handleScheduled` and `Durable execution (pause/resume) on Workers` are gone from it); deployment.md keeps a short `cloudflare-worker` summary that links to the page. Links to `deployment.md#cron-triggers-and-handlescheduled` moved to `cloudflare-workers.md#scheduled-runs`. -- Two dead heading links fixed: docs/executor-api.md now links to `tools.md#advanced-toolregistry`, and docs/troubleshooting.md links to `mcp.md#use-mcp-servers-in-an-agent` (the configuration anchor moved with the MCP section). No other link in `docs/` or the README points to a missing heading. docs/approvals.md lists `fileStore(dir)` among the approval stores. -- Long pages split; text moved unchanged. New pages: [Runs](docs/runs.md), [Models and cost](docs/models-and-cost.md), [Stream events](docs/stream-events.md), [Queued input and steering](docs/queue-and-steer.md). Moved anchors (old to new): `api-overview.md#finish-reasons`, `#cancellation` and `#parallel-tool-calls` to `runs.md` (same anchors); `api-overview.md#models-tokens-and-cost` to `models-and-cost.md#models-and-the-price-table`; `api-overview.md#usage-and-cost-of-a-run` to `models-and-cost.md#usage-and-cost-of-a-run`; `api-overview.md#flow-expressions` to `flows.md#flow-expressions`; `api-overview.md#todo-tools` to `tools.md#todo-tools`; `api-overview.md#tool-errors` to `tools.md#errors`; `api-overview.md#context-compaction` to `compaction.md`; `streaming.md#event-schema-version-1`, `#ordering-guarantees`, `#typescript` and `#versioning` to `stream-events.md` (same anchors); `streaming.md#queued-input` and `#steering` to `queue-and-steer.md` (same anchors); the `AgentExecutor.execute()` options table of `configuration.md` to `executor-api.md#options-of-agentexecutorexecute`. The error codes page gained a "Find a code by area" table; its headings are unchanged. -- The API overview, providers, structured output, tools, tracing, guardrails and testing guides teach `createAgent()` first: usage and multimodal examples use it, the testing tool example uses `defineTool()`, and every executor, `AgentBuilder` or `ToolRegistry` snippet sits under an "Advanced" heading that links to [the executor API](docs/executor-api.md). They say that `createAgent()` does not take `exporter`, `captureContent` or `sandbox` yet. -- New page, [Triggers](docs/triggers.md): the `@lousho/build-ai-agent/triggers` adapters, a table that says when to use triggers, channels or schedules, the `TriggerAdapter` interface (with a custom adapter and `TriggerRegistry`), and the webhook, Slack and cron sections that moved from the API overview, unchanged. The API overview now points to it; links to `api-overview.md#triggers`, `#webhook-authentication` and `#slack-request-signatures` moved to `triggers.md`. The missing-`auth` warning of `WebhookTriggerAdapter` and the missing-`signingSecret` warning of `SlackTriggerAdapter` now link to `docs/triggers.md`. -- New page docs/migrating-to-create-agent.md: from `AgentBuilder`, `AgentExecutor`, `ToolRegistry` and `resumeAfterApproval()` to `createAgent()`, with a before-and-after example, a mapping table, the `ExecuteOptions` that `createAgent()` does not take yet (with alternatives), the behavior differences and a step-by-step checklist. Linked from docs/executor-api.md and the README docs table. -- New page, [Hooks](docs/hooks.md): the four hook points, the outcomes of a `preToolCall` / `postToolCall` hook, four type-checked recipes (deny a tool, add a default argument, redact a result, inject context), ordering, and `HookRegistry`. The `HookRegistry` bullet and the `### Hook outcomes` section moved there from the API overview, which now points to it; links to `api-overview.md#hook-outcomes` moved to `hooks.md#hook-outcomes`. -- Approvals, sessions, streaming, sub-agents, compaction and skills guides show `createAgent()` first. Executor snippets (`AgentExecutor`, `resumeAfterApproval`, `streamResumeAfterApproval`, `InputQueue`, `HookRegistry`) moved under headings named "Advanced: the executor API" (links to docs/executor-api.md); `approvals` and `streaming` renamed a section to that name. `createAgent({ hooks: [createCompactionHook(...)] })` is the compaction example. Removed a statement from the sub-agents guide that `createAgent()` takes no `approvalStore` (it does, and `agent.approvals` resolves sub-agent pauses). -- Stale statements removed: ticket ids in the Agent Forge page, the "removed next minor" promise on `AgentType` in the API overview, and the installation note that read as if every user needs Node 22.19 for the `http` tool (it comes from the `undici@8` dependency, which is loaded only for `validateSSL: false` requests). The `ProposedAction` doc comment no longer refers to an unlanded ticket. -- New page docs/mcp.md, "MCP (Model Context Protocol)": using MCP servers in an agent, `connectMcp()`, approval for MCP tools, `loadMcpTools()`, serving an agent with `serveMcp()` and `lousho mcp`, and a Limits list. These sections moved out of docs/configuration.md, which keeps the `mcpServers` spec field and links to the new page; links from approvals, CLI, tools, API overview and the README now point to `mcp.md`. -- `docs/durable-execution.md` and `docs/workspace-tools.md` lead with `createAgent()` (`send()` with a `sessionId`, `agent.resume()`, `agent.approvals.resolve()`, `agent.fork()`) instead of `AgentExecutor`. The executor snippets (`execute()` with a `checkpointStore`, `resumeAfterApproval()`, `AgentExecutor.fork()`, the `ToolRegistry` setup) moved under "Advanced: the executor API". In durable-execution the heading "What happens when you call `execute()` again with the same `sessionId`" is now "What happens when you send again with the same `sessionId`". -- New page docs/troubleshooting.md: start from the symptom (setup, runs that end without an answer, tools, providers, sandbox and MCP), with cause, fix and a link for each. README docs table, `docs/installation.md` and `docs/errors.md` link to it. -- New guide, [Build a coding agent](docs/build-a-coding-agent.md): a terminal coding agent in six steps (workspace tools confined to the project, approval before writes and risky commands, a streamed UI that handles the approval pause, a session for follow-up questions, offline tests with `MemoryWorkspace` and `mockModel`), with an offline test of the same agent (`docs/buildACodingAgent.test.ts`; its replay of a recorded real-model run is skipped until the cassette `docs/__cassettes__/build-a-coding-agent.json` is recorded). -- The Quick Start is rewritten around `createAgent()`: hello world, a tool, streaming, a session, an approval, an offline test with `mockModel`, a custom provider and spec files, each runnable with no API key (the first excepted). The `AgentBuilder` + `AgentExecutor` section moved to a new page, docs/executor-api.md (with the `ToolRegistry` note); the Quick Start no longer shows either API. -- `create-lousho-agent` has a README (its npm page was empty) and `license`, `homepage` and `repository` fields; they appear on npm with its next release. `lousho init --help` no longer says `--sdk-path` is needed until the package is on npm. - README revamped into a short front page (LOU-D52); the details it dropped moved to `docs/` (new pages: tools, approvals, providers, cli, flows, guardrails, utilities). ### Renamed @@ -225,17 +246,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Published - `@lousho/build-ai-agent` and `create-lousho-agent` are on npm. The README and the installation, quick start, CLI and Agent Forge docs no longer say the package is unpublished; "Installing before the first release" is now "Installing from a local build" (docs/installation.md#installing-from-a-local-build) and covers trying an unreleased commit. The hint printed after a failed `lousho init` install no longer mentions a 404; it points to `--sdk-path`. The docs site moved to https://lousho.com. - -## [1.0.0-alpha.8] - 2025-10-05 - -### Added -- `telegramChannel({ botToken, secretToken, botUsername?, name?, fetch?, approvers?, onError? })` (N11a): an agent behind a Telegram bot, mounted like the Slack and Discord channels (`POST &channels&telegram`). The webhook secret token header (`X-Telegram-Bot-Api-Secret-Token`) is compared in constant time (401 otherwise); updates are acknowledged with `200` and the turn runs after. A private chat is one session per chat; in a group only `&ask`, `&ask@`, an `@` mention or a reply to the bot wakes it, and a forum topic is its own session. Replies are plain text, split at 4096 characters. Tool approvals are an inline keyboard (Approve & Deny) that only `approvers` (default: the user who started the turn) can tap; the tap names its chat, so it works after a restart. An `ask_question` is sent with `force_reply`; a pending one survives a restart given durable stores, like Slack and Discord. The bot token is never put in an error message or log line. `src&channels&channelSupport.ts` gains the internal `secretsEqual()` (constant-time compare with Web Crypto). The `LOUSHO_CHANNEL_INVALID` hint, docs&agent-directories.md and docs&errors.md now list `discordChannel()` and `telegramChannel()`. New section `## Telegram` at the end of docs&channels.md (attachments are not read yet).&r -- ✅ Complete Phase 5 migration: Security, Storage, Templates, and Utils modules -- ✅ Security module with encryption, hashing, and quota validation -- ✅ Storage service with file locking mechanism -- ✅ Template rendering engine with Jinja2-like syntax -- ✅ Comprehensive utility functions (errors, formatters, validators) -- ✅ File extraction and processing utilities -- ✅ JSON path navigation utilities -- ✅ Framework-agnostic architecture (zero framework dependencies) -