@amalgm/chat 0.1.2 → 0.2.0

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 (286) hide show
  1. package/AGENTS.md +98 -0
  2. package/PURPOSE.md +143 -112
  3. package/README.md +82 -54
  4. package/dist/acp/index.d.ts +7 -0
  5. package/dist/acp/index.d.ts.map +1 -0
  6. package/dist/acp/index.js +7 -0
  7. package/dist/acp/index.js.map +1 -0
  8. package/dist/acp/projection.d.ts +30 -0
  9. package/dist/acp/projection.d.ts.map +1 -0
  10. package/dist/acp/projection.js +147 -0
  11. package/dist/acp/projection.js.map +1 -0
  12. package/dist/api/conversations.d.ts +66 -0
  13. package/dist/api/conversations.d.ts.map +1 -0
  14. package/dist/api/conversations.js +202 -0
  15. package/dist/api/conversations.js.map +1 -0
  16. package/dist/api/index.d.ts +3 -3
  17. package/dist/api/index.d.ts.map +1 -1
  18. package/dist/api/index.js +1 -2
  19. package/dist/api/index.js.map +1 -1
  20. package/dist/conversations/index.d.ts +3 -0
  21. package/dist/conversations/index.d.ts.map +1 -0
  22. package/dist/conversations/index.js +3 -0
  23. package/dist/conversations/index.js.map +1 -0
  24. package/dist/conversations/transcript.d.ts +13 -0
  25. package/dist/conversations/transcript.d.ts.map +1 -0
  26. package/dist/conversations/transcript.js +171 -0
  27. package/dist/conversations/transcript.js.map +1 -0
  28. package/dist/conversations/types.d.ts +147 -0
  29. package/dist/conversations/types.d.ts.map +1 -0
  30. package/dist/conversations/types.js +9 -0
  31. package/dist/conversations/types.js.map +1 -0
  32. package/dist/events/constructors.d.ts +9 -15
  33. package/dist/events/constructors.d.ts.map +1 -1
  34. package/dist/events/constructors.js +6 -12
  35. package/dist/events/constructors.js.map +1 -1
  36. package/dist/events/index.d.ts +2 -3
  37. package/dist/events/index.d.ts.map +1 -1
  38. package/dist/events/index.js +2 -3
  39. package/dist/events/index.js.map +1 -1
  40. package/dist/events/schema.d.ts +2 -5
  41. package/dist/events/schema.d.ts.map +1 -1
  42. package/dist/events/schema.js +2 -5
  43. package/dist/events/schema.js.map +1 -1
  44. package/dist/execution/contract.d.ts +51 -0
  45. package/dist/execution/contract.d.ts.map +1 -0
  46. package/dist/execution/contract.js +127 -0
  47. package/dist/execution/contract.js.map +1 -0
  48. package/dist/execution/index.d.ts +6 -0
  49. package/dist/execution/index.d.ts.map +1 -0
  50. package/dist/execution/index.js +4 -0
  51. package/dist/execution/index.js.map +1 -0
  52. package/dist/execution/revision.d.ts +5 -0
  53. package/dist/execution/revision.d.ts.map +1 -0
  54. package/dist/execution/revision.js +17 -0
  55. package/dist/execution/revision.js.map +1 -0
  56. package/dist/execution/store.d.ts +26 -0
  57. package/dist/execution/store.d.ts.map +1 -0
  58. package/dist/execution/store.js +38 -0
  59. package/dist/execution/store.js.map +1 -0
  60. package/dist/index.d.ts +8 -25
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +7 -25
  63. package/dist/index.js.map +1 -1
  64. package/dist/input/index.d.ts +3 -0
  65. package/dist/input/index.d.ts.map +1 -0
  66. package/dist/input/index.js +2 -0
  67. package/dist/input/index.js.map +1 -0
  68. package/dist/input/prompt.d.ts +17 -0
  69. package/dist/input/prompt.d.ts.map +1 -0
  70. package/dist/input/prompt.js +104 -0
  71. package/dist/input/prompt.js.map +1 -0
  72. package/dist/normalizers/claude.d.ts +6 -9
  73. package/dist/normalizers/claude.d.ts.map +1 -1
  74. package/dist/normalizers/claude.js +5 -8
  75. package/dist/normalizers/claude.js.map +1 -1
  76. package/dist/normalizers/codex.d.ts +5 -8
  77. package/dist/normalizers/codex.d.ts.map +1 -1
  78. package/dist/normalizers/codex.js +4 -7
  79. package/dist/normalizers/codex.js.map +1 -1
  80. package/dist/normalizers/cursor.d.ts +1 -2
  81. package/dist/normalizers/cursor.d.ts.map +1 -1
  82. package/dist/normalizers/cursor.js +2 -3
  83. package/dist/normalizers/cursor.js.map +1 -1
  84. package/dist/normalizers/index.d.ts +3 -5
  85. package/dist/normalizers/index.d.ts.map +1 -1
  86. package/dist/normalizers/index.js +3 -5
  87. package/dist/normalizers/index.js.map +1 -1
  88. package/dist/normalizers/opencode.d.ts +2 -4
  89. package/dist/normalizers/opencode.d.ts.map +1 -1
  90. package/dist/normalizers/opencode.js +2 -4
  91. package/dist/normalizers/opencode.js.map +1 -1
  92. package/dist/normalizers/pi.d.ts +3 -4
  93. package/dist/normalizers/pi.d.ts.map +1 -1
  94. package/dist/normalizers/pi.js +3 -4
  95. package/dist/normalizers/pi.js.map +1 -1
  96. package/dist/sessions/chat.d.ts +176 -0
  97. package/dist/sessions/chat.d.ts.map +1 -0
  98. package/dist/sessions/chat.js +701 -0
  99. package/dist/sessions/chat.js.map +1 -0
  100. package/dist/sessions/index.d.ts +3 -0
  101. package/dist/sessions/index.d.ts.map +1 -0
  102. package/dist/sessions/index.js +2 -0
  103. package/dist/sessions/index.js.map +1 -0
  104. package/dist/transcript/index.d.ts +1 -1
  105. package/dist/transcript/index.js +1 -1
  106. package/dist/transcript/message-reducer.d.ts +1 -1
  107. package/dist/transcript/message-reducer.js +1 -1
  108. package/dist/transcript/part-accumulator.d.ts +1 -1
  109. package/dist/transcript/part-accumulator.js +1 -1
  110. package/dist/transcript/tool-display.d.ts +1 -1
  111. package/dist/transcript/tool-display.js +1 -1
  112. package/dist/transcript/tool-shape.d.ts +1 -1
  113. package/dist/transcript/tool-shape.js +1 -1
  114. package/dist/usage/finalize.d.ts +7 -9
  115. package/dist/usage/finalize.d.ts.map +1 -1
  116. package/dist/usage/finalize.js +11 -13
  117. package/dist/usage/finalize.js.map +1 -1
  118. package/dist/usage/index.d.ts +2 -3
  119. package/dist/usage/index.d.ts.map +1 -1
  120. package/dist/usage/index.js +2 -3
  121. package/dist/usage/index.js.map +1 -1
  122. package/dist/usage/model-catalog.d.ts +2 -3
  123. package/dist/usage/model-catalog.d.ts.map +1 -1
  124. package/dist/usage/model-catalog.js +10 -3
  125. package/dist/usage/model-catalog.js.map +1 -1
  126. package/dist/usage/normalize.d.ts +5 -7
  127. package/dist/usage/normalize.d.ts.map +1 -1
  128. package/dist/usage/normalize.js +3 -5
  129. package/dist/usage/normalize.js.map +1 -1
  130. package/dist/usage/ports.d.ts +14 -24
  131. package/dist/usage/ports.d.ts.map +1 -1
  132. package/dist/usage/ports.js +9 -17
  133. package/dist/usage/ports.js.map +1 -1
  134. package/dist/usage/steps.d.ts +70 -0
  135. package/dist/usage/steps.d.ts.map +1 -0
  136. package/dist/usage/steps.js +152 -0
  137. package/dist/usage/steps.js.map +1 -0
  138. package/dist/usage/tokens.d.ts +4 -7
  139. package/dist/usage/tokens.d.ts.map +1 -1
  140. package/dist/usage/tokens.js +4 -7
  141. package/dist/usage/tokens.js.map +1 -1
  142. package/docs/contracts/acp-and-step-usage.md +51 -0
  143. package/docs/contracts/conversation-persistence.md +47 -0
  144. package/docs/contracts/input-and-execution.md +177 -0
  145. package/docs/contracts/platform-authorization.md +17 -0
  146. package/host/adapters/acp-capabilities.js +30 -0
  147. package/host/adapters/acp-client.js +156 -0
  148. package/host/adapters/acp.js +284 -0
  149. package/host/adapters/claude.js +235 -0
  150. package/host/adapters/codex.js +733 -0
  151. package/host/adapters/cursor.js +347 -0
  152. package/host/adapters/input-capabilities.js +89 -0
  153. package/host/adapters/opencode.js +486 -0
  154. package/host/adapters/pi.js +519 -0
  155. package/host/adapters/prompt.js +212 -0
  156. package/host/auth.js +257 -0
  157. package/host/credentials/store.js +209 -0
  158. package/host/http.d.ts +32 -0
  159. package/host/http.js +238 -0
  160. package/host/index.d.ts +84 -0
  161. package/host/index.js +59 -0
  162. package/host/lib/runtime-paths.js +44 -0
  163. package/host/model-resolution.js +313 -0
  164. package/host/native-contract.js +185 -0
  165. package/host/native-runtime.js +194 -0
  166. package/host/permissions.js +27 -0
  167. package/host/platform-egress.js +114 -0
  168. package/host/recorder.js +61 -0
  169. package/host/sqlite/index.d.ts +69 -0
  170. package/host/sqlite/index.js +704 -0
  171. package/host/tooling/mcp-bundle.js +214 -0
  172. package/host/tooling/mcp-relay.js +83 -0
  173. package/host/tooling/native-binaries.js +667 -0
  174. package/host/tooling/native-config.js +460 -0
  175. package/host/tooling/package-import.js +111 -0
  176. package/host/tooling/runtime-home.js +26 -0
  177. package/host/tooling/system-instructions.js +69 -0
  178. package/host/tooling/system-prompt.js +35 -0
  179. package/package.json +44 -24
  180. package/skills/chat/SKILL.md +97 -0
  181. package/skills/chat/agents/openai.yaml +4 -0
  182. package/skills/chat/references/contracts.md +131 -0
  183. package/dist/api/chat-payloads.d.ts +0 -10
  184. package/dist/api/chat-payloads.d.ts.map +0 -1
  185. package/dist/api/chat-payloads.js +0 -57
  186. package/dist/api/chat-payloads.js.map +0 -1
  187. package/dist/contract/chat-input.d.ts +0 -144
  188. package/dist/contract/chat-input.d.ts.map +0 -1
  189. package/dist/contract/chat-input.js +0 -261
  190. package/dist/contract/chat-input.js.map +0 -1
  191. package/dist/contract/create.d.ts +0 -191
  192. package/dist/contract/create.d.ts.map +0 -1
  193. package/dist/contract/create.js +0 -249
  194. package/dist/contract/create.js.map +0 -1
  195. package/dist/contract/freeze.d.ts +0 -85
  196. package/dist/contract/freeze.d.ts.map +0 -1
  197. package/dist/contract/freeze.js +0 -151
  198. package/dist/contract/freeze.js.map +0 -1
  199. package/dist/contract/harnesses.d.ts +0 -21
  200. package/dist/contract/harnesses.d.ts.map +0 -1
  201. package/dist/contract/harnesses.js +0 -39
  202. package/dist/contract/harnesses.js.map +0 -1
  203. package/dist/contract/index.d.ts +0 -17
  204. package/dist/contract/index.d.ts.map +0 -1
  205. package/dist/contract/index.js +0 -17
  206. package/dist/contract/index.js.map +0 -1
  207. package/dist/contract/input.d.ts +0 -45
  208. package/dist/contract/input.d.ts.map +0 -1
  209. package/dist/contract/input.js +0 -78
  210. package/dist/contract/input.js.map +0 -1
  211. package/dist/contract/models.d.ts +0 -57
  212. package/dist/contract/models.d.ts.map +0 -1
  213. package/dist/contract/models.js +0 -380
  214. package/dist/contract/models.js.map +0 -1
  215. package/dist/contract/payload-merge.d.ts +0 -76
  216. package/dist/contract/payload-merge.d.ts.map +0 -1
  217. package/dist/contract/payload-merge.js +0 -215
  218. package/dist/contract/payload-merge.js.map +0 -1
  219. package/dist/events/tool-display.d.ts +0 -22
  220. package/dist/events/tool-display.d.ts.map +0 -1
  221. package/dist/events/tool-display.js +0 -60
  222. package/dist/events/tool-display.js.map +0 -1
  223. package/dist/events/tool-shape.d.ts +0 -48
  224. package/dist/events/tool-shape.d.ts.map +0 -1
  225. package/dist/events/tool-shape.js +0 -225
  226. package/dist/events/tool-shape.js.map +0 -1
  227. package/dist/relay/index.d.ts +0 -65
  228. package/dist/relay/index.d.ts.map +0 -1
  229. package/dist/relay/index.js +0 -197
  230. package/dist/relay/index.js.map +0 -1
  231. package/dist/turns/index.d.ts +0 -11
  232. package/dist/turns/index.d.ts.map +0 -1
  233. package/dist/turns/index.js +0 -11
  234. package/dist/turns/index.js.map +0 -1
  235. package/dist/turns/queries.d.ts +0 -36
  236. package/dist/turns/queries.d.ts.map +0 -1
  237. package/dist/turns/queries.js +0 -37
  238. package/dist/turns/queries.js.map +0 -1
  239. package/dist/turns/replay.d.ts +0 -105
  240. package/dist/turns/replay.d.ts.map +0 -1
  241. package/dist/turns/replay.js +0 -163
  242. package/dist/turns/replay.js.map +0 -1
  243. package/dist/turns/status.d.ts +0 -49
  244. package/dist/turns/status.d.ts.map +0 -1
  245. package/dist/turns/status.js +0 -56
  246. package/dist/turns/status.js.map +0 -1
  247. package/dist/turns/turn-store.d.ts +0 -109
  248. package/dist/turns/turn-store.d.ts.map +0 -1
  249. package/dist/turns/turn-store.js +0 -169
  250. package/dist/turns/turn-store.js.map +0 -1
  251. package/dist/warm/client-cache.d.ts +0 -47
  252. package/dist/warm/client-cache.d.ts.map +0 -1
  253. package/dist/warm/client-cache.js +0 -48
  254. package/dist/warm/client-cache.js.map +0 -1
  255. package/dist/warm/envelope-cache.d.ts +0 -79
  256. package/dist/warm/envelope-cache.d.ts.map +0 -1
  257. package/dist/warm/envelope-cache.js +0 -85
  258. package/dist/warm/envelope-cache.js.map +0 -1
  259. package/dist/warm/index.d.ts +0 -18
  260. package/dist/warm/index.d.ts.map +0 -1
  261. package/dist/warm/index.js +0 -12
  262. package/dist/warm/index.js.map +0 -1
  263. package/dist/warm/payload.d.ts +0 -137
  264. package/dist/warm/payload.d.ts.map +0 -1
  265. package/dist/warm/payload.js +0 -112
  266. package/dist/warm/payload.js.map +0 -1
  267. package/dist/warm/revision.d.ts +0 -70
  268. package/dist/warm/revision.d.ts.map +0 -1
  269. package/dist/warm/revision.js +0 -63
  270. package/dist/warm/revision.js.map +0 -1
  271. package/dist/warm/store-policy.d.ts +0 -57
  272. package/dist/warm/store-policy.d.ts.map +0 -1
  273. package/dist/warm/store-policy.js +0 -89
  274. package/dist/warm/store-policy.js.map +0 -1
  275. package/dist/warm/store.d.ts +0 -42
  276. package/dist/warm/store.d.ts.map +0 -1
  277. package/dist/warm/store.js +0 -69
  278. package/dist/warm/store.js.map +0 -1
  279. package/dist/wire/frames.d.ts +0 -208
  280. package/dist/wire/frames.d.ts.map +0 -1
  281. package/dist/wire/frames.js +0 -245
  282. package/dist/wire/frames.js.map +0 -1
  283. package/dist/wire/index.d.ts +0 -7
  284. package/dist/wire/index.d.ts.map +0 -1
  285. package/dist/wire/index.js +0 -7
  286. package/dist/wire/index.js.map +0 -1
