amalgm 0.0.0 → 0.0.32

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 (116) hide show
  1. package/README.md +41 -1
  2. package/bin/amalgm.js +8 -2
  3. package/lib/auth-store.js +223 -0
  4. package/lib/cli.js +1132 -0
  5. package/lib/paths.js +30 -0
  6. package/lib/supervisor.js +476 -0
  7. package/lib/tunnel-chat.js +328 -0
  8. package/lib/tunnel-events.js +499 -0
  9. package/package.json +30 -3
  10. package/runtime/README.md +4 -0
  11. package/runtime/lib/chatInput.js +315 -0
  12. package/runtime/lib/harnesses.js +988 -0
  13. package/runtime/lib/local/amalgmStore.js +136 -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 +144 -0
  18. package/runtime/scripts/amalgm-mcp/agents/store.js +684 -0
  19. package/runtime/scripts/amalgm-mcp/agents/talk.js +1162 -0
  20. package/runtime/scripts/amalgm-mcp/agents/tools.js +221 -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 +157 -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 +140 -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 +113 -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 +441 -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 +87 -0
  50. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +234 -0
  51. package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
  52. package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
  53. package/runtime/scripts/amalgm-mcp/server/http.js +377 -0
  54. package/runtime/scripts/amalgm-mcp/server/mcp.js +127 -0
  55. package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
  56. package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
  57. package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
  58. package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
  59. package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
  60. package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
  61. package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
  62. package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
  63. package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
  64. package/runtime/scripts/amalgm-mcp/tasks/store.js +154 -0
  65. package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
  66. package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
  67. package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
  68. package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
  69. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
  70. package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
  71. package/runtime/scripts/amalgm-mcp/workspace/rest.js +497 -0
  72. package/runtime/scripts/chat-core/adapters/claude.js +165 -0
  73. package/runtime/scripts/chat-core/adapters/codex.js +313 -0
  74. package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
  75. package/runtime/scripts/chat-core/auth.js +177 -0
  76. package/runtime/scripts/chat-core/contract.js +328 -0
  77. package/runtime/scripts/chat-core/credentials/store.js +212 -0
  78. package/runtime/scripts/chat-core/egress.js +87 -0
  79. package/runtime/scripts/chat-core/engine.js +253 -0
  80. package/runtime/scripts/chat-core/event-schema.js +231 -0
  81. package/runtime/scripts/chat-core/events.js +190 -0
  82. package/runtime/scripts/chat-core/index.js +11 -0
  83. package/runtime/scripts/chat-core/input.js +50 -0
  84. package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
  85. package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
  86. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
  87. package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
  88. package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
  89. package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
  90. package/runtime/scripts/chat-core/parts.js +253 -0
  91. package/runtime/scripts/chat-core/recorder.js +65 -0
  92. package/runtime/scripts/chat-core/runtime.js +86 -0
  93. package/runtime/scripts/chat-core/server.js +163 -0
  94. package/runtime/scripts/chat-core/sse.js +196 -0
  95. package/runtime/scripts/chat-core/stores.js +100 -0
  96. package/runtime/scripts/chat-core/tool-display.js +149 -0
  97. package/runtime/scripts/chat-core/tool-shape.js +143 -0
  98. package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
  99. package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
  100. package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
  101. package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
  102. package/runtime/scripts/chat-core/usage.js +343 -0
  103. package/runtime/scripts/chat-server/config.js +110 -0
  104. package/runtime/scripts/chat-server/db.js +529 -0
  105. package/runtime/scripts/chat-server/index.js +33 -0
  106. package/runtime/scripts/chat-server/model-catalog.js +327 -0
  107. package/runtime/scripts/chat-server.js +75 -0
  108. package/runtime/scripts/credential-adapter.js +131 -0
  109. package/runtime/scripts/fs-watcher.js +888 -0
  110. package/runtime/scripts/local-gateway.js +854 -0
  111. package/runtime/scripts/platform-context.txt +246 -0
  112. package/runtime/scripts/port-monitor.js +175 -0
  113. package/runtime/scripts/proxy-token-store.js +162 -0
  114. package/runtime/scripts/runtime-auth.js +163 -0
  115. package/runtime/scripts/test-claude-code-models.js +87 -0
  116. package/runtime/tsconfig.json +15 -0
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ const { assertSameContract } = require('./contract');
4
+
5
+ class RuntimeController {
6
+ constructor({ adapters }) {
7
+ this.adapters = adapters;
8
+ this.sessions = new Map();
9
+ }
10
+
11
+ async create(contract) {
12
+ const existing = this.sessions.get(contract.sessionId);
13
+ if (existing) await this.destroy(contract.sessionId);
14
+ const factory = this.adapters[contract.harness];
15
+ if (!factory) throw new Error(`No adapter for ${contract.harness}`);
16
+ const adapter = factory();
17
+ const runtimeSession = await adapter.create(contract);
18
+ const record = {
19
+ contract,
20
+ adapter,
21
+ runtimeSession,
22
+ providerSessionId: runtimeSession.providerSessionId || runtimeSession.sessionId || null,
23
+ state: 'ready',
24
+ createdAt: Date.now(),
25
+ lastUsedAt: Date.now(),
26
+ };
27
+ this.sessions.set(contract.sessionId, record);
28
+ return record;
29
+ }
30
+
31
+ async resume(contract) {
32
+ const existing = this.sessions.get(contract.sessionId);
33
+ if (!existing) return this.create(contract);
34
+ assertSameContract(existing.contract, contract);
35
+ if (typeof existing.adapter.resume === 'function') {
36
+ existing.runtimeSession = await existing.adapter.resume(existing.runtimeSession, contract);
37
+ }
38
+ existing.contract = contract;
39
+ existing.providerSessionId = existing.runtimeSession.providerSessionId || existing.providerSessionId;
40
+ existing.state = 'ready';
41
+ existing.lastUsedAt = Date.now();
42
+ return existing;
43
+ }
44
+
45
+ async prompt(contract, input) {
46
+ const record = await this.resume(contract);
47
+ record.state = 'running';
48
+ record.lastUsedAt = Date.now();
49
+ const source = await record.adapter.prompt(record.runtimeSession, input, contract);
50
+ const self = this;
51
+ return (async function* stream() {
52
+ try {
53
+ for await (const e of source) yield e;
54
+ } finally {
55
+ const current = self.sessions.get(contract.sessionId);
56
+ if (current && current.state === 'running') {
57
+ current.state = 'ready';
58
+ current.lastUsedAt = Date.now();
59
+ }
60
+ }
61
+ })();
62
+ }
63
+
64
+ async stop(sessionId) {
65
+ const record = this.sessions.get(sessionId);
66
+ if (!record) return false;
67
+ if (typeof record.adapter.stop === 'function') await record.adapter.stop(record.runtimeSession);
68
+ record.state = 'stopped';
69
+ record.lastUsedAt = Date.now();
70
+ return true;
71
+ }
72
+
73
+ async destroy(sessionId) {
74
+ const record = this.sessions.get(sessionId);
75
+ if (!record) return false;
76
+ if (typeof record.adapter.destroy === 'function') await record.adapter.destroy(record.runtimeSession);
77
+ this.sessions.delete(sessionId);
78
+ return true;
79
+ }
80
+
81
+ get(sessionId) {
82
+ return this.sessions.get(sessionId) || null;
83
+ }
84
+ }
85
+
86
+ module.exports = { RuntimeController };
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const { PORT, PROXY_BASE_URL, ensureFreshProxyToken } = require('../chat-server/config');
5
+ const db = require('../chat-server/db');
6
+ const { CodexAdapter } = require('./adapters/codex');
7
+ const { ClaudeAdapter } = require('./adapters/claude');
8
+ const { OpenCodeAdapter } = require('./adapters/opencode');
9
+ const { ChatCore } = require('./engine');
10
+ const { forwardEgress, parseEgressPath } = require('./egress');
11
+ const { forwardMcpRelay, parseMcpRelayPath } = require('./tooling/mcp-relay');
12
+ const { RuntimeController } = require('./runtime');
13
+ const { writeSseHeaders } = require('./sse');
14
+ const { TurnStore } = require('./stores');
15
+ const { authorizeRuntimeHttp } = require('../runtime-auth');
16
+
17
+ function readBody(req) {
18
+ return new Promise((resolve, reject) => {
19
+ const chunks = [];
20
+ req.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
21
+ req.on('end', () => {
22
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
23
+ if (!raw) return resolve({});
24
+ try { resolve(JSON.parse(raw)); } catch (err) { reject(err); }
25
+ });
26
+ req.on('error', reject);
27
+ });
28
+ }
29
+
30
+ function sendJson(res, status, payload) {
31
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' });
32
+ res.end(JSON.stringify(payload));
33
+ }
34
+
35
+ function parseRoute(url) {
36
+ const u = new URL(url, 'http://localhost');
37
+ const parts = u.pathname.split('/').filter(Boolean);
38
+ if (parts[0] === 'chat' && parts.length === 1) return { handler: 'chat' };
39
+ if (parts[0] === 'raw-temp' && parts[1]) return { handler: 'raw-temp', id: parts[1], afterIndex: Number(u.searchParams.get('afterIndex') ?? -1) };
40
+ if (parts[0] === 'active' && parts[1]) return { handler: 'active', id: parts[1] };
41
+ if (parts[0] === 'stop' && parts[1]) return { handler: 'stop', id: parts[1] };
42
+ if (parts[0] === 'destroy' && parts[1]) return { handler: 'destroy', id: parts[1] };
43
+ if (parts[0] === 'egress' && parts[1]) {
44
+ const parsed = parseEgressPath(u.pathname);
45
+ return parsed ? { handler: 'egress', ...parsed, search: u.search } : null;
46
+ }
47
+ if (parts[0] === 'mcp-relay' && parts[1] && parts[2]) {
48
+ const parsed = parseMcpRelayPath(u.pathname);
49
+ return parsed ? { handler: 'mcp-relay', ...parsed, search: u.search } : null;
50
+ }
51
+ if (parts[0] === 'healthz') return { handler: 'healthz' };
52
+ return null;
53
+ }
54
+
55
+ function writeRawTemp(core, route, res) {
56
+ const entry = core.turns.latest(route.id) || core.turns.get(route.id);
57
+ if (!entry) return sendJson(res, 404, { error: 'No active stream', codeSessionId: route.id });
58
+ writeSseHeaders(res);
59
+ const chunks = entry.chunks.filter((chunk) => chunk.index > (Number.isFinite(route.afterIndex) ? route.afterIndex : -1));
60
+ if (chunks.length) {
61
+ res.write(`event: existing\ndata: ${JSON.stringify({ codeSessionId: entry.sessionId, assistantMessageId: entry.assistantMessageId, chunks, total: chunks.length })}\n\n`);
62
+ }
63
+ if (entry.status !== 'streaming') {
64
+ res.write(`event: complete\ndata: ${JSON.stringify({ codeSessionId: entry.sessionId, total: entry.chunks.length })}\n\n`);
65
+ res.end();
66
+ return;
67
+ }
68
+ const off = core.turns.subscribe(entry.turnId, (chunk, current) => {
69
+ if (chunk) res.write(`event: chunk\ndata: ${JSON.stringify({ codeSessionId: current.sessionId, chunk, total: current.chunks.length })}\n\n`);
70
+ else {
71
+ res.write(`event: complete\ndata: ${JSON.stringify({ codeSessionId: current.sessionId, total: current.chunks.length })}\n\n`);
72
+ res.end();
73
+ }
74
+ });
75
+ res.on('close', off);
76
+ }
77
+
78
+ function createCore(options = {}) {
79
+ const turns = new TurnStore();
80
+ const runtime = new RuntimeController({
81
+ adapters: {
82
+ claude_code: () => new ClaudeAdapter(),
83
+ codex: () => new CodexAdapter(),
84
+ opencode: () => new OpenCodeAdapter(),
85
+ },
86
+ });
87
+ return new ChatCore({ db, runtime, turns, options: { ...options, port: PORT } });
88
+ }
89
+
90
+ function createServer(core = createCore()) {
91
+ return http.createServer(async (req, res) => {
92
+ if (!authorizeRuntimeHttp(req, res, { exemptPrefixes: ['/egress/'] })) return;
93
+ const route = parseRoute(req.url);
94
+ if (!route) return sendJson(res, 404, { error: 'Not found' });
95
+ try {
96
+ if (route.handler === 'healthz') return sendJson(res, 200, { status: 'ok', runtime: 'chat-core', uptime: process.uptime() });
97
+ if (route.handler === 'active') return sendJson(res, 200, core.active(route.id));
98
+ if (route.handler === 'stop') {
99
+ if (req.method !== 'POST') return sendJson(res, 405, { error: 'Method not allowed' });
100
+ return sendJson(res, 200, { stopped: await core.stop(route.id), codeSessionId: route.id });
101
+ }
102
+ if (route.handler === 'destroy') {
103
+ if (req.method !== 'DELETE') return sendJson(res, 405, { error: 'Method not allowed' });
104
+ return sendJson(res, 200, { destroyed: await core.destroy(route.id), codeSessionId: route.id });
105
+ }
106
+ if (route.handler === 'raw-temp') return writeRawTemp(core, route, res);
107
+ if (route.handler === 'egress') {
108
+ const contract = core.contracts.get(route.sessionId);
109
+ if (!contract || contract.authMethod !== 'amalgm') return sendJson(res, 404, { error: 'No Amalgm egress session', sessionId: route.sessionId });
110
+ const proxyToken = await ensureFreshProxyToken();
111
+ return await forwardEgress({
112
+ req,
113
+ res,
114
+ contract,
115
+ turn: core.turns.active(route.sessionId),
116
+ proxyBaseUrl: PROXY_BASE_URL || 'https://amalgm-api-proxy-v2.fly.dev',
117
+ proxyToken,
118
+ upstreamPath: `${route.upstreamPath}${route.search || ''}`,
119
+ });
120
+ }
121
+ if (route.handler === 'mcp-relay') {
122
+ const contract = core.contracts.get(route.sessionId);
123
+ if (!contract) return sendJson(res, 404, { error: 'No MCP relay session', sessionId: route.sessionId });
124
+ return await forwardMcpRelay({ req, res, contract, serverName: route.serverName, search: route.search || '' });
125
+ }
126
+ if (route.handler === 'chat') {
127
+ if (req.method !== 'POST') return sendJson(res, 405, { error: 'Method not allowed' });
128
+ const payload = await readBody(req);
129
+ writeSseHeaders(res);
130
+ let open = true;
131
+ const heartbeat = setInterval(() => {
132
+ if (open && !res.writableEnded) res.write(': keep-alive\n\n');
133
+ }, 15_000);
134
+ heartbeat.unref?.();
135
+ res.on('close', () => { open = false; });
136
+ try {
137
+ await core.runTurn(payload, (frame) => {
138
+ if (open && !res.writableEnded) res.write(frame);
139
+ });
140
+ } finally {
141
+ clearInterval(heartbeat);
142
+ }
143
+ if (!res.writableEnded) res.end();
144
+ }
145
+ } catch (err) {
146
+ console.error('[ChatCore] error:', err);
147
+ if (!res.headersSent) sendJson(res, 500, { error: err.message || 'Internal server error' });
148
+ else {
149
+ res.write(`data: ${JSON.stringify({ _type: 'error', message: err.message || 'Internal server error' })}\n\n`);
150
+ res.write(`data: ${JSON.stringify({ _type: 'complete', stopReason: 'error' })}\n\n`);
151
+ res.end();
152
+ }
153
+ }
154
+ });
155
+ }
156
+
157
+ module.exports = {
158
+ createCore,
159
+ createServer,
160
+ parseRoute,
161
+ readBody,
162
+ sendJson,
163
+ };
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ const { toolKind: canonicalToolKind } = require('./tool-shape');
4
+ const { totalUsageTokens } = require('./usage');
5
+
6
+ function toPromptUsage(usage) {
7
+ if (!usage) return undefined;
8
+ const totalTokens = totalUsageTokens(usage);
9
+ const tokenList = usage.tokenList || {
10
+ input: Number(usage.inputTokens || 0),
11
+ output: Number(usage.outputTokens || 0),
12
+ cacheRead: Number(usage.cacheReadTokens || 0),
13
+ cacheWrite: Number(usage.cacheWriteTokens || 0),
14
+ thought: Number(usage.thoughtTokens || 0),
15
+ total: totalTokens,
16
+ };
17
+ return {
18
+ tokenList,
19
+ cumulativeTokenList: usage.cumulativeTokenList ?? null,
20
+ previousCumulativeTokenList: usage.previousCumulativeTokenList ?? null,
21
+ inputTokens: Number(tokenList.input || 0),
22
+ outputTokens: Number(tokenList.output || 0),
23
+ cacheReadTokens: Number(tokenList.cacheRead || 0),
24
+ cacheWriteTokens: Number(tokenList.cacheWrite || 0),
25
+ thoughtTokens: Number(tokenList.thought || 0),
26
+ inputContextSize: usage.inputContextSize ?? tokenList.total ?? totalTokens,
27
+ totalTokens,
28
+ contextUsedTokens: usage.contextUsedTokens ?? usage.inputContextSize ?? tokenList.total ?? null,
29
+ contextLimit: usage.contextLimit ?? null,
30
+ usedTokens: usage.usedTokens ?? usage.contextUsedTokens ?? usage.inputContextSize ?? tokenList.total ?? null,
31
+ lastUsedTokens: usage.lastUsedTokens ?? usage.contextUsedTokens ?? usage.inputContextSize ?? tokenList.total ?? null,
32
+ maxTokens: usage.maxTokens ?? usage.contextLimit ?? null,
33
+ totalProcessedTokens: usage.totalProcessedTokens ?? null,
34
+ costUsd: usage.costUsd ?? null,
35
+ marketCostUsd: usage.marketCostUsd ?? null,
36
+ operation: usage.operation,
37
+ billable: usage.billable !== false,
38
+ source: usage.source,
39
+ exact: usage.exact === true,
40
+ };
41
+ }
42
+
43
+ function usageContextUsed(usage) {
44
+ const parsed = Number(usage?.inputContextSize ?? usage?.tokenList?.total ?? usage?.contextUsedTokens ?? usage?.usedTokens);
45
+ return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : 0;
46
+ }
47
+
48
+ function usageContextLimit(usage) {
49
+ const parsed = Number(usage?.contextLimit ?? usage?.maxTokens);
50
+ return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : 0;
51
+ }
52
+
53
+ function toolKind(name) {
54
+ const clean = String(name || '').toLowerCase();
55
+ const canonical = canonicalToolKind(name);
56
+ if (canonical && canonical !== 'unknown') return canonical;
57
+ if (clean.includes('bash') || clean.includes('command')) return 'execute';
58
+ if (clean.includes('read')) return 'read';
59
+ if (clean.includes('edit') || clean.includes('write') || clean.includes('file')) return 'edit';
60
+ if (clean.includes('grep') || clean.includes('glob') || clean.includes('search')) return 'search';
61
+ if (clean.includes('fetch')) return 'fetch';
62
+ return 'other';
63
+ }
64
+
65
+ function toAgentStreamEvent(e, context = {}) {
66
+ if (e.type === 'text.delta') {
67
+ return { _type: 'update', eventTimestamp: e.ts, sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: e.text || '' } };
68
+ }
69
+ if (e.type === 'reasoning.delta') {
70
+ return { _type: 'update', eventTimestamp: e.ts, sessionUpdate: 'agent_thought_chunk', content: { type: 'text', text: e.text || '' } };
71
+ }
72
+ if (e.type === 'tool.started') {
73
+ return {
74
+ _type: 'update',
75
+ eventTimestamp: e.ts,
76
+ sessionUpdate: 'tool_call',
77
+ toolCallId: e.toolCallId,
78
+ toolName: e.toolName || e.name,
79
+ title: e.title || e.toolName,
80
+ kind: e.kind || toolKind(e.toolName),
81
+ status: 'in_progress',
82
+ rawInput: e.input || {},
83
+ metadata: e.metadata || undefined,
84
+ timing: e.timing || undefined,
85
+ };
86
+ }
87
+ if (e.type === 'tool.updated') {
88
+ return {
89
+ _type: 'update',
90
+ eventTimestamp: e.ts,
91
+ sessionUpdate: 'tool_call_update',
92
+ toolCallId: e.toolCallId,
93
+ toolName: e.toolName || e.name,
94
+ title: e.title || null,
95
+ kind: e.kind || null,
96
+ status: e.status || 'in_progress',
97
+ rawInput: e.inputDelta || undefined,
98
+ rawOutput: e.outputDelta ?? e.output ?? undefined,
99
+ error: e.error || undefined,
100
+ metadata: e.metadata || undefined,
101
+ timing: e.timing || undefined,
102
+ };
103
+ }
104
+ if (e.type === 'tool.completed') {
105
+ return {
106
+ _type: 'update',
107
+ eventTimestamp: e.ts,
108
+ sessionUpdate: 'tool_call_update',
109
+ toolCallId: e.toolCallId,
110
+ toolName: e.toolName || e.name,
111
+ title: e.title || null,
112
+ kind: e.kind || null,
113
+ status: e.isError ? 'failed' : 'completed',
114
+ rawInput: e.input || undefined,
115
+ rawOutput: e.output ?? e.result ?? '',
116
+ error: e.error || undefined,
117
+ metadata: e.metadata || undefined,
118
+ timing: e.timing || undefined,
119
+ };
120
+ }
121
+ if (e.type === 'usage.final') {
122
+ const usage = toPromptUsage(e.usage);
123
+ const contextUsed = usageContextUsed(e.usage);
124
+ const contextLimit = usageContextLimit(e.usage);
125
+ const fallbackTotal = usage?.totalTokens || 0;
126
+ return {
127
+ _type: 'update',
128
+ eventTimestamp: e.ts,
129
+ sessionUpdate: 'usage_update',
130
+ used: contextUsed || fallbackTotal,
131
+ size: contextLimit || null,
132
+ inputContextSize: usage?.inputContextSize ?? undefined,
133
+ cost: usage?.costUsd != null ? { amount: usage.costUsd, currency: 'USD' } : null,
134
+ usage,
135
+ };
136
+ }
137
+ if (e.type === 'compaction.started') {
138
+ return {
139
+ _type: 'update',
140
+ eventTimestamp: e.ts,
141
+ sessionUpdate: 'compaction_boundary',
142
+ phase: 'started',
143
+ trigger: e.trigger || 'auto',
144
+ };
145
+ }
146
+ if (e.type === 'compaction.finished') {
147
+ return {
148
+ _type: 'update',
149
+ eventTimestamp: e.ts,
150
+ sessionUpdate: 'compaction_boundary',
151
+ phase: 'finished',
152
+ trigger: e.trigger || 'auto',
153
+ preTokens: e.preTokens ?? null,
154
+ postTokens: e.postTokens ?? null,
155
+ durationMs: e.durationMs ?? null,
156
+ };
157
+ }
158
+ if (e.type === 'warning') {
159
+ return {
160
+ _type: 'model_error',
161
+ error: e.message || 'Runtime warning',
162
+ requestedModel: e.requestedModel || '',
163
+ eventTimestamp: e.ts,
164
+ };
165
+ }
166
+ if (e.type === 'error') return { _type: 'error', message: e.message || 'Unknown error', eventTimestamp: e.ts };
167
+ if (e.type === 'done') {
168
+ return {
169
+ _type: 'complete',
170
+ codeSessionId: context.sessionId,
171
+ providerSessionId: e.providerSessionId || context.providerSessionId || null,
172
+ stopReason: e.stopReason || 'end_turn',
173
+ usage: toPromptUsage(context.usage || e.usage),
174
+ eventTimestamp: e.ts,
175
+ };
176
+ }
177
+ return { _type: 'update', eventTimestamp: e.ts, sessionUpdate: 'session_info_update' };
178
+ }
179
+
180
+ function frameFor(e, context) {
181
+ return `data: ${JSON.stringify(toAgentStreamEvent(e, context))}\n\n`;
182
+ }
183
+
184
+ function titleFrame({ sessionId, title }) {
185
+ return `data: ${JSON.stringify({ _type: 'title_generated', sessionId, title })}\n\n`;
186
+ }
187
+
188
+ function writeSseHeaders(res) {
189
+ res.writeHead(200, {
190
+ 'Content-Type': 'text/event-stream',
191
+ 'Cache-Control': 'no-cache, no-transform',
192
+ Connection: 'keep-alive',
193
+ });
194
+ }
195
+
196
+ module.exports = { frameFor, titleFrame, toAgentStreamEvent, toPromptUsage, writeSseHeaders };
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ class TurnStore {
4
+ constructor() {
5
+ this.turns = new Map();
6
+ this.activeBySession = new Map();
7
+ this.latestBySession = new Map();
8
+ this.listeners = new Map();
9
+ }
10
+
11
+ start({ sessionId, turnId, assistantMessageId, userMessageId }) {
12
+ const previous = this.activeBySession.get(sessionId);
13
+ if (previous && previous !== turnId) this.mark(previous, 'aborted');
14
+ const entry = {
15
+ sessionId,
16
+ turnId,
17
+ assistantMessageId,
18
+ userMessageId,
19
+ status: 'streaming',
20
+ chunks: [],
21
+ parts: [],
22
+ createdAt: Date.now(),
23
+ updatedAt: Date.now(),
24
+ };
25
+ this.turns.set(turnId, entry);
26
+ this.activeBySession.set(sessionId, turnId);
27
+ this.latestBySession.set(sessionId, turnId);
28
+ return entry;
29
+ }
30
+
31
+ active(sessionId) {
32
+ const turnId = this.activeBySession.get(sessionId);
33
+ return turnId ? this.turns.get(turnId) || null : null;
34
+ }
35
+
36
+ latest(sessionId) {
37
+ return this.active(sessionId) || this.get(this.latestBySession.get(sessionId));
38
+ }
39
+
40
+ get(turnId) {
41
+ return this.turns.get(turnId) || null;
42
+ }
43
+
44
+ addChunk(turnId, frame) {
45
+ const entry = this.turns.get(turnId);
46
+ if (!entry || !['streaming', 'cancelling'].includes(entry.status)) return null;
47
+ const chunk = { index: entry.chunks.length, data: frame, timestamp: Date.now() };
48
+ entry.chunks.push(chunk);
49
+ entry.updatedAt = Date.now();
50
+ this.emit(turnId, chunk, entry);
51
+ return chunk;
52
+ }
53
+
54
+ setParts(turnId, parts) {
55
+ const entry = this.turns.get(turnId);
56
+ if (entry) entry.parts = parts;
57
+ }
58
+
59
+ mark(turnId, status) {
60
+ const entry = this.turns.get(turnId);
61
+ if (!entry) return;
62
+ entry.status = status;
63
+ entry.updatedAt = Date.now();
64
+ if (status !== 'cancelling' && this.activeBySession.get(entry.sessionId) === turnId) this.activeBySession.delete(entry.sessionId);
65
+ this.emit(turnId, null, entry);
66
+ }
67
+
68
+ clear(turnId) {
69
+ const entry = this.turns.get(turnId);
70
+ if (entry && this.activeBySession.get(entry.sessionId) === turnId) this.activeBySession.delete(entry.sessionId);
71
+ if (entry && this.latestBySession.get(entry.sessionId) === turnId) this.latestBySession.delete(entry.sessionId);
72
+ this.turns.delete(turnId);
73
+ this.listeners.delete(turnId);
74
+ }
75
+
76
+ chunksAfter(turnId, afterIndex = -1) {
77
+ const entry = this.turns.get(turnId);
78
+ return entry ? entry.chunks.filter((chunk) => chunk.index > afterIndex) : [];
79
+ }
80
+
81
+ subscribe(turnId, callback) {
82
+ if (!this.listeners.has(turnId)) this.listeners.set(turnId, new Set());
83
+ const set = this.listeners.get(turnId);
84
+ set.add(callback);
85
+ return () => {
86
+ set.delete(callback);
87
+ if (set.size === 0) this.listeners.delete(turnId);
88
+ };
89
+ }
90
+
91
+ emit(turnId, chunk, entry) {
92
+ const set = this.listeners.get(turnId);
93
+ if (!set) return;
94
+ for (const callback of set) {
95
+ try { callback(chunk, entry); } catch {}
96
+ }
97
+ }
98
+ }
99
+
100
+ module.exports = { TurnStore };
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ function isRecord(value) {
4
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
5
+ }
6
+
7
+ function trimText(value) {
8
+ if (typeof value !== 'string') return '';
9
+ return value.trim();
10
+ }
11
+
12
+ function firstString(...values) {
13
+ for (const value of values) {
14
+ const text = trimText(value);
15
+ if (text) return text;
16
+ }
17
+ return '';
18
+ }
19
+
20
+ function compactText(value, max = 160) {
21
+ const text = trimText(String(value ?? '').replace(/\s+/g, ' '));
22
+ if (!text) return '';
23
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 1)).trimEnd()}...` : text;
24
+ }
25
+
26
+ function withActionDescriptor(input, detail) {
27
+ const next = isRecord(input) ? { ...input } : {};
28
+ const cleanDetail = compactText(detail);
29
+ if (cleanDetail && typeof next.actionDescriptor !== 'string') {
30
+ next.actionDescriptor = cleanDetail;
31
+ }
32
+ return next;
33
+ }
34
+
35
+ function withOutputDescriptor(output, detail) {
36
+ if (!isRecord(output)) return output;
37
+ const cleanDetail = compactText(detail);
38
+ if (!cleanDetail || typeof output.actionDescriptor === 'string') return output;
39
+ return { ...output, actionDescriptor: cleanDetail };
40
+ }
41
+
42
+ function commandText(value) {
43
+ if (Array.isArray(value)) return value.map((part) => String(part)).join(' ').trim();
44
+ return trimText(value);
45
+ }
46
+
47
+ function summarizeToolRequest(toolName, input = {}, fallback = '') {
48
+ const args = isRecord(input) ? input : {};
49
+ const normalized = String(toolName || '').toLowerCase();
50
+ const explicit = firstString(
51
+ args.actionDescriptor,
52
+ args.description,
53
+ args.summary,
54
+ args.title,
55
+ );
56
+ if (explicit) return compactText(explicit);
57
+
58
+ if (normalized.includes('bash') || normalized.includes('command')) {
59
+ const command = commandText(args.command || args.rawCommand);
60
+ return command ? `$ ${command}` : compactText(fallback);
61
+ }
62
+
63
+ if (
64
+ normalized.includes('read') ||
65
+ normalized.includes('write') ||
66
+ normalized.includes('edit') ||
67
+ normalized.includes('file') ||
68
+ normalized.includes('path')
69
+ ) {
70
+ return compactText(firstString(args.path, args.filePath, args.file_path, args.file, fallback));
71
+ }
72
+
73
+ if (normalized.includes('grep') || normalized.includes('glob') || normalized.includes('search')) {
74
+ return compactText(firstString(args.query, args.pattern, args.url, args.path, fallback));
75
+ }
76
+
77
+ if (normalized.includes('fetch') || normalized.includes('web')) {
78
+ return compactText(firstString(args.url, args.query, fallback));
79
+ }
80
+
81
+ if (normalized.includes('mcp')) {
82
+ return compactText(firstString(args.toolName, args.name, args.serverName, fallback));
83
+ }
84
+
85
+ return compactText(firstString(
86
+ args.query,
87
+ args.prompt,
88
+ args.input,
89
+ args.url,
90
+ args.path,
91
+ args.filePath,
92
+ args.file_path,
93
+ args.pattern,
94
+ fallback,
95
+ ));
96
+ }
97
+
98
+ function summarizeToolOutput(output) {
99
+ if (output == null) return '';
100
+ if (typeof output === 'string') return summarizeTextOutput(output);
101
+ if (Array.isArray(output)) return output.length ? `${output.length} item${output.length === 1 ? '' : 's'}` : '';
102
+ if (!isRecord(output)) return compactText(output);
103
+
104
+ const explicit = firstString(output.actionDescriptor, output.summary, output.title, output.message, output.error);
105
+ if (explicit) return compactText(explicit);
106
+
107
+ const content = firstString(output.output, output.stdout, output.content, output.result, output.text);
108
+ if (content) return summarizeTextOutput(content);
109
+
110
+ if (Array.isArray(output.files)) {
111
+ return `${output.files.length} file${output.files.length === 1 ? '' : 's'}`;
112
+ }
113
+ if (Array.isArray(output.results)) {
114
+ return `${output.results.length} result${output.results.length === 1 ? '' : 's'}`;
115
+ }
116
+ if (Array.isArray(output.links)) {
117
+ return `${output.links.length} link${output.links.length === 1 ? '' : 's'}`;
118
+ }
119
+ return '';
120
+ }
121
+
122
+ function summarizeTextOutput(value) {
123
+ const lines = String(value || '')
124
+ .split(/\r?\n/)
125
+ .map((line) => compactText(line, 160))
126
+ .filter(Boolean);
127
+ if (lines[0]) return lines[0];
128
+ return '';
129
+ }
130
+
131
+ function stringifyResult(value) {
132
+ if (value == null) return '';
133
+ if (typeof value === 'string') return value;
134
+ try {
135
+ return JSON.stringify(value);
136
+ } catch {
137
+ return String(value);
138
+ }
139
+ }
140
+
141
+ module.exports = {
142
+ compactText,
143
+ isRecord,
144
+ stringifyResult,
145
+ summarizeToolOutput,
146
+ summarizeToolRequest,
147
+ withActionDescriptor,
148
+ withOutputDescriptor,
149
+ };