amalgm 0.0.0 → 0.0.1

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 (107) hide show
  1. package/README.md +37 -1
  2. package/bin/amalgm.js +8 -2
  3. package/lib/auth-store.js +223 -0
  4. package/lib/cli.js +1000 -0
  5. package/lib/paths.js +30 -0
  6. package/lib/supervisor.js +467 -0
  7. package/lib/tunnel-chat.js +328 -0
  8. package/lib/tunnel-events.js +499 -0
  9. package/package.json +29 -3
  10. package/runtime/README.md +4 -0
  11. package/runtime/lib/chatInput.js +306 -0
  12. package/runtime/lib/harnesses.js +988 -0
  13. package/runtime/lib/local/amalgmStore.js +128 -0
  14. package/runtime/lib/local/credentialResolver.js +425 -0
  15. package/runtime/lib/mcpApps/registry.js +619 -0
  16. package/runtime/package.json +5 -0
  17. package/runtime/scripts/amalgm-mcp/agents/rest.js +165 -0
  18. package/runtime/scripts/amalgm-mcp/agents/store.js +153 -0
  19. package/runtime/scripts/amalgm-mcp/agents/talk.js +1156 -0
  20. package/runtime/scripts/amalgm-mcp/agents/tools.js +210 -0
  21. package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
  22. package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
  23. package/runtime/scripts/amalgm-mcp/artifacts/store.js +141 -0
  24. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
  25. package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
  26. package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
  27. package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
  28. package/runtime/scripts/amalgm-mcp/config.js +138 -0
  29. package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
  30. package/runtime/scripts/amalgm-mcp/deps.js +40 -0
  31. package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
  32. package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
  33. package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
  34. package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
  35. package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
  36. package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
  37. package/runtime/scripts/amalgm-mcp/events/store.js +98 -0
  38. package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
  39. package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
  40. package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
  41. package/runtime/scripts/amalgm-mcp/index.js +100 -0
  42. package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
  43. package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
  44. package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
  45. package/runtime/scripts/amalgm-mcp/lib/prefs.js +393 -0
  46. package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
  47. package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
  48. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
  49. package/runtime/scripts/amalgm-mcp/local/rest.js +80 -0
  50. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +151 -0
  51. package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
  52. package/runtime/scripts/amalgm-mcp/server/http.js +335 -0
  53. package/runtime/scripts/amalgm-mcp/server/mcp.js +116 -0
  54. package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
  55. package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
  56. package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
  57. package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
  58. package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
  59. package/runtime/scripts/amalgm-mcp/tasks/store.js +139 -0
  60. package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
  61. package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/rest.js +389 -0
  63. package/runtime/scripts/chat-core/adapters/claude.js +163 -0
  64. package/runtime/scripts/chat-core/adapters/codex.js +313 -0
  65. package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
  66. package/runtime/scripts/chat-core/auth.js +177 -0
  67. package/runtime/scripts/chat-core/contract.js +326 -0
  68. package/runtime/scripts/chat-core/credentials/store.js +212 -0
  69. package/runtime/scripts/chat-core/egress.js +87 -0
  70. package/runtime/scripts/chat-core/engine.js +195 -0
  71. package/runtime/scripts/chat-core/event-schema.js +231 -0
  72. package/runtime/scripts/chat-core/events.js +190 -0
  73. package/runtime/scripts/chat-core/index.js +11 -0
  74. package/runtime/scripts/chat-core/input.js +50 -0
  75. package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
  76. package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
  77. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
  78. package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
  79. package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
  80. package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
  81. package/runtime/scripts/chat-core/parts.js +253 -0
  82. package/runtime/scripts/chat-core/recorder.js +65 -0
  83. package/runtime/scripts/chat-core/runtime.js +86 -0
  84. package/runtime/scripts/chat-core/server.js +163 -0
  85. package/runtime/scripts/chat-core/sse.js +196 -0
  86. package/runtime/scripts/chat-core/stores.js +100 -0
  87. package/runtime/scripts/chat-core/tool-display.js +149 -0
  88. package/runtime/scripts/chat-core/tool-shape.js +143 -0
  89. package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
  90. package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
  91. package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
  92. package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
  93. package/runtime/scripts/chat-core/usage.js +343 -0
  94. package/runtime/scripts/chat-server/config.js +110 -0
  95. package/runtime/scripts/chat-server/db.js +529 -0
  96. package/runtime/scripts/chat-server/index.js +33 -0
  97. package/runtime/scripts/chat-server/model-catalog.js +327 -0
  98. package/runtime/scripts/chat-server.js +75 -0
  99. package/runtime/scripts/credential-adapter.js +129 -0
  100. package/runtime/scripts/fs-watcher.js +888 -0
  101. package/runtime/scripts/local-gateway.js +852 -0
  102. package/runtime/scripts/platform-context.txt +246 -0
  103. package/runtime/scripts/port-monitor.js +175 -0
  104. package/runtime/scripts/proxy-token-store.js +162 -0
  105. package/runtime/scripts/runtime-auth.js +163 -0
  106. package/runtime/scripts/test-claude-code-models.js +87 -0
  107. package/runtime/tsconfig.json +15 -0