@@ -0,0 +1,156 @@
1
+ // Generic Agent Client Protocol client.
2
+
3
+ import { spawn } from 'node:child_process';
4
+
5
+ /**
6
+ * Minimal Agent Client Protocol (ACP) client: JSON-RPC 2.0 over the stdio of a
7
+ * spawned agent process. This is the amalgm *client* side; the CLI (cursor-agent,
8
+ * gemini, ...) is the ACP agent. One instance per native agent process.
9
+ *
10
+ * The reader is tolerant of non-JSON stdout lines because young ACP servers are
11
+ * known to corrupt stdout with log output.
12
+ */
13
+ class AcpClient {
14
+ constructor({ binary, args = [], cwd, env, label = 'acp' }) {
15
+ this.label = label;
16
+ this.id = 1;
17
+ this.pending = new Map();
18
+ this.notificationHandlers = new Set();
19
+ this.requestHandler = null;
20
+ this.buffer = '';
21
+ this.closed = false;
22
+ this.closeError = null;
23
+ this.child = spawn(binary, args, {
24
+ cwd,
25
+ env,
26
+ stdio: ['pipe', 'pipe', 'pipe'],
27
+ detached: process.platform !== 'win32',
28
+ windowsHide: true,
29
+ });
30
+ this.child.stdout.setEncoding('utf8');
31
+ this.child.stdout.on('data', (chunk) => this.onData(chunk));
32
+ this.child.stderr.setEncoding('utf8');
33
+ this.child.stderr.on('data', (chunk) => {
34
+ const text = String(chunk || '').trim();
35
+ if (text && process.env.AMALGM_CHAT_DEBUG) console.warn(`[${this.label}]`, text.slice(0, 1000));
36
+ });
37
+ this.child.on('error', (err) => this.failAll(err));
38
+ this.child.on('exit', (code, signal) => {
39
+ this.failAll(new Error(`${this.label} agent exited (${code ?? signal ?? 'unknown'})`));
40
+ });
41
+ this.child.stdin.on('error', (err) => this.failAll(err));
42
+ }
43
+
44
+ failAll(err) {
45
+ this.closed = true;
46
+ this.closeError = err;
47
+ for (const pending of this.pending.values()) pending.reject(err);
48
+ this.pending.clear();
49
+ }
50
+
51
+ onData(chunk) {
52
+ this.buffer += chunk;
53
+ const lines = this.buffer.split('\n');
54
+ this.buffer = lines.pop() || '';
55
+ for (const line of lines) {
56
+ if (!line.trim()) continue;
57
+ let msg;
58
+ try { msg = JSON.parse(line); } catch { continue; }
59
+ if (process.env.AMALGM_CHAT_DEBUG_ACP === '1') {
60
+ console.log(`[${this.label}:recv]`, JSON.stringify(msg).slice(0, 4000));
61
+ }
62
+ this.dispatch(msg);
63
+ }
64
+ }
65
+
66
+ dispatch(msg) {
67
+ if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
68
+ const pending = this.pending.get(msg.id);
69
+ if (!pending) return;
70
+ this.pending.delete(msg.id);
71
+ if (msg.error) {
72
+ const err = new Error(msg.error.data?.message || msg.error.message || JSON.stringify(msg.error));
73
+ err.code = msg.error.code;
74
+ pending.reject(err);
75
+ } else {
76
+ pending.resolve(msg.result);
77
+ }
78
+ return;
79
+ }
80
+ if (msg.id !== undefined && msg.method) {
81
+ this.handleServerRequest(msg);
82
+ return;
83
+ }
84
+ if (msg.method) {
85
+ for (const handler of this.notificationHandlers) handler(msg);
86
+ }
87
+ }
88
+
89
+ async handleServerRequest(msg) {
90
+ if (!this.requestHandler) {
91
+ this.write({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: `Method not supported: ${msg.method}` } });
92
+ return;
93
+ }
94
+ try {
95
+ const result = await this.requestHandler(msg.method, msg.params || {});
96
+ this.write({ jsonrpc: '2.0', id: msg.id, result: result ?? {} });
97
+ } catch (err) {
98
+ this.write({ jsonrpc: '2.0', id: msg.id, error: { code: -32603, message: err.message || 'Internal error' } });
99
+ }
100
+ }
101
+
102
+ write(payload) {
103
+ if (this.closed || this.child.exitCode !== null || this.child.signalCode !== null) return;
104
+ try {
105
+ this.child.stdin.write(`${JSON.stringify(payload)}\n`, (err) => {
106
+ if (err) this.failAll(err);
107
+ });
108
+ } catch (err) {
109
+ this.failAll(err);
110
+ }
111
+ }
112
+
113
+ request(method, params = {}, timeoutMs = 180000) {
114
+ if (this.closeError) return Promise.reject(this.closeError);
115
+ if (this.closed || this.child.exitCode !== null || this.child.signalCode !== null) {
116
+ return Promise.reject(new Error(`${this.label} agent is not running`));
117
+ }
118
+ const id = this.id++;
119
+ return new Promise((resolve, reject) => {
120
+ const timer = timeoutMs > 0
121
+ ? setTimeout(() => {
122
+ this.pending.delete(id);
123
+ reject(new Error(`${method} timed out`));
124
+ }, timeoutMs)
125
+ : null;
126
+ this.pending.set(id, {
127
+ resolve: (value) => { if (timer) clearTimeout(timer); resolve(value); },
128
+ reject: (err) => { if (timer) clearTimeout(timer); reject(err); },
129
+ });
130
+ this.write({ jsonrpc: '2.0', id, method, params });
131
+ });
132
+ }
133
+
134
+ notify(method, params) {
135
+ this.write(params === undefined ? { jsonrpc: '2.0', method } : { jsonrpc: '2.0', method, params });
136
+ }
137
+
138
+ onNotification(handler) {
139
+ this.notificationHandlers.add(handler);
140
+ return () => this.notificationHandlers.delete(handler);
141
+ }
142
+
143
+ onRequest(handler) {
144
+ this.requestHandler = handler;
145
+ }
146
+
147
+ close() {
148
+ try { this.child.stdin.end(); } catch {}
149
+ try {
150
+ if (process.platform !== 'win32' && this.child.pid) process.kill(-this.child.pid, 'SIGTERM');
151
+ else this.child.kill('SIGTERM');
152
+ } catch {}
153
+ }
154
+ }
155
+
156
+ export { AcpClient };
@@ -0,0 +1,284 @@
1
+ /** Generic ACP agent adapter. Agent-specific launch data comes from the resolved revision. */
2
+
3
+ import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk';
4
+ import { toAcpMcpServers } from '../tooling/mcp-bundle.js';
5
+ import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
6
+ import { composeSystemPrompt } from '../tooling/system-prompt.js';
7
+ import { requestPermission } from '../permissions.js';
8
+ import { AcpClient } from './acp-client.js';
9
+ import { assertAcpMcpCapabilities, assertAcpPromptCapabilities } from './acp-capabilities.js';
10
+ import { nativeInputCapabilities, UnsupportedExecutionInputError } from './input-capabilities.js';
11
+ import { acpPromptParts } from './prompt.js';
12
+
13
+ function record(value) {
14
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
15
+ }
16
+
17
+ function launchFor(contract) {
18
+ const launch = record(contract.acpLaunch);
19
+ if (!launch) throw new UnsupportedExecutionInputError('acp', 'agent revision', 'missing acpLaunch descriptor');
20
+ const command = String(launch.command || '').trim();
21
+ if (!command) throw new UnsupportedExecutionInputError('acp', 'agent revision', 'acpLaunch.command is required');
22
+ if (!Array.isArray(launch.args) || launch.args.some((arg) => typeof arg !== 'string')) {
23
+ throw new UnsupportedExecutionInputError('acp', 'agent revision', 'acpLaunch.args must be a string array');
24
+ }
25
+ const supportedAuthModes = Array.isArray(launch.supportedAuthModes) ? launch.supportedAuthModes : [];
26
+ if (!supportedAuthModes.includes(contract.auth.mode)) {
27
+ throw new UnsupportedExecutionInputError('acp', 'auth.mode', `${contract.auth.mode} is not declared by this agent revision`);
28
+ }
29
+ return { ...launch, command, args: [...launch.args] };
30
+ }
31
+
32
+ function authEnvironment(contract, launch) {
33
+ const configured = record(record(launch.auth)?.[contract.auth.mode]) || {};
34
+ const out = { ...(record(launch.env) || {}) };
35
+ if (contract.auth.mode !== 'subscription') {
36
+ const tokenEnv = String(configured.tokenEnv || '').trim();
37
+ if (!tokenEnv) {
38
+ throw new UnsupportedExecutionInputError('acp', 'auth', `${contract.auth.mode} requires acpLaunch.auth.${contract.auth.mode}.tokenEnv`);
39
+ }
40
+ out[tokenEnv] = contract.auth.tokenRef || '';
41
+ const baseUrlEnv = String(configured.baseUrlEnv || '').trim();
42
+ if (baseUrlEnv && contract.auth.baseUrl) out[baseUrlEnv] = contract.auth.baseUrl;
43
+ }
44
+ return out;
45
+ }
46
+
47
+ function optionValues(option) {
48
+ if (!option || option.type !== 'select' || !Array.isArray(option.options)) return [];
49
+ return option.options.flatMap((entry) => Array.isArray(entry?.options) ? entry.options : [entry]);
50
+ }
51
+
52
+ function matchingValue(option, candidates) {
53
+ const wanted = new Set(candidates.filter(Boolean));
54
+ return optionValues(option).find((entry) => wanted.has(entry?.value))?.value || null;
55
+ }
56
+
57
+ async function setConfig(client, sessionId, option, value) {
58
+ const request = { sessionId, configId: option.id, value };
59
+ if (option.type === 'boolean') request.type = 'boolean';
60
+ return client.request('session/set_config_option', request, 30000);
61
+ }
62
+
63
+ async function applyConfiguration(client, sessionId, response, contract, launch) {
64
+ let options = Array.isArray(response?.configOptions) ? response.configOptions : [];
65
+ const modelOption = options.find((option) => option?.category === 'model')
66
+ || options.find((option) => option?.id === 'model');
67
+ if (!modelOption) {
68
+ if (launch.modelConfiguredAtLaunch !== true) {
69
+ throw new UnsupportedExecutionInputError('acp', 'provider + model', 'agent exposes no ACP model config option');
70
+ }
71
+ } else {
72
+ const value = matchingValue(modelOption, [
73
+ `${contract.providerId}/${contract.providerModelId}`,
74
+ contract.providerModelId,
75
+ contract.cliModel,
76
+ ]);
77
+ if (!value) {
78
+ throw new UnsupportedExecutionInputError(
79
+ 'acp',
80
+ 'provider + model',
81
+ `${contract.providerId}/${contract.providerModelId} is not offered by the agent`,
82
+ );
83
+ }
84
+ if (modelOption.currentValue !== value) {
85
+ const updated = await setConfig(client, sessionId, modelOption, value);
86
+ if (Array.isArray(updated?.configOptions)) options = updated.configOptions;
87
+ }
88
+ }
89
+
90
+ for (const selection of contract.modelSettings) {
91
+ const option = options.find((candidate) => candidate?.id === selection.configId);
92
+ if (!option) {
93
+ throw new UnsupportedExecutionInputError('acp', `model.settings.${selection.configId}`, 'agent did not advertise this ACP config option');
94
+ }
95
+ if (option.type === 'boolean' && selection.type !== 'boolean') {
96
+ throw new UnsupportedExecutionInputError('acp', `model.settings.${selection.configId}`, 'agent requires a boolean value');
97
+ }
98
+ if (option.type !== 'boolean' && !matchingValue(option, [selection.value])) {
99
+ throw new UnsupportedExecutionInputError('acp', `model.settings.${selection.configId}`, `agent does not offer ${selection.value}`);
100
+ }
101
+ if (option.currentValue !== selection.value) {
102
+ const updated = await setConfig(client, sessionId, option, selection.value);
103
+ if (Array.isArray(updated?.configOptions)) options = updated.configOptions;
104
+ }
105
+ }
106
+ }
107
+
108
+ function choosePermission(options, outcome) {
109
+ const kind = outcome === 'allow_always'
110
+ ? 'allow_always'
111
+ : outcome === 'allow_once'
112
+ ? 'allow_once'
113
+ : outcome === 'deny'
114
+ ? 'reject_once'
115
+ : null;
116
+ return options.find((option) => option?.kind === kind) || null;
117
+ }
118
+
119
+ export class AcpAdapter {
120
+ constructor(options = {}) {
121
+ this.clientFactory = options.clientFactory || ((input) => new AcpClient(input));
122
+ this.permissionPort = options.requestPermission || null;
123
+ this.outputType = 'acp';
124
+ this.inputCapabilities = nativeInputCapabilities({
125
+ authModes: ['platform', 'subscription', 'byok'],
126
+ modelSettings: 'negotiated',
127
+ tools: true,
128
+ });
129
+ }
130
+
131
+ validatePrepared(contract) {
132
+ const launch = launchFor(contract);
133
+ authEnvironment(contract, launch);
134
+ }
135
+
136
+ async create(contract) {
137
+ const launch = launchFor(contract);
138
+ const runtime = prepareHarnessRuntime(contract);
139
+ const client = this.clientFactory({
140
+ binary: launch.command,
141
+ args: launch.args,
142
+ cwd: contract.cwd,
143
+ env: { ...runtime.env, ...authEnvironment(contract, launch) },
144
+ label: launch.label || 'ACP',
145
+ });
146
+ const session = {
147
+ sessionId: contract.sessionId,
148
+ providerSessionId: contract.providerSessionId || null,
149
+ client,
150
+ capabilities: {},
151
+ permissionMode: contract.permissionMode,
152
+ needsSystemPrompt: false,
153
+ activePrompt: false,
154
+ };
155
+ client.onRequest((method, params) => this.handleRequest(session, method, params));
156
+ const initialized = await client.request('initialize', {
157
+ protocolVersion: PROTOCOL_VERSION,
158
+ clientInfo: { name: 'amalgm-chat', version: '0.1.2' },
159
+ clientCapabilities: {
160
+ fs: { readTextFile: false, writeTextFile: false },
161
+ session: { configOptions: { boolean: {} } },
162
+ },
163
+ });
164
+ if (initialized?.protocolVersion !== PROTOCOL_VERSION) {
165
+ throw new UnsupportedExecutionInputError('acp', 'protocol', `agent negotiated version ${initialized?.protocolVersion}`);
166
+ }
167
+ session.capabilities = initialized.agentCapabilities || {};
168
+ const methodId = record(launch.authMethodIds)?.[contract.auth.mode];
169
+ if (methodId) await client.request('authenticate', { methodId }, 120000);
170
+
171
+ const mcpServers = toAcpMcpServers(contract);
172
+ assertAcpMcpCapabilities('acp', session.capabilities, mcpServers);
173
+ const sessionParams = { cwd: contract.cwd, mcpServers };
174
+ let response;
175
+ if (session.providerSessionId) {
176
+ if (session.capabilities.loadSession === true) {
177
+ response = await client.request('session/load', {
178
+ ...sessionParams,
179
+ sessionId: session.providerSessionId,
180
+ }, 600000);
181
+ } else if (session.capabilities.sessionCapabilities?.resume != null) {
182
+ response = await client.request('session/resume', {
183
+ ...sessionParams,
184
+ sessionId: session.providerSessionId,
185
+ }, 600000);
186
+ } else {
187
+ throw new UnsupportedExecutionInputError('acp', 'resume', 'agent cannot load or resume its provider session');
188
+ }
189
+ } else {
190
+ response = await client.request('session/new', sessionParams, 600000);
191
+ session.providerSessionId = response?.sessionId || null;
192
+ session.needsSystemPrompt = true;
193
+ }
194
+ if (!session.providerSessionId) throw new Error('ACP agent did not return a sessionId');
195
+ await applyConfiguration(client, session.providerSessionId, response, contract, launch);
196
+ return session;
197
+ }
198
+
199
+ async handleRequest(session, method, params) {
200
+ if (method !== 'session/request_permission') throw new Error(`Method not supported: ${method}`);
201
+ if (session.permissionMode === 'full_access') {
202
+ const selected = choosePermission(params.options || [], 'allow_always')
203
+ || choosePermission(params.options || [], 'allow_once');
204
+ return selected
205
+ ? { outcome: { outcome: 'selected', optionId: selected.optionId } }
206
+ : { outcome: { outcome: 'cancelled' } };
207
+ }
208
+ const decision = await requestPermission(this.permissionPort, {
209
+ adapterId: 'acp',
210
+ providerSessionId: session.providerSessionId,
211
+ kind: 'tool',
212
+ title: params.title || 'Agent requests permission',
213
+ input: params,
214
+ });
215
+ const selected = choosePermission(params.options || [], decision.outcome);
216
+ return selected
217
+ ? { outcome: { outcome: 'selected', optionId: selected.optionId } }
218
+ : { outcome: { outcome: 'cancelled' } };
219
+ }
220
+
221
+ async stop(session) {
222
+ if (session.activePrompt) {
223
+ session.client.notify('session/cancel', { sessionId: session.providerSessionId });
224
+ }
225
+ }
226
+
227
+ async destroy(session) {
228
+ if (session.capabilities.sessionCapabilities?.close != null) {
229
+ await session.client.request('session/close', { sessionId: session.providerSessionId }, 30000).catch(() => {});
230
+ }
231
+ session.client.close();
232
+ }
233
+
234
+ async *prompt(session, prompt, contract) {
235
+ assertAcpPromptCapabilities('acp', session.capabilities, prompt);
236
+ const queue = [];
237
+ let wake = null;
238
+ let finished = false;
239
+ let failure = null;
240
+ const wakeLoop = () => { if (wake) { wake(); wake = null; } };
241
+ const off = session.client.onNotification((message) => {
242
+ if (message.method !== 'session/update') return;
243
+ if (message.params?.sessionId && message.params.sessionId !== session.providerSessionId) return;
244
+ if (message.params?.update) queue.push(message.params.update);
245
+ wakeLoop();
246
+ });
247
+ let preamble = '';
248
+ if (session.needsSystemPrompt) {
249
+ const systemPrompt = composeSystemPrompt(contract);
250
+ if (systemPrompt) preamble = `<system-instructions>\n${systemPrompt}\n</system-instructions>`;
251
+ session.needsSystemPrompt = false;
252
+ }
253
+ session.activePrompt = true;
254
+ session.client.request('session/prompt', {
255
+ sessionId: session.providerSessionId,
256
+ prompt: acpPromptParts(prompt, preamble),
257
+ }, 0).then(() => {
258
+ finished = true;
259
+ wakeLoop();
260
+ }).catch((error) => {
261
+ failure = error;
262
+ finished = true;
263
+ wakeLoop();
264
+ });
265
+ try {
266
+ while (!finished || queue.length) {
267
+ if (queue.length) yield queue.shift();
268
+ else await new Promise((resolve) => { wake = resolve; });
269
+ }
270
+ if (failure) throw failure;
271
+ } finally {
272
+ off();
273
+ session.activePrompt = false;
274
+ }
275
+ }
276
+ }
277
+
278
+ export const __private = {
279
+ applyConfiguration,
280
+ assertMcpCapabilities: (capabilities, servers) => assertAcpMcpCapabilities('acp', capabilities, servers),
281
+ assertPromptCapabilities: (capabilities, prompt) => assertAcpPromptCapabilities('acp', capabilities, prompt),
282
+ authEnvironment,
283
+ launchFor,
284
+ };
@@ -0,0 +1,235 @@
1
+ // Claude Code Agent SDK adapter.
2
+ //
3
+ // Event constructors, normalization, and usage laws come from the SDK.
4
+ // Effectful tooling (runtime
5
+ // homes, mcp bundle, native binaries, system prompt, package import) and the
6
+ // recorder are sibling host modules.
7
+ //
8
+ // Hook settings are provided through the injected constructor option
9
+ // `options.renderers.toClaudeSettings`; rendering stays inside the adapter.
10
+ // The uninjected default renders no settings (an agentConfig without hooks).
11
+
12
+ import path from 'node:path';
13
+ import { createEventConstructors } from '../../dist/events/index.js';
14
+ import {
15
+ createClaudeNormalizer,
16
+ usageFromClaude,
17
+ usageRecordsFromClaudeResult,
18
+ } from '../../dist/normalizers/claude.js';
19
+ import { recordNativeEvent } from '../recorder.js';
20
+ import { toClaudeMcpServers } from '../tooling/mcp-bundle.js';
21
+ import { bundledClaudeBinary } from '../tooling/native-binaries.js';
22
+ import { importPackage } from '../tooling/package-import.js';
23
+ import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
24
+ import { composeSystemPrompt } from '../tooling/system-prompt.js';
25
+ import { allowed, requestPermission } from '../permissions.js';
26
+ import {
27
+ COMMON_REASONING_SETTINGS,
28
+ COMMON_SPEED_SETTINGS,
29
+ nativeInputCapabilities,
30
+ } from './input-capabilities.js';
31
+ import { claudePrompt } from './prompt.js';
32
+
33
+ const events = createEventConstructors({ clockMs: () => Date.now() });
34
+ const { done, errorEvent, reasoningStarted, usageFinal } = events;
35
+ const { normalizeClaudeMessage } = createClaudeNormalizer(events);
36
+
37
+ // Engine default: require('../../amalgm-mcp/agent-config/renderers/hooks').
38
+ const defaultRenderers = {
39
+ toClaudeSettings: () => ({}),
40
+ };
41
+
42
+ function redactSecrets(text) {
43
+ return String(text || '')
44
+ .replace(/\b(npm_[A-Za-z0-9]{20,})\b/g, '[REDACTED_NPM_TOKEN]')
45
+ .replace(/\b(sk-[A-Za-z0-9_-]{20,})\b/g, '[REDACTED_API_KEY]')
46
+ .replace(/\b(sk-ant-[A-Za-z0-9_-]{20,})\b/g, '[REDACTED_ANTHROPIC_KEY]')
47
+ .replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{20,}/gi, '$1[REDACTED_TOKEN]');
48
+ }
49
+
50
+ class ClaudeAdapter {
51
+ constructor(options = {}) {
52
+ this.renderers = { ...defaultRenderers, ...(options.renderers || {}) };
53
+ this.permissionPort = options.requestPermission || null;
54
+ this.inputCapabilities = nativeInputCapabilities({
55
+ authModes: ['platform', 'subscription', 'byok'],
56
+ modelSettings: [...COMMON_REASONING_SETTINGS, ...COMMON_SPEED_SETTINGS],
57
+ tools: true,
58
+ });
59
+ }
60
+
61
+ async create(contract) {
62
+ return {
63
+ sessionId: contract.sessionId,
64
+ providerSessionId: contract.providerSessionId || null,
65
+ currentCliModel: contract.cliModel,
66
+ currentQuery: null,
67
+ closed: false,
68
+ };
69
+ }
70
+
71
+ options(contract, extra = {}) {
72
+ const runtime = prepareHarnessRuntime(contract);
73
+ const systemPrompt = composeSystemPrompt(contract);
74
+ const pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_BINARY || bundledClaudeBinary();
75
+ const settings = {
76
+ ...this.renderers.toClaudeSettings(contract.agentConfig),
77
+ ...(contract.fastMode ? { fastMode: true } : {}),
78
+ };
79
+ const fullAccess = contract.permissionMode === 'full_access';
80
+ const canUseTool = fullAccess ? undefined : async (toolName, input, context) => {
81
+ const decision = await requestPermission(this.permissionPort, {
82
+ adapterId: 'claude_code',
83
+ providerSessionId: contract.providerSessionId,
84
+ kind: 'tool',
85
+ title: context.title || context.displayName || `Claude requests ${toolName}`,
86
+ description: context.description || context.decisionReason || null,
87
+ toolName,
88
+ toolUseId: context.toolUseID,
89
+ input,
90
+ });
91
+ if (!allowed(decision)) {
92
+ return { behavior: 'deny', message: 'Denied by the user', toolUseID: context.toolUseID };
93
+ }
94
+ return {
95
+ behavior: 'allow',
96
+ updatedInput: decision.updatedInput || input,
97
+ ...(decision.outcome === 'allow_always' && context.suggestions
98
+ ? { updatedPermissions: context.suggestions }
99
+ : {}),
100
+ toolUseID: context.toolUseID,
101
+ };
102
+ };
103
+ return {
104
+ cwd: contract.cwd,
105
+ env: runtime.env,
106
+ model: contract.cliModel,
107
+ ...(contract.reasoningEffort ? { effort: contract.reasoningEffort } : {}),
108
+ ...(Object.keys(settings).length > 0 ? { settings } : {}),
109
+ ...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
110
+ ...(systemPrompt ? { systemPrompt: { type: 'preset', preset: 'claude_code', append: systemPrompt } } : {}),
111
+ mcpServers: toClaudeMcpServers(contract),
112
+ permissionMode: fullAccess ? 'bypassPermissions' : 'default',
113
+ ...(canUseTool ? { canUseTool } : {}),
114
+ ...(fullAccess ? { allowDangerouslySkipPermissions: true } : {}),
115
+ includePartialMessages: true,
116
+ ...(process.env.AMALGM_CHAT_DEBUG_CLAUDE === '1'
117
+ ? { debug: true, debugFile: path.join(runtime.runtimeHome, 'claude-debug.log') }
118
+ : {}),
119
+ settingSources: [],
120
+ strictMcpConfig: false,
121
+ ...extra,
122
+ };
123
+ }
124
+
125
+ async resume(session, contract) {
126
+ session.currentCliModel = contract.cliModel;
127
+ session.closed = false;
128
+ return session;
129
+ }
130
+
131
+ async stop(session) {
132
+ session.currentQuery?.close?.();
133
+ session.currentQuery = null;
134
+ session.closed = true;
135
+ }
136
+
137
+ async destroy(session) {
138
+ session.currentQuery?.close?.();
139
+ session.currentQuery = null;
140
+ session.closed = true;
141
+ }
142
+
143
+ async *prompt(session, input, contract) {
144
+ const contextLimit = Number(contract.contextWindow || 0) || undefined;
145
+ const state = {
146
+ providerSessionId: session.providerSessionId,
147
+ contextLimit,
148
+ claudeTools: new Map(),
149
+ claudeCompletedTools: new Set(),
150
+ claudeUsageSnapshotFingerprints: new Set(),
151
+ };
152
+ const stderrChunks = [];
153
+ const captureStderr = (chunk) => {
154
+ const text = redactSecrets(String(chunk || ''));
155
+ if (!text) return;
156
+ stderrChunks.push(text);
157
+ while (stderrChunks.length > 20) stderrChunks.shift();
158
+ if (process.env.AMALGM_CHAT_DEBUG_CLAUDE === '1' && text.trim()) {
159
+ console.warn('[Claude]', text.trim().slice(0, 2000));
160
+ }
161
+ };
162
+ try {
163
+ const sdk = await importPackage('@anthropic-ai/claude-agent-sdk');
164
+ const query = sdk.query({
165
+ prompt: claudePrompt(input),
166
+ options: {
167
+ ...this.options(contract, { stderr: captureStderr }),
168
+ ...(session.providerSessionId ? { resume: session.providerSessionId } : {}),
169
+ },
170
+ });
171
+ session.currentQuery = query;
172
+ session.closed = false;
173
+ yield reasoningStarted({ providerSessionId: state.providerSessionId });
174
+ for await (const message of query) {
175
+ recordNativeEvent('claude.sdk.message', message, {
176
+ providerSessionId: state.providerSessionId,
177
+ sessionId: contract.sessionId,
178
+ assistantMessageId: contract.assistantMessageId,
179
+ });
180
+ if (message.session_id) {
181
+ session.providerSessionId = message.session_id;
182
+ state.providerSessionId = message.session_id;
183
+ }
184
+ const normalized = normalizeClaudeMessage(message, state);
185
+ if (normalized.length > 0) {
186
+ for (const e of normalized) yield e;
187
+ continue;
188
+ }
189
+ if (message.type === 'result') {
190
+ const usageRecords = usageRecordsFromClaudeResult(message, contextLimit);
191
+ if (usageRecords.length === 0) {
192
+ usageRecords.push(usageFromClaude(message.usage, contextLimit));
193
+ }
194
+ for (const [index, usageRecord] of usageRecords.entries()) {
195
+ yield usageFinal({
196
+ ...usageRecord,
197
+ costUsd: usageRecords.length === 1 ? message.total_cost_usd ?? null : null,
198
+ }, {
199
+ providerSessionId: state.providerSessionId,
200
+ raw: {
201
+ source: 'claude.sdk.message',
202
+ method: 'claude/result',
203
+ payload: {
204
+ ...message,
205
+ usageRecordIndex: index,
206
+ usageRecordCount: usageRecords.length,
207
+ },
208
+ },
209
+ });
210
+ }
211
+ if (message.is_error) {
212
+ yield errorEvent(message.errors?.join('\n') || message.stop_reason || 'Claude turn failed', {
213
+ providerSessionId: state.providerSessionId,
214
+ raw: { source: 'claude.sdk.message', method: 'claude/result', payload: message },
215
+ });
216
+ }
217
+ yield done({
218
+ providerSessionId: state.providerSessionId,
219
+ stopReason: message.stop_reason || 'end_turn',
220
+ raw: { source: 'claude.sdk.message', method: 'claude/result', payload: message },
221
+ });
222
+ }
223
+ }
224
+ } catch (err) {
225
+ const stderrTail = stderrChunks.join('').trim().slice(-3000);
226
+ const message = stderrTail ? `${err.message}\n${stderrTail}` : err.message;
227
+ yield errorEvent(message, { providerSessionId: session.providerSessionId });
228
+ yield done({ providerSessionId: session.providerSessionId, stopReason: session.closed ? 'cancelled' : 'error' });
229
+ } finally {
230
+ session.currentQuery = null;
231
+ }
232
+ }
233
+ }
234
+
235
+ export { ClaudeAdapter };