pikiloom 0.4.37 → 0.4.39
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/dashboard/dist/assets/{AgentTab-DbFzaIyZ.js → AgentTab-Ds8D6yrl.js} +1 -1
- package/dashboard/dist/assets/{ConnectionModal-QNYOWHCU.js → ConnectionModal-Buj2d5L5.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-BGaKHjFT.js → DirBrowser-BTv7rH8E.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-D4Gv9b8z.js → ExtensionsTab-BnGwxS9U.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-DxxoZd84.js → IMAccessTab-DjQnpOmF.js} +1 -1
- package/dashboard/dist/assets/{Modal-Nm2e93s5.js → Modal-DpuinsE3.js} +1 -1
- package/dashboard/dist/assets/{Modals-BwxZmU0U.js → Modals-Bs842H3n.js} +1 -1
- package/dashboard/dist/assets/{Select-SGlII0yx.js → Select-Dvia29HZ.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-CVItEgcH.js +1 -0
- package/dashboard/dist/assets/{SystemTab-BF6lIAYM.js → SystemTab-BzPtwDxq.js} +1 -1
- package/dashboard/dist/assets/index-Bthwt6K_.css +1 -0
- package/dashboard/dist/assets/{index-DZiAiRNt.js → index-S0NmlDEH.js} +14 -14
- package/dashboard/dist/assets/{index-BP8R_bLT.js → index-yZ-iG1qk.js} +2 -2
- package/dashboard/dist/assets/{shared-S0kcs5yP.js → shared-p3kZpiD4.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/kernel-bridge.js +207 -0
- package/dist/agent/mcp/capabilities.js +39 -0
- package/dist/agent/stream.js +19 -1
- package/dist/bot/bot.js +4 -13
- package/dist/cli/kernel-app.js +115 -0
- package/dist/cli/main.js +7 -0
- package/package.json +4 -2
- package/packages/kernel/README.md +305 -0
- package/packages/kernel/dist/contracts/driver.d.ts +95 -0
- package/packages/kernel/dist/contracts/driver.js +1 -0
- package/packages/kernel/dist/contracts/ports.d.ts +84 -0
- package/packages/kernel/dist/contracts/ports.js +1 -0
- package/packages/kernel/dist/contracts/surface.d.ts +92 -0
- package/packages/kernel/dist/contracts/surface.js +1 -0
- package/packages/kernel/dist/drivers/claude.d.ts +34 -0
- package/packages/kernel/dist/drivers/claude.js +500 -0
- package/packages/kernel/dist/drivers/codex.d.ts +24 -0
- package/packages/kernel/dist/drivers/codex.js +415 -0
- package/packages/kernel/dist/drivers/echo.d.ts +20 -0
- package/packages/kernel/dist/drivers/echo.js +61 -0
- package/packages/kernel/dist/drivers/gemini.d.ts +18 -0
- package/packages/kernel/dist/drivers/gemini.js +143 -0
- package/packages/kernel/dist/drivers/hermes.d.ts +14 -0
- package/packages/kernel/dist/drivers/hermes.js +194 -0
- package/packages/kernel/dist/drivers/index.d.ts +5 -0
- package/packages/kernel/dist/drivers/index.js +5 -0
- package/packages/kernel/dist/index.d.ts +18 -0
- package/packages/kernel/dist/index.js +29 -0
- package/packages/kernel/dist/ports/defaults.d.ts +59 -0
- package/packages/kernel/dist/ports/defaults.js +137 -0
- package/packages/kernel/dist/protocol/index.d.ts +309 -0
- package/packages/kernel/dist/protocol/index.js +59 -0
- package/packages/kernel/dist/runtime/hub.d.ts +68 -0
- package/packages/kernel/dist/runtime/hub.js +334 -0
- package/packages/kernel/dist/runtime/loom.d.ts +45 -0
- package/packages/kernel/dist/runtime/loom.js +72 -0
- package/packages/kernel/dist/runtime/pty.d.ts +23 -0
- package/packages/kernel/dist/runtime/pty.js +69 -0
- package/packages/kernel/dist/runtime/session-runner.d.ts +27 -0
- package/packages/kernel/dist/runtime/session-runner.js +210 -0
- package/packages/kernel/dist/runtime/tui.d.ts +8 -0
- package/packages/kernel/dist/runtime/tui.js +35 -0
- package/packages/kernel/dist/runtime/turn.d.ts +17 -0
- package/packages/kernel/dist/runtime/turn.js +16 -0
- package/packages/kernel/dist/surfaces/cli.d.ts +19 -0
- package/packages/kernel/dist/surfaces/cli.js +65 -0
- package/packages/kernel/dist/surfaces/index.d.ts +2 -0
- package/packages/kernel/dist/surfaces/index.js +2 -0
- package/packages/kernel/dist/surfaces/web.d.ts +39 -0
- package/packages/kernel/dist/surfaces/web.js +244 -0
- package/dashboard/dist/assets/SessionPanel-DYfSlreh.js +0 -1
- package/dashboard/dist/assets/index-CtS48Jn-.css +0 -1
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
// Real driver: shells the local `claude` CLI in stream-json mode and normalizes its
|
|
3
|
+
// events into kernel DriverEvents. Faithful to pikiloom's claude.ts event shapes
|
|
4
|
+
// (system / stream_event{message_start,content_block_delta,message_delta} / assistant / result),
|
|
5
|
+
// but fully self-contained. Proves "下层 Claude 不变".
|
|
6
|
+
export class ClaudeDriver {
|
|
7
|
+
bin;
|
|
8
|
+
id = 'claude';
|
|
9
|
+
capabilities = { steer: true, interact: false, resume: true, tui: true };
|
|
10
|
+
constructor(bin = 'claude') {
|
|
11
|
+
this.bin = bin;
|
|
12
|
+
}
|
|
13
|
+
// Interactive Claude Code TUI (no -p): the kernel spawns this in a PTY and passes
|
|
14
|
+
// the terminal through. Model/resume/BYOK-env come from the kernel's resolution.
|
|
15
|
+
tui(input) {
|
|
16
|
+
const args = [];
|
|
17
|
+
if (input.model)
|
|
18
|
+
args.push('--model', input.model);
|
|
19
|
+
if (input.sessionId)
|
|
20
|
+
args.push('--resume', input.sessionId);
|
|
21
|
+
if (input.extraArgs?.length)
|
|
22
|
+
args.push(...input.extraArgs);
|
|
23
|
+
return { command: this.bin, args, cwd: input.workdir, env: input.env };
|
|
24
|
+
}
|
|
25
|
+
run(input, ctx) {
|
|
26
|
+
const steerable = !!input.steerable;
|
|
27
|
+
const args = ['-p', '--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
|
|
28
|
+
if (input.model)
|
|
29
|
+
args.push('--model', input.model);
|
|
30
|
+
if (input.effort)
|
|
31
|
+
args.push('--effort', input.effort === 'ultra' ? 'max' : input.effort); // request extended thinking (ultra is a display-only alias for max)
|
|
32
|
+
if (input.sessionId)
|
|
33
|
+
args.push('--resume', input.sessionId);
|
|
34
|
+
if (input.systemPrompt)
|
|
35
|
+
args.push('--append-system-prompt', input.systemPrompt);
|
|
36
|
+
if (input.mcpConfigPath)
|
|
37
|
+
args.push('--mcp-config', input.mcpConfigPath);
|
|
38
|
+
if (input.permissionMode)
|
|
39
|
+
args.push('--permission-mode', input.permissionMode); // parity: keep bypass/accept-edits on the kernel path
|
|
40
|
+
if (steerable)
|
|
41
|
+
args.push('--input-format', 'stream-json', '--replay-user-messages'); // parity: mid-turn steer
|
|
42
|
+
if (input.extraArgs?.length)
|
|
43
|
+
args.push(...input.extraArgs);
|
|
44
|
+
const state = {
|
|
45
|
+
text: '', reasoning: '', streamedText: false, streamedReasoning: false,
|
|
46
|
+
sessionId: null, model: null,
|
|
47
|
+
stopReason: null, error: null,
|
|
48
|
+
input: null, output: null, cached: null,
|
|
49
|
+
cacheCreation: null,
|
|
50
|
+
contextWindow: null, turnOutputTokensBase: 0,
|
|
51
|
+
subAgents: new Map(),
|
|
52
|
+
tools: new Map(),
|
|
53
|
+
};
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
let child;
|
|
56
|
+
let settled = false;
|
|
57
|
+
const finish = (r) => {
|
|
58
|
+
if (settled)
|
|
59
|
+
return;
|
|
60
|
+
settled = true;
|
|
61
|
+
try {
|
|
62
|
+
child?.stdin?.end();
|
|
63
|
+
}
|
|
64
|
+
catch { /* ignore */ }
|
|
65
|
+
if (steerable) {
|
|
66
|
+
try {
|
|
67
|
+
child?.kill('SIGTERM');
|
|
68
|
+
}
|
|
69
|
+
catch { /* ignore */ }
|
|
70
|
+
} // replay mode keeps the process alive past the turn
|
|
71
|
+
resolve(r);
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
child = spawn(this.bin, args, { cwd: input.workdir, env: { ...process.env, ...(input.env || {}) }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
finish({ ok: false, text: '', error: `spawn failed: ${err?.message || err}`, stopReason: 'error' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const onAbort = () => { try {
|
|
81
|
+
child.kill('SIGTERM');
|
|
82
|
+
}
|
|
83
|
+
catch { /* ignore */ } };
|
|
84
|
+
if (ctx.signal.aborted)
|
|
85
|
+
onAbort();
|
|
86
|
+
else
|
|
87
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
88
|
+
if (steerable) {
|
|
89
|
+
ctx.registerSteer(async (prompt) => {
|
|
90
|
+
try {
|
|
91
|
+
child.stdin.write(claudeUserMessage(prompt) + '\n');
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const usageOf = () => this.usage(state);
|
|
100
|
+
let buf = '';
|
|
101
|
+
let stderr = '';
|
|
102
|
+
child.stdout.on('data', (chunk) => {
|
|
103
|
+
buf += chunk.toString('utf8');
|
|
104
|
+
const lines = buf.split('\n');
|
|
105
|
+
buf = lines.pop() || '';
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
const trimmed = line.trim();
|
|
108
|
+
if (!trimmed)
|
|
109
|
+
continue;
|
|
110
|
+
let ev;
|
|
111
|
+
try {
|
|
112
|
+
ev = JSON.parse(trimmed);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
handleClaudeEvent(ev, state, ctx.emit);
|
|
118
|
+
if (steerable && ev.type === 'result') {
|
|
119
|
+
// In replay mode the turn ends on `result` but the process lingers; settle now.
|
|
120
|
+
finish({ ok: !state.error, text: state.text, reasoning: state.reasoning || undefined, error: state.error, stopReason: state.stopReason, sessionId: state.sessionId, usage: usageOf() });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
child.stderr.on('data', (c) => { stderr += c.toString('utf8'); });
|
|
125
|
+
child.on('error', (err) => finish({ ok: false, text: state.text, error: `claude spawn error: ${err.message}`, stopReason: 'error' }));
|
|
126
|
+
child.on('close', (code) => {
|
|
127
|
+
if (ctx.signal.aborted) {
|
|
128
|
+
finish({ ok: false, text: state.text, reasoning: state.reasoning, error: 'Interrupted by user.', stopReason: 'interrupted', sessionId: state.sessionId, usage: usageOf() });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const ok = !state.error && code === 0;
|
|
132
|
+
finish({ ok, text: state.text, reasoning: state.reasoning || undefined, error: state.error || (ok ? null : `claude exited ${code}${stderr ? `: ${stderr.slice(0, 300)}` : ''}`), stopReason: state.stopReason, sessionId: state.sessionId, usage: usageOf() });
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
if (steerable)
|
|
136
|
+
child.stdin.write(claudeUserMessage(input.prompt) + '\n'); // keep stdin open for steering
|
|
137
|
+
else {
|
|
138
|
+
child.stdin.write(input.prompt);
|
|
139
|
+
child.stdin.end();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch { /* ignore */ }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
usage(s) {
|
|
146
|
+
return claudeUsageOf(s);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export function claudeUsageOf(s) {
|
|
150
|
+
const used = (s.input ?? 0) + (s.cached ?? 0) + (s.cacheCreation ?? 0) + (s.output ?? 0);
|
|
151
|
+
const window = s.contextWindow ?? null;
|
|
152
|
+
const contextPercent = window && used > 0 ? Math.min(99.9, Math.round((used / window) * 1000) / 10) : null;
|
|
153
|
+
const turnOutput = (s.turnOutputTokensBase ?? 0) + (s.output ?? 0);
|
|
154
|
+
return {
|
|
155
|
+
inputTokens: s.input,
|
|
156
|
+
outputTokens: s.output,
|
|
157
|
+
cachedInputTokens: s.cached,
|
|
158
|
+
contextUsedTokens: used > 0 ? used : null,
|
|
159
|
+
contextPercent,
|
|
160
|
+
turnOutputTokens: turnOutput > 0 ? turnOutput : null,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Advertised context window by Claude model id (best-effort; unknown -> null so the
|
|
164
|
+
// percent simply stays absent rather than wrong). Anchor-free so vendor-prefixed ids
|
|
165
|
+
// (us.anthropic.claude-…) still match.
|
|
166
|
+
export function claudeContextWindowFromModel(model) {
|
|
167
|
+
const id = String(model ?? '').trim().toLowerCase();
|
|
168
|
+
if (!id)
|
|
169
|
+
return null;
|
|
170
|
+
if (id === 'haiku' || /claude-haiku-/.test(id))
|
|
171
|
+
return 200_000;
|
|
172
|
+
if (id === 'opus' || id === 'sonnet' || id === 'fable')
|
|
173
|
+
return 1_000_000;
|
|
174
|
+
if (/claude-(opus|sonnet)-/.test(id) || /claude-fable-/.test(id))
|
|
175
|
+
return 1_000_000;
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
// Usable window = advertised minus Claude's max-output (20k) + autocompact (13k) reserve.
|
|
179
|
+
const CLAUDE_USABLE_WINDOW_RESERVE = 33_000;
|
|
180
|
+
export function claudeEffectiveContextWindow(advertised) {
|
|
181
|
+
if (advertised == null)
|
|
182
|
+
return null;
|
|
183
|
+
return advertised <= CLAUDE_USABLE_WINDOW_RESERVE ? advertised : advertised - CLAUDE_USABLE_WINDOW_RESERVE;
|
|
184
|
+
}
|
|
185
|
+
// Parse one claude stream-json event into kernel DriverEvents (pure + exported for
|
|
186
|
+
// hermetic testing). Faithful to pikiloom's claudeParse shapes.
|
|
187
|
+
export function handleClaudeEvent(ev, s, emit) {
|
|
188
|
+
const t = ev.type || '';
|
|
189
|
+
// Child events of a spawned sub-agent are tagged with parent_tool_use_id; route their
|
|
190
|
+
// tool_uses into that sub-agent rather than the main turn (mirrors pikiloom).
|
|
191
|
+
const parentId = (typeof ev.parent_tool_use_id === 'string' && ev.parent_tool_use_id) ? ev.parent_tool_use_id : null;
|
|
192
|
+
if (parentId) {
|
|
193
|
+
const sub = s.subAgents?.get?.(parentId);
|
|
194
|
+
if (sub) {
|
|
195
|
+
if (t === 'assistant') {
|
|
196
|
+
for (const b of (ev.message?.content || [])) {
|
|
197
|
+
if (b?.type !== 'tool_use')
|
|
198
|
+
continue;
|
|
199
|
+
sub.tools.push({ id: String(b.id || ''), name: String(b.name || 'Tool'), summary: String(b.name || 'Tool') });
|
|
200
|
+
}
|
|
201
|
+
const m = ev.message?.model;
|
|
202
|
+
if (typeof m === 'string' && m.trim())
|
|
203
|
+
sub.model = m;
|
|
204
|
+
emit({ type: 'subagent', subagent: { ...sub, tools: [...sub.tools] } });
|
|
205
|
+
}
|
|
206
|
+
else if (t === 'system' && typeof ev.model === 'string' && ev.model.trim()) {
|
|
207
|
+
sub.model = ev.model;
|
|
208
|
+
emit({ type: 'subagent', subagent: { ...sub, tools: [...sub.tools] } });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (t === 'system') {
|
|
214
|
+
if (ev.session_id && ev.session_id !== s.sessionId) {
|
|
215
|
+
s.sessionId = ev.session_id;
|
|
216
|
+
emit({ type: 'session', sessionId: ev.session_id });
|
|
217
|
+
}
|
|
218
|
+
s.model = ev.model ?? s.model;
|
|
219
|
+
s.contextWindow = claudeEffectiveContextWindow(claudeContextWindowFromModel(s.model)) ?? s.contextWindow;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (t === 'stream_event') {
|
|
223
|
+
const inner = ev.event || {};
|
|
224
|
+
if (inner.type === 'message_start') {
|
|
225
|
+
const u = inner.message?.usage;
|
|
226
|
+
// Claude emits one message per tool-use round within a single turn. Carry the prior
|
|
227
|
+
// message's output into the per-turn base, then reset to the new message's prompt size
|
|
228
|
+
// so contextUsedTokens tracks current occupancy while turnOutputTokens sums the turn.
|
|
229
|
+
s.turnOutputTokensBase = (s.turnOutputTokensBase ?? 0) + (s.output ?? 0);
|
|
230
|
+
s.input = u?.input_tokens ?? 0;
|
|
231
|
+
s.cached = u?.cache_read_input_tokens ?? 0;
|
|
232
|
+
s.cacheCreation = u?.cache_creation_input_tokens ?? 0;
|
|
233
|
+
s.output = 0;
|
|
234
|
+
}
|
|
235
|
+
else if (inner.type === 'content_block_delta') {
|
|
236
|
+
const d = inner.delta || {};
|
|
237
|
+
if (d.type === 'text_delta' && d.text) {
|
|
238
|
+
s.text += d.text;
|
|
239
|
+
s.streamedText = true;
|
|
240
|
+
emit({ type: 'text', delta: d.text });
|
|
241
|
+
}
|
|
242
|
+
else if (d.type === 'thinking_delta' && d.thinking) {
|
|
243
|
+
s.reasoning += d.thinking;
|
|
244
|
+
s.streamedReasoning = true;
|
|
245
|
+
emit({ type: 'reasoning', delta: d.thinking });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (inner.type === 'message_delta') {
|
|
249
|
+
const u = inner.usage;
|
|
250
|
+
if (u) {
|
|
251
|
+
if (u.output_tokens != null)
|
|
252
|
+
s.output = u.output_tokens;
|
|
253
|
+
if (u.input_tokens != null)
|
|
254
|
+
s.input = u.input_tokens;
|
|
255
|
+
if (u.cache_read_input_tokens != null)
|
|
256
|
+
s.cached = u.cache_read_input_tokens;
|
|
257
|
+
if (u.cache_creation_input_tokens != null)
|
|
258
|
+
s.cacheCreation = u.cache_creation_input_tokens;
|
|
259
|
+
emit({ type: 'usage', usage: claudeUsageOf(s) });
|
|
260
|
+
}
|
|
261
|
+
if (inner.delta?.stop_reason)
|
|
262
|
+
s.stopReason = inner.delta.stop_reason;
|
|
263
|
+
}
|
|
264
|
+
if (ev.session_id && ev.session_id !== s.sessionId) {
|
|
265
|
+
s.sessionId = ev.session_id;
|
|
266
|
+
emit({ type: 'session', sessionId: ev.session_id });
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (t === 'assistant') {
|
|
271
|
+
const contents = ev.message?.content || [];
|
|
272
|
+
for (const b of contents) {
|
|
273
|
+
if (b?.type !== 'tool_use')
|
|
274
|
+
continue;
|
|
275
|
+
const id = String(b.id || '');
|
|
276
|
+
const name = String(b.name || 'Tool');
|
|
277
|
+
if (name === 'TodoWrite') {
|
|
278
|
+
const plan = todoWriteToPlan(b.input);
|
|
279
|
+
if (plan)
|
|
280
|
+
emit({ type: 'plan', plan });
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (name === 'Task' || name === 'Agent') {
|
|
284
|
+
const input = b.input || {};
|
|
285
|
+
const sub = {
|
|
286
|
+
id,
|
|
287
|
+
kind: typeof input.subagent_type === 'string' ? input.subagent_type : null,
|
|
288
|
+
description: typeof input.description === 'string' ? input.description : null,
|
|
289
|
+
model: null, tools: [], status: 'running',
|
|
290
|
+
};
|
|
291
|
+
(s.subAgents ||= new Map()).set(id, sub);
|
|
292
|
+
emit({ type: 'subagent', subagent: { ...sub, tools: [] } });
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const summary = summarizeToolUse(name, b.input);
|
|
296
|
+
(s.tools ||= new Map()).set(id, { name, summary });
|
|
297
|
+
emit({ type: 'tool', call: { id, name, summary, input: shortToolValue(toolInputDetail(name, b.input), 200) || null, status: 'running' } });
|
|
298
|
+
}
|
|
299
|
+
emitClaudeImages(contents, s, emit);
|
|
300
|
+
// Reasoning fallback: when thinking is NOT streamed as thinking_delta (claude can deliver
|
|
301
|
+
// it as a complete `thinking` block in the assistant message instead), capture it here.
|
|
302
|
+
// Mirrors the legacy driver — without this the kernel path silently drops all thinking.
|
|
303
|
+
if (!s.streamedReasoning) {
|
|
304
|
+
const th = contents.filter((b) => b?.type === 'thinking').map((b) => b.thinking || '').filter(Boolean).join('\n\n');
|
|
305
|
+
if (th) {
|
|
306
|
+
const delta = s.reasoning ? '\n\n' + th : th;
|
|
307
|
+
s.reasoning += delta;
|
|
308
|
+
emit({ type: 'reasoning', delta });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!s.streamedText) {
|
|
312
|
+
const tx = contents.filter((b) => b?.type === 'text').map((b) => b.text || '').join('\n\n');
|
|
313
|
+
if (tx) {
|
|
314
|
+
s.text = tx;
|
|
315
|
+
emit({ type: 'text', delta: tx });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (t === 'user') {
|
|
321
|
+
// Tool results: surface generated images as artifacts AND close out the tool call
|
|
322
|
+
// (status done/failed + a result detail) so toolCalls is a faithful structured SSOT
|
|
323
|
+
// and the runtime's activity projection can render the execution trail.
|
|
324
|
+
const contents = ev.message?.content || [];
|
|
325
|
+
for (const b of contents) {
|
|
326
|
+
if (b?.type !== 'tool_result')
|
|
327
|
+
continue;
|
|
328
|
+
emitClaudeImages(b.content || [], s, emit);
|
|
329
|
+
const id = String(b.tool_use_id || '').trim();
|
|
330
|
+
const tool = id ? s.tools?.get(id) : undefined;
|
|
331
|
+
if (!tool)
|
|
332
|
+
continue;
|
|
333
|
+
const isError = !!b.is_error;
|
|
334
|
+
// File-shaped tools have no useful result detail (mirrors pikiloom): just mark done.
|
|
335
|
+
const fileTool = tool.name === 'Read' || tool.name === 'Edit' || tool.name === 'Write' || tool.name === 'TodoWrite';
|
|
336
|
+
const detail = (isError || !fileTool) ? firstResultLine(b.content) : null;
|
|
337
|
+
emit({ type: 'tool', call: { id, name: tool.name, summary: tool.summary, status: isError ? 'failed' : 'done', result: detail || null } });
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (t === 'result') {
|
|
342
|
+
if (ev.session_id)
|
|
343
|
+
s.sessionId = ev.session_id;
|
|
344
|
+
if (ev.is_error && Array.isArray(ev.errors) && ev.errors.length)
|
|
345
|
+
s.error = ev.errors.join('; ');
|
|
346
|
+
if (ev.result && !s.text.trim()) {
|
|
347
|
+
s.text = ev.result;
|
|
348
|
+
}
|
|
349
|
+
if (ev.stop_reason && !s.stopReason)
|
|
350
|
+
s.stopReason = ev.stop_reason;
|
|
351
|
+
const u = ev.usage;
|
|
352
|
+
if (u) {
|
|
353
|
+
if (s.input == null && u.input_tokens != null)
|
|
354
|
+
s.input = u.input_tokens;
|
|
355
|
+
if (s.output == null && u.output_tokens != null)
|
|
356
|
+
s.output = u.output_tokens;
|
|
357
|
+
const cached = u.cache_read_input_tokens ?? u.cached_input_tokens;
|
|
358
|
+
if (s.cached == null && cached != null)
|
|
359
|
+
s.cached = cached;
|
|
360
|
+
if (s.cacheCreation == null && u.cache_creation_input_tokens != null)
|
|
361
|
+
s.cacheCreation = u.cache_creation_input_tokens;
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// A stream-json user message (for --input-format stream-json; used to send the prompt and
|
|
367
|
+
// to inject mid-turn steer messages while stdin stays open).
|
|
368
|
+
export function claudeUserMessage(text) {
|
|
369
|
+
return JSON.stringify({ type: 'user', message: { role: 'user', content: [{ type: 'text', text }] } });
|
|
370
|
+
}
|
|
371
|
+
// Surface base64 image content blocks as artifacts (data URLs), deduped per turn.
|
|
372
|
+
export function emitClaudeImages(blocks, s, emit) {
|
|
373
|
+
if (!Array.isArray(blocks))
|
|
374
|
+
return;
|
|
375
|
+
s.seenImages ||= new Set();
|
|
376
|
+
let n = 0;
|
|
377
|
+
for (const b of blocks) {
|
|
378
|
+
if (b?.type !== 'image' || b?.source?.type !== 'base64' || typeof b.source.data !== 'string')
|
|
379
|
+
continue;
|
|
380
|
+
const mime = String(b.source.media_type || 'image/png');
|
|
381
|
+
const key = `${mime}:${b.source.data.length}:${b.source.data.slice(0, 32)}`;
|
|
382
|
+
if (s.seenImages.has(key))
|
|
383
|
+
continue;
|
|
384
|
+
s.seenImages.add(key);
|
|
385
|
+
const ext = mime.split('/')[1] || 'png';
|
|
386
|
+
emit({ type: 'artifact', artifact: { url: `data:${mime};base64,${b.source.data}`, fileName: `image-${++n}.${ext}`, mime, kind: 'photo' } });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Claude's TodoWrite tool input -> a normalized UniversalPlan (ported from pikiloom).
|
|
390
|
+
export function todoWriteToPlan(input) {
|
|
391
|
+
if (!input || typeof input !== 'object')
|
|
392
|
+
return null;
|
|
393
|
+
const rawTodos = Array.isArray(input.todos) ? input.todos : [];
|
|
394
|
+
const steps = [];
|
|
395
|
+
for (const todo of rawTodos) {
|
|
396
|
+
if (!todo || typeof todo !== 'object')
|
|
397
|
+
continue;
|
|
398
|
+
const text = typeof todo.content === 'string' ? todo.content.trim() : '';
|
|
399
|
+
if (!text)
|
|
400
|
+
continue;
|
|
401
|
+
const raw = typeof todo.status === 'string' ? todo.status : 'pending';
|
|
402
|
+
const status = raw === 'completed' ? 'completed' : raw === 'in_progress' ? 'inProgress' : 'pending';
|
|
403
|
+
steps.push({ text, status });
|
|
404
|
+
}
|
|
405
|
+
return steps.length ? { explanation: null, steps } : null;
|
|
406
|
+
}
|
|
407
|
+
// ── Tool-call summarization (ported from pikiloom's summarizeClaudeToolUse) ──────────
|
|
408
|
+
// Turns a Claude tool_use {name,input} into a one-line human summary. The runtime's
|
|
409
|
+
// activity projector joins these into snapshot.activity; the structured form lives in
|
|
410
|
+
// toolCalls. Kept driver-local: knowing Claude's tool input shapes is the driver's job.
|
|
411
|
+
export function shortToolValue(value, max = 140) {
|
|
412
|
+
if (value == null)
|
|
413
|
+
return '';
|
|
414
|
+
const text = (typeof value === 'string' ? value : String(value)).replace(/\s+/g, ' ').trim();
|
|
415
|
+
if (text.length <= max)
|
|
416
|
+
return text;
|
|
417
|
+
return text.slice(0, Math.max(0, max - 1)).trimEnd() + '…';
|
|
418
|
+
}
|
|
419
|
+
function toolInputDetail(name, input) {
|
|
420
|
+
const i = input || {};
|
|
421
|
+
return i.command || i.file_path || i.path || i.pattern || i.query || i.url || i.description || '';
|
|
422
|
+
}
|
|
423
|
+
export function summarizeToolUse(name, input) {
|
|
424
|
+
const tool = String(name || '').trim() || 'Tool';
|
|
425
|
+
const i = input || {};
|
|
426
|
+
const description = shortToolValue(i.description, 120);
|
|
427
|
+
switch (tool) {
|
|
428
|
+
case 'Read': {
|
|
429
|
+
const t = shortToolValue(i.file_path || i.path);
|
|
430
|
+
return t ? `Read ${t}` : 'Read file';
|
|
431
|
+
}
|
|
432
|
+
case 'Edit': {
|
|
433
|
+
const t = shortToolValue(i.file_path || i.path);
|
|
434
|
+
return t ? `Edit ${t}` : 'Edit file';
|
|
435
|
+
}
|
|
436
|
+
case 'Write': {
|
|
437
|
+
const t = shortToolValue(i.file_path || i.path);
|
|
438
|
+
return t ? `Write ${t}` : 'Write file';
|
|
439
|
+
}
|
|
440
|
+
case 'Glob': {
|
|
441
|
+
const p = shortToolValue(i.pattern || i.glob, 120);
|
|
442
|
+
return p ? `List files: ${p}` : 'List files';
|
|
443
|
+
}
|
|
444
|
+
case 'Grep': {
|
|
445
|
+
const p = shortToolValue(i.pattern || i.query, 120);
|
|
446
|
+
return p ? `Search text: ${p}` : 'Search text';
|
|
447
|
+
}
|
|
448
|
+
case 'WebFetch': {
|
|
449
|
+
const u = shortToolValue(i.url, 120);
|
|
450
|
+
return u ? `Fetch ${u}` : 'Fetch web page';
|
|
451
|
+
}
|
|
452
|
+
case 'WebSearch': {
|
|
453
|
+
const q = shortToolValue(i.query, 120);
|
|
454
|
+
return q ? `Search web: ${q}` : 'Search web';
|
|
455
|
+
}
|
|
456
|
+
case 'TodoWrite': return 'Update plan';
|
|
457
|
+
case 'Task': {
|
|
458
|
+
const p = shortToolValue(i.description || i.prompt, 120);
|
|
459
|
+
return p ? `Run task: ${p}` : 'Run task';
|
|
460
|
+
}
|
|
461
|
+
case 'Bash': {
|
|
462
|
+
if (description)
|
|
463
|
+
return `Run shell: ${description}`;
|
|
464
|
+
const c = shortToolValue(i.command, 120);
|
|
465
|
+
return c ? `Run shell: ${c}` : 'Run shell command';
|
|
466
|
+
}
|
|
467
|
+
default: {
|
|
468
|
+
const mcpMatch = tool.match(/^mcp__[^_]+__(.+)$/);
|
|
469
|
+
const bare = mcpMatch ? mcpMatch[1] : tool;
|
|
470
|
+
if (bare === 'im_send_file') {
|
|
471
|
+
const p = shortToolValue(i.path, 120);
|
|
472
|
+
return p ? `Send file: ${p}` : 'Send file';
|
|
473
|
+
}
|
|
474
|
+
if (bare === 'im_list_files')
|
|
475
|
+
return 'List workspace files';
|
|
476
|
+
if (bare === 'im_ask_user') {
|
|
477
|
+
const q = shortToolValue(i.question, 120);
|
|
478
|
+
return q ? `Ask user: ${q}` : 'Ask user';
|
|
479
|
+
}
|
|
480
|
+
if (description)
|
|
481
|
+
return `${tool}: ${description}`;
|
|
482
|
+
const d = shortToolValue(toolInputDetail(tool, i), 120);
|
|
483
|
+
return d ? `${tool}: ${d}` : tool;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// First non-empty line of a tool_result content (string | block[]), for the "summary -> detail" form.
|
|
488
|
+
export function firstResultLine(content) {
|
|
489
|
+
let text = '';
|
|
490
|
+
if (typeof content === 'string')
|
|
491
|
+
text = content;
|
|
492
|
+
else if (Array.isArray(content))
|
|
493
|
+
text = content.map((b) => (typeof b === 'string' ? b : b?.type === 'text' ? b.text || '' : '')).join('\n');
|
|
494
|
+
for (const line of text.split('\n')) {
|
|
495
|
+
const t = line.trim();
|
|
496
|
+
if (t)
|
|
497
|
+
return shortToolValue(t, 120);
|
|
498
|
+
}
|
|
499
|
+
return '';
|
|
500
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, TuiInput, TuiSpec } from '../contracts/driver.js';
|
|
2
|
+
import type { UniversalUsage } from '../protocol/index.js';
|
|
3
|
+
export declare class CodexDriver implements AgentDriver {
|
|
4
|
+
private readonly bin;
|
|
5
|
+
readonly id = "codex";
|
|
6
|
+
readonly capabilities: {
|
|
7
|
+
steer: boolean;
|
|
8
|
+
interact: boolean;
|
|
9
|
+
resume: boolean;
|
|
10
|
+
tui: boolean;
|
|
11
|
+
};
|
|
12
|
+
constructor(bin?: string);
|
|
13
|
+
run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
|
|
14
|
+
tui(input: TuiInput): TuiSpec;
|
|
15
|
+
}
|
|
16
|
+
export interface CodexUsageState {
|
|
17
|
+
input: number | null;
|
|
18
|
+
output: number | null;
|
|
19
|
+
cached: number | null;
|
|
20
|
+
contextUsed?: number | null;
|
|
21
|
+
contextWindow?: number | null;
|
|
22
|
+
}
|
|
23
|
+
export declare function applyCodexTokenUsage(s: CodexUsageState, rawUsage: any): void;
|
|
24
|
+
export declare function codexUsageOf(s: CodexUsageState): UniversalUsage;
|