@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.
- package/AGENTS.md +1 -0
- package/PURPOSE.md +44 -2
- package/README.md +10 -0
- package/dist/api/conversations.d.ts +7 -0
- package/dist/api/conversations.d.ts.map +1 -1
- package/dist/api/conversations.js +39 -1
- package/dist/api/conversations.js.map +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js.map +1 -1
- package/dist/execution/contract.d.ts +1 -1
- package/dist/execution/contract.d.ts.map +1 -1
- package/dist/execution/index.d.ts +2 -0
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +136 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/types.d.ts +17 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/normalizers/claude.d.ts +46 -46
- package/dist/normalizers/codex.d.ts +23 -23
- package/docs/contracts/capabilities-and-instructions.md +103 -0
- package/docs/contracts/conversation-persistence.md +3 -0
- package/docs/contracts/input-and-execution.md +8 -3
- package/host/adapters/acp-capabilities.js +32 -0
- package/host/adapters/acp.js +8 -12
- package/host/adapters/claude.js +3 -2
- package/host/adapters/codex.js +19 -6
- package/host/adapters/cursor.js +9 -15
- package/host/adapters/input-capabilities.js +1 -0
- package/host/adapters/opencode.js +4 -3
- package/host/adapters/pi.js +3 -2
- package/host/adapters/prompt.js +2 -3
- package/host/auth.js +1 -1
- package/host/http.d.ts +4 -0
- package/host/http.js +43 -0
- package/host/index.d.ts +45 -3
- package/host/index.js +3 -0
- package/host/native-contract.js +26 -4
- package/host/native-runtime.js +8 -0
- package/host/platform-egress.js +46 -25
- package/host/title-generator.js +110 -0
- package/host/tooling/mcp-bundle.js +107 -56
- package/host/tooling/system-prompt.js +7 -4
- package/package.json +6 -1
- package/skills/chat/SKILL.md +300 -89
- package/skills/chat/references/contracts.md +73 -47
package/skills/chat/SKILL.md
CHANGED
|
@@ -1,97 +1,308 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: chat
|
|
3
|
-
description: Build
|
|
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
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
`
|
|
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
|
-
#
|
|
1
|
+
# Contract reference
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
15
|
-
|
|
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
|
-
##
|
|
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:
|
|
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
|
-
|
|
40
|
-
`ExecutionHandle` containing only `id
|
|
41
|
-
a new revision.
|
|
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
|
-
|
|
60
|
+
const stream = await session.send({
|
|
50
61
|
execution,
|
|
51
62
|
prompt: { parts: [{ type: "text", text: "Hello" }] },
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
}
|
|
63
|
+
});
|
|
64
|
+
for await (const update of stream) render(update);
|
|
55
65
|
```
|
|
56
66
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
`
|
|
60
|
-
`
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
turns
|
|
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
|
-
##
|
|
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
|
|
80
|
-
compliant ACP agent from
|
|
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
|
|
107
|
-
absolute prepared cwd, `ask` or `full_access`,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
118
|
-
- `agent_thought_chunk
|
|
119
|
-
- `tool_call
|
|
120
|
-
- `tool_call_update
|
|
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
|
-
|
|
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
|
|
131
|
-
steps only
|
|
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.
|