@@ -0,0 +1,380 @@
1
+ 'use strict';
2
+
3
+ const { compactionFinished, compactionStarted, done, errorEvent, reasoningDelta, textDelta, toolCompleted, toolStarted, toolUpdated, usageFinal } = require('../events');
4
+ const { stringifyResult, summarizeToolOutput, summarizeToolRequest } = require('../tool-display');
5
+ const { normalizeError, titleForTool, toolKind } = require('../tool-shape');
6
+ const { normalizeUsage } = require('../usage');
7
+
8
+ const NON_TOOL_ITEM_TYPES = new Set(['agentMessage', 'contextCompaction', 'reasoning', 'userMessage']);
9
+
10
+ function isCodexToolItem(item = {}) {
11
+ return Boolean(item?.id && item?.type && !NON_TOOL_ITEM_TYPES.has(item.type));
12
+ }
13
+
14
+ function codexToolName(item = {}) {
15
+ if (item.type === 'commandExecution') return 'Bash';
16
+ if (item.type === 'fileChange') return 'Edit';
17
+ if (item.type === 'webSearch') return 'WebSearch';
18
+ if (item.type === 'collabAgentToolCall') return 'Agent';
19
+ if (item.type === 'mcpToolCall') return item.tool || item.toolName || item.name || 'mcpToolCall';
20
+ return item.name || item.toolName || item.type || 'unknown';
21
+ }
22
+
23
+ function codexToolInput(item = {}) {
24
+ if (item.type === 'commandExecution') {
25
+ return { command: item.command || item.rawCommand || '', cwd: item.cwd };
26
+ }
27
+ if (item.type === 'collabAgentToolCall') {
28
+ return {
29
+ prompt: item.prompt || '',
30
+ model: item.model || undefined,
31
+ reasoningEffort: item.reasoningEffort || undefined,
32
+ receiverThreadIds: item.receiverThreadIds || undefined,
33
+ };
34
+ }
35
+ const input = {};
36
+ for (const key of ['path', 'filePath', 'file_path', 'query', 'pattern', 'url', 'server', 'serverName', 'tool', 'toolName', 'name']) {
37
+ if (item[key] !== undefined) input[key] = item[key];
38
+ }
39
+ if (item.input && typeof item.input === 'object') Object.assign(input, item.input);
40
+ if (item.arguments && typeof item.arguments === 'object') Object.assign(input, item.arguments);
41
+ if (item.action && typeof item.action === 'object') {
42
+ if (useful(item.action.query) || !useful(input.query)) input.query = item.action.query;
43
+ if (useful(item.action.url) || !useful(input.url)) input.url = item.action.url;
44
+ if (useful(item.action.pattern) || !useful(input.pattern)) input.pattern = item.action.pattern;
45
+ if (Array.isArray(item.action.queries) && item.action.queries.length > 0) input.queries = item.action.queries;
46
+ }
47
+ if (Object.keys(input).length === 0) input.itemType = item.type || 'unknown';
48
+ return input;
49
+ }
50
+
51
+ function useful(value) {
52
+ return value !== undefined && value !== null && value !== '';
53
+ }
54
+
55
+ function codexToolOutput(item = {}) {
56
+ if (item.status === 'failed' && item.error) return '';
57
+ const direct = item.aggregatedOutput ?? item.output ?? item.result ?? item.diff ?? item.error ?? item.message ?? item.text ?? item.finalText;
58
+ if (direct == null) return '';
59
+ return direct;
60
+ }
61
+
62
+ function codexToolError(item = {}) {
63
+ if (item.status !== 'failed' && !item.error) return undefined;
64
+ return normalizeError(item.error || item.message || item.text || 'Codex tool failed');
65
+ }
66
+
67
+ function codexToolMetadata(item = {}) {
68
+ const metadata = {
69
+ providerItemType: item.type || 'unknown',
70
+ providerStatus: item.status,
71
+ };
72
+ if (item.type === 'webSearch' && item.action) metadata.action = item.action;
73
+ if (item.type === 'mcpToolCall') {
74
+ metadata.mcp = {
75
+ server: item.server || item.serverName || 'unknown',
76
+ tool: item.tool || item.toolName || item.name || 'unknown',
77
+ };
78
+ }
79
+ if (item.type === 'collabAgentToolCall') {
80
+ metadata.subagent = true;
81
+ metadata.senderThreadId = item.senderThreadId;
82
+ metadata.receiverThreadIds = item.receiverThreadIds;
83
+ metadata.agentsStates = item.agentsStates;
84
+ }
85
+ if (Number.isFinite(Number(item.durationMs))) metadata.durationMs = Number(item.durationMs);
86
+ return metadata;
87
+ }
88
+
89
+ function codexToolDetail(item = {}) {
90
+ const input = codexToolInput(item);
91
+ const inputDetail = summarizeToolRequest(
92
+ codexToolName(item),
93
+ input,
94
+ item.command || item.title || item.summary || item.text || item.path || item.prompt || '',
95
+ );
96
+ if (inputDetail) return inputDetail;
97
+ return summarizeToolOutput(codexToolOutput(item));
98
+ }
99
+
100
+ function codexToolTitle(item = {}) {
101
+ const input = codexToolInput(item);
102
+ const metadata = codexToolMetadata(item);
103
+ return titleForTool({
104
+ name: codexToolName(item),
105
+ input,
106
+ output: codexToolOutput(item),
107
+ error: codexToolError(item),
108
+ metadata,
109
+ fallback: item.title || item.summary || item.text || item.path || item.prompt || item.command || '',
110
+ });
111
+ }
112
+
113
+ function codexToolData(item = {}) {
114
+ return {
115
+ providerItemType: item.type || 'unknown',
116
+ item,
117
+ };
118
+ }
119
+
120
+ function codexDelta(msg, p = {}) {
121
+ return msg.textDelta ?? p.delta ?? p.textDelta ?? p.outputDelta ?? p.text ?? '';
122
+ }
123
+
124
+ function codexItemId(p = {}) {
125
+ return p.itemId || p.item?.id || p.id || p.callId || p.toolCallId;
126
+ }
127
+
128
+ function codexTokenTotal(value = {}) {
129
+ const direct = Number(value.totalTokens);
130
+ if (Number.isFinite(direct) && direct > 0) return direct;
131
+ return Number(value.inputTokens || 0) + Number(value.outputTokens || 0);
132
+ }
133
+
134
+ function codexTokenBucketTotal(value = {}) {
135
+ return Number(value.inputTokens || 0)
136
+ + Number(value.outputTokens || 0)
137
+ + Number(value.cachedInputTokens || 0)
138
+ + Number(value.cacheWriteTokens || value.cacheWriteInputTokens || value.cacheCreationInputTokens || 0);
139
+ }
140
+
141
+ function codexTokenList(value = {}) {
142
+ const cacheRead = Number(value.cachedInputTokens || 0);
143
+ const cacheWrite = Number(value.cacheWriteTokens || value.cacheWriteInputTokens || value.cacheCreationInputTokens || 0);
144
+ const output = Number(value.outputTokens || 0);
145
+ const thought = Number(value.reasoningOutputTokens || 0);
146
+ const totalInput = Number(value.inputTokens || 0);
147
+ const input = Math.max(0, totalInput - cacheRead - cacheWrite);
148
+ return {
149
+ input,
150
+ output,
151
+ cacheRead,
152
+ cacheWrite,
153
+ thought,
154
+ total: input + output + cacheRead + cacheWrite,
155
+ };
156
+ }
157
+
158
+ function codexCumulativeUsage(total = {}, state) {
159
+ if (codexTokenBucketTotal(total) <= 0) return {};
160
+ const cumulativeTokenList = codexTokenList(total);
161
+ const previousCumulativeTokenList = state?.codexCumulativeTokenList || null;
162
+ if (state) state.codexCumulativeTokenList = cumulativeTokenList;
163
+ return { cumulativeTokenList, previousCumulativeTokenList };
164
+ }
165
+
166
+ function codexErrorMessage(error, fallback = 'Codex turn failed') {
167
+ if (!error) return fallback;
168
+ if (typeof error === 'string') return error;
169
+ if (error.data?.message) return error.data.message;
170
+ if (error.data?.error?.message) return error.data.error.message;
171
+ if (error.cause?.message) return error.cause.message;
172
+ if (error.message) return error.message;
173
+ if (error.name) return error.name;
174
+ try {
175
+ return JSON.stringify(error);
176
+ } catch {
177
+ return fallback;
178
+ }
179
+ }
180
+
181
+ function eventRaw(method, payload) {
182
+ return { raw: { source: 'codex.app-server.notification', method, payload } };
183
+ }
184
+
185
+ function isCodexCompactionItem(item = {}) {
186
+ return item?.type === 'contextCompaction';
187
+ }
188
+
189
+ function normalizeCodexNotification(msg, providerSessionId, state) {
190
+ const method = msg.method;
191
+ const p = msg.params || msg.payload || {};
192
+ const raw = eventRaw(method || 'unknown', msg);
193
+ if (method === 'item/started' && isCodexCompactionItem(p.item)) {
194
+ if (state) {
195
+ state.codexCompactionActive = true;
196
+ state.codexCompactionUsageEmitted = false;
197
+ state.codexCompactionItemId = p.item.id || null;
198
+ }
199
+ return [compactionStarted({
200
+ providerSessionId,
201
+ trigger: 'auto',
202
+ ...raw,
203
+ })];
204
+ }
205
+ if (method === 'item/completed' && isCodexCompactionItem(p.item)) {
206
+ if (state) state.codexCompactionActive = false;
207
+ return [compactionFinished({
208
+ providerSessionId,
209
+ trigger: 'auto',
210
+ ...raw,
211
+ })];
212
+ }
213
+ if (method === 'item/agentMessage/delta' && codexDelta(msg, p)) return [textDelta(codexDelta(msg, p), { providerSessionId, ...raw })];
214
+ if ((method === 'item/reasoning/delta' || method === 'item/reasoning/textDelta' || method === 'item/reasoning/summaryTextDelta') && codexDelta(msg, p)) {
215
+ return [reasoningDelta(codexDelta(msg, p), { providerSessionId, ...raw })];
216
+ }
217
+ if (method === 'item/plan/delta' && codexDelta(msg, p)) return [reasoningDelta(codexDelta(msg, p), { providerSessionId, streamKind: 'plan', ...raw })];
218
+ if ((method === 'item/commandExecution/outputDelta' || method === 'item/fileChange/outputDelta') && codexDelta(msg, p)) {
219
+ const id = codexItemId(p);
220
+ if (!id) return [];
221
+ return [toolUpdated(id, {
222
+ providerSessionId,
223
+ outputDelta: codexDelta(msg, p),
224
+ status: 'in_progress',
225
+ ...raw,
226
+ })];
227
+ }
228
+ if (method === 'item/mcpToolCall/progress') {
229
+ const id = codexItemId(p);
230
+ if (!id) return [];
231
+ return [toolUpdated(id, {
232
+ providerSessionId,
233
+ outputDelta: p.message || p.progress || p.summary || '',
234
+ status: 'in_progress',
235
+ ...raw,
236
+ })];
237
+ }
238
+ if (method === 'item/started' && isCodexToolItem(p.item)) {
239
+ const toolName = codexToolName(p.item);
240
+ const input = codexToolInput(p.item);
241
+ const detail = codexToolDetail(p.item);
242
+ const metadata = codexToolMetadata(p.item);
243
+ return [toolStarted(p.item.id, {
244
+ providerSessionId,
245
+ toolName,
246
+ title: codexToolTitle(p.item),
247
+ kind: toolKind(toolName, input, metadata),
248
+ detail,
249
+ input,
250
+ metadata,
251
+ data: codexToolData(p.item),
252
+ ...raw,
253
+ })];
254
+ }
255
+ if (method === 'item/updated' && isCodexToolItem(p.item)) {
256
+ const input = codexToolInput(p.item);
257
+ const toolName = codexToolName(p.item);
258
+ const metadata = codexToolMetadata(p.item);
259
+ return [toolUpdated(p.item.id, {
260
+ providerSessionId,
261
+ toolName,
262
+ title: codexToolTitle(p.item),
263
+ kind: toolKind(toolName, input, metadata),
264
+ detail: codexToolDetail(p.item),
265
+ inputDelta: input,
266
+ status: 'in_progress',
267
+ metadata,
268
+ data: codexToolData(p.item),
269
+ ...raw,
270
+ })];
271
+ }
272
+ if (method === 'item/completed' && isCodexToolItem(p.item)) {
273
+ const toolName = codexToolName(p.item);
274
+ const input = codexToolInput(p.item);
275
+ const output = codexToolOutput(p.item);
276
+ const error = codexToolError(p.item);
277
+ const metadata = codexToolMetadata(p.item);
278
+ const detail = codexToolDetail(p.item);
279
+ return [toolCompleted(p.item.id, {
280
+ providerSessionId,
281
+ toolName,
282
+ title: codexToolTitle(p.item),
283
+ kind: toolKind(toolName, input, metadata),
284
+ detail: detail || summarizeToolOutput(output),
285
+ input,
286
+ result: stringifyResult(output),
287
+ output,
288
+ error,
289
+ isError: p.item.status === 'failed' || Boolean(error),
290
+ timing: Number.isFinite(Number(p.item.durationMs)) ? { elapsedMs: Number(p.item.durationMs) } : undefined,
291
+ metadata,
292
+ data: codexToolData(p.item),
293
+ ...raw,
294
+ })];
295
+ }
296
+ if (method === 'thread/tokenUsage/updated') {
297
+ const last = p.tokenUsage?.last || (Array.isArray(p.tokenUsage?.lastTurn) ? p.tokenUsage.lastTurn[0] : p.tokenUsage?.lastTurn);
298
+ const total = p.tokenUsage?.total || {};
299
+ const totalProcessedTokens = codexTokenTotal(total);
300
+ const lastTotal = codexTokenTotal(last);
301
+ const lastBucketTotal = codexTokenBucketTotal(last);
302
+ const cumulativeUsage = codexCumulativeUsage(total, state);
303
+ if (state?.codexCompactionActive) {
304
+ if (state.codexCompactionUsageEmitted && state.lastCodexCompactionSnapshotTotal === lastTotal) return [];
305
+ state.codexCompactionUsageEmitted = true;
306
+ state.lastCodexCompactionSnapshotTotal = lastTotal;
307
+ if (lastBucketTotal === 0 && lastTotal > 0) {
308
+ return [usageFinal(normalizeUsage({
309
+ tokenList: {
310
+ input: lastTotal,
311
+ output: 0,
312
+ cacheRead: 0,
313
+ cacheWrite: 0,
314
+ thought: 0,
315
+ total: lastTotal,
316
+ },
317
+ inputContextSize: lastTotal,
318
+ contextLimit: p.tokenUsage?.modelContextWindow,
319
+ totalProcessedTokens: totalProcessedTokens > 0 ? totalProcessedTokens : undefined,
320
+ ...cumulativeUsage,
321
+ operation: 'compaction',
322
+ billable: false,
323
+ source: 'codex_app_server_context_snapshot',
324
+ exact: false,
325
+ }), { providerSessionId, ...raw })];
326
+ }
327
+ return [usageFinal(normalizeUsage({
328
+ totalInputTokens: last?.inputTokens,
329
+ outputTokens: last?.outputTokens,
330
+ cachedInputTokens: last?.cachedInputTokens,
331
+ cacheWriteTokens: last?.cacheWriteTokens ?? last?.cacheWriteInputTokens ?? last?.cacheCreationInputTokens,
332
+ thoughtTokens: last?.reasoningOutputTokens,
333
+ contextLimit: p.tokenUsage?.modelContextWindow,
334
+ totalProcessedTokens: totalProcessedTokens > 0 ? totalProcessedTokens : undefined,
335
+ ...cumulativeUsage,
336
+ operation: 'compaction',
337
+ source: 'codex_app_server',
338
+ exact: true,
339
+ }), { providerSessionId, ...raw })];
340
+ }
341
+ if (state?.lastCodexCompactionSnapshotTotal === lastTotal && lastBucketTotal === 0 && lastTotal > 0) return [];
342
+ return [usageFinal(normalizeUsage({
343
+ totalInputTokens: last?.inputTokens,
344
+ outputTokens: last?.outputTokens,
345
+ cachedInputTokens: last?.cachedInputTokens,
346
+ cacheWriteTokens: last?.cacheWriteTokens ?? last?.cacheWriteInputTokens ?? last?.cacheCreationInputTokens,
347
+ thoughtTokens: last?.reasoningOutputTokens,
348
+ contextLimit: p.tokenUsage?.modelContextWindow,
349
+ totalProcessedTokens: totalProcessedTokens > 0 ? totalProcessedTokens : undefined,
350
+ ...cumulativeUsage,
351
+ operation: 'chat_step',
352
+ source: 'codex_app_server',
353
+ exact: true,
354
+ }), { providerSessionId, ...raw })];
355
+ }
356
+ if (method === 'turn/completed') {
357
+ const error = p.error || p.lastError || p.turn?.error || p.turn?.lastError;
358
+ if (error) return [errorEvent(codexErrorMessage(error), { providerSessionId, ...raw }), done({ providerSessionId, stopReason: 'error', ...raw })];
359
+ return [done({ providerSessionId, ...raw })];
360
+ }
361
+ if ((method === 'turn/error' || method === 'error') && p.willRetry === true) {
362
+ return [];
363
+ }
364
+ if (method === 'turn/failed' || method === 'turn/error' || method === 'error') {
365
+ return [errorEvent(codexErrorMessage(p.error || p), { providerSessionId, ...raw }), done({ providerSessionId, stopReason: 'error', ...raw })];
366
+ }
367
+ return [];
368
+ }
369
+
370
+ module.exports = {
371
+ codexErrorMessage,
372
+ codexToolInput,
373
+ codexToolDetail,
374
+ codexToolError,
375
+ codexToolName,
376
+ codexToolOutput,
377
+ codexToolTitle,
378
+ isCodexToolItem,
379
+ normalizeCodexNotification,
380
+ };
@@ -0,0 +1,259 @@
1
+ # Agent event normalization matrix
2
+
3
+ Source of truth for how Claude Code / Codex / OpenCode native events map to the unified amalgm event shape. Lives next to the normalizers so the contract stays close to the code.
4
+
5
+ **Files**
6
+ - Normalizers (this dir): `claude.js`, `codex.js`, `opencode.js`
7
+ - Tool contract: `tool_contract.md`
8
+ - Adapters: `../adapters/{claude,codex,opencode}.js`
9
+ - Wire framing: `../sse.js` → `toAgentStreamEvent()`
10
+ - UI consumer: `amalgm-ui/lib/agents/acpAdapter.ts`
11
+ - UI schema: `amalgm-ui/lib/agents/acpTypes.ts`
12
+
13
+ **Capturing fixtures**
14
+
15
+ ```sh
16
+ CHAT_CORE_RECORD=1 <run a session>
17
+ # writes ~/.amalgm/chat-core-recordings/<date>.ndjson
18
+ ```
19
+
20
+ Cell legend: ✅ handled · ⚠️ handled, with gaps · ❌ dropped · `TBD` not yet captured from a real fixture.
21
+
22
+ ---
23
+
24
+ ## 1. Text parts
25
+
26
+ Streamed assistant text. The user-visible response.
27
+
28
+ | | Claude Code | Codex | OpenCode |
29
+ |---|---|---|---|
30
+ | Native | `assistant.message.content[]` block `type:'text'` | `item/agentMessage/delta` | `message.part.delta` type `text` + `message.part.updated` snapshots |
31
+ | Amalgm | `agent_message_chunk` | `agent_message_chunk` | `agent_message_chunk` |
32
+ | Dedup | ❌ none | ❌ none | ✅ suffix-prefix overlap (40 / 80 char thresholds) |
33
+ | Gaps | SDK retransmit would double — untested | Same | Magic thresholds undocumented; no test |
34
+
35
+ ---
36
+
37
+ ## 2. Reasoning parts
38
+
39
+ Model thinking stream. Separate from text.
40
+
41
+ | | Claude Code | Codex | OpenCode |
42
+ |---|---|---|---|
43
+ | Native | blocks `type:'thinking'` or `'redacted_thinking'` | `item/reasoning/delta`, `reasoning/textDelta`, `reasoning/summaryTextDelta` | `message.part` type `reasoning` |
44
+ | Amalgm | `agent_thought_chunk` | `agent_thought_chunk` | `agent_thought_chunk` |
45
+ | Sub-types | `redacted_thinking` flattened, lost | `summaryTextDelta` vs `textDelta` flattened, lost | none |
46
+ | Done signal | implicit (assistant message ends) | implicit | implicit |
47
+ | Gaps | Sub-type info lost; UI can't differentiate redacted | Same | TBD |
48
+
49
+ ---
50
+
51
+ ## 3. Tool parts (the call)
52
+
53
+ Tool invocation: id, name, title.
54
+
55
+ | | Claude Code | Codex | OpenCode |
56
+ |---|---|---|---|
57
+ | Native | content block `tool_use` / `server_tool_use` / `mcp_tool_use` | `item/started` (tool item) | `message.part` type `tool`, status `pending` / `running` |
58
+ | Amalgm | `tool_call` | `tool_call` | `tool_call` |
59
+ | Tool name | canonical (`Read`, `Edit`, `Bash`, …) | semantic (`commandExecution`, `fileChange`, `webSearch`, `mcpToolCall`) | inferred from input shape via `resolveToolName()` |
60
+ | Gaps | ✅ matches UI taxonomy | ⚠️ Codex names ≠ Claude names → UI falls through to `tool-unknown` for `fileChange` etc. | ⚠️ heuristic; ambiguous inputs → `unknown` |
61
+
62
+ ---
63
+
64
+ ## 4. Tool input
65
+
66
+ Initial input + streaming JSON deltas while model writes args.
67
+
68
+ | | Claude Code | Codex | OpenCode |
69
+ |---|---|---|---|
70
+ | Native (initial) | `tool_use.input` object | `item.input` / `item.arguments` | `part.state.input` |
71
+ | Native (delta) | `content_block_delta` with `delta.type:'input_json_delta'` (partial JSON string) | `item/updated` (full input snapshot) | `message.part.updated` (snapshot) |
72
+ | Amalgm | `tool_call_update.inputDelta` (parsed object) | same (full snapshot) | same (full snapshot) |
73
+ | Partial-parse | `tool.partialInputJson` accumulator + `stableStringify` fingerprint dedup | none | none |
74
+ | Gaps | Never-closing JSON when cancelled mid-args — untested | No granularity (snapshot only) | Same |
75
+
76
+ ---
77
+
78
+ ## 5. Tool state
79
+
80
+ Lifecycle: started → in_progress → completed / failed.
81
+
82
+ | | Claude Code | Codex | OpenCode |
83
+ |---|---|---|---|
84
+ | started | `tool_use` block first seen | `item/started` | `part.state.status === 'pending' \| 'running'` |
85
+ | in_progress | `tool_progress` messages | `item/commandExecution/outputDelta`, `item/fileChange/outputDelta`, `item/mcpToolCall/progress` | `part.state.status === 'running'` |
86
+ | completed | `user.content` `tool_result` block | `item/completed` (status not failed) | `part.state.status === 'completed'` |
87
+ | failed | `tool_result.is_error: true` | `item/completed` with `status:'failed'` | `part.state.status === 'error'` |
88
+ | Amalgm | `tool_call_update` with `status ∈ { calling, output-available, output-error }` | same | same |
89
+ | Gaps | TBD | TBD | TBD |
90
+
91
+ ---
92
+
93
+ ## 6. Tool output
94
+
95
+ Actual result content (string, blob, structured).
96
+
97
+ | | Claude Code | Codex | OpenCode |
98
+ |---|---|---|---|
99
+ | Native field | `tool_result.content` (string or blocks) | `item.aggregatedOutput ?? output ?? result ?? diff ?? error ?? message ?? text ?? finalText` | `part.state.output ?? part.state.error` |
100
+ | Amalgm | `tool_call_update.content` + `rawOutput` | same | same |
101
+ | Image outputs | possible but unhandled | none observed | none observed |
102
+ | Gaps | Multi-block `tool_result` may not flatten correctly — TBD | Long fallback chain may pick wrong field | TBD |
103
+
104
+ ---
105
+
106
+ ## 7. Permissions / approvals
107
+
108
+ Tool gating: the model wants to run a tool, user must approve. Interrupts the tool lifecycle between **started** and **completed**. Distinct from tool state.
109
+
110
+ | | Claude Code | Codex | OpenCode |
111
+ |---|---|---|---|
112
+ | Native | SDK exposes `canUseTool` callback; in stream form, TBD: confirm whether a `permission_request` event is emitted | TBD: Codex has approval mode (`approval_policy`); items carry an `approval_required` flag — confirm event shape from fixture | `question.asked` event (closest analog) — partially mapped today as a tool call named `AskUserQuestion` |
113
+ | User response | resume via callback / decision RPC | resume RPC | `question.replied` (with answer) or `question.rejected` |
114
+ | Decline behavior | tool not executed; turn continues without it | TBD | tool reported as completed-with-rejection |
115
+ | Amalgm | ❌ no first-class event today; auto-approve assumed | ❌ same | ⚠️ shoehorned into `tool_call` for `AskUserQuestion` |
116
+ | Proposed | new `SessionUpdate` variant `permission_request { toolCallId, action, args, severity }` plus `permission_response { toolCallId, approved, reason? }` | same | same |
117
+ | Gaps | No standardized permission UI across CLIs. Cross-CLI parity needed. |
118
+
119
+ ---
120
+
121
+ ## 8. Stop / end turn / cancellation
122
+
123
+ Turn termination signal + reason.
124
+
125
+ | | Claude Code | Codex | OpenCode |
126
+ |---|---|---|---|
127
+ | End turn | `result` message → `done(stopReason: stop_reason)` | `turn/completed` → `done` | `session.idle` / `message.finish` → `done` |
128
+ | Cancel | SDK iterator close → `done(stopReason:'cancelled')` | RPC cancel → `done` | prompt abort → `done(stopReason:'cancelled')` |
129
+ | Error end | `result.is_error` → `errorEvent` + `done(stopReason:'error')` | `turn/failed` / `turn/error` → same | `session.error` → same |
130
+ | Amalgm | `complete` event with `stopReason` | same | same |
131
+ | Gaps | `stop_reason` values not enumerated/typed | TBD | TBD |
132
+
133
+ ---
134
+
135
+ ## 9. Usage (tokens)
136
+
137
+ Per-turn token counts.
138
+
139
+ | | Claude Code | Codex | OpenCode |
140
+ |---|---|---|---|
141
+ | Source | `result.usage` at turn end | `thread/tokenUsage/updated` (multiple per turn) | `step-finish` parts + `message.tokens` |
142
+ | input | `input_tokens` | `inputTokens` | `tokens.input` |
143
+ | output | `output_tokens` | `outputTokens` | `tokens.output` |
144
+ | cache read | `cache_read_input_tokens` | `cachedInputTokens` | `tokens.cache.read` |
145
+ | cache write | `cache_creation_input_tokens` | ❓ folded into input? | `tokens.cache.write` |
146
+ | reasoning | folded into output | `reasoningOutputTokens` → `thoughtTokens` | `tokens.reasoning` → `thoughtTokens` |
147
+ | Amalgm | `usage_update` (normalized) | same | same |
148
+ | Cadence | once per turn | per notification (multi) | per step (multi) |
149
+ | Gaps | Cache-write field semantics differ — confirm Codex | Same | TBD |
150
+
151
+ ---
152
+
153
+ ## 10. Cost (USD)
154
+
155
+ Dollar cost; separate concern from tokens.
156
+
157
+ | | Claude Code | Codex | OpenCode |
158
+ |---|---|---|---|
159
+ | Native | `result.total_cost_usd` once at turn end | ❌ never emitted | per-step `tokens.cost` |
160
+ | Amalgm | `usage_update.cost.amount` once | ❌ never set | `usage_update.cost.amount` per step |
161
+ | Cadence | once / turn | never | per step |
162
+ | Gaps | Three different semantics across CLIs → downstream sums are wrong. Pick one canonical place to compute. |
163
+
164
+ ---
165
+
166
+ ## 11. Compaction
167
+
168
+ End-to-end wiring shipped. Canonical event shape: `compaction.started` and `compaction.finished`, converted to ACP `SessionUpdate.compaction_boundary { phase: 'started' | 'finished', trigger, preTokens?, postTokens?, durationMs?, summary? }`. UI renders compaction as a ProcessChain element with the same white loading dot used by the message waiting state: `Compacting context` while active, then `Compacted context in 23s` when timing is known. Saved as a normal assistant message part so users can see prior compactions after reload.
169
+
170
+ | | Claude Code | Codex | OpenCode |
171
+ |---|---|---|---|
172
+ | Native start | `{ type:'system', subtype:'status', status:'compacting' }` (`SDKStatusMessage`) | `item/started` with `item.type:'contextCompaction'` | `message.part.updated` with `part.type:'compaction'` |
173
+ | Native finish | `{ type:'system', subtype:'compact_boundary', compact_metadata:{ trigger, pre_tokens, post_tokens, duration_ms } }` | `item/completed` with `item.type:'contextCompaction'` | `{ type:'session.compacted', properties:{ sessionID } }` |
174
+ | In-progress signal | ✅ `status:'compacting'` continues until boundary | ✅ context compaction item lifecycle | ✅ compaction part before session finish |
175
+ | Engine emit | ✅ `compactionStarted` + `compactionFinished` | ✅ `compactionStarted` + `compactionFinished` | ✅ `compactionStarted` + `compactionFinished` |
176
+ | UI render | Same ProcessChain element | Same ProcessChain element | Same ProcessChain element |
177
+ | Persistence | ✅ saved in assistant `messages.parts` as `{ type:'compaction', phase, startedAt, finishedAt, durationMs, ... }` | same | same |
178
+ | Gaps | Compaction billing needs usage decision | Compaction billing unclear; usage shows context reset | Native exposes compaction cost/tokens; usage logging decision pending |
179
+
180
+ ---
181
+
182
+ ## 12. Errors
183
+
184
+ Auth, model, transport — three flavors that need separate signals.
185
+
186
+ | | Claude Code | Codex | OpenCode |
187
+ |---|---|---|---|
188
+ | Auth | SDK throws with auth message | RPC error w/ auth code | session error with auth flag |
189
+ | Model (rate limit, overflow, unavailable) | `result.is_error` with `errors[]` | `turn/error` with `error.type` | `session.error` |
190
+ | Runtime / transport | adapter try-catch | RPC reject + child exit | stream / prompt reject |
191
+ | Amalgm | `error`, `auth_error`, `model_error` (inconsistent) | same | same |
192
+ | Gaps | All three often flatten to plain `error`. UI can't tell severity. `auth_error` / `model_error` exist in ACP but not consistently emitted. |
193
+
194
+ ---
195
+
196
+ ## 13. Session init
197
+
198
+ Fires once per session. Identifies provider session, cwd, model, tool list.
199
+
200
+ | | Claude Code | Codex | OpenCode |
201
+ |---|---|---|---|
202
+ | Native | `system` msg `subtype:'init'` with `cwd`, `model`, `tools[]` | `session/created` notification | `session.created` event |
203
+ | Amalgm | `session_init { sessionId, containerId }` | same | same |
204
+ | Extra fields (cwd / model / tools) | TBD: do they reach UI? | TBD | TBD |
205
+ | Title | `title_generated` event — when does each CLI emit one? | TBD | TBD |
206
+ | Gaps | Confirm cwd/model/tool propagation. |
207
+
208
+ ---
209
+
210
+ ## 14. Multimodal content (images, audio, files)
211
+
212
+ Non-text content blocks. Today only images are partially handled.
213
+
214
+ | | Claude Code | Codex | OpenCode |
215
+ |---|---|---|---|
216
+ | Image input | block `type:'image'` (`source.type:'base64'` or `'url'`) | TBD | TBD |
217
+ | Image output | block `type:'image'` (rare; vision-capable models) | none observed | none observed |
218
+ | Audio | ACP defines `content.type:'audio'`; never observed in stream | TBD | TBD |
219
+ | Files / attachments | ACP defines `content.type:'resource'` and `'resource_link'`; never observed | TBD | TBD |
220
+ | Amalgm | `agent_message_chunk` with `content.type:'image'` only — audio / resource paths exist but unused | TBD | TBD |
221
+ | Gaps | UI image-part path rarely exercised. Tool screenshot outputs (e.g. browser/preview) untested. Audio + file blocks parked in schema, no renderer. |
222
+
223
+ ---
224
+
225
+ ## 15. MCP tool metadata
226
+
227
+ Branding: server name, app id, icon.
228
+
229
+ | | Claude Code | Codex | OpenCode |
230
+ |---|---|---|---|
231
+ | Native marker | tool name pattern `mcp__<server>__<tool>` | `mcpToolCall` item with `serverName`, `toolName` | name pattern `mcp__<server>__<tool>` |
232
+ | Amalgm | `tool_call`; UI infers app via `resolveMcpAppMeta()` (3 strategies) | same | same |
233
+ | App branding | `mcpMappings` lookup at UI | same | same |
234
+ | Gaps | No metadata field on the wire — relies on string parsing. If server name ≠ configured app id, branding silently falls through. |
235
+
236
+ ---
237
+
238
+ ## 16. Dropped today
239
+
240
+ Thrown away at [acpAdapter.ts:147-153](../../../../../amalgm-ui/lib/agents/acpAdapter.ts). Listed so we know what we're losing — not necessarily wrong to drop, but worth tracking.
241
+
242
+ | | Claude Code | Codex | OpenCode |
243
+ |---|---|---|---|
244
+ | Plan | `system.subtype:'plan'` (Plan Mode) | TBD | TBD |
245
+ | Mode | `current_mode_update` | TBD | TBD |
246
+ | Available commands | `available_commands_update` | TBD | TBD |
247
+ | Config option changes | `config_option_update` | TBD | TBD |
248
+ | User message echo | `user_message_chunk` | TBD | TBD |
249
+
250
+ If any of these become product-relevant, lift the drop in the ACP adapter and add a UI renderer.
251
+
252
+ ---
253
+
254
+ ## How to fill this in
255
+
256
+ 1. Set `CHAT_CORE_RECORD=1` and run the scenarios from the test plan against each CLI.
257
+ 2. For each cell currently `TBD`, find the matching event in the recorder ndjson and replace `TBD` with the real shape.
258
+ 3. For each ⚠️ / ❌ row, link the resulting fixture file so future readers can see real data.
259
+ 4. When a CLI vendor ships an SDK update that adds new event types, the unmapped-event counter (proposed in critique) will surface them — add a row here and decide whether to map or drop.