toolroll 0.9.63 → 0.9.65

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 (292) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +15 -5
  3. package/dist/acp-agents.d.ts +43 -0
  4. package/dist/acp-agents.js +62 -0
  5. package/dist/acp-attest.d.ts +44 -0
  6. package/dist/acp-attest.js +269 -0
  7. package/dist/acp-check.d.ts +28 -0
  8. package/dist/acp-check.js +410 -0
  9. package/dist/acp-client.d.ts +163 -0
  10. package/dist/acp-client.js +738 -0
  11. package/dist/acp-inspect.d.ts +23 -0
  12. package/dist/acp-inspect.js +109 -0
  13. package/dist/agent-board.d.ts +19 -6
  14. package/dist/agent-board.js +30 -9
  15. package/dist/agent-fence.d.ts +50 -2
  16. package/dist/agent-fence.js +157 -10
  17. package/dist/agent-pulse.js +9 -2
  18. package/dist/agent-state-words.d.ts +19 -0
  19. package/dist/agent-state-words.js +20 -0
  20. package/dist/agent-state.d.ts +65 -0
  21. package/dist/agent-state.js +229 -0
  22. package/dist/agentconfig.js +36 -13
  23. package/dist/agents-board.d.ts +34 -2
  24. package/dist/agents-board.js +110 -1
  25. package/dist/approval-rules-ui.js +2 -2
  26. package/dist/assignment-delivery.d.ts +14 -0
  27. package/dist/assignment-delivery.js +13 -0
  28. package/dist/assignment.js +13 -0
  29. package/dist/attest.js +7 -0
  30. package/dist/backup-ui.js +2 -2
  31. package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
  32. package/dist/browser/terminal.css +37 -0
  33. package/dist/browser/terminal.js +117 -0
  34. package/dist/browser/workspace.css +2 -2
  35. package/dist/browser/workspace.js +42 -38
  36. package/dist/browser-shell.js +6 -3
  37. package/dist/browser-workspace.d.ts +235 -1
  38. package/dist/browser-workspace.js +60 -0
  39. package/dist/builder.d.ts +6 -1
  40. package/dist/builder.js +34 -13
  41. package/dist/chat-actions.d.ts +13 -1
  42. package/dist/chat-actions.js +197 -15
  43. package/dist/chat-ask.d.ts +4 -0
  44. package/dist/chat-ask.js +14 -0
  45. package/dist/chat-channel.js +20 -0
  46. package/dist/chat-continuity.js +5 -4
  47. package/dist/chat-decide.d.ts +0 -14
  48. package/dist/chat-decide.js +4 -1
  49. package/dist/chat-delivery.d.ts +9 -0
  50. package/dist/chat-delivery.js +45 -6
  51. package/dist/chat-messages.d.ts +8 -0
  52. package/dist/chat-messages.js +36 -4
  53. package/dist/check-levels-ui.js +2 -2
  54. package/dist/checkout-proof.d.ts +28 -0
  55. package/dist/checkout-proof.js +62 -0
  56. package/dist/claim.d.ts +12 -0
  57. package/dist/claim.js +19 -0
  58. package/dist/cli-help.d.ts +1 -1
  59. package/dist/cli-help.js +4 -1
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.js +21 -2
  62. package/dist/coding-ui.js +6 -4
  63. package/dist/console-instance.d.ts +124 -0
  64. package/dist/console-instance.js +337 -0
  65. package/dist/contracts/chat-actions.d.ts +16 -0
  66. package/dist/contracts/chat-actions.js +4 -0
  67. package/dist/contracts/chat-content.d.ts +19 -0
  68. package/dist/contracts/chat-content.js +6 -0
  69. package/dist/contracts/console-api.d.ts +12 -4
  70. package/dist/contracts/console-api.js +12 -4
  71. package/dist/contracts/flow-card.d.ts +4 -0
  72. package/dist/contracts/flow-card.js +3 -0
  73. package/dist/contracts/flow.d.ts +46 -0
  74. package/dist/contracts/flow.js +11 -2
  75. package/dist/contracts/lead-tools.d.ts +92 -0
  76. package/dist/contracts/lead-tools.js +13 -0
  77. package/dist/contracts/provider.d.ts +11 -1
  78. package/dist/contracts/provider.js +1 -1
  79. package/dist/contracts/route.d.ts +76 -1
  80. package/dist/contracts/route.js +1 -1
  81. package/dist/contracts/store-json.d.ts +2 -0
  82. package/dist/control-setup.d.ts +1 -1
  83. package/dist/control-ui.js +2 -2
  84. package/dist/converse.d.ts +16 -0
  85. package/dist/converse.js +32 -4
  86. package/dist/demo-chat.js +4 -4
  87. package/dist/demo.js +2 -2
  88. package/dist/desktop-update-gate.js +6 -0
  89. package/dist/desktop-update.d.ts +1 -1
  90. package/dist/discord-chat.js +4 -1
  91. package/dist/doctor.js +1 -1
  92. package/dist/egress-bridge.d.ts +17 -0
  93. package/dist/egress-bridge.js +111 -0
  94. package/dist/egress.d.ts +119 -0
  95. package/dist/egress.js +423 -0
  96. package/dist/exec.d.ts +78 -0
  97. package/dist/exec.js +367 -9
  98. package/dist/export-ui.js +2 -2
  99. package/dist/flow-engine.js +24 -5
  100. package/dist/flow-gallery-ui.js +2 -2
  101. package/dist/flow-gallery.js +27 -8
  102. package/dist/flow-share.js +16 -5
  103. package/dist/flow-triggers.d.ts +77 -8
  104. package/dist/flow-triggers.js +517 -70
  105. package/dist/flows-cli.js +8 -2
  106. package/dist/flows-ui.d.ts +0 -2
  107. package/dist/flows-ui.js +52 -4
  108. package/dist/flows.d.ts +4 -0
  109. package/dist/flows.js +9 -1
  110. package/dist/html.d.ts +9 -0
  111. package/dist/html.js +11 -0
  112. package/dist/invoke.d.ts +17 -0
  113. package/dist/invoke.js +217 -21
  114. package/dist/keys.d.ts +18 -14
  115. package/dist/keys.js +5 -0
  116. package/dist/lead-capabilities.js +3 -2
  117. package/dist/lead-contract.js +2 -0
  118. package/dist/lead-doors.d.ts +2 -0
  119. package/dist/lead-doors.js +7 -0
  120. package/dist/lead-follow.d.ts +2 -0
  121. package/dist/lead-follow.js +58 -2
  122. package/dist/lead-progress.js +1 -0
  123. package/dist/lead-queue-runtime.d.ts +46 -0
  124. package/dist/lead-queue-runtime.js +191 -0
  125. package/dist/lead-queue.d.ts +104 -0
  126. package/dist/lead-queue.js +187 -0
  127. package/dist/lead-sweep.d.ts +67 -0
  128. package/dist/lead-sweep.js +99 -0
  129. package/dist/lead-tools.d.ts +22 -0
  130. package/dist/lead-tools.js +152 -1
  131. package/dist/lead.d.ts +13 -5
  132. package/dist/lead.js +89 -29
  133. package/dist/limits-ui.d.ts +8 -1
  134. package/dist/limits-ui.js +26 -0
  135. package/dist/live-bus.d.ts +8 -0
  136. package/dist/live-bus.js +40 -4
  137. package/dist/live-detail.d.ts +27 -0
  138. package/dist/live-detail.js +57 -0
  139. package/dist/live-notes.d.ts +20 -0
  140. package/dist/live-notes.js +47 -0
  141. package/dist/live-records.d.ts +140 -0
  142. package/dist/live-records.js +935 -0
  143. package/dist/live-terminal.d.ts +258 -0
  144. package/dist/live-terminal.js +649 -0
  145. package/dist/live.d.ts +38 -27
  146. package/dist/live.js +142 -124
  147. package/dist/mcp-oauth.d.ts +6 -3
  148. package/dist/mcp-oauth.js +8 -4
  149. package/dist/memory-pass.js +2 -2
  150. package/dist/mobile-viewport.d.ts +5 -1
  151. package/dist/mobile-viewport.js +18 -2
  152. package/dist/model-catalog.js +4 -0
  153. package/dist/monitoring-ui.js +2 -2
  154. package/dist/needs-you.d.ts +1 -1
  155. package/dist/needs-you.js +11 -4
  156. package/dist/operate.d.ts +2 -0
  157. package/dist/operate.js +275 -17
  158. package/dist/passwordless.d.ts +23 -0
  159. package/dist/passwordless.js +26 -0
  160. package/dist/peek-cli.d.ts +12 -4
  161. package/dist/peek-cli.js +19 -11
  162. package/dist/phase-routing.js +10 -3
  163. package/dist/planner-source.d.ts +3 -0
  164. package/dist/planner-source.js +14 -0
  165. package/dist/planner.d.ts +2 -1
  166. package/dist/planner.js +7 -1
  167. package/dist/policy-ui.js +2 -2
  168. package/dist/policy.d.ts +1 -20
  169. package/dist/policy.js +29 -2
  170. package/dist/project-delete-ui.d.ts +12 -0
  171. package/dist/project-delete-ui.js +41 -9
  172. package/dist/project-delete.js +2 -2
  173. package/dist/project-memory.d.ts +3 -1
  174. package/dist/project-memory.js +57 -25
  175. package/dist/project-tools.d.ts +5 -3
  176. package/dist/project-tools.js +12 -9
  177. package/dist/provider-auth.d.ts +4 -3
  178. package/dist/provider-auth.js +5 -4
  179. package/dist/provider-connection.d.ts +2 -1
  180. package/dist/provider-connection.js +5 -0
  181. package/dist/provider.d.ts +25 -2
  182. package/dist/provider.js +63 -9
  183. package/dist/pull-request-ui.js +2 -2
  184. package/dist/push.js +1 -1
  185. package/dist/quick-start.d.ts +24 -0
  186. package/dist/quick-start.js +127 -0
  187. package/dist/release.d.ts +3 -0
  188. package/dist/release.js +14 -8
  189. package/dist/request-budget-ui.js +2 -2
  190. package/dist/result-review.d.ts +1 -1
  191. package/dist/result-review.js +9 -3
  192. package/dist/retention-ui.js +2 -2
  193. package/dist/retention.js +10 -1
  194. package/dist/run-efficiency.d.ts +73 -0
  195. package/dist/run-efficiency.js +228 -0
  196. package/dist/runner.d.ts +5 -1
  197. package/dist/runner.js +22 -1
  198. package/dist/scope.d.ts +170 -5
  199. package/dist/scope.js +251 -31
  200. package/dist/scout-net.d.ts +4 -1
  201. package/dist/scout-net.js +37 -12
  202. package/dist/scout.d.ts +20 -6
  203. package/dist/scout.js +27 -3
  204. package/dist/serve.js +247 -40
  205. package/dist/server/chat.d.ts +1 -1
  206. package/dist/server/chat.js +122 -31
  207. package/dist/server/chrome.d.ts +6 -1
  208. package/dist/server/chrome.js +18 -7
  209. package/dist/server/console.css +52 -38
  210. package/dist/server/flows.js +38 -8
  211. package/dist/server/guards.d.ts +1 -0
  212. package/dist/server/guards.js +23 -9
  213. package/dist/server/http.d.ts +7 -0
  214. package/dist/server/http.js +3 -2
  215. package/dist/server/live.d.ts +34 -1
  216. package/dist/server/live.js +39 -17
  217. package/dist/server/pages-assets.js +5 -0
  218. package/dist/server/pages.js +90 -13
  219. package/dist/server/people-tokens.d.ts +3 -0
  220. package/dist/server/people-tokens.js +55 -17
  221. package/dist/server/remote-hooks.js +12 -6
  222. package/dist/server/remote.d.ts +2 -1
  223. package/dist/server/remote.js +14 -5
  224. package/dist/server/render-chat.d.ts +12 -3
  225. package/dist/server/render-chat.js +47 -16
  226. package/dist/server/render-pages.d.ts +4 -1
  227. package/dist/server/render-pages.js +5 -4
  228. package/dist/server/render-people.d.ts +3 -2
  229. package/dist/server/render-people.js +3 -2
  230. package/dist/server/render-results.d.ts +18 -2
  231. package/dist/server/render-results.js +37 -28
  232. package/dist/server/render-settings.js +6 -5
  233. package/dist/server/render-tasks.d.ts +35 -10
  234. package/dist/server/render-tasks.js +110 -27
  235. package/dist/server/request-context.d.ts +2 -0
  236. package/dist/server/request-context.js +2 -1
  237. package/dist/server/route-table.d.ts +3 -0
  238. package/dist/server/route-table.js +46 -5
  239. package/dist/server/runtime.d.ts +33 -2
  240. package/dist/server/session.d.ts +25 -1
  241. package/dist/server/session.js +18 -4
  242. package/dist/server/settings.js +121 -10
  243. package/dist/server/sweeps.d.ts +4 -0
  244. package/dist/server/sweeps.js +58 -0
  245. package/dist/server/task-views.d.ts +2 -1
  246. package/dist/server/task-views.js +96 -6
  247. package/dist/server/tasks.js +320 -74
  248. package/dist/server/terminals.d.ts +31 -0
  249. package/dist/server/terminals.js +365 -0
  250. package/dist/setup-guide.js +3 -2
  251. package/dist/slack-chat.js +4 -1
  252. package/dist/spend-ui.js +3 -3
  253. package/dist/spend.js +2 -1
  254. package/dist/steer-capabilities.d.ts +32 -0
  255. package/dist/steer-capabilities.js +60 -0
  256. package/dist/steer-controls.d.ts +14 -0
  257. package/dist/steer-controls.js +36 -0
  258. package/dist/steer-status.js +2 -2
  259. package/dist/storage-ui.js +4 -4
  260. package/dist/store-baseline.d.ts +1 -1
  261. package/dist/store-baseline.js +1 -1
  262. package/dist/store.d.ts +430 -15
  263. package/dist/store.js +1041 -52
  264. package/dist/subagent-desk.js +1 -1
  265. package/dist/summary.js +3 -0
  266. package/dist/task-source.d.ts +58 -0
  267. package/dist/task-source.js +93 -0
  268. package/dist/team-leads.js +1 -1
  269. package/dist/teams-chat.js +4 -1
  270. package/dist/telegram-lead.d.ts +3 -0
  271. package/dist/telegram-lead.js +44 -7
  272. package/dist/telegram.d.ts +11 -2
  273. package/dist/telegram.js +51 -17
  274. package/dist/terminal-catalog.d.ts +11 -0
  275. package/dist/terminal-catalog.js +129 -0
  276. package/dist/text-limits.d.ts +2 -0
  277. package/dist/text-limits.js +2 -0
  278. package/dist/tool-broker.d.ts +2 -0
  279. package/dist/tool-broker.js +1 -0
  280. package/dist/toolroll-update-ui.js +2 -2
  281. package/dist/toolroll-update.js +13 -3
  282. package/dist/tools-ui.js +2 -2
  283. package/dist/webhook-limits.d.ts +23 -0
  284. package/dist/webhook-limits.js +30 -0
  285. package/dist/webhook-readers.d.ts +56 -0
  286. package/dist/webhook-readers.js +138 -0
  287. package/dist/webhook-signing.d.ts +27 -0
  288. package/dist/webhook-signing.js +119 -0
  289. package/dist/work-index.d.ts +29 -0
  290. package/dist/work-index.js +16 -2
  291. package/dist/workspace-revision.js +3 -1
  292. package/package.json +11 -2
@@ -0,0 +1,738 @@
1
+ /**
2
+ * The Agent Client Protocol, from the plane's side (docs/plans/acp-adapter.md).
3
+ *
4
+ * ACP is JSON-RPC 2.0, one message per line, over the agent's stdio. This
5
+ * module is the whole conversation as a pure state machine: it is handed the
6
+ * agent's lines one at a time and answers with the lines to send back. It
7
+ * never spawns, reads a clock or touches a file — the runner (exec.ts) owns
8
+ * the process, the timers and the shutdown.
9
+ *
10
+ * One conversation is one build: `initialize`, `session/new` in the
11
+ * worktree, the sealed model selected and seen to be current, one
12
+ * `session/prompt` carrying the brief, then `session/update`s until the
13
+ * response to that prompt. Success is that response with
14
+ * `stopReason: "end_turn"` and nothing else — no update, exit code or
15
+ * message text can stand in for it.
16
+ *
17
+ * The plane offers no client capabilities (no file system, no terminal), so
18
+ * every agent request except `session/request_permission` is answered
19
+ * "method not found". A model is chosen only through a `model` config option,
20
+ * whose answer shows the model current: `session/set_model` answers with
21
+ * nothing to observe, so an agent that offers only that is refused until a
22
+ * fixture proves how it confirms. Builds are full access inside the OS fence, so a
23
+ * permission request is granted once (`allow_once`, never `allow_always`);
24
+ * the fence, not this answer, is the boundary — OpenCode runs commands
25
+ * without asking at all.
26
+ *
27
+ * What the runner keeps is not the agent's stream but a short synthetic
28
+ * record (`retained()`), one JSON object per line, typed `acp.*` — a type
29
+ * no agent can emit — which `acpParse` reads into the gateway's envelope.
30
+ */
31
+ /** How many offered models a conversation keeps (each id ≤ 200 characters). */
32
+ export const ACP_OFFERED_CAP = 2000;
33
+ /** The largest line (UTF-8 bytes) the plane reads from an agent; a longer one is a protocol failure. */
34
+ export const ACP_LINE_CAP = 1024 * 1024;
35
+ const MESSAGE_CAP = 64 * 1024;
36
+ const TEXT_CAP = 2048;
37
+ const USAGE_CAP = 8 * 1024;
38
+ const PROTOCOL_VERSION = 1;
39
+ const STOP_REASONS = new Set(["end_turn", "max_tokens", "max_turn_requests", "refusal", "cancelled"]);
40
+ function isRecord(value) {
41
+ return typeof value === "object" && value !== null && !Array.isArray(value);
42
+ }
43
+ function bounded(text, cap) {
44
+ return text.length <= cap ? text : text.slice(0, cap);
45
+ }
46
+ function line(message) {
47
+ return JSON.stringify(message);
48
+ }
49
+ /** The model a `session/new` (or `set_config_option`) result offers, by mechanism. */
50
+ function modelOffer(result) {
51
+ const options = result["configOptions"];
52
+ if (Array.isArray(options)) {
53
+ const model = options.find(one => isRecord(one) && one["id"] === "model" && one["type"] === "select");
54
+ if (isRecord(model) && Array.isArray(model["options"])) {
55
+ // Choices, or groups of choices — read as the same variant validation accepted: all choices, or all groups.
56
+ const options = model["options"];
57
+ const choices = options.every(configChoice) ? options : options.every(configGroup) ? options.flatMap(one => one["options"]) : [];
58
+ const offered = choices.flatMap(one => isRecord(one) && typeof one["value"] === "string" ? [one["value"]] : []);
59
+ return { mechanism: "config-option", offered, current: typeof model["currentValue"] === "string" ? model["currentValue"] : null };
60
+ }
61
+ }
62
+ const models = result["models"];
63
+ if (isRecord(models) && Array.isArray(models["availableModels"])) {
64
+ const offered = models["availableModels"].flatMap(one => isRecord(one) && typeof one["modelId"] === "string" ? [one["modelId"]] : []);
65
+ return { mechanism: "set-model", offered, current: typeof models["currentModelId"] === "string" ? models["currentModelId"] : null };
66
+ }
67
+ return null;
68
+ }
69
+ const OPTION_KINDS = new Set(["allow_once", "allow_always", "reject_once", "reject_always"]);
70
+ /**
71
+ * The answer to a permission request: the `allow_once` option, else the `reject_once` one. The whole list must be
72
+ * well formed — every option an object with a non-empty id and a known kind, no id twice — or nothing is chosen: an
73
+ * id shared by "once" and "always" would make the answer mean either.
74
+ */
75
+ export function permissionAnswer(params) {
76
+ if (!isRecord(params) || !meta(params) || !Array.isArray(params["options"]) || params["options"].length === 0 || params["options"].length > 16)
77
+ return null;
78
+ const options = [];
79
+ const ids = new Set();
80
+ for (const one of params["options"]) {
81
+ if (!isRecord(one) || typeof one["optionId"] !== "string" || one["optionId"] === "" || one["optionId"].length > 256)
82
+ return null;
83
+ if (typeof one["kind"] !== "string" || !OPTION_KINDS.has(one["kind"]))
84
+ return null;
85
+ if (typeof one["name"] !== "string" || !meta(one))
86
+ return null;
87
+ if (ids.has(one["optionId"]))
88
+ return null;
89
+ ids.add(one["optionId"]);
90
+ options.push({ optionId: one["optionId"], kind: one["kind"] });
91
+ }
92
+ const allow = options.find(one => one.kind === "allow_once");
93
+ if (allow !== undefined)
94
+ return { optionId: allow.optionId, granted: true };
95
+ const reject = options.find(one => one.kind === "reject_once");
96
+ if (reject !== undefined)
97
+ return { optionId: reject.optionId, granted: false };
98
+ return null;
99
+ }
100
+ const str = (value) => typeof value === "string";
101
+ const TOOL_KINDS = new Set(["read", "edit", "delete", "move", "search", "execute", "think", "fetch", "switch_mode", "other"]);
102
+ const TOOL_STATUSES = new Set(["pending", "in_progress", "completed", "failed"]);
103
+ const PLAN_PRIORITIES = new Set(["high", "medium", "low"]);
104
+ const PLAN_STATUSES = new Set(["pending", "in_progress", "completed"]);
105
+ /** An optional field: absent, null, or of its type. */
106
+ const optional = (value, is) => value === undefined || value === null || is(value);
107
+ /** Each parsed wire object's number tokens, by key, exactly as written (acp-client's JSON.parse reviver records them). */
108
+ const NUMBER_SOURCES = new WeakMap();
109
+ /** The integer a JSON number token writes exactly (1e3, 1000.0, 0e-999 and 1.000…0 do), or null when it writes none. */
110
+ function exactInteger(source) {
111
+ const parts = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(source);
112
+ if (parts === null)
113
+ return null;
114
+ let digits = `${parts[2]}${parts[3] ?? ""}`.replace(/^0+/, "");
115
+ if (digits === "")
116
+ return 0n; // zero, at any exponent
117
+ const trailing = digits.length - digits.replace(/0+$/, "").length;
118
+ digits = digits.slice(0, digits.length - trailing);
119
+ const exponent = Number(parts[4] ?? "0") - (parts[3]?.length ?? 0) + trailing;
120
+ if (!Number.isSafeInteger(exponent) || exponent < 0)
121
+ return null; // a fraction, or no number at all
122
+ if (digits.length + exponent > 400)
123
+ return null; // far past any field's range
124
+ const exact = BigInt(digits) * 10n ** BigInt(exponent);
125
+ return parts[1] === "-" ? -exact : exact;
126
+ }
127
+ /** `holder[key]` as an integer in [min, max]: on the wire, exactly the integer its token writes; built in memory, a safe one. */
128
+ function wireInteger(holder, key, min, max) {
129
+ const value = holder[key];
130
+ if (typeof value !== "number")
131
+ return false;
132
+ // Every number the client reads came off the wire with its token: one without is refused, never taken as it reads.
133
+ const source = NUMBER_SOURCES.get(holder)?.get(key);
134
+ const exact = source === undefined ? null : exactInteger(source);
135
+ return exact !== null && exact >= min && exact <= max;
136
+ }
137
+ /** A number as the agent wrote it: past 2^53, its exact decimal string (when its token writes an integer). */
138
+ function exactNumber(holder, key, item) {
139
+ if (Number.isSafeInteger(item))
140
+ return item;
141
+ const source = NUMBER_SOURCES.get(holder)?.get(key);
142
+ const exact = source === undefined ? null : exactInteger(source);
143
+ return exact === null ? item : exact.toString();
144
+ }
145
+ /** `holder[key]`, copied with every integer JavaScript would round (past 2^53) as its exact decimal string, so what
146
+ * is shown and kept is what the agent said — at any depth, in objects and arrays alike (iteratively: no nesting can
147
+ * exhaust the stack). Every key, `__proto__` included, is the copy's own data, never an inherited field. */
148
+ function exactField(holder, key) {
149
+ const root = holder[key];
150
+ if (typeof root === "number")
151
+ return exactNumber(holder, key, root);
152
+ if (root === null || typeof root !== "object")
153
+ return root;
154
+ const fresh = (one) => (Array.isArray(one) ? [] : {});
155
+ const out = fresh(root);
156
+ const stack = [[root, out]];
157
+ for (let next = stack.pop(); next !== undefined; next = stack.pop()) {
158
+ const [source, target] = next;
159
+ const entries = Array.isArray(source) ? source.map((item, index) => [String(index), item]) : Object.entries(source);
160
+ for (const [name, item] of entries) {
161
+ let value = item;
162
+ if (typeof item === "number")
163
+ value = exactNumber(source, name, item);
164
+ else if (item !== null && typeof item === "object") {
165
+ value = fresh(item);
166
+ stack.push([item, value]);
167
+ }
168
+ Object.defineProperty(target, name, { value, enumerable: true, writable: true, configurable: true });
169
+ }
170
+ }
171
+ return out;
172
+ }
173
+ const optionalInteger = (holder, key, min, max) => holder[key] === undefined || holder[key] === null || wireInteger(holder, key, min, max);
174
+ const INT64 = [-(2n ** 63n), 2n ** 63n - 1n];
175
+ const UINT64 = [0n, 2n ** 64n - 1n];
176
+ const UINT32 = [0n, 2n ** 32n - 1n];
177
+ const INT32 = [-(2n ** 31n), 2n ** 31n - 1n];
178
+ const bool = (value) => typeof value === "boolean";
179
+ /** A list whose every entry passes. */
180
+ const listOf = (is) => (value) => Array.isArray(value) && value.every(is);
181
+ /** `_meta`, wherever the protocol allows it: absent, null, or an object. */
182
+ const meta = (value) => optional(value["_meta"], isRecord);
183
+ const ROLES = new Set(["user", "assistant"]);
184
+ const annotations = (value) => optional(value, one => isRecord(one) && meta(one)
185
+ && optional(one["audience"], list => Array.isArray(list) && list.every(role => str(role) && ROLES.has(role)))
186
+ && optional(one["priority"], n => typeof n === "number" && Number.isFinite(n))
187
+ && optional(one["lastModified"], str));
188
+ /** One ACP content block, every field the protocol defines for its type checked (text, image, audio, a resource link, an embedded resource). */
189
+ function wellFormedContent(block) {
190
+ if (!isRecord(block) || !str(block["type"]) || !annotations(block["annotations"]) || !meta(block))
191
+ return false;
192
+ switch (block["type"]) {
193
+ case "text": return str(block["text"]);
194
+ case "image": return str(block["data"]) && str(block["mimeType"]) && optional(block["uri"], str);
195
+ case "audio": return str(block["data"]) && str(block["mimeType"]);
196
+ case "resource_link":
197
+ return str(block["uri"]) && str(block["name"]) && optional(block["mimeType"], str) && optional(block["title"], str)
198
+ && optional(block["description"], str) && optionalInteger(block, "size", ...INT64);
199
+ case "resource": {
200
+ // An embedded resource is one of two shapes — text contents or blob contents — beside its uri; a field the
201
+ // matched shape doesn't define is the agent's own, and isn't read.
202
+ const resource = block["resource"];
203
+ return isRecord(resource) && meta(resource) && str(resource["uri"]) && (str(resource["text"]) || str(resource["blob"]))
204
+ && optional(resource["mimeType"], str);
205
+ }
206
+ default: return false;
207
+ }
208
+ }
209
+ /** A tool call's content: content blocks, diffs, or terminals. */
210
+ function wellFormedToolContent(item) {
211
+ if (!isRecord(item) || !str(item["type"]) || !meta(item))
212
+ return false;
213
+ if (item["type"] === "content")
214
+ return wellFormedContent(item["content"]);
215
+ if (item["type"] === "diff")
216
+ return str(item["path"]) && str(item["newText"]) && (item["oldText"] === undefined || item["oldText"] === null || str(item["oldText"]));
217
+ if (item["type"] === "terminal")
218
+ return str(item["terminalId"]);
219
+ return false;
220
+ }
221
+ /** The fields a tool call or its update may carry: each, when present, of the type the protocol gives it. */
222
+ /** `initial`: a tool call's first report, whose fields are never null when present; an update's may be (unchanged). */
223
+ function wellFormedToolFields(update, initial = false) {
224
+ if (!meta(update))
225
+ return false;
226
+ const field = (key, is) => initial ? absentOr(update[key], is) : optional(update[key], is);
227
+ if (!field("title", str))
228
+ return false;
229
+ if (!field("kind", one => str(one) && TOOL_KINDS.has(one)))
230
+ return false;
231
+ if (!field("status", one => str(one) && TOOL_STATUSES.has(one)))
232
+ return false;
233
+ if (!field("locations", list => Array.isArray(list) && list.every(one => isRecord(one) && meta(one) && str(one["path"]) && optionalInteger(one, "line", ...UINT32))))
234
+ return false;
235
+ if (!optional(update["name"], str))
236
+ return false;
237
+ if (update["content"] !== undefined && update["content"] !== null && !(Array.isArray(update["content"]) && update["content"].every(wellFormedToolContent)))
238
+ return false;
239
+ return true;
240
+ }
241
+ /** A session config option, every field its schema gives (a select's current value and choices, or groups of them). */
242
+ const configChoice = (one) => isRecord(one) && meta(one) && str(one["value"]) && str(one["name"]) && optional(one["description"], str);
243
+ const configGroup = (one) => isRecord(one) && meta(one) && str(one["group"]) && str(one["name"]) && listOf(configChoice)(one["options"]);
244
+ function wellFormedConfigOption(option) {
245
+ if (!isRecord(option) || !meta(option) || !str(option["id"]) || !str(option["name"]) || !optional(option["description"], str)
246
+ || !optional(option["category"], str) || !str(option["type"]))
247
+ return false;
248
+ if (option["type"] !== "select")
249
+ return false;
250
+ const choices = option["options"];
251
+ return str(option["currentValue"]) && Array.isArray(choices) && (choices.every(configChoice) || choices.every(configGroup));
252
+ }
253
+ const wellFormedConfigOptions = listOf(wellFormedConfigOption);
254
+ const sessionModes = (value) => isRecord(value) && meta(value) && str(value["currentModeId"])
255
+ && listOf(mode => isRecord(mode) && meta(mode) && str(mode["id"]) && str(mode["name"]) && optional(mode["description"], str))(value["availableModes"]);
256
+ const sessionModels = (value) => isRecord(value) && meta(value) && str(value["currentModelId"])
257
+ && listOf(model => isRecord(model) && meta(model) && str(model["modelId"]) && str(model["name"]) && optional(model["description"], str))(value["availableModels"]);
258
+ /** A field that may be left out but, when present, is never null. */
259
+ const absentOr = (value, is) => value === undefined || is(value);
260
+ const capabilities = (value, flags) => absentOr(value, one => isRecord(one) && meta(one) && flags.every(flag => absentOr(one[flag], bool)));
261
+ /** The session capabilities the schema names (each an optional, nullable object); others are left uninterpreted. */
262
+ const SESSION_CAPABILITIES = ["list", "delete", "additionalDirectories", "resume", "close"];
263
+ /** initialize's answer, every field the schema gives it. */
264
+ function wellFormedInitialize(result) {
265
+ const info = result["agentInfo"];
266
+ if (!meta(result) || !isRecord(info) || !meta(info) || !str(info["name"]) || !str(info["version"]) || !optional(info["title"], str))
267
+ return false;
268
+ if (!absentOr(result["authMethods"], listOf(method => isRecord(method) && meta(method) && str(method["id"]) && str(method["name"]) && optional(method["description"], str))))
269
+ return false;
270
+ return absentOr(result["agentCapabilities"], caps => isRecord(caps) && meta(caps) && absentOr(caps["loadSession"], bool)
271
+ && capabilities(caps["promptCapabilities"], ["image", "audio", "embeddedContext"])
272
+ && capabilities(caps["mcpCapabilities"], ["http", "sse"])
273
+ && absentOr(caps["sessionCapabilities"], sessions => isRecord(sessions) && meta(sessions)
274
+ && SESSION_CAPABILITIES.every(name => optional(sessions[name], entry => isRecord(entry) && meta(entry))))
275
+ && absentOr(caps["auth"], auth => isRecord(auth) && meta(auth) && optional(auth["logout"], logout => isRecord(logout) && meta(logout))));
276
+ }
277
+ /** session/new's answer: the id, and every field of the modes, models and config options it may carry. */
278
+ function wellFormedNewSession(result) {
279
+ return meta(result) && optional(result["configOptions"], wellFormedConfigOptions) && optional(result["modes"], sessionModes) && optional(result["models"], sessionModels);
280
+ }
281
+ /** Whether a known update kind carries what it must; an unknown kind is left alone (not shown, not trusted). */
282
+ function wellFormedUpdate(kind, update) {
283
+ const id = (key) => str(update[key]) && update[key] !== "" && update[key].length <= 256;
284
+ switch (kind) {
285
+ case "agent_message_chunk":
286
+ case "agent_thought_chunk":
287
+ case "user_message_chunk":
288
+ return wellFormedContent(update["content"]) && optional(update["messageId"], str);
289
+ case "tool_call":
290
+ // The first report of a call carries its content as a list when it carries any (null is for updates).
291
+ return id("toolCallId") && str(update["title"]) && update["content"] !== null && wellFormedToolFields(update, true);
292
+ case "tool_call_update":
293
+ return id("toolCallId") && wellFormedToolFields(update);
294
+ case "plan":
295
+ return Array.isArray(update["entries"]) && update["entries"].every(one => isRecord(one) && meta(one) && str(one["content"]) && str(one["priority"]) && PLAN_PRIORITIES.has(one["priority"]) && str(one["status"]) && PLAN_STATUSES.has(one["status"]));
296
+ case "available_commands_update":
297
+ return Array.isArray(update["availableCommands"]) && update["availableCommands"].every(one => isRecord(one) && meta(one) && str(one["name"]) && str(one["description"])
298
+ && optional(one["input"], input => isRecord(input) && meta(input) && str(input["hint"])));
299
+ case "current_mode_update":
300
+ return str(update["currentModeId"]);
301
+ case "config_option_update":
302
+ return wellFormedConfigOptions(update["configOptions"]);
303
+ case "session_info_update":
304
+ return optional(update["title"], str) && optional(update["updatedAt"], str);
305
+ case "usage_update":
306
+ return wireInteger(update, "used", ...UINT64) && wireInteger(update, "size", ...UINT64)
307
+ && optional(update["cost"], cost => isRecord(cost) && meta(cost) && typeof cost["amount"] === "number" && Number.isFinite(cost["amount"]) && str(cost["currency"]));
308
+ case "notice":
309
+ return str(update["severity"]) && str(update["title"]) && update["title"] !== "" && optional(update["description"], str);
310
+ case "compaction_update":
311
+ return str(update["compactionId"]) && str(update["status"]) && optional(update["summary"], listOf(wellFormedContent)) && optional(update["error"], str);
312
+ case "compaction_summary_chunk":
313
+ return str(update["compactionId"]) && wellFormedContent(update["content"]);
314
+ default:
315
+ return true;
316
+ }
317
+ }
318
+ /** The update kinds passed on to the live view: the ones the plane knows and never declined. Notices and compaction,
319
+ * which this client does not advertise, are validated when sent anyway, and go no further. */
320
+ const KNOWN_UPDATES = new Set(["agent_message_chunk", "agent_thought_chunk", "user_message_chunk", "tool_call", "tool_call_update", "plan",
321
+ "available_commands_update", "current_mode_update", "config_option_update", "session_info_update", "usage_update"]);
322
+ const AUTH_REQUIRED = -32000;
323
+ export function createAcpConversation(options) {
324
+ let phase = "initialize";
325
+ let failure = null;
326
+ let terminal = null;
327
+ let sessionId = null;
328
+ let agent = null;
329
+ let canClose = false;
330
+ let closeId = null;
331
+ let closeDone = false;
332
+ let mechanism = null;
333
+ let offered = [];
334
+ let nextId = 0;
335
+ /** Set once the plane asked the prompt to stop: from then on every permission request is answered "cancelled". */
336
+ let cancelled = false;
337
+ /** Our requests not yet answered, by id. */
338
+ const pending = new Map();
339
+ /** Ids we have seen answered — a second answer to any of them is a protocol failure. */
340
+ const answered = new Set();
341
+ /** The words since the last tool call: the agent's last message. */
342
+ let message = "";
343
+ /** The agent's own accounts of what it used: the prompt answer's `usage`, and the last `usage_update`. Never priced. */
344
+ let promptUsage = null;
345
+ let usageUpdate = null;
346
+ const retained = [];
347
+ const keep = (record) => { retained.push(line(record)); };
348
+ const fail = (kind, text) => {
349
+ if (failure === null) {
350
+ failure = { kind, text: bounded(text, TEXT_CAP) };
351
+ keep({ type: "acp.failure", kind, text: failure.text });
352
+ }
353
+ phase = "failed";
354
+ return { send: [], live: [] };
355
+ };
356
+ const request = (method, params, purpose) => {
357
+ const id = ++nextId;
358
+ pending.set(id, purpose);
359
+ return line({ jsonrpc: "2.0", id, method, params });
360
+ };
361
+ /** A reply under the request's id, written as its token: an integer exactly (int64, past 2^53 too), a string, or null. */
362
+ const reply = (idToken, outcome) => {
363
+ const body = line({ jsonrpc: "2.0", ...outcome });
364
+ return `{"jsonrpc":"2.0","id":${idToken},${body.slice('{"jsonrpc":"2.0",'.length)}`;
365
+ };
366
+ const promptLine = () => {
367
+ phase = "prompt";
368
+ return request("session/prompt", { sessionId, prompt: [{ type: "text", text: options.prompt }] }, "prompt");
369
+ };
370
+ const onResult = (purpose, result, error) => {
371
+ const errorText = error === null ? "" : typeof error["message"] === "string" ? error["message"] : "the agent answered with an error";
372
+ const errorCode = error === null ? null : typeof error["code"] === "number" ? error["code"] : null;
373
+ if (purpose === "close") {
374
+ closeDone = true;
375
+ // An error to close is the agent's to have; an answer must be the empty object the schema gives it.
376
+ if (error === null && !(isRecord(result) && meta(result)))
377
+ return fail("protocol", "session/close was answered with something other than an object");
378
+ return { send: [], live: [] };
379
+ }
380
+ if (purpose === "prompt") {
381
+ if (error !== null) {
382
+ terminal = { error: { code: errorCode, message: bounded(errorText, TEXT_CAP) } };
383
+ keep({ type: "acp.terminal", error: terminal.error });
384
+ if (errorCode === AUTH_REQUIRED)
385
+ return fail("auth-required", errorText);
386
+ phase = "ended";
387
+ return { send: [], live: [] };
388
+ }
389
+ const stop = isRecord(result) ? result["stopReason"] : undefined;
390
+ if (typeof stop !== "string" || !STOP_REASONS.has(stop))
391
+ return fail("protocol", "the prompt's answer carried no known stop reason");
392
+ if (!isRecord(result) || !meta(result))
393
+ return fail("protocol", "the prompt's answer carried a malformed _meta");
394
+ terminal = { stopReason: stop };
395
+ if (isRecord(result) && result["usage"] !== undefined)
396
+ promptUsage = exactField(result, "usage");
397
+ if (message !== "")
398
+ keep({ type: "acp.message", content: bounded(message, MESSAGE_CAP) });
399
+ keep({ type: "acp.terminal", stopReason: stop });
400
+ phase = "ended";
401
+ return { send: [], live: [] };
402
+ }
403
+ if (error !== null) {
404
+ if (errorCode === AUTH_REQUIRED)
405
+ return fail("auth-required", errorText);
406
+ return fail(purpose === "model" ? "model-unavailable" : "protocol", `${purpose}: ${errorText}`);
407
+ }
408
+ if (!isRecord(result))
409
+ return fail("protocol", `${purpose}: the answer was not an object`);
410
+ if (purpose === "initialize") {
411
+ if (!wireInteger(result, "protocolVersion", 0n, 65535n) || result["protocolVersion"] !== PROTOCOL_VERSION)
412
+ return fail("protocol", `the agent speaks protocol version ${String(result["protocolVersion"])}, not ${PROTOCOL_VERSION}`);
413
+ const info = result["agentInfo"];
414
+ if (!isRecord(info) || typeof info["name"] !== "string" || typeof info["version"] !== "string")
415
+ return fail("identity", "the agent did not say its name and version");
416
+ if (!wellFormedInitialize(result))
417
+ return fail("protocol", "initialize was answered with fields the protocol doesn't allow");
418
+ agent = { name: bounded(info["name"], 128), version: bounded(info["version"], 64) };
419
+ if (options.expect !== null && (agent.name !== options.expect.name || agent.version !== options.expect.version)) {
420
+ return fail("identity", `the agent is ${agent.name} ${agent.version}, not the checked ${options.expect.name} ${options.expect.version}`);
421
+ }
422
+ const capabilities = result["agentCapabilities"];
423
+ const sessions = isRecord(capabilities) ? capabilities["sessionCapabilities"] : undefined;
424
+ canClose = isRecord(sessions) && isRecord(sessions["close"]);
425
+ keep({ type: "acp.agent", name: agent.name, version: agent.version });
426
+ phase = "session";
427
+ return { send: [request("session/new", { cwd: options.cwd, mcpServers: options.mcpServers ?? [] }, "session/new")], live: [] };
428
+ }
429
+ if (purpose === "session/new") {
430
+ const id = result["sessionId"];
431
+ if (typeof id !== "string" || id.trim() === "" || id.length > 256)
432
+ return fail("protocol", "the session had no id");
433
+ if (!wellFormedNewSession(result))
434
+ return fail("protocol", "session/new was answered with fields the protocol doesn't allow");
435
+ sessionId = id;
436
+ keep({ type: "acp.session", sessionId: id });
437
+ const offer = modelOffer(result);
438
+ if (offer === null)
439
+ return fail("model-unavailable", "the agent offers no way to choose a model");
440
+ mechanism = offer.mechanism;
441
+ // An agent can offer many hundreds (OpenCode lists every model of every provider it knows): kept bounded, and
442
+ // the model asked for is always among them when offered.
443
+ offered = offer.offered.filter(one => one.length <= 200).slice(0, ACP_OFFERED_CAP);
444
+ if (offer.offered.includes(options.model) && !offered.includes(options.model))
445
+ offered.push(options.model);
446
+ keep({ type: "acp.offered", models: offered });
447
+ if (offer.mechanism !== "config-option")
448
+ return fail("model-unavailable", "the agent can't confirm which model it uses (it offers only session/set_model)");
449
+ if (!offer.offered.includes(options.model))
450
+ return fail("model-unavailable", `the agent doesn't offer ${options.model}`);
451
+ phase = "model";
452
+ return { send: [request("session/set_config_option", { sessionId: id, configId: "model", value: options.model }, "model")], live: [] };
453
+ }
454
+ // purpose === "model": the answer must be a well-formed set of config options, showing the model current.
455
+ if (!meta(result) || !wellFormedConfigOptions(result["configOptions"]))
456
+ return fail("model-unavailable", "the model choice was answered with malformed config options");
457
+ const chosen = modelOffer(result);
458
+ if (chosen === null || chosen.mechanism !== "config-option" || chosen.current !== options.model)
459
+ return fail("model-unavailable", `the agent did not make ${options.model} current`);
460
+ keep({ type: "acp.model", model: options.model, mechanism });
461
+ return { send: [promptLine()], live: [] };
462
+ };
463
+ const onUpdate = (params) => {
464
+ if (!isRecord(params) || !isRecord(params["update"]))
465
+ return fail("protocol", "a session update without an update");
466
+ if (sessionId === null || params["sessionId"] !== sessionId)
467
+ return fail("protocol", "an update for another session");
468
+ const update = params["update"];
469
+ const kind = update["sessionUpdate"];
470
+ if (typeof kind !== "string" || kind === "" || kind.length > 64)
471
+ return fail("protocol", "a session update without a kind");
472
+ if (!meta(update) || !meta(params))
473
+ return fail("protocol", "a session update whose _meta isn't an object");
474
+ if (!wellFormedUpdate(kind, update))
475
+ return fail("protocol", `a malformed ${kind} update`);
476
+ if (kind === "agent_message_chunk") {
477
+ const content = update["content"];
478
+ if (isRecord(content) && content["type"] === "text" && typeof content["text"] === "string" && message.length < MESSAGE_CAP) {
479
+ message = bounded(message + content["text"], MESSAGE_CAP);
480
+ }
481
+ }
482
+ else if (kind === "tool_call") {
483
+ message = "";
484
+ }
485
+ else if (kind === "usage_update") {
486
+ usageUpdate = exactField(params, "update");
487
+ }
488
+ return { send: [], live: KNOWN_UPDATES.has(kind) ? [exactField(params, "update")] : [] };
489
+ };
490
+ /** One line from the agent: what to send back, and what to show. */
491
+ const receiveLine = (raw) => {
492
+ if (raw.length > ACP_LINE_CAP || Buffer.byteLength(raw, "utf8") > ACP_LINE_CAP)
493
+ return fail("protocol", "the agent wrote a line too long to read");
494
+ if (raw.trim() === "")
495
+ return { send: [], live: [] };
496
+ let incoming;
497
+ try {
498
+ // Every number's token is kept beside it: where the client reads an integer (a location's line, a resource's
499
+ // size, an error's code, a usage count), it is exactly the integer on the wire — 1e3 is 1000, but
500
+ // 1.0000000000000001 is not 1. Opaque data (raw input and output, metadata) is never read, so never judged.
501
+ incoming = JSON.parse(raw, function (key, value, context) {
502
+ if (typeof value === "number" && context?.source !== undefined && this !== null && typeof this === "object") {
503
+ const holder = this;
504
+ const sources = NUMBER_SOURCES.get(holder) ?? new Map();
505
+ sources.set(key, context.source);
506
+ NUMBER_SOURCES.set(holder, sources);
507
+ }
508
+ return value;
509
+ });
510
+ }
511
+ catch {
512
+ return fail("protocol", "the agent wrote a line that is not JSON");
513
+ }
514
+ if (!isRecord(incoming) || incoming["jsonrpc"] !== "2.0")
515
+ return fail("protocol", "the agent wrote something that is not JSON-RPC 2.0");
516
+ // The message's own id is read by its token, as an exact integer (4, 4e0 and 4.0 are one id; 1.0000000000000001
517
+ // is none), so no reply goes out under, and no answer is taken for, an id other than the one on the wire.
518
+ const hasId = Object.prototype.hasOwnProperty.call(incoming, "id");
519
+ const id = incoming["id"];
520
+ let idToken = null;
521
+ let idNumber = null;
522
+ if (typeof id === "number") {
523
+ const source = NUMBER_SOURCES.get(incoming)?.get("id");
524
+ const exact = source === undefined ? null : exactInteger(source);
525
+ if (exact === null || exact < INT64[0] || exact > INT64[1])
526
+ return fail("protocol", "a message whose id isn't an integer");
527
+ idToken = exact.toString();
528
+ if (BigInt(Number(exact)) === exact && Number.isSafeInteger(Number(exact)))
529
+ idNumber = Number(exact);
530
+ }
531
+ else if (typeof id === "string" && id.length <= 256)
532
+ idToken = JSON.stringify(id);
533
+ else if (id === null && hasId)
534
+ idToken = "null";
535
+ const method = incoming["method"];
536
+ if (typeof method === "string") {
537
+ const params = incoming["params"];
538
+ // Anything that names a session must name ours.
539
+ if (isRecord(params) && params["sessionId"] !== undefined && params["sessionId"] !== sessionId)
540
+ return fail("protocol", "a message for another session");
541
+ if (!hasId) {
542
+ // A notification. Only session updates mean anything; the rest are ignored.
543
+ return method === "session/update" ? onUpdate(params) : { send: [], live: [] };
544
+ }
545
+ // An id we answer under goes back exactly as sent: an integer, a bounded string, or null.
546
+ if (idToken === null)
547
+ return fail("protocol", "an agent request with a malformed id");
548
+ if (method === "session/request_permission") {
549
+ if (!isRecord(params) || sessionId === null || params["sessionId"] !== sessionId)
550
+ return fail("protocol", "a permission request for another session");
551
+ const call = params["toolCall"];
552
+ // The tool call it asks about must be as well formed as one sent in an update, or nothing is granted.
553
+ const answer = isRecord(call) && typeof call["toolCallId"] === "string" && call["toolCallId"] !== "" && call["toolCallId"].length <= 256 && wellFormedToolFields(call)
554
+ ? permissionAnswer(params)
555
+ : null;
556
+ const step = {
557
+ sessionUpdate: "permission",
558
+ toolCallId: isRecord(call) && typeof call["toolCallId"] === "string" ? call["toolCallId"] : null,
559
+ title: isRecord(call) && typeof call["title"] === "string" ? call["title"] : null,
560
+ kind: isRecord(call) && typeof call["kind"] === "string" ? call["kind"] : null,
561
+ };
562
+ // Granted only while our prompt runs and was not cancelled: before the model is confirmed, after the answer,
563
+ // or after a cancel, the request is answered "cancelled".
564
+ if (cancelled || phase !== "prompt") {
565
+ return { send: [reply(idToken, { result: { outcome: { outcome: "cancelled" } } })], live: [{ ...step, granted: false }] };
566
+ }
567
+ if (answer === null) {
568
+ return { send: [reply(idToken, { error: { code: -32602, message: "a permission request needs a tool call and an option this client can choose" } })], live: [{ ...step, granted: false }] };
569
+ }
570
+ return {
571
+ send: [reply(idToken, { result: { outcome: { outcome: "selected", optionId: answer.optionId } } })],
572
+ live: [{ ...step, granted: answer.granted }],
573
+ };
574
+ }
575
+ return { send: [reply(idToken, { error: { code: -32601, message: "this client offers no such method" } })], live: [] };
576
+ }
577
+ // A response to one of ours.
578
+ if (idNumber === null)
579
+ return fail("protocol", "a response with an id the plane never sent");
580
+ if (answered.has(idNumber))
581
+ return fail("protocol", "a second answer to the same request");
582
+ const purpose = pending.get(idNumber);
583
+ if (purpose === undefined)
584
+ return fail("protocol", "a response with an id the plane never sent");
585
+ pending.delete(idNumber);
586
+ answered.add(idNumber);
587
+ const hasResult = Object.prototype.hasOwnProperty.call(incoming, "result");
588
+ const hasError = Object.prototype.hasOwnProperty.call(incoming, "error");
589
+ if (hasResult === hasError)
590
+ return fail("protocol", "a response must carry a result or an error, not both or neither");
591
+ const error = incoming["error"];
592
+ if (hasError && !(isRecord(error) && wireInteger(error, "code", ...INT32) && typeof error["message"] === "string")) {
593
+ return fail("protocol", "a response with a malformed error");
594
+ }
595
+ return onResult(purpose, incoming["result"], hasError ? error : null);
596
+ };
597
+ return {
598
+ start() {
599
+ return [request("initialize", {
600
+ protocolVersion: PROTOCOL_VERSION,
601
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
602
+ }, "initialize")];
603
+ },
604
+ receive(raw) {
605
+ if (phase === "failed")
606
+ return { send: [], live: [] };
607
+ // Nothing the agent writes may throw out of the client: whatever does is the agent's protocol failure.
608
+ try {
609
+ return receiveLine(raw);
610
+ }
611
+ catch (error) {
612
+ return fail("protocol", `the agent wrote a message the client couldn't read (${error instanceof Error ? error.name : "error"})`);
613
+ }
614
+ },
615
+ cancel() {
616
+ if (phase !== "prompt" || sessionId === null)
617
+ return [];
618
+ cancelled = true;
619
+ return [line({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } })];
620
+ },
621
+ refuse(text) {
622
+ fail("protocol", text);
623
+ },
624
+ close() {
625
+ if (!canClose || sessionId === null || closeId !== null)
626
+ return [];
627
+ const sent = request("session/close", { sessionId }, "close");
628
+ closeId = nextId;
629
+ return [sent];
630
+ },
631
+ closeAnswered: () => closeDone || phase === "failed",
632
+ phase: () => phase,
633
+ failure: () => failure,
634
+ terminal: () => terminal,
635
+ sessionId: () => sessionId,
636
+ agent: () => agent,
637
+ mechanism: () => mechanism,
638
+ offered: () => offered.slice(),
639
+ retained: () => {
640
+ // The agent's own usage accounts ride every outcome — a failure spent too.
641
+ const usage = promptUsage === null && usageUpdate === null ? [] : [line({ type: "acp.reported", usage: bounded(JSON.stringify({ prompt: promptUsage, update: usageUpdate }), USAGE_CAP) })];
642
+ const all = [...retained, ...usage];
643
+ return all.length === 0 ? "" : `${all.join("\n")}\n`;
644
+ },
645
+ };
646
+ }
647
+ /** Read a conversation's retained record. Lines not typed `acp.*` are ignored. */
648
+ export function acpParse(stdout) {
649
+ let agentSeen = false;
650
+ let sessionId = null;
651
+ let conflict = false;
652
+ let finalMessage = null;
653
+ let reportedUsage = null;
654
+ let stop = null;
655
+ let rpcError = null;
656
+ let terminals = 0;
657
+ let failure = null;
658
+ let offered = [];
659
+ let agent = null;
660
+ let exit = null;
661
+ for (const raw of stdout.split("\n")) {
662
+ if (raw.trim() === "")
663
+ continue;
664
+ let event;
665
+ try {
666
+ event = JSON.parse(raw);
667
+ }
668
+ catch {
669
+ continue;
670
+ }
671
+ if (!isRecord(event))
672
+ continue;
673
+ const type = event["type"];
674
+ if (type === "acp.agent") {
675
+ agentSeen = true;
676
+ if (agent === null && typeof event["name"] === "string" && typeof event["version"] === "string")
677
+ agent = { name: event["name"], version: event["version"] };
678
+ }
679
+ else if (type === "acp.session" && typeof event["sessionId"] === "string") {
680
+ if (sessionId === null)
681
+ sessionId = event["sessionId"];
682
+ else if (sessionId !== event["sessionId"])
683
+ conflict = true;
684
+ }
685
+ else if (type === "acp.message" && typeof event["content"] === "string")
686
+ finalMessage = event["content"];
687
+ else if (type === "acp.reported" && typeof event["usage"] === "string")
688
+ reportedUsage = event["usage"];
689
+ else if (type === "acp.terminal") {
690
+ terminals += 1;
691
+ if (typeof event["stopReason"] === "string")
692
+ stop = event["stopReason"];
693
+ else if (isRecord(event["error"])) {
694
+ const error = event["error"];
695
+ rpcError = { code: typeof error["code"] === "number" ? error["code"] : null, message: typeof error["message"] === "string" ? error["message"] : "" };
696
+ }
697
+ }
698
+ else if (type === "acp.offered" && Array.isArray(event["models"])) {
699
+ offered = event["models"].filter((one) => typeof one === "string").slice(0, ACP_OFFERED_CAP + 1);
700
+ }
701
+ else if (type === "acp.exit" && typeof event["how"] === "string" && ["eof", "term", "kill", "self", "error"].includes(event["how"])) {
702
+ exit = { how: event["how"], leftover: event["leftover"] === true, timedOut: event["timedOut"] === true };
703
+ }
704
+ else if (type === "acp.failure" && failure === null) {
705
+ failure = { kind: typeof event["kind"] === "string" ? event["kind"] : "protocol", text: typeof event["text"] === "string" ? event["text"] : "" };
706
+ }
707
+ }
708
+ const protocolError = conflict
709
+ ? "the agent named two sessions"
710
+ : terminals > 1
711
+ ? "the prompt was answered twice"
712
+ : failure !== null && failure.kind === "protocol"
713
+ ? `the agent broke the protocol: ${failure.text}`
714
+ : null;
715
+ const success = protocolError === null && failure === null && terminals === 1 && stop === "end_turn";
716
+ const diagnostic = failure?.text ?? rpcError?.message ?? null;
717
+ const code = failure !== null
718
+ ? failure.kind
719
+ : rpcError !== null
720
+ ? `rpc:${rpcError.code ?? "?"}`
721
+ : stop !== null && stop !== "end_turn"
722
+ ? stop
723
+ : null;
724
+ return {
725
+ sessionId: conflict ? null : sessionId,
726
+ protocolError,
727
+ finalMessage,
728
+ reportedUsage,
729
+ initObserved: agentSeen && sessionId !== null,
730
+ promptConsumed: success,
731
+ diagnostic: diagnostic === "" ? null : diagnostic,
732
+ terminal: success || code === null ? null : { failed: true, text: diagnostic === "" ? null : diagnostic, code },
733
+ offered,
734
+ exit,
735
+ agent,
736
+ stop: terminals === 1 ? stop : null,
737
+ };
738
+ }