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,552 @@
1
+ 'use strict';
2
+
3
+ const { compactionFinished, compactionStarted, done, errorEvent, reasoningDelta, textDelta, toolCompleted, toolStarted, toolUpdated, usageFinal, warningEvent } = require('../events');
4
+ const { stringifyResult, summarizeToolOutput, summarizeToolRequest } = require('../tool-display');
5
+ const { mcpMetadataFromName, normalizeError, titleForTool, toolKind } = require('../tool-shape');
6
+ const { normalizeUsage } = require('../usage');
7
+
8
+ const CROSS_PART_DEDUPE_MIN_CHARS = 40;
9
+ const CROSS_PART_OVERLAP_MIN_CHARS = 80;
10
+
11
+ function usageFromOpenCodeTokens(tokens, cost, contextLimit, operation = 'chat_step') {
12
+ const inputTokens = Number(tokens?.input || 0);
13
+ const cacheReadTokens = Number(tokens?.cache?.read || 0);
14
+ const cacheWriteTokens = Number(tokens?.cache?.write || 0);
15
+ const thoughtTokens = Number(tokens?.reasoning || 0);
16
+ const outputTokens = Number(tokens?.output || 0) + thoughtTokens;
17
+ return normalizeUsage({
18
+ inputTokens,
19
+ outputTokens,
20
+ thoughtTokens,
21
+ cacheReadTokens,
22
+ cacheWriteTokens,
23
+ contextLimit,
24
+ costUsd: cost,
25
+ operation,
26
+ source: 'opencode_sdk',
27
+ exact: true,
28
+ });
29
+ }
30
+
31
+ function openCodeErrorMessage(err, fallback = 'OpenCode session error') {
32
+ if (!err) return fallback;
33
+ if (typeof err === 'string') return err;
34
+ if (err.data?.message) return err.data.message;
35
+ if (err.data?.error?.message) return err.data.error.message;
36
+ if (err.cause?.message) return err.cause.message;
37
+ if (err.message) return err.message;
38
+ if (err.name) return err.name;
39
+ try {
40
+ return JSON.stringify(err);
41
+ } catch {
42
+ return fallback;
43
+ }
44
+ }
45
+
46
+ function openCodeRequestId(prefix, id) {
47
+ return `${prefix}_${String(id || 'unknown')}`;
48
+ }
49
+
50
+ function eventRaw(type, payload) {
51
+ return { raw: { source: 'opencode.sdk.event', method: type || 'unknown', payload } };
52
+ }
53
+
54
+ function commonPrefixLength(a = '', b = '') {
55
+ const max = Math.min(a.length, b.length);
56
+ let index = 0;
57
+ while (index < max && a[index] === b[index]) index += 1;
58
+ return index;
59
+ }
60
+
61
+ function suffixPrefixOverlap(text = '', delta = '') {
62
+ const max = Math.min(text.length, delta.length);
63
+ for (let length = max; length > 0; length -= 1) {
64
+ if (text.endsWith(delta.slice(0, length))) return length;
65
+ }
66
+ return 0;
67
+ }
68
+
69
+ function mergeOpenCodeSnapshot(previousText = '', nextText = '') {
70
+ const latestText = previousText.length > nextText.length && previousText.startsWith(nextText)
71
+ ? previousText
72
+ : nextText;
73
+ return {
74
+ latestText,
75
+ deltaToEmit: latestText.slice(commonPrefixLength(previousText, latestText)),
76
+ };
77
+ }
78
+
79
+ function appendOpenCodeDelta(previousText = '', delta = '') {
80
+ const deltaToEmit = delta.slice(suffixPrefixOverlap(previousText, delta));
81
+ return {
82
+ nextText: `${previousText}${deltaToEmit}`,
83
+ deltaToEmit,
84
+ };
85
+ }
86
+
87
+ function dedupeOpenCodeAssistantText(state, text = '') {
88
+ if (!text) return '';
89
+ const aggregate = String(state.assistantTextAggregate || '');
90
+ let emitText = text;
91
+
92
+ if (emitText.length >= CROSS_PART_DEDUPE_MIN_CHARS && aggregate.endsWith(emitText)) {
93
+ emitText = '';
94
+ } else {
95
+ const overlap = suffixPrefixOverlap(aggregate, emitText);
96
+ const mostlyDuplicate =
97
+ emitText.length >= CROSS_PART_DEDUPE_MIN_CHARS &&
98
+ overlap > 0 &&
99
+ (overlap >= CROSS_PART_OVERLAP_MIN_CHARS || overlap / emitText.length >= 0.8);
100
+ if (mostlyDuplicate) emitText = emitText.slice(overlap);
101
+ }
102
+
103
+ if (emitText) state.assistantTextAggregate = `${aggregate}${emitText}`;
104
+ return emitText;
105
+ }
106
+
107
+ function openCodeToolDetail(part) {
108
+ const toolName = part?.tool || 'unknown';
109
+ const status = part?.state?.status;
110
+ if (status === 'completed' || status === 'error') {
111
+ return summarizeToolOutput(part?.state?.output ?? part?.state?.error ?? '');
112
+ }
113
+ return summarizeToolRequest(toolName, part?.state?.input || {}, part?.state?.title || '');
114
+ }
115
+
116
+ function openCodeToolData(part) {
117
+ return {
118
+ tool: part?.tool || 'unknown',
119
+ state: part?.state || {},
120
+ };
121
+ }
122
+
123
+ function openCodeToolMetadata(part) {
124
+ const toolName = part?.tool || 'unknown';
125
+ const metadata = {
126
+ providerTool: toolName,
127
+ providerStatus: part?.state?.status,
128
+ ...mcpMetadataFromName(toolName),
129
+ };
130
+ const stateMetadata = part?.state?.metadata;
131
+ if (stateMetadata && typeof stateMetadata === 'object') {
132
+ Object.assign(metadata, stateMetadata);
133
+ if (stateMetadata.sessionId) {
134
+ metadata.childSessionId = stateMetadata.sessionId;
135
+ metadata.subagent = true;
136
+ }
137
+ }
138
+ if (part?.tool === 'task') metadata.subagent = true;
139
+ if (part?.state?.title) metadata.title = part.state.title;
140
+ return metadata;
141
+ }
142
+
143
+ function openCodeToolError(part) {
144
+ if (part?.state?.status !== 'error') return undefined;
145
+ return normalizeError(part?.state?.error || part?.state?.output || 'Tool failed');
146
+ }
147
+
148
+ function openCodeToolTitle(part) {
149
+ const toolName = part?.tool || 'unknown';
150
+ const input = part?.state?.input || {};
151
+ const metadata = openCodeToolMetadata(part);
152
+ return titleForTool({
153
+ name: toolName,
154
+ input,
155
+ output: part?.state?.output,
156
+ error: part?.state?.error,
157
+ metadata,
158
+ fallback: part?.state?.title || toolName,
159
+ });
160
+ }
161
+
162
+ function stopReasonFromOpenCodeFinish(finish) {
163
+ if (finish === 'length') return 'max_tokens';
164
+ if (finish === 'content_filter') return 'content_filter';
165
+ if (finish === 'error') return 'error';
166
+ if (finish === 'stop') return 'end_turn';
167
+ return null;
168
+ }
169
+
170
+ function pendingParts(state) {
171
+ if (!state.pendingParts) state.pendingParts = new Map();
172
+ return state.pendingParts;
173
+ }
174
+
175
+ function messageRoles(state) {
176
+ if (!state.messageRoles) state.messageRoles = new Map();
177
+ return state.messageRoles;
178
+ }
179
+
180
+ function compactionContinuationMessages(state) {
181
+ if (!state.compactionContinuationMessageIds) state.compactionContinuationMessageIds = new Set();
182
+ return state.compactionContinuationMessageIds;
183
+ }
184
+
185
+ function rememberCompactionContinuation(part, state) {
186
+ if (part?.messageID) compactionContinuationMessages(state).add(part.messageID);
187
+ }
188
+
189
+ function isOpenCodeInternalUsage(info, state) {
190
+ if (!info || info.sessionID !== state.providerSessionId || info.role !== 'assistant') return false;
191
+ if (info.mode === 'compaction' || info.agent === 'compaction') return true;
192
+ return Boolean(info.parentID && state.compactionContinuationMessageIds?.has(info.parentID));
193
+ }
194
+
195
+ function cachePendingPart(part, state, delta) {
196
+ if (!part?.id) return;
197
+ const cache = pendingParts(state);
198
+ const previous = cache.get(part.id)?.part || part;
199
+ const next = { ...previous, ...part };
200
+ if (typeof delta === 'string' && (part.type === 'text' || part.type === 'reasoning')) {
201
+ const base = String(previous.text || part.text || '');
202
+ const merged = appendOpenCodeDelta(base, delta);
203
+ next.text = merged.nextText;
204
+ }
205
+ cache.set(part.id, { part: next, delta: undefined });
206
+ }
207
+
208
+ function dropPendingParts(messageId, state) {
209
+ const cache = pendingParts(state);
210
+ for (const [partId, entry] of [...cache.entries()]) {
211
+ if (entry.part?.messageID === messageId) cache.delete(partId);
212
+ }
213
+ }
214
+
215
+ function flushPendingParts(messageId, state) {
216
+ const cache = pendingParts(state);
217
+ const events = [];
218
+ for (const [partId, entry] of [...cache.entries()]) {
219
+ if (entry.part?.messageID !== messageId) continue;
220
+ cache.delete(partId);
221
+ events.push(...normalizeOpenCodePart(entry.part, state, entry.delta));
222
+ }
223
+ return events;
224
+ }
225
+
226
+ function normalizeOpenCodePart(part, state, delta) {
227
+ if (!part || part.sessionID !== state.providerSessionId) return [];
228
+ if (part.type === 'compaction') {
229
+ if (part.tail_start_id) return [];
230
+ if (!state.compactionsStarted) state.compactionsStarted = new Set();
231
+ const compactionId = part.id || part.messageID || 'opencode-compaction';
232
+ if (state.compactionsStarted.has(compactionId)) return [];
233
+ state.compactionsStarted.add(compactionId);
234
+ return [compactionStarted({
235
+ providerSessionId: state.providerSessionId,
236
+ trigger: part.auto === false ? 'manual' : 'auto',
237
+ ...eventRaw('part/compaction', part),
238
+ })];
239
+ }
240
+ if (!state.assistantMessageIds.has(part.messageID)) return [];
241
+ if (part.id && part.type) {
242
+ if (!state.partTypes) state.partTypes = new Map();
243
+ state.partTypes.set(part.id, part.type);
244
+ }
245
+ if (part.metadata?.compaction_continue) {
246
+ rememberCompactionContinuation(part, state);
247
+ return [];
248
+ }
249
+ const seen = state.partText.get(part.id) || '';
250
+ if (part.type === 'text') {
251
+ const merged = typeof delta === 'string'
252
+ ? appendOpenCodeDelta(seen, delta)
253
+ : mergeOpenCodeSnapshot(seen, String(part.text || ''));
254
+ const text = dedupeOpenCodeAssistantText(state, merged.deltaToEmit);
255
+ state.partText.set(part.id, merged.latestText || merged.nextText || `${seen}${text}`);
256
+ return text ? [textDelta(text, { providerSessionId: state.providerSessionId, ts: part.time?.start, ...eventRaw(`part/${part.type}`, part) })] : [];
257
+ }
258
+ if (part.type === 'reasoning') {
259
+ const merged = typeof delta === 'string'
260
+ ? appendOpenCodeDelta(seen, delta)
261
+ : mergeOpenCodeSnapshot(seen, String(part.text || ''));
262
+ const text = merged.deltaToEmit;
263
+ state.partText.set(part.id, merged.latestText || merged.nextText || `${seen}${text}`);
264
+ return text ? [reasoningDelta(text, { providerSessionId: state.providerSessionId, ts: part.time?.start, ...eventRaw(`part/${part.type}`, part) })] : [];
265
+ }
266
+ if (part.type === 'tool') {
267
+ const toolId = part.callID || part.id;
268
+ const status = part.state?.status;
269
+ const toolName = part.tool || 'unknown';
270
+ const input = part.state?.input || {};
271
+ const metadata = openCodeToolMetadata(part);
272
+ const title = openCodeToolTitle(part);
273
+ const kind = toolKind(toolName, input, metadata);
274
+ const timing = part.state?.time || part.time;
275
+ if (status === 'pending' || status === 'running') {
276
+ const detail = openCodeToolDetail(part);
277
+ if (state.toolsStarted.has(toolId)) {
278
+ return [toolUpdated(toolId, {
279
+ providerSessionId: state.providerSessionId,
280
+ toolName,
281
+ title,
282
+ kind,
283
+ detail,
284
+ inputDelta: input,
285
+ status: 'in_progress',
286
+ metadata,
287
+ timing,
288
+ data: openCodeToolData(part),
289
+ ...eventRaw('part/tool', part),
290
+ })];
291
+ }
292
+ state.toolsStarted.add(toolId);
293
+ return [toolStarted(toolId, {
294
+ providerSessionId: state.providerSessionId,
295
+ toolName,
296
+ title,
297
+ kind,
298
+ detail,
299
+ input,
300
+ ts: part.state?.time?.start,
301
+ metadata,
302
+ timing,
303
+ data: openCodeToolData(part),
304
+ ...eventRaw('part/tool', part),
305
+ })];
306
+ }
307
+ if ((status === 'completed' || status === 'error') && !state.toolsCompleted.has(toolId)) {
308
+ state.toolsCompleted.add(toolId);
309
+ const output = part.state?.output ?? part.state?.error ?? '';
310
+ const detail = openCodeToolDetail(part);
311
+ return [toolCompleted(toolId, {
312
+ providerSessionId: state.providerSessionId,
313
+ toolName,
314
+ title,
315
+ kind,
316
+ detail,
317
+ input,
318
+ output,
319
+ result: stringifyResult(output),
320
+ isError: status === 'error',
321
+ error: openCodeToolError(part),
322
+ ts: part.state?.time?.end,
323
+ metadata,
324
+ timing,
325
+ data: openCodeToolData(part),
326
+ ...eventRaw('part/tool', part),
327
+ })];
328
+ }
329
+ }
330
+ if (part.type === 'step-finish') {
331
+ const stopReason = stopReasonFromOpenCodeFinish(part.reason);
332
+ if (stopReason) state.pendingStopReason = stopReason;
333
+ return [usageFinal(usageFromOpenCodeTokens(part.tokens, part.cost, state.contextLimit), { providerSessionId: state.providerSessionId, ...eventRaw('part/step-finish', part) })];
334
+ }
335
+ return [];
336
+ }
337
+
338
+ function isOurAssistant(info, state) {
339
+ if (!info || info.sessionID !== state.providerSessionId || info.role !== 'assistant') return false;
340
+ if (state.userMessageId && info.parentID && info.parentID !== state.userMessageId) return false;
341
+ const created = Number(info.time?.created || 0);
342
+ if (state.turnStartedAt && created && created < state.turnStartedAt) return false;
343
+ return true;
344
+ }
345
+
346
+ function normalizeOpenCodeEvent(event, state) {
347
+ const payload = event?.payload || event;
348
+ if (!payload || typeof payload !== 'object') return [];
349
+ const raw = eventRaw(payload.type, event);
350
+ if (payload.type === 'message.updated') {
351
+ const info = payload.properties?.info;
352
+ if (info?.id && info.sessionID === state.providerSessionId && info.role) {
353
+ messageRoles(state).set(info.id, info.role);
354
+ if (info.role === 'user') {
355
+ const created = Number(info.time?.created || 0);
356
+ const isCurrentTurnUser = !state.turnStartedAt || !created || created >= state.turnStartedAt;
357
+ if (!state.userMessageId && isCurrentTurnUser) state.userMessageId = info.id;
358
+ dropPendingParts(info.id, state);
359
+ return [];
360
+ }
361
+ }
362
+ }
363
+ if (payload.type === 'message.part.delta') {
364
+ const props = payload.properties || {};
365
+ const delta = typeof props.delta === 'string' ? props.delta : '';
366
+ if (!delta) return [];
367
+ if (!state.assistantMessageIds.has(props.messageID)) {
368
+ const pending = state.pendingParts?.get(props.partID);
369
+ if (pending?.part) cachePendingPart(pending.part, state, delta);
370
+ return [];
371
+ }
372
+ const knownType = state.partTypes?.get(props.partID);
373
+ const previous = state.partText.get(props.partID) || '';
374
+ const merged = appendOpenCodeDelta(previous, delta);
375
+ state.partText.set(props.partID, merged.nextText);
376
+ if (!merged.deltaToEmit) return [];
377
+ if (knownType === 'text' || (!knownType && props.field === 'text')) {
378
+ const text = dedupeOpenCodeAssistantText(state, merged.deltaToEmit);
379
+ return text ? [textDelta(text, { providerSessionId: state.providerSessionId, ts: payload.time || payload.properties?.time, ...raw })] : [];
380
+ }
381
+ if (knownType === 'reasoning' || props.field === 'reasoning' || props.field === 'thinking') {
382
+ return [reasoningDelta(merged.deltaToEmit, { providerSessionId: state.providerSessionId, ...raw })];
383
+ }
384
+ return [];
385
+ }
386
+ if (payload.type === 'message.part.updated') {
387
+ const part = payload.properties?.part;
388
+ if (part?.id && part?.type) {
389
+ if (!state.partTypes) state.partTypes = new Map();
390
+ state.partTypes.set(part.id, part.type);
391
+ }
392
+ if (part?.metadata?.compaction_continue) {
393
+ rememberCompactionContinuation(part, state);
394
+ return [];
395
+ }
396
+ const role = part?.messageID ? state.messageRoles?.get(part.messageID) : undefined;
397
+ if (role && role !== 'assistant') return [];
398
+ if (part?.type === 'compaction') return normalizeOpenCodePart(part, state, payload.properties?.delta);
399
+ if (part?.messageID && !state.assistantMessageIds.has(part.messageID)) {
400
+ cachePendingPart(part, state, payload.properties?.delta);
401
+ return [];
402
+ }
403
+ const events = normalizeOpenCodePart(part, state, payload.properties?.delta);
404
+ if (part?.type === 'step-finish' && part.messageID) state.usageEmittedForAssistant.add(part.messageID);
405
+ return events;
406
+ }
407
+ if (payload.type === 'message.updated') {
408
+ const info = payload.properties?.info;
409
+ if (isOpenCodeInternalUsage(info, state)) {
410
+ if (info.id && info.tokens && (info.finish || info.time?.completed)) {
411
+ if (state.finishedAssistantIds.has(info.id)) return [];
412
+ state.finishedAssistantIds.add(info.id);
413
+ const operation = info.mode === 'compaction' || info.agent === 'compaction' ? 'compaction' : 'chat_step';
414
+ return [usageFinal(usageFromOpenCodeTokens(info.tokens, info.cost, state.contextLimit, operation), { providerSessionId: state.providerSessionId, ...raw })];
415
+ }
416
+ return [];
417
+ }
418
+ if (!isOurAssistant(info, state)) return [];
419
+ if (info.id) {
420
+ state.assistantMessageIds.add(info.id);
421
+ state.sawTurnActivity = true;
422
+ }
423
+ const flushed = info.id ? flushPendingParts(info.id, state) : [];
424
+ if (info.tokens && (info.finish || info.time?.completed)) {
425
+ if (state.finishedAssistantIds.has(info.id)) return [];
426
+ state.finishedAssistantIds.add(info.id);
427
+ const events = [...flushed];
428
+ if (!state.usageEmittedForAssistant.has(info.id)) {
429
+ state.usageEmittedForAssistant.add(info.id);
430
+ events.push(usageFinal(usageFromOpenCodeTokens(info.tokens, info.cost, state.contextLimit), { providerSessionId: state.providerSessionId, ...raw }));
431
+ }
432
+ if (!state.doneEmitted && (info.finish === 'stop' || info.finish === 'length' || info.finish === 'content_filter')) {
433
+ state.pendingStopReason = stopReasonFromOpenCodeFinish(info.finish) || 'end_turn';
434
+ }
435
+ return events;
436
+ }
437
+ return flushed;
438
+ }
439
+ if (payload.type === 'session.compacted' && payload.properties?.sessionID === state.providerSessionId) {
440
+ return [compactionFinished({
441
+ providerSessionId: state.providerSessionId,
442
+ trigger: 'auto',
443
+ ...raw,
444
+ })];
445
+ }
446
+ if (payload.type === 'session.error' && payload.properties?.sessionID === state.providerSessionId) {
447
+ const err = payload.properties?.error;
448
+ const message = openCodeErrorMessage(err);
449
+ if (state.doneEmitted) return [errorEvent(message, { providerSessionId: state.providerSessionId, ...raw })];
450
+ state.doneEmitted = true;
451
+ return [errorEvent(message, { providerSessionId: state.providerSessionId, ...raw }), done({ providerSessionId: state.providerSessionId, stopReason: 'error', ...raw })];
452
+ }
453
+ if (payload.type === 'permission.asked' || payload.type === 'permission.replied') return [];
454
+ if (payload.type === 'question.asked') {
455
+ const props = payload.properties || {};
456
+ if (props.sessionID && props.sessionID !== state.providerSessionId) return [];
457
+ const id = openCodeRequestId('question', props.id);
458
+ return [toolStarted(id, {
459
+ providerSessionId: state.providerSessionId,
460
+ toolName: 'AskUserQuestion',
461
+ title: 'Question',
462
+ kind: 'question',
463
+ detail: 'Question',
464
+ input: {
465
+ questions: props.questions || props.question || [],
466
+ },
467
+ metadata: { providerEvent: payload.type, question: true },
468
+ data: { question: props },
469
+ ts: payload.time,
470
+ ...raw,
471
+ })];
472
+ }
473
+ if (payload.type === 'question.replied' || payload.type === 'question.rejected') {
474
+ const props = payload.properties || {};
475
+ if (props.sessionID && props.sessionID !== state.providerSessionId) return [];
476
+ const id = openCodeRequestId('question', props.requestID || props.id);
477
+ const rejected = payload.type === 'question.rejected';
478
+ return [toolCompleted(id, {
479
+ providerSessionId: state.providerSessionId,
480
+ toolName: 'AskUserQuestion',
481
+ title: 'Question',
482
+ kind: 'question',
483
+ detail: rejected ? 'Question rejected' : 'Question answered',
484
+ result: rejected ? 'Question rejected' : JSON.stringify(props.answers || {}),
485
+ output: props,
486
+ isError: rejected,
487
+ error: rejected ? normalizeError('Question rejected') : undefined,
488
+ metadata: { providerEvent: payload.type, question: true },
489
+ data: { question: props },
490
+ ts: payload.time,
491
+ ...raw,
492
+ })];
493
+ }
494
+ if ((payload.type === 'session.idle' || payload.type === 'session.status') && payload.properties?.sessionID === state.providerSessionId) {
495
+ const status = payload.properties?.status;
496
+ if (status?.type === 'busy' || status === 'busy') state.sawBusy = true;
497
+ if (status?.type === 'retry') {
498
+ const attempt = status.attempt ?? status.count ?? status.retry ?? '?';
499
+ const reason = status.message || openCodeErrorMessage(status.error, 'unknown upstream error');
500
+ const message = `OpenCode upstream retrying: ${reason} (attempt ${attempt})`;
501
+ return [warningEvent(message, { providerSessionId: state.providerSessionId, ...raw })];
502
+ }
503
+ if ((!status || status === 'idle' || status?.type === 'idle') && state.sawTurnActivity && !state.doneEmitted) {
504
+ state.doneEmitted = true;
505
+ return [done({ providerSessionId: state.providerSessionId, stopReason: state.pendingStopReason, ...raw })];
506
+ }
507
+ }
508
+ return [];
509
+ }
510
+
511
+ function normalizeOpenCodePromptResult(data, state) {
512
+ const raw = eventRaw('prompt.result', data);
513
+ const resultError = data?.error || data?.info?.error;
514
+ if (resultError) {
515
+ const message = openCodeErrorMessage(resultError);
516
+ if (state.doneEmitted) return [errorEvent(message, { providerSessionId: state.providerSessionId, ...raw })];
517
+ state.doneEmitted = true;
518
+ return [errorEvent(message, { providerSessionId: state.providerSessionId, ...raw }), done({ providerSessionId: state.providerSessionId, stopReason: 'error', ...raw })];
519
+ }
520
+ const info = data?.info;
521
+ if (!info?.id) return [];
522
+ if (!isOurAssistant(info, state)) return [];
523
+ state.assistantMessageIds.add(info.id);
524
+ state.sawTurnActivity = true;
525
+ const events = [];
526
+ for (const part of data.parts || []) {
527
+ events.push(...normalizeOpenCodePart(part, state));
528
+ }
529
+ const finished = Boolean(info.finish || info.time?.completed);
530
+ if (!finished) return events;
531
+ if (info.tokens && !state.usageEmittedForAssistant.has(info.id)) {
532
+ state.usageEmittedForAssistant.add(info.id);
533
+ events.push(usageFinal(usageFromOpenCodeTokens(info.tokens, info.cost, state.contextLimit), { providerSessionId: state.providerSessionId, ...raw }));
534
+ }
535
+ if (!state.doneEmitted) {
536
+ state.doneEmitted = true;
537
+ const stopReason = info.finish === 'length' ? 'max_tokens'
538
+ : info.finish === 'content_filter' ? 'content_filter'
539
+ : 'end_turn';
540
+ events.push(done({ providerSessionId: state.providerSessionId, stopReason, ...raw }));
541
+ }
542
+ return events;
543
+ }
544
+
545
+ module.exports = {
546
+ normalizeOpenCodeEvent,
547
+ normalizeOpenCodePart,
548
+ normalizeOpenCodePromptResult,
549
+ openCodeErrorMessage,
550
+ suffixPrefixOverlap,
551
+ usageFromOpenCodeTokens,
552
+ };
@@ -0,0 +1,123 @@
1
+ # Tool normalization contract
2
+
3
+ This contract is only for tool parts. Text and reasoning normalization stay as-is.
4
+
5
+ ## Canonical shape
6
+
7
+ Every provider tool event should normalize toward this shape:
8
+
9
+ ```ts
10
+ type ToolState = 'started' | 'running' | 'complete' | 'error';
11
+
12
+ type ToolKind =
13
+ | 'web_search'
14
+ | 'browser'
15
+ | 'file_read'
16
+ | 'file_write'
17
+ | 'file_edit'
18
+ | 'bash'
19
+ | 'subagent'
20
+ | 'mcp'
21
+ | 'question'
22
+ | 'todo'
23
+ | 'unknown';
24
+
25
+ type NormalizedToolPart = {
26
+ id: string;
27
+ name: string; // canonical machine-ish name: Bash, Read, webSearch, browser_navigate
28
+ kind: ToolKind; // stable UI bucket for icon/grouping
29
+ title: string; // human title shown in UI
30
+ input: unknown; // provider args, normalized only enough to be inspectable
31
+ output?: unknown; // successful result, if present
32
+ error?: {
33
+ message: string;
34
+ code?: string;
35
+ details?: unknown;
36
+ };
37
+ state: ToolState;
38
+ timing?: {
39
+ startedAt?: string | number;
40
+ completedAt?: string | number;
41
+ elapsedMs?: number;
42
+ };
43
+ metadata?: Record<string, unknown>; // provider-specific detail that UI may use later
44
+ };
45
+ ```
46
+
47
+ ## Rules
48
+
49
+ 1. `id` must be stable for the whole lifecycle.
50
+ 2. `name` is the normalized tool identity. It should not be guessed from input if
51
+ the provider gave an explicit tool name.
52
+ 3. `kind` is a small taxonomy for UI rendering. It is not the provider tool name.
53
+ 4. `title` is the human line. Prefer action text over raw names.
54
+ 5. `input` should preserve meaningful args: command, cwd, path, query, url,
55
+ prompt, MCP arguments.
56
+ 6. `output` is only successful result data.
57
+ 7. `error` is only failure data. Do not force errors into `output`.
58
+ 8. `metadata` preserves useful provider detail without expanding the core shape:
59
+ MCP server/tool, Codex action type, Claude task id, OpenCode child session id,
60
+ provider raw item type, attachments, truncation flags.
61
+ 9. Avoid persisting giant raw provider blobs in `metadata`. Keep raw fixtures in
62
+ the recorder/audit layer.
63
+
64
+ ## Title examples
65
+
66
+ | Native source | Title |
67
+ | --- | --- |
68
+ | Codex `webSearch.action.type = "search"` | `Searching Reuters top news` |
69
+ | Codex `webSearch.action.type = "openPage"` | `Opening apnews.com` |
70
+ | Codex `webSearch.action.type = "findInPage"` | `Finding "Updated" on apnews.com` |
71
+ | Codex `mcpToolCall` `{ server:"amalgm", tool:"browser_snapshot" }` | `Reading browser page` |
72
+ | Codex `mcpToolCall` `{ server:"amalgm", tool:"browser_navigate" }` | `Opening browser page` |
73
+ | Claude `mcp__amalgm__artifacts_register` | `Registering artifact Spotify Clone` |
74
+ | OpenCode `amalgm_browser_tabs_new` | `Opening browser tab` |
75
+ | OpenCode `task` with description | `Research Northern India politics` |
76
+
77
+ ## Provider mapping targets
78
+
79
+ ### Claude Code
80
+
81
+ - `tool_use.name` -> `name`
82
+ - `tool_use.input` -> `input`
83
+ - `tool_result.content` -> `output`
84
+ - `tool_result.is_error` -> `error`
85
+ - `mcp__<server>__<tool>` -> `metadata.mcp = { server, tool }`
86
+ - `task_started/task_progress/task_completed` -> `kind: "subagent"` or
87
+ `metadata.task`, without changing text/reasoning.
88
+
89
+ ### Codex
90
+
91
+ - `item.id` -> `id`
92
+ - `item.type` + explicit tool fields -> `name` / `kind`
93
+ - `item.arguments` or provider args -> `input`
94
+ - `item.result` / `aggregatedOutput` / `diff` -> `output`
95
+ - `item.error` or failed status -> `error`
96
+ - `item.durationMs` -> `timing.elapsedMs`
97
+ - `mcpToolCall.server/tool` -> `metadata.mcp`
98
+ - `webSearch.action` -> `metadata.action` and title derivation
99
+ - `collabAgentToolCall` -> `kind: "subagent"`
100
+
101
+ ### OpenCode
102
+
103
+ - `part.callID || part.id` -> `id`
104
+ - `part.tool` -> `name`
105
+ - `part.state.input` -> `input`
106
+ - `part.state.output` -> `output`
107
+ - `part.state.error` -> `error`
108
+ - `part.state.time` -> `timing`
109
+ - `part.state.metadata.actionDescriptor` -> preferred `title`
110
+ - `part.state.metadata.sessionId` on `task` -> `metadata.childSessionId`
111
+
112
+ ## Compatibility path
113
+
114
+ The first implementation should add fields without breaking existing consumers:
115
+
116
+ 1. Engine emits richer tool events.
117
+ 2. ACP frames carry `toolName`, `kind`, `title`, `rawInput`, `rawOutput`,
118
+ `error`, `metadata`, `timing`.
119
+ 3. UI maps these into the existing `ToolPart` while keeping old
120
+ `calling/output-available/output-error` state names until the renderer is
121
+ ready to switch.
122
+ 4. Once fixtures pass, rename UI state internally to
123
+ `started/running/complete/error`.