@amalgm/chat 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/AGENTS.md +1 -0
  2. package/PURPOSE.md +44 -2
  3. package/README.md +10 -0
  4. package/dist/api/conversations.d.ts +7 -0
  5. package/dist/api/conversations.d.ts.map +1 -1
  6. package/dist/api/conversations.js +39 -1
  7. package/dist/api/conversations.js.map +1 -1
  8. package/dist/api/index.d.ts +1 -1
  9. package/dist/api/index.d.ts.map +1 -1
  10. package/dist/api/index.js.map +1 -1
  11. package/dist/execution/contract.d.ts +1 -1
  12. package/dist/execution/contract.d.ts.map +1 -1
  13. package/dist/execution/index.d.ts +2 -0
  14. package/dist/execution/index.d.ts.map +1 -1
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/mcp/index.d.ts +3 -0
  21. package/dist/mcp/index.d.ts.map +1 -0
  22. package/dist/mcp/index.js +2 -0
  23. package/dist/mcp/index.js.map +1 -0
  24. package/dist/mcp/server.d.ts +7 -0
  25. package/dist/mcp/server.d.ts.map +1 -0
  26. package/dist/mcp/server.js +136 -0
  27. package/dist/mcp/server.js.map +1 -0
  28. package/dist/mcp/types.d.ts +17 -0
  29. package/dist/mcp/types.d.ts.map +1 -0
  30. package/dist/mcp/types.js +2 -0
  31. package/dist/mcp/types.js.map +1 -0
  32. package/dist/normalizers/claude.d.ts +46 -46
  33. package/dist/normalizers/codex.d.ts +23 -23
  34. package/docs/contracts/capabilities-and-instructions.md +103 -0
  35. package/docs/contracts/conversation-persistence.md +3 -0
  36. package/docs/contracts/input-and-execution.md +8 -3
  37. package/host/adapters/acp-capabilities.js +32 -0
  38. package/host/adapters/acp.js +8 -12
  39. package/host/adapters/claude.js +3 -2
  40. package/host/adapters/codex.js +19 -6
  41. package/host/adapters/cursor.js +9 -15
  42. package/host/adapters/input-capabilities.js +1 -0
  43. package/host/adapters/opencode.js +4 -3
  44. package/host/adapters/pi.js +3 -2
  45. package/host/adapters/prompt.js +2 -3
  46. package/host/auth.js +1 -1
  47. package/host/http.d.ts +4 -0
  48. package/host/http.js +43 -0
  49. package/host/index.d.ts +45 -3
  50. package/host/index.js +3 -0
  51. package/host/native-contract.js +26 -4
  52. package/host/native-runtime.js +8 -0
  53. package/host/platform-egress.js +46 -25
  54. package/host/title-generator.js +110 -0
  55. package/host/tooling/mcp-bundle.js +107 -56
  56. package/host/tooling/system-prompt.js +7 -4
  57. package/package.json +6 -1
  58. package/skills/chat/SKILL.md +300 -89
  59. package/skills/chat/references/contracts.md +73 -47
@@ -1,97 +1,308 @@
1
1
  ---
2
2
  name: chat
3
- description: Build and verify provider-agnostic agent chat with the Amalgm Chat SDK. Use for chat interfaces, ACP prompt and update shapes, prepared execution, SQLite conversations, durable streams, Claude Code, Codex, Cursor, OpenCode, Pi, MCP tools, permissions, model-step usage, reconnect, or adapter conformance.
3
+ description: Build provider-agnostic agent chat with the Chat SDK (@amalgm/chat) use it whenever you integrate Claude Code, Codex, Cursor, OpenCode, Pi, or any ACP agent into a chat interface with durable conversations, streaming updates, reconnect, and normalized usage.
4
4
  ---
5
5
 
6
- # Build with Amalgm Chat
7
-
8
- Keep application code on the public SDK. Keep native behavior in host adapters.
9
-
10
- ## Establish the contract
11
-
12
- Read [references/contracts.md](references/contracts.md) before changing input,
13
- execution preparation, sessions, adapters, or usage. In a source checkout, also
14
- read the repository `AGENTS.md`; it contains current architecture and known gaps.
15
-
16
- Use these package doors:
17
-
18
- - `@amalgm/chat` or `/sessions` for `Chat`, start, resume, and send;
19
- - `/input` for ordered ACP prompt content;
20
- - `/execution` for prepared execution contracts and handles;
21
- - `/acp` for official ACP types and session updates;
22
- - `/usage` for model-step usage and turn aggregation.
23
- - `/conversations` for owner-scoped records and UI projections;
24
- - `/sqlite` for the WAL-backed reference conversation store.
25
-
26
- Do not copy ACP types into application code.
27
-
28
- ## Follow the user path
29
-
30
- 1. Prepare while the user composes. Resolve the exact agent revision, provider-
31
- scoped model, model settings, auth binding, tool revision, cwd, permission
32
- mode, and computer.
33
- 2. Start or resume with the returned `{ id, revision }` handle.
34
- 3. Send an ordered ACP prompt with that handle.
35
- 4. Render official ACP updates without provider branches.
36
- 5. Treat `send()` as one background runtime pump and its returned iterable as
37
- a subscriber. Reconnect by turn id and committed sequence.
38
-
39
- Never fetch models, credentials, tools, preferences, agent configuration, or a
40
- workspace because the user pressed Enter. A missing or stale prepared revision
41
- must fail explicitly.
42
-
43
- ## Preserve semantics
44
-
45
- - Pass text and attachments as ACP content blocks. Never flatten or discard an
46
- attachment. Translate it natively or return a typed unsupported-capability
47
- result.
48
- - Treat model identity as `(agent revision, provider, model)`.
49
- - Treat auth and computer as per-turn selections. Never silently fall back to a
50
- different auth mode.
51
- - Expose ordinary tools and MCP tools through one MCP-shaped capability model.
52
- The resolved selection is complete; never add an ambient MCP server.
53
- - Use the five native adapters for Claude Code, Codex, Cursor, OpenCode, and Pi.
54
- Register every other compliant agent through the generic `acp` adapter and
55
- an immutable launch descriptor; do not add product-name branches for Amp,
56
- Gemini CLI, Kiro, or later ACP agents.
57
- - Require every adapter to apply an input exactly or throw a typed unsupported
58
- error before prompting. Capability negotiation is validation, not fallback.
59
- - Render `agent_message_chunk`, `agent_thought_chunk`, `tool_call`, and
60
- `tool_call_update`. Use ACP tool states.
61
- - Use `@amalgm/stream` for rendering only; it is not the durable journal.
62
- - Persist accepted input before provider invocation. Commit every update before
63
- fanout, seal terminal state once, and recover uncertain running turns as
64
- interrupted after restart.
65
- - Use bounded SQLite batches for content deltas. Tool, terminal, permission,
66
- warning, and other control updates flush immediately.
67
- - Never sync a live SQLite file. Live synchronizes logical conversation rows,
68
- sealed turns, update batches, messages, and provider checkpoints.
69
-
70
- ## Normalize usage honestly
71
-
72
- Create one finalized usage record per provider model invocation. Keep input,
73
- output, cache-read, and cache-write tokens nullable. Derive turn usage from its
74
- steps. Preserve the raw provider record. Never use ACP context fullness as a
75
- billing ledger or turn missing values into zero.
76
-
77
- ## Verify through real sessions
78
-
79
- Test laws with fixtures, then start a real session through `Chat`; do not stop at
80
- calling an adapter helper. For every claimed adapter, verify start/resume, text
81
- and supported attachments, tools, permissions, normalized updates, terminal
82
- state, and usage. Record unsupported capabilities literally.
83
-
84
- Use the adapter capability matrix and generic ACP launch shape in
85
- [references/contracts.md](references/contracts.md). A generic ACP conformance
86
- run does not prove that every named ACP product is installed or authenticated;
87
- record each product acceptance separately.
88
-
89
- Run:
6
+ # Chat
7
+
8
+ Chat is a provider-agnostic SDK for building agent conversations. Your
9
+ application prepares an execution (which agent, model, auth, tools, and
10
+ workspace), starts or resumes a session, sends prompts as official
11
+ [Agent Client Protocol (ACP)](https://agentclientprotocol.com) content, and
12
+ renders official ACP updates — the same code path for Claude Code, Codex,
13
+ Cursor, OpenCode, Pi, and any compliant ACP agent. Every turn is journaled to
14
+ SQLite before it streams, so clients can disconnect, reconnect from their last
15
+ committed sequence, and recover cleanly after a restart. Token usage is
16
+ normalized per model step so you always know what a turn actually consumed.
17
+
18
+ ## Installation
90
19
 
91
20
  ```bash
92
- npm test
93
- npm pack --dry-run
21
+ npm install @amalgm/chat
22
+ ```
23
+
24
+ Chat requires Node 20 or later. The bundled SQLite conversation store uses
25
+ `better-sqlite3`, a native module that compiles during install on platforms
26
+ without a prebuilt binary.
27
+
28
+ ## Quickstart
29
+
30
+ The fastest path is the composed host: it wires the native runtime, the SQLite
31
+ conversation store, and crash recovery for you.
32
+
33
+ ```ts
34
+ import { createChatHost } from "@amalgm/chat/host";
35
+
36
+ const host = await createChatHost({
37
+ ownerUserId: "user-1",
38
+ databasePath: "./conversations.sqlite",
39
+ });
40
+
41
+ // 1. Prepare the execution while the user is composing.
42
+ const execution = await host.chat.prepareExecution("composer", {
43
+ agent: { adapterId: "claude_code", installationId: "default", revisionId: "rev-1" },
44
+ model: { providerId: "anthropic", modelId: "sonnet", settings: [] },
45
+ auth: { mode: "subscription", bindingId: "default" },
46
+ tools: { revisionId: null, toolIds: [] },
47
+ cwd: { path: process.cwd(), workspaceId: null, materializationRevision: null },
48
+ permissionMode: "ask",
49
+ computerId: "computer-1",
50
+ });
51
+
52
+ // 2. Start a session and send an ordered ACP prompt.
53
+ const session = await host.chat.startSession({ execution });
54
+ const stream = await session.send({
55
+ execution,
56
+ prompt: { parts: [{ type: "text", text: "Summarize this repository." }] },
57
+ });
58
+
59
+ // 3. Render official ACP updates — no provider branches.
60
+ for await (const update of stream) {
61
+ if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
62
+ process.stdout.write(update.content.text);
63
+ }
64
+ }
65
+
66
+ await host.close();
67
+ ```
68
+
69
+ `createChatHost` also accepts your own `conversations` store or `runtime` if
70
+ you need to replace either half, and it returns `recoveredTurns` — the turns
71
+ that were still running when the previous process died.
72
+
73
+ ## Core concepts
74
+
75
+ ### Prepared execution
76
+
77
+ Everything a turn needs is resolved ahead of time into an `ExecutionContract`:
78
+ the exact agent revision, the provider-scoped model and its settings, the auth
79
+ binding, the selected tool set, the working directory, the permission mode
80
+ (`"ask"` or `"full_access"`), and the computer that runs it. Call
81
+ `chat.prepareExecution(id, contract)` while the user is composing; it returns a
82
+ small `ExecutionHandle` of `{ id, revision }`. Any changed field produces a new
83
+ revision, and preparing the same contract twice is a no-op.
84
+
85
+ `startSession` and `send` only consume a prepared handle — they never fetch
86
+ models, credentials, tools, or workspaces themselves. This keeps pressing Enter
87
+ instant, and it means a stale handle fails explicitly with
88
+ `EXECUTION_REVISION_MISMATCH` instead of silently re-resolving to something the
89
+ user did not pick. The full contract shape is in
90
+ [references/contracts.md](references/contracts.md).
91
+
92
+ ### Sessions, turns, and updates
93
+
94
+ A session is one durable conversation. Each `send()` creates a turn: the
95
+ accepted prompt is persisted first, then one background pump runs the provider
96
+ and commits every update to the journal before fanning it out to subscribers.
97
+ The iterable that `send()` returns is just a subscriber — dropping it does not
98
+ stop the turn, and any number of clients can attach to the same live turn.
99
+
100
+ Updates are official ACP `SessionUpdate` values: `agent_message_chunk` (text),
101
+ `agent_thought_chunk` (reasoning), `tool_call` and `tool_call_update` (tool
102
+ identity, input, and state), plus ACP permission requests and stop reasons.
103
+ Every update carries `_meta.amalgm.{conversationId, turnId, sequence}` so you
104
+ can resume precisely, and the raw native event is preserved under
105
+ `_meta.amalgm.native`. Render the ACP grammar and your UI works for every
106
+ supported agent.
107
+
108
+ ### Durable conversations
109
+
110
+ Conversations, messages, turns, and update journals live in an owner-scoped
111
+ store. Production hosts should use `SqliteConversationStore` from
112
+ `@amalgm/chat/sqlite` (WAL-backed) together with
113
+ `createSqliteJournalBatchPolicy()`, which batches high-frequency content deltas
114
+ while flushing tool, permission, and terminal updates immediately. A turn ends
115
+ in exactly one status: `completed`, `failed`, `cancelled`, or `interrupted`.
116
+ After a process restart, `chat.recoverInterruptedTurns()` (called automatically
117
+ by `createChatHost`) marks turns that were still `running` as `interrupted` —
118
+ their committed updates remain replayable, and nothing is silently reported as
119
+ completed.
120
+
121
+ ### Usage
122
+
123
+ One turn can contain many model steps. Each step records nullable
124
+ `inputTokens`, `outputTokens`, `cacheReadTokens`, and `cacheWriteTokens`, plus
125
+ the model identity, the source and exactness of the numbers, and the raw
126
+ provider record as evidence. Turn usage is aggregated from steps only, and a
127
+ bucket stays `null` when any contributing step is unknown — missing data is
128
+ never turned into zero. The `@amalgm/chat/usage` entry point exports the
129
+ normalization helpers (`normalizeUsage`, `aggregateTurnUsage`,
130
+ `finalizeTurnUsage`) and a model catalog with `calculateCost`.
131
+
132
+ ## Working with sessions
133
+
134
+ ### Send text and attachments
135
+
136
+ Prompts are ordered lists of ACP content blocks. Attachments travel as ACP
137
+ `image`, `audio`, `resource`, or `resource_link` blocks with bytes or
138
+ references already prepared — they are delivered to the agent natively, never
139
+ flattened into text. `promptFromText` from `@amalgm/chat/input` builds the
140
+ common case:
141
+
142
+ ```ts
143
+ import { promptFromText } from "@amalgm/chat/input";
144
+
145
+ const stream = await session.send({
146
+ execution,
147
+ prompt: promptFromText("What does this diagram show?", [
148
+ { type: "image", mimeType: "image/png", data: base64Png },
149
+ ]),
150
+ });
94
151
  ```
95
152
 
96
- Do not claim parity from one successful provider. Record current limitations in
97
- `PURPOSE.md` or the owning contract document.
153
+ If the selected agent cannot accept a block, the send fails up front with a
154
+ typed `UNSUPPORTED_PROMPT_PART` error no part of your input is silently
155
+ dropped.
156
+
157
+ ### Reconnect after a disconnect
158
+
159
+ Track the highest `_meta.amalgm.sequence` you have rendered. After a network
160
+ drop or page reload, reattach to the same turn from that cursor:
161
+
162
+ ```ts
163
+ for await (const update of session.reconnect(turnId, { afterSequence: 42 })) {
164
+ render(update);
165
+ }
166
+ ```
167
+
168
+ Reconnect replays the committed journal past your cursor and then continues
169
+ live if the turn is still running. `stream.completion` resolves to the sealed
170
+ turn record when you need the final status, and `session.interrupt()` requests
171
+ a stop for the active turn.
172
+
173
+ ### Resume a stored conversation
174
+
175
+ ```ts
176
+ const session = await chat.resumeSession({ sessionId, execution });
177
+ // or, over the composed host's store, without re-preparing:
178
+ const { session: resumed, latestTurn } = await chat.resumeStoredSession(sessionId);
179
+ ```
180
+
181
+ Conversation identity survives changing the model, auth mode, or computer
182
+ between turns — the adapter transparently opens a new native provider session
183
+ when it must. Sessions also expose `inspect()`, `messages()`,
184
+ `updateTitle(title)`, and `close()`; the `Chat` instance adds
185
+ `listConversations()`, `archiveSession(id)`, `activeTurn(id)`,
186
+ `latestTurn(id)`, and `replayTurn(id, turnId, afterSequence)`.
187
+
188
+ ## Supported agents
189
+
190
+ Five agents have native adapters; every other compliant ACP agent registers
191
+ through the generic `acp` adapter with an immutable launch descriptor — there
192
+ are no product-name branches to wait for.
193
+
194
+ | Adapter id | Agent | Auth modes | Model settings |
195
+ | --- | --- | --- | --- |
196
+ | `claude_code` | Claude Code | platform, subscription, BYOK | reasoning, speed |
197
+ | `codex` | Codex | platform, subscription, BYOK | reasoning, speed |
198
+ | `cursor` | Cursor | subscription, BYOK API key | the advertised model's exact parameters |
199
+ | `opencode` | OpenCode | platform, subscription, BYOK | exact model-catalog variant |
200
+ | `pi` | Pi | platform, subscription, BYOK | reasoning |
201
+ | `acp` | any ACP agent | declared by the revision | negotiated ACP config options |
202
+
203
+ Ordinary tools and MCP tools flow through one MCP-shaped capability model: the
204
+ prepared tool selection is complete, and no adapter adds an ambient MCP server
205
+ behind your back. Capability negotiation is validation — an adapter applies
206
+ your input exactly or throws a typed unsupported error before prompting. The
207
+ generic ACP launch shape and the full capability matrix are in
208
+ [references/contracts.md](references/contracts.md).
209
+
210
+ Use the official ACP `McpServer` union from `@amalgm/chat/execution` for every
211
+ prepared capability. Tools, Agents, Chat, and other platform products are
212
+ selected MCP servers, not adapter-specific built-ins. Compose machine, project,
213
+ and exact Agent-revision instructions during preparation; adapters consume the
214
+ prepared bundle through a genuine native system/developer field. ACP v1 has no
215
+ system-prompt field, so require the advertised `amalgm.dev`
216
+ `sessionInstructions` extension for non-empty instructions. Never prepend
217
+ instructions to `session/prompt` content.
218
+
219
+ ## The HTTP surface
220
+
221
+ The host package ships one HTTP/SSE handler over the same capability, for
222
+ serving Chat to browser or remote clients:
223
+
224
+ ```ts
225
+ import { createServer } from "node:http";
226
+ import { createChatHost } from "@amalgm/chat/host";
227
+
228
+ const host = await createChatHost({ ownerUserId: "user-1", databasePath: "./chat.sqlite" });
229
+ createServer(async (request, response) => {
230
+ if (await host.handleHttp(request, response)) return;
231
+ response.writeHead(404).end();
232
+ }).listen(8080);
233
+ ```
234
+
235
+ Routes are mounted under `/chat` by default (configurable via
236
+ `createChatHttpHandler({ chat, prefix })`):
237
+
238
+ | Method and path | Purpose |
239
+ | --- | --- |
240
+ | `POST /chat/executions` | prepare an execution (`{ id, execution }` → handle) |
241
+ | `GET /chat/conversations` | list conversations |
242
+ | `POST /chat/conversations` | start a session |
243
+ | `GET /chat/conversations/:id` | conversation record, messages, latest turn |
244
+ | `POST /chat/conversations/:id/resume` | resume (with or without a new handle) |
245
+ | `POST /chat/conversations/:id/turns` | send a prompt; streams SSE updates |
246
+ | `GET /chat/conversations/:id/turns/:turnId/events` | reconnect from `afterSequence` or `Last-Event-ID` |
247
+ | `POST /chat/conversations/:id/interrupt` | interrupt the active turn |
248
+ | `GET /chat/conversations/:id/active` | the running turn, if any |
249
+ | `PATCH /chat/conversations/:id` | rename, or mark seen with `{ newMessages: false }` |
250
+ | `DELETE /chat/conversations/:id` | archive |
251
+
252
+ Error responses carry `{ error: { code, message } }` with the same codes the
253
+ SDK throws.
254
+
255
+ ## Error handling
256
+
257
+ Chat fails loudly with typed errors; every error object carries a stable
258
+ `code` string.
259
+
260
+ | Code | Meaning |
261
+ | --- | --- |
262
+ | `INVALID_EXECUTION_CONTRACT` | a contract field is missing or malformed |
263
+ | `EXECUTION_NOT_PREPARED` | the handle's id was never prepared on this host |
264
+ | `EXECUTION_REVISION_MISMATCH` | the handle is stale; re-prepare and retry |
265
+ | `INVALID_PROMPT` | the prompt is empty or contains an invalid block |
266
+ | `UNSUPPORTED_PROMPT_PART` | the selected agent cannot accept a content block |
267
+ | `UNSUPPORTED_EXECUTION_INPUT` | the agent cannot apply part of the contract |
268
+ | `SESSION_NOT_FOUND` / `SESSION_ALREADY_EXISTS` | session id problems |
269
+ | `SESSION_BUSY` | the session already has a running turn |
270
+ | `SESSION_AGENT_MISMATCH` | the new handle targets a different agent installation |
271
+ | `SUBSCRIBER_BACKPRESSURE` | one slow subscriber overflowed its buffer and was disconnected (the turn keeps running) |
272
+ | `INTERRUPT_UNSUPPORTED` | the adapter cannot stop this turn |
273
+
274
+ A failed or interrupted turn surfaces on the stream as a `TurnStreamError`
275
+ whose `turn` property is the sealed record:
276
+
277
+ ```ts
278
+ import { TurnStreamError } from "@amalgm/chat";
279
+
280
+ try {
281
+ for await (const update of stream) render(update);
282
+ } catch (error) {
283
+ if (error instanceof TurnStreamError) showTurnFailure(error.code, error.turn);
284
+ else throw error;
285
+ }
286
+ ```
287
+
288
+ The sealed turn is still stored, so a reconnecting client sees the same
289
+ terminal state.
290
+
291
+ ## Package entry points
292
+
293
+ | Entry point | Contents |
294
+ | --- | --- |
295
+ | `@amalgm/chat` | `Chat`, `ChatSession`, `ChatTurnStream`, session errors |
296
+ | `@amalgm/chat/sessions` | the same session capability, plus option types |
297
+ | `@amalgm/chat/input` | `PromptInput`, `promptFromText`, `normalizePrompt` |
298
+ | `@amalgm/chat/execution` | `ExecutionContract`, handles, revisions, contract errors |
299
+ | `@amalgm/chat/acp` | the official ACP SDK re-exported, plus `_meta` projection types |
300
+ | `@amalgm/chat/usage` | step and turn usage normalization, model catalog, `calculateCost` |
301
+ | `@amalgm/chat/conversations` | conversation, turn, and message record types |
302
+ | `@amalgm/chat/sqlite` | `SqliteConversationStore`, `createSqliteJournalBatchPolicy` |
303
+ | `@amalgm/chat/host` | `createChatHost`, `NativeChatRuntime`, `createChatHttpHandler` |
304
+
305
+ Import ACP types from `@amalgm/chat/acp` rather than copying them into your
306
+ application; the SDK versions that boundary for you. Advanced entry points
307
+ (`/events`, `/transcript`, `/normalizers`, `/api`) expose the native event
308
+ grammar and transcript reduction for hosts that build custom runtimes.
@@ -1,33 +1,40 @@
1
- # Chat SDK contract
1
+ # Contract reference
2
2
 
3
- Load this reference when building a chat surface or changing a provider adapter.
3
+ The exact shapes Chat accepts and emits. Everything here is importable from
4
+ the package entry points — never copy these types into application code.
4
5
 
5
6
  ## Prompt
6
7
 
7
8
  ```ts
9
+ import type { PromptInput } from "@amalgm/chat/input";
10
+
8
11
  type PromptInput = {
9
12
  parts: ContentBlock[]; // official ACP ContentBlock
10
13
  };
11
14
  ```
12
15
 
13
16
  The list is non-empty and ordered. Attachments are ACP `image`, `audio`,
14
- `resource`, or `resource_link` blocks. The host must prepare bytes or references
15
- before send.
17
+ `resource`, or `resource_link` blocks; prepare bytes or references before
18
+ calling `send`. Attachments are delivered natively — if the selected agent
19
+ cannot accept a block, the send fails with `UNSUPPORTED_PROMPT_PART` instead
20
+ of flattening or dropping it.
16
21
 
17
- ## Prepared execution
22
+ ## Execution contract
18
23
 
19
24
  ```ts
25
+ import type { ExecutionContract } from "@amalgm/chat/execution";
26
+
20
27
  type ExecutionContract = {
21
28
  agent: { adapterId: string; installationId: string; revisionId: string };
22
29
  model: {
23
30
  providerId: string;
24
31
  modelId: string;
25
- settings: SetSessionConfigOptionRequest[];
32
+ settings: ModelSetting[]; // ACP SetSessionConfigOptionRequest, minus session identity
26
33
  };
27
34
  auth: { mode: "platform" | "subscription" | "byok"; bindingId: string };
28
35
  tools: { revisionId: string | null; toolIds: string[] };
29
36
  cwd: {
30
- path: string;
37
+ path: string; // absolute
31
38
  workspaceId: string | null;
32
39
  materializationRevision: string | null;
33
40
  };
@@ -36,9 +43,13 @@ type ExecutionContract = {
36
43
  };
37
44
  ```
38
45
 
39
- Preparation resolves this to an opaque host binding and returns an
40
- `ExecutionHandle` containing only `id` and `revision`. Any changed field creates
41
- a new revision. Start and send consume a prepared handle; they never prepare.
46
+ `chat.prepareExecution(id, contract)` resolves this to an opaque host binding
47
+ and returns an `ExecutionHandle` containing only `{ id, revision }`. Any
48
+ changed field creates a new revision. `startSession` and `send` consume a
49
+ prepared handle; they never prepare, discover, or silently re-resolve. Model
50
+ identity is always the triple `(agent revision, provider, model)`, and auth
51
+ and computer are per-turn selections — Chat never falls back to a different
52
+ auth mode on its own.
42
53
 
43
54
  ## Session path
44
55
 
@@ -46,38 +57,34 @@ a new revision. Start and send consume a prepared handle; they never prepare.
46
57
  const execution = await chat.prepareExecution("composer", contract);
47
58
  const session = await chat.startSession({ execution });
48
59
 
49
- for await (const update of await session.send({
60
+ const stream = await session.send({
50
61
  execution,
51
62
  prompt: { parts: [{ type: "text", text: "Hello" }] },
52
- })) {
53
- render(update);
54
- }
63
+ });
64
+ for await (const update of stream) render(update);
55
65
  ```
56
66
 
57
- Construct `Chat` with the authenticated `ownerUserId`, a
58
- `ConversationStorePort`, and a host clock. Production hosts should use
59
- `SqliteConversationStore` from `@amalgm/chat/sqlite` plus
60
- `createSqliteJournalBatchPolicy()`.
61
-
62
- `send()` begins one background provider pump after the input transaction
63
- commits. Iterating its result subscribes to the durable turn; it does not own
64
- provider execution. Reconnect with `session.reconnect(turnId, {
65
- afterSequence })`. Every yielded update carries
66
- `_meta.amalgm.{conversationId,turnId,sequence}`.
67
+ `send()` starts one background provider pump after the input transaction
68
+ commits. Iterating the returned `ChatTurnStream` subscribes to the durable
69
+ turn; it does not own provider execution, and `stream.completion` resolves to
70
+ the sealed turn. Reconnect with `session.reconnect(turnId, { afterSequence })`.
71
+ Every yielded update carries `_meta.amalgm.{conversationId, turnId, sequence}`.
67
72
 
68
- On startup call `chat.recoverInterruptedTurns()` before admitting new work.
69
- Committed updates remain replayable. Running turns become `interrupted` unless
70
- an adapter later supplies a proven reattachment path.
73
+ Use `chat.resumeSession({ sessionId, execution })` for an existing
74
+ conversation, or `chat.resumeStoredSession(sessionId)` to reuse its stored
75
+ execution. Conversation identity survives an explicit auth or computer change
76
+ between turns; the adapter may open a different native provider session
77
+ underneath.
71
78
 
72
- Use `chat.resumeSession({ sessionId, execution })` for an existing conversation.
73
- The conversation identity survives an explicit auth or computer change between
74
- turns. The adapter may need a different native provider session.
79
+ On process startup, call `chat.recoverInterruptedTurns()` before admitting new
80
+ work (`createChatHost` does this for you). Committed updates remain
81
+ replayable; turns that were `running` become `interrupted`.
75
82
 
76
- ## Adapter admission and exact inputs
83
+ ## Adapters and exact inputs
77
84
 
78
85
  The adapter ids are `claude_code`, `codex`, `cursor`, `opencode`, `pi`, and
79
- `acp`. The first five are native adapters. The final path admits any
80
- compliant ACP agent from its resolved immutable revision:
86
+ `acp`. The first five are native adapters; the `acp` adapter admits any
87
+ compliant ACP agent from an immutable launch descriptor:
81
88
 
82
89
  ```ts
83
90
  acpLaunch: {
@@ -103,29 +110,48 @@ acpLaunch: {
103
110
  | Pi | platform, subscription, BYOK | reasoning only |
104
111
  | ACP | revision-declared modes | negotiated ACP config options |
105
112
 
106
- Every path takes the complete selected tool set (no ambient MCP server), an
107
- absolute prepared cwd, `ask` or `full_access`,
108
- an immutable agent revision, a provider-scoped model, and an execution
109
- computer. Optional prompt/MCP/config capabilities are negotiated for ACP.
110
- Anything that cannot be applied throws `UNSUPPORTED_EXECUTION_INPUT` or
111
- `UNSUPPORTED_PROMPT_PART`; no selected input disappears.
113
+ Every adapter receives the complete selected tool set (no ambient MCP server),
114
+ an absolute prepared cwd, `ask` or `full_access`, an immutable agent revision,
115
+ a provider-scoped model, and an execution computer. Optional prompt, MCP, and
116
+ config capabilities are negotiated for generic ACP agents. Anything an adapter
117
+ cannot apply throws `UNSUPPORTED_EXECUTION_INPUT` or `UNSUPPORTED_PROMPT_PART`
118
+ before prompting — no selected input disappears.
119
+
120
+ ## Capabilities and instructions
121
+
122
+ Import `McpServer` from `@amalgm/chat/execution`; it is the official
123
+ `@agentclientprotocol/sdk` union. A host resolves the immutable Toolbox
124
+ selection to the complete `McpServer[]` during preparation. Amalgm Tools,
125
+ Agents, and Chat use this same selected-server path when exposed as platform
126
+ tools. Product `_meta` is descriptive and never grants a capability.
127
+
128
+ The host also resolves one instruction bundle from machine, project, and exact
129
+ Agent-revision inputs. Native adapters use the bundle through a real
130
+ system/developer-instruction field. For ACP agents, non-empty instructions
131
+ require `agentCapabilities._meta["amalgm.dev"].sessionInstructions === true`;
132
+ Chat then supplies the bundle under the same namespace on `session/new`,
133
+ `session/load`, or `session/resume`. Instructions never become user
134
+ `session/prompt` blocks.
112
135
 
113
136
  ## Output
114
137
 
115
138
  ACP is the public update grammar:
116
139
 
117
- - `agent_message_chunk`: text;
118
- - `agent_thought_chunk`: reasoning;
119
- - `tool_call`: tool identity, input, and initial state;
120
- - `tool_call_update`: input/result deltas and state;
140
+ - `agent_message_chunk` — assistant text;
141
+ - `agent_thought_chunk` reasoning;
142
+ - `tool_call` tool identity, input, and initial state;
143
+ - `tool_call_update` input/result deltas and ACP tool states;
121
144
  - ACP permission requests and stop reasons.
122
145
 
123
- Amalgm extensions retain durable ids/cursors, raw events, ownership, and richer
124
- usage without changing the ACP meaning.
146
+ The `_meta.amalgm` extension adds durable ids and cursors, the raw native
147
+ event, and richer usage without changing the ACP meaning of any update. Render
148
+ the ACP grammar and every supported agent works without provider branches.
125
149
 
126
150
  ## Usage
127
151
 
128
152
  One turn can contain many model steps. Each step reports nullable
129
153
  `inputTokens`, `outputTokens`, `cacheReadTokens`, and `cacheWriteTokens`, plus
130
- source, exactness, model identity, and raw evidence. Aggregate the turn from
131
- steps only. A bucket remains `null` when any contributing step is unknown.
154
+ source, exactness, model identity, and the raw provider record as evidence.
155
+ Turn usage is aggregated from steps only, and a bucket remains `null` when any
156
+ contributing step is unknown — Chat never converts missing values to zero, and
157
+ ACP context-fullness signals are not a billing ledger.