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.
- package/README.md +41 -1
- package/bin/amalgm.js +8 -2
- package/lib/auth-store.js +223 -0
- package/lib/cli.js +1132 -0
- package/lib/paths.js +30 -0
- package/lib/supervisor.js +476 -0
- package/lib/tunnel-chat.js +328 -0
- package/lib/tunnel-events.js +499 -0
- package/package.json +30 -3
- package/runtime/README.md +4 -0
- package/runtime/lib/chatInput.js +315 -0
- package/runtime/lib/harnesses.js +988 -0
- package/runtime/lib/local/amalgmStore.js +136 -0
- package/runtime/lib/local/credentialResolver.js +425 -0
- package/runtime/lib/mcpApps/registry.js +619 -0
- package/runtime/package.json +5 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +144 -0
- package/runtime/scripts/amalgm-mcp/agents/store.js +684 -0
- package/runtime/scripts/amalgm-mcp/agents/talk.js +1162 -0
- package/runtime/scripts/amalgm-mcp/agents/tools.js +221 -0
- package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +157 -0
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
- package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
- package/runtime/scripts/amalgm-mcp/config.js +140 -0
- package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
- package/runtime/scripts/amalgm-mcp/deps.js +40 -0
- package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
- package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
- package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
- package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
- package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
- package/runtime/scripts/amalgm-mcp/events/store.js +113 -0
- package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
- package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
- package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
- package/runtime/scripts/amalgm-mcp/index.js +100 -0
- package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
- package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
- package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +441 -0
- package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
- package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
- package/runtime/scripts/amalgm-mcp/local/rest.js +87 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +234 -0
- package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +377 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +127 -0
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
- package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
- package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
- package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
- package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
- package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
- package/runtime/scripts/amalgm-mcp/tasks/store.js +154 -0
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
- package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
- package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
- package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +497 -0
- package/runtime/scripts/chat-core/adapters/claude.js +165 -0
- package/runtime/scripts/chat-core/adapters/codex.js +313 -0
- package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
- package/runtime/scripts/chat-core/auth.js +177 -0
- package/runtime/scripts/chat-core/contract.js +328 -0
- package/runtime/scripts/chat-core/credentials/store.js +212 -0
- package/runtime/scripts/chat-core/egress.js +87 -0
- package/runtime/scripts/chat-core/engine.js +253 -0
- package/runtime/scripts/chat-core/event-schema.js +231 -0
- package/runtime/scripts/chat-core/events.js +190 -0
- package/runtime/scripts/chat-core/index.js +11 -0
- package/runtime/scripts/chat-core/input.js +50 -0
- package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
- package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
- package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
- package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
- package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
- package/runtime/scripts/chat-core/parts.js +253 -0
- package/runtime/scripts/chat-core/recorder.js +65 -0
- package/runtime/scripts/chat-core/runtime.js +86 -0
- package/runtime/scripts/chat-core/server.js +163 -0
- package/runtime/scripts/chat-core/sse.js +196 -0
- package/runtime/scripts/chat-core/stores.js +100 -0
- package/runtime/scripts/chat-core/tool-display.js +149 -0
- package/runtime/scripts/chat-core/tool-shape.js +143 -0
- package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
- package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
- package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
- package/runtime/scripts/chat-core/usage.js +343 -0
- package/runtime/scripts/chat-server/config.js +110 -0
- package/runtime/scripts/chat-server/db.js +529 -0
- package/runtime/scripts/chat-server/index.js +33 -0
- package/runtime/scripts/chat-server/model-catalog.js +327 -0
- package/runtime/scripts/chat-server.js +75 -0
- package/runtime/scripts/credential-adapter.js +131 -0
- package/runtime/scripts/fs-watcher.js +888 -0
- package/runtime/scripts/local-gateway.js +854 -0
- package/runtime/scripts/platform-context.txt +246 -0
- package/runtime/scripts/port-monitor.js +175 -0
- package/runtime/scripts/proxy-token-store.js +162 -0
- package/runtime/scripts/runtime-auth.js +163 -0
- package/runtime/scripts/test-claude-code-models.js +87 -0
- 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
|
+
};
|