pikiloom 0.4.36 → 0.4.38

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 (50) hide show
  1. package/dist/agent/drivers/codex.js +20 -3
  2. package/dist/agent/index.js +2 -2
  3. package/dist/agent/kernel-bridge.js +206 -0
  4. package/dist/agent/stream.js +60 -2
  5. package/dist/cli/kernel-app.js +115 -0
  6. package/dist/cli/main.js +7 -0
  7. package/package.json +4 -2
  8. package/packages/kernel/README.md +107 -0
  9. package/packages/kernel/dist/contracts/driver.d.ts +95 -0
  10. package/packages/kernel/dist/contracts/driver.js +1 -0
  11. package/packages/kernel/dist/contracts/ports.d.ts +84 -0
  12. package/packages/kernel/dist/contracts/ports.js +1 -0
  13. package/packages/kernel/dist/contracts/surface.d.ts +75 -0
  14. package/packages/kernel/dist/contracts/surface.js +1 -0
  15. package/packages/kernel/dist/drivers/claude.d.ts +23 -0
  16. package/packages/kernel/dist/drivers/claude.js +453 -0
  17. package/packages/kernel/dist/drivers/codex.d.ts +14 -0
  18. package/packages/kernel/dist/drivers/codex.js +346 -0
  19. package/packages/kernel/dist/drivers/echo.d.ts +20 -0
  20. package/packages/kernel/dist/drivers/echo.js +61 -0
  21. package/packages/kernel/dist/drivers/gemini.d.ts +18 -0
  22. package/packages/kernel/dist/drivers/gemini.js +143 -0
  23. package/packages/kernel/dist/drivers/hermes.d.ts +14 -0
  24. package/packages/kernel/dist/drivers/hermes.js +194 -0
  25. package/packages/kernel/dist/drivers/index.d.ts +5 -0
  26. package/packages/kernel/dist/drivers/index.js +5 -0
  27. package/packages/kernel/dist/index.d.ts +18 -0
  28. package/packages/kernel/dist/index.js +29 -0
  29. package/packages/kernel/dist/ports/defaults.d.ts +59 -0
  30. package/packages/kernel/dist/ports/defaults.js +137 -0
  31. package/packages/kernel/dist/protocol/index.d.ts +309 -0
  32. package/packages/kernel/dist/protocol/index.js +59 -0
  33. package/packages/kernel/dist/runtime/hub.d.ts +65 -0
  34. package/packages/kernel/dist/runtime/hub.js +260 -0
  35. package/packages/kernel/dist/runtime/loom.d.ts +44 -0
  36. package/packages/kernel/dist/runtime/loom.js +69 -0
  37. package/packages/kernel/dist/runtime/pty.d.ts +23 -0
  38. package/packages/kernel/dist/runtime/pty.js +69 -0
  39. package/packages/kernel/dist/runtime/session-runner.d.ts +27 -0
  40. package/packages/kernel/dist/runtime/session-runner.js +210 -0
  41. package/packages/kernel/dist/runtime/tui.d.ts +8 -0
  42. package/packages/kernel/dist/runtime/tui.js +35 -0
  43. package/packages/kernel/dist/runtime/turn.d.ts +17 -0
  44. package/packages/kernel/dist/runtime/turn.js +16 -0
  45. package/packages/kernel/dist/surfaces/cli.d.ts +19 -0
  46. package/packages/kernel/dist/surfaces/cli.js +65 -0
  47. package/packages/kernel/dist/surfaces/index.d.ts +2 -0
  48. package/packages/kernel/dist/surfaces/index.js +2 -0
  49. package/packages/kernel/dist/surfaces/web.d.ts +39 -0
  50. package/packages/kernel/dist/surfaces/web.js +244 -0
@@ -0,0 +1,346 @@
1
+ import { spawn } from 'node:child_process';
2
+ // Minimal newline-delimited JSON-RPC client for `codex app-server` (ported faithfully
3
+ // from pikiloom's CodexAppServer; trimmed to what the kernel needs).
4
+ class AppServer {
5
+ bin;
6
+ configOverrides;
7
+ env;
8
+ proc = null;
9
+ buf = '';
10
+ nextId = 1;
11
+ pending = new Map();
12
+ notify;
13
+ requestResponder;
14
+ constructor(bin, configOverrides, env) {
15
+ this.bin = bin;
16
+ this.configOverrides = configOverrides;
17
+ this.env = env;
18
+ }
19
+ onNotification(cb) { this.notify = cb; }
20
+ onServerRequest(cb) { this.requestResponder = cb; }
21
+ async start() {
22
+ const args = ['app-server'];
23
+ const overrides = this.configOverrides.some(c => /^features\.goals\s*=/.test(c)) ? this.configOverrides : [...this.configOverrides, 'features.goals=true'];
24
+ for (const c of overrides)
25
+ args.push('-c', c);
26
+ try {
27
+ this.proc = spawn(this.bin, args, { stdio: ['pipe', 'pipe', 'pipe'], env: this.env ? { ...process.env, ...this.env } : process.env });
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ this.proc.stdout.on('data', (chunk) => this.onData(chunk));
33
+ this.proc.on('close', () => { for (const cb of this.pending.values())
34
+ cb({ error: { message: 'app-server exited' } }); this.pending.clear(); });
35
+ this.proc.on('error', () => { });
36
+ const init = await this.call('initialize', { clientInfo: { name: '@pikiloom/kernel', version: '0.1.0' }, capabilities: { experimentalApi: true } }, 15_000);
37
+ return !init.error;
38
+ }
39
+ onData(chunk) {
40
+ this.buf += chunk.toString('utf8');
41
+ const lines = this.buf.split('\n');
42
+ this.buf = lines.pop() || '';
43
+ for (const line of lines) {
44
+ if (!line.trim())
45
+ continue;
46
+ let msg;
47
+ try {
48
+ msg = JSON.parse(line);
49
+ }
50
+ catch {
51
+ continue;
52
+ }
53
+ if (msg.method && msg.id != null) { // server -> client request
54
+ const id = msg.id;
55
+ Promise.resolve(this.requestResponder ? this.requestResponder(msg.method, msg.params ?? {}, id) : {})
56
+ .then((result) => this.respond(id, result ?? {}))
57
+ .catch(() => this.respond(id, {}));
58
+ }
59
+ else if (msg.id != null) { // response to our call
60
+ const cb = this.pending.get(msg.id);
61
+ if (cb) {
62
+ this.pending.delete(msg.id);
63
+ cb(msg);
64
+ }
65
+ }
66
+ else if (msg.method) { // notification
67
+ this.notify?.(msg.method, msg.params ?? {});
68
+ }
69
+ }
70
+ }
71
+ call(method, params, timeoutMs = 60_000) {
72
+ return new Promise((resolve) => {
73
+ if (!this.proc || this.proc.killed) {
74
+ resolve({ error: { message: 'not connected' } });
75
+ return;
76
+ }
77
+ const id = this.nextId++;
78
+ const timer = setTimeout(() => { this.pending.delete(id); resolve({ error: { message: `RPC '${method}' timed out` } }); }, timeoutMs);
79
+ this.pending.set(id, (m) => { clearTimeout(timer); resolve(m); });
80
+ const msg = { jsonrpc: '2.0', id, method };
81
+ if (params !== undefined)
82
+ msg.params = params;
83
+ try {
84
+ this.proc.stdin.write(JSON.stringify(msg) + '\n');
85
+ }
86
+ catch {
87
+ clearTimeout(timer);
88
+ this.pending.delete(id);
89
+ resolve({ error: { message: 'write failed' } });
90
+ }
91
+ });
92
+ }
93
+ respond(id, result) {
94
+ try {
95
+ this.proc?.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
96
+ }
97
+ catch { /* closed */ }
98
+ }
99
+ kill() { try {
100
+ this.proc?.kill('SIGTERM');
101
+ }
102
+ catch { /* ignore */ } this.proc = null; }
103
+ }
104
+ function planFromUpdate(params) {
105
+ const rawSteps = Array.isArray(params?.plan?.steps) ? params.plan.steps : Array.isArray(params?.steps) ? params.steps : Array.isArray(params?.plan) ? params.plan : [];
106
+ const steps = rawSteps.map((st) => {
107
+ const text = typeof st?.step === 'string' ? st.step : typeof st?.text === 'string' ? st.text : '';
108
+ const raw = String(st?.status || 'pending');
109
+ const status = raw === 'completed' ? 'completed' : (raw === 'in_progress' || raw === 'inProgress') ? 'inProgress' : 'pending';
110
+ return text ? { text, status } : null;
111
+ }).filter(Boolean);
112
+ return steps.length ? { explanation: typeof params?.explanation === 'string' ? params.explanation : null, steps } : null;
113
+ }
114
+ function codexCommandPreview(command) {
115
+ const text = (Array.isArray(command) ? command.join(' ') : typeof command === 'string' ? command : '').replace(/\s+/g, ' ').trim();
116
+ return text.length > 120 ? text.slice(0, 119).trimEnd() + '…' : text;
117
+ }
118
+ function codexFileChangeSummary(item) {
119
+ const changes = Array.isArray(item?.changes) ? item.changes : [];
120
+ const paths = changes.map((c) => (typeof c?.path === 'string' ? c.path.split('/').filter(Boolean).slice(-2).join('/') : '')).filter(Boolean);
121
+ if (paths.length === 1)
122
+ return `Edit ${paths[0]}`;
123
+ if (paths.length > 1)
124
+ return `Edit ${paths.length} files`;
125
+ return 'Edit files';
126
+ }
127
+ // Normalize a codex app-server item -> {id, name, summary}. The summary mirrors pikiloom's
128
+ // vocabulary ("Run shell: <cmd>", "Edit <path>") so the activity projection reads naturally.
129
+ function codexToolSummary(item) {
130
+ const id = String(item?.id || '');
131
+ if (!id)
132
+ return null;
133
+ if (item.type === 'commandExecution') {
134
+ const c = codexCommandPreview(item.command);
135
+ return { id, name: 'shell', summary: c ? `Run shell: ${c}` : 'Run shell command' };
136
+ }
137
+ if (item.type === 'fileChange' || item.type === 'patch')
138
+ return { id, name: 'edit', summary: codexFileChangeSummary(item) };
139
+ const tool = typeof item?.tool === 'string' ? item.tool : typeof item?.name === 'string' ? item.name : item?.type;
140
+ return tool ? { id, name: String(tool), summary: String(tool) } : null;
141
+ }
142
+ // codex `item/tool/requestUserInput` params -> a normalized UniversalInteraction
143
+ // (faithful to the legacy codex driver's toAgentInteraction shape).
144
+ function codexUserInputToInteraction(params, promptId) {
145
+ const raw = Array.isArray(params?.questions) ? params.questions : [];
146
+ const questions = raw.map((q) => {
147
+ const opts = Array.isArray(q?.options) ? q.options : [];
148
+ const hasOpts = opts.length > 0;
149
+ return {
150
+ id: String(q?.id || ''),
151
+ header: String(q?.header || '') || 'Question',
152
+ text: String(q?.question || ''),
153
+ type: (hasOpts ? 'select' : 'text'),
154
+ choices: hasOpts ? opts.map((o) => ({ label: String(o?.label || ''), description: String(o?.description || ''), value: String(o?.label || '') })) : undefined,
155
+ allowFreeform: !!q?.isOther || !hasOpts,
156
+ allowEmpty: true,
157
+ };
158
+ }).filter((q) => q.id && q.text);
159
+ if (!questions.length)
160
+ return null;
161
+ return { promptId, kind: 'user-input', title: 'User Input Required', hint: 'Use the buttons when available, or reply with text.', questions };
162
+ }
163
+ export class CodexDriver {
164
+ bin;
165
+ id = 'codex';
166
+ capabilities = { steer: true, interact: false, resume: true, tui: true };
167
+ constructor(bin = 'codex') {
168
+ this.bin = bin;
169
+ }
170
+ async run(input, ctx) {
171
+ // BYOK provider routing arrives as `-c key=value` overrides; pass them through so the
172
+ // kernel path keeps third-party models (e.g. glm via OpenRouter) instead of falling back
173
+ // to the native account.
174
+ const config = [...(input.configOverrides || [])];
175
+ const srv = new AppServer(this.bin, config, input.env);
176
+ const state = { text: '', reasoning: '', sessionId: input.sessionId ?? null, input: null, output: null, cached: null, status: null, error: null, turnId: null };
177
+ const phases = new Map();
178
+ const toolSummaries = new Map();
179
+ let steerRegistered = false;
180
+ const ok = await srv.start();
181
+ if (!ok)
182
+ return { ok: false, text: '', error: 'failed to start codex app-server', stopReason: 'error' };
183
+ const onAbort = () => srv.kill();
184
+ if (ctx.signal.aborted)
185
+ onAbort();
186
+ else
187
+ ctx.signal.addEventListener('abort', onAbort, { once: true });
188
+ try {
189
+ const threadParams = { cwd: input.workdir, model: input.model || null };
190
+ if (input.systemPrompt)
191
+ threadParams.developerInstructions = input.systemPrompt;
192
+ if (input.fullAccess) {
193
+ threadParams.approvalPolicy = 'never';
194
+ threadParams.sandbox = 'danger-full-access';
195
+ }
196
+ const threadResp = input.sessionId
197
+ ? await srv.call('thread/resume', { threadId: input.sessionId, ...threadParams })
198
+ : await srv.call('thread/start', threadParams);
199
+ if (threadResp.error)
200
+ return { ok: false, text: '', error: threadResp.error.message || 'thread/start failed', stopReason: 'error' };
201
+ const threadId = threadResp.result?.thread?.id ?? input.sessionId ?? null;
202
+ if (threadId && threadId !== state.sessionId) {
203
+ state.sessionId = threadId;
204
+ ctx.emit({ type: 'session', sessionId: threadId });
205
+ }
206
+ let settle = () => { };
207
+ const turnDone = new Promise((res) => { settle = res; });
208
+ srv.onNotification((method, params) => {
209
+ if (params?.threadId && params.threadId !== state.sessionId && method !== 'turn/started')
210
+ return;
211
+ switch (method) {
212
+ case 'turn/started':
213
+ state.turnId = params?.turn?.id ?? null;
214
+ if (!steerRegistered && state.turnId) {
215
+ steerRegistered = true;
216
+ ctx.registerSteer(async (prompt, attachments = []) => {
217
+ if (!state.sessionId || !state.turnId)
218
+ return false;
219
+ const r = await srv.call('turn/steer', { threadId: state.sessionId, expectedTurnId: state.turnId, input: buildTurnInput(prompt, attachments) }, 30_000);
220
+ if (r.error)
221
+ return false;
222
+ state.turnId = r.result?.turnId ?? state.turnId;
223
+ return true;
224
+ });
225
+ }
226
+ break;
227
+ case 'item/started': {
228
+ const item = params?.item || {};
229
+ if (item.type === 'agentMessage' && item.id)
230
+ phases.set(item.id, item.phase || 'final_answer');
231
+ const t = codexToolSummary(item);
232
+ if (t && !toolSummaries.has(t.id)) {
233
+ toolSummaries.set(t.id, t.summary);
234
+ ctx.emit({ type: 'tool', call: { id: t.id, name: t.name, summary: t.summary, status: 'running' } });
235
+ }
236
+ break;
237
+ }
238
+ case 'item/agentMessage/delta': {
239
+ const phase = params?.itemId ? (phases.get(params.itemId) || 'final_answer') : 'final_answer';
240
+ if (phase === 'final_answer' && params?.delta) {
241
+ state.text += params.delta;
242
+ ctx.emit({ type: 'text', delta: params.delta });
243
+ }
244
+ break;
245
+ }
246
+ case 'item/reasoning/textDelta':
247
+ case 'item/reasoning/summaryTextDelta':
248
+ if (params?.delta) {
249
+ state.reasoning += params.delta;
250
+ ctx.emit({ type: 'reasoning', delta: params.delta });
251
+ }
252
+ break;
253
+ case 'item/completed': {
254
+ const item = params?.item || {};
255
+ const t = codexToolSummary(item);
256
+ if (t && toolSummaries.has(t.id))
257
+ ctx.emit({ type: 'tool', call: { id: t.id, name: t.name, summary: toolSummaries.get(t.id) || t.summary, status: item.status === 'failed' ? 'failed' : 'done' } });
258
+ break;
259
+ }
260
+ case 'turn/plan/updated': {
261
+ const plan = planFromUpdate(params);
262
+ if (plan)
263
+ ctx.emit({ type: 'plan', plan });
264
+ break;
265
+ }
266
+ case 'thread/tokenUsage/updated': {
267
+ const u = params?.tokenUsage || params?.usage || {};
268
+ if (u.input_tokens != null || u.inputTokens != null)
269
+ state.input = u.input_tokens ?? u.inputTokens;
270
+ if (u.output_tokens != null || u.outputTokens != null)
271
+ state.output = u.output_tokens ?? u.outputTokens;
272
+ if (u.cached_input_tokens != null || u.cachedInputTokens != null)
273
+ state.cached = u.cached_input_tokens ?? u.cachedInputTokens;
274
+ ctx.emit({ type: 'usage', usage: { inputTokens: state.input, outputTokens: state.output, cachedInputTokens: state.cached, contextPercent: null } });
275
+ break;
276
+ }
277
+ case 'turn/completed': {
278
+ const turn = params?.turn || {};
279
+ state.status = turn.status ?? 'completed';
280
+ if (turn.error)
281
+ state.error = turn.error.message || turn.error.code || 'turn error';
282
+ settle();
283
+ break;
284
+ }
285
+ }
286
+ });
287
+ // Codex server->client requests: route user-input to the HITL seam (ctx.askUser),
288
+ // accept approvals by default (parity with the legacy codex driver).
289
+ srv.onServerRequest(async (method, params, id) => {
290
+ if (method === 'item/tool/requestUserInput') {
291
+ const interaction = codexUserInputToInteraction(params, `codex-input-${id}`);
292
+ if (!interaction)
293
+ return { answers: {} };
294
+ const answers = await ctx.askUser(interaction);
295
+ return { answers: Object.fromEntries(Object.entries(answers).map(([qid, vals]) => [qid, { answers: vals }])) };
296
+ }
297
+ if (method === 'item/commandExecution/requestApproval' || method === 'item/fileChange/requestApproval')
298
+ return { decision: 'accept' };
299
+ if (method === 'item/permissions/requestApproval')
300
+ return { permissions: {}, scope: 'turn' };
301
+ return {};
302
+ });
303
+ const turnResp = await srv.call('turn/start', {
304
+ threadId: state.sessionId,
305
+ input: buildTurnInput(input.prompt, input.attachments || []),
306
+ model: input.model || undefined,
307
+ effort: input.effort || undefined,
308
+ });
309
+ if (turnResp.error)
310
+ return { ok: false, text: state.text, error: turnResp.error.message || 'turn/start failed', stopReason: 'error', sessionId: state.sessionId };
311
+ await turnDone;
312
+ const usage = { inputTokens: state.input, outputTokens: state.output, cachedInputTokens: state.cached, contextPercent: null };
313
+ const ok2 = (state.status === 'completed' || state.status == null) && !state.error && !ctx.signal.aborted;
314
+ return {
315
+ ok: ok2,
316
+ text: state.text,
317
+ reasoning: state.reasoning || undefined,
318
+ error: state.error || (ctx.signal.aborted ? 'Interrupted by user.' : null),
319
+ stopReason: ctx.signal.aborted ? 'interrupted' : (state.status || 'end_turn'),
320
+ sessionId: state.sessionId,
321
+ usage,
322
+ };
323
+ }
324
+ finally {
325
+ srv.kill();
326
+ }
327
+ }
328
+ tui(input) {
329
+ const args = [];
330
+ if (input.model)
331
+ args.push('-m', input.model);
332
+ if (input.extraArgs?.length)
333
+ args.push(...input.extraArgs);
334
+ return { command: this.bin, args, cwd: input.workdir, env: input.env };
335
+ }
336
+ }
337
+ const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp']);
338
+ function buildTurnInput(prompt, attachments) {
339
+ const input = [];
340
+ for (const f of attachments) {
341
+ const ext = f.slice(f.lastIndexOf('.')).toLowerCase();
342
+ input.push(IMAGE_EXTS.has(ext) ? { type: 'localImage', path: f } : { type: 'text', text: `[Attached file: ${f}]` });
343
+ }
344
+ input.push({ type: 'text', text: prompt });
345
+ return input;
346
+ }
@@ -0,0 +1,20 @@
1
+ import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult } from '../contracts/driver.js';
2
+ export declare class EchoDriver implements AgentDriver {
3
+ readonly id = "echo";
4
+ readonly capabilities: {
5
+ steer: boolean;
6
+ interact: boolean;
7
+ resume: boolean;
8
+ tui: boolean;
9
+ };
10
+ tui(input: {
11
+ workdir: string;
12
+ env?: Record<string, string>;
13
+ }): {
14
+ command: string;
15
+ args: string[];
16
+ cwd: string;
17
+ env?: Record<string, string>;
18
+ };
19
+ run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
20
+ }
@@ -0,0 +1,61 @@
1
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
2
+ // Deterministic, dependency-free driver used for hermetic E2E. Its behavior is keyed
3
+ // off prompt prefixes so tests can exercise every event + control verb without timing
4
+ // races:
5
+ // "ASK: <q>" -> emits an interaction, awaits the answer, echoes it
6
+ // "HOLD ..." -> registers steer + blocks until steered OR stopped, then echoes
7
+ // anything -> reasoning + tool(running/done) + streamed text echo + usage
8
+ const ECHO_TUI = `process.stdout.write('ECHO-TUI-READY\\n');process.stdin.on('data',d=>{const s=d.toString();if(s.includes('Q')){process.stdout.write('BYE\\n');setTimeout(()=>process.exit(0),10);return;}process.stdout.write('GOT:'+s);});`;
9
+ export class EchoDriver {
10
+ id = 'echo';
11
+ capabilities = { steer: true, interact: true, resume: true, tui: true };
12
+ // A hermetic raw-PTY "TUI": prints a banner, echoes input, exits on 'Q'. Lets the
13
+ // Lane R passthrough path be exercised without a real agent.
14
+ tui(input) {
15
+ return { command: process.execPath, args: ['-e', ECHO_TUI], cwd: input.workdir, env: input.env };
16
+ }
17
+ async run(input, ctx) {
18
+ const prompt = input.prompt;
19
+ ctx.emit({ type: 'reasoning', delta: `Considering: ${prompt}` });
20
+ if (prompt.startsWith('ASK:')) {
21
+ const q = prompt.slice(4).trim() || 'Your input?';
22
+ const answers = await ctx.askUser({
23
+ promptId: 'echo-ask', kind: 'user-input', title: 'Echo asks',
24
+ questions: [{ id: 'a', text: q, allowFreeform: true }],
25
+ });
26
+ const a = answers['a']?.[0] ?? '(none)';
27
+ const text = `You said: ${a}`;
28
+ ctx.emit({ type: 'text', delta: text });
29
+ return { ok: true, text, reasoning: `Considering: ${prompt}`, stopReason: 'end_turn' };
30
+ }
31
+ let steered = null;
32
+ if (prompt.startsWith('HOLD')) {
33
+ ctx.emit({ type: 'activity', line: 'holding for steer' });
34
+ await new Promise((resolve) => {
35
+ ctx.registerSteer(async (p) => { steered = p; resolve(); return true; });
36
+ if (ctx.signal.aborted)
37
+ resolve();
38
+ else
39
+ ctx.signal.addEventListener('abort', () => resolve(), { once: true });
40
+ });
41
+ if (ctx.signal.aborted) {
42
+ return { ok: false, text: '', error: 'Interrupted by user.', stopReason: 'interrupted' };
43
+ }
44
+ }
45
+ ctx.emit({ type: 'tool', call: { id: 't1', name: 'echo_tool', summary: 'echo the prompt', status: 'running' } });
46
+ const base = `Echo: ${prompt}` + (steered ? ` | steered: ${steered}` : '');
47
+ let acc = '';
48
+ for (const tok of base.split(/(\s+)/)) {
49
+ if (ctx.signal.aborted)
50
+ return { ok: false, text: acc, error: 'Interrupted by user.', stopReason: 'interrupted' };
51
+ acc += tok;
52
+ if (tok)
53
+ ctx.emit({ type: 'text', delta: tok });
54
+ await sleep(2);
55
+ }
56
+ ctx.emit({ type: 'tool', call: { id: 't1', name: 'echo_tool', summary: 'echo the prompt', status: 'done', result: 'ok' } });
57
+ const usage = { inputTokens: prompt.length, outputTokens: base.length, cachedInputTokens: 0, contextPercent: null };
58
+ ctx.emit({ type: 'usage', usage });
59
+ return { ok: true, text: acc, reasoning: `Considering: ${prompt}`, usage, stopReason: 'end_turn' };
60
+ }
61
+ }
@@ -0,0 +1,18 @@
1
+ import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, DriverEvent, TuiInput, TuiSpec } from '../contracts/driver.js';
2
+ export declare class GeminiDriver implements AgentDriver {
3
+ private readonly bin;
4
+ readonly id = "gemini";
5
+ readonly capabilities: {
6
+ steer: boolean;
7
+ interact: boolean;
8
+ resume: boolean;
9
+ tui: boolean;
10
+ };
11
+ constructor(bin?: string);
12
+ run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
13
+ tui(input: TuiInput): TuiSpec;
14
+ }
15
+ export declare function parseGeminiEvent(ev: any, s: any, tools: Map<string, {
16
+ name: string;
17
+ summary: string;
18
+ }>, emit: (e: DriverEvent) => void): void;
@@ -0,0 +1,143 @@
1
+ import { spawn } from 'node:child_process';
2
+ // Native kernel Gemini driver: `gemini --output-format stream-json ... -p <prompt>` and
3
+ // parse its stream-json events into kernel DriverEvents. Faithful to pikiloom's geminiParse.
4
+ export class GeminiDriver {
5
+ bin;
6
+ id = 'gemini';
7
+ capabilities = { steer: false, interact: false, resume: true, tui: true };
8
+ constructor(bin = 'gemini') {
9
+ this.bin = bin;
10
+ }
11
+ run(input, ctx) {
12
+ const args = ['--output-format', 'stream-json'];
13
+ if (input.model)
14
+ args.push('--model', input.model);
15
+ if (input.sessionId)
16
+ args.push('--resume', input.sessionId);
17
+ const extra = input.extraArgs || [];
18
+ if (!extra.some(a => a === '--approval-mode' || a === '--yolo' || a === '-y'))
19
+ args.push('--approval-mode', 'yolo');
20
+ if (extra.length)
21
+ args.push(...extra);
22
+ args.push('-p', input.prompt);
23
+ const s = { text: '', sessionId: input.sessionId ?? null, model: input.model ?? null, input: null, output: null, cached: null, stopReason: null, error: null };
24
+ const tools = new Map();
25
+ return new Promise((resolve) => {
26
+ let child;
27
+ try {
28
+ child = spawn(this.bin, args, { cwd: input.workdir, env: input.env ? { ...process.env, ...input.env } : process.env, stdio: ['ignore', 'pipe', 'pipe'] });
29
+ }
30
+ catch (err) {
31
+ resolve({ ok: false, text: '', error: `spawn failed: ${err?.message || err}`, stopReason: 'error' });
32
+ return;
33
+ }
34
+ const onAbort = () => { try {
35
+ child.kill('SIGTERM');
36
+ }
37
+ catch { /* ignore */ } };
38
+ if (ctx.signal.aborted)
39
+ onAbort();
40
+ else
41
+ ctx.signal.addEventListener('abort', onAbort, { once: true });
42
+ let buf = '';
43
+ let stderr = '';
44
+ child.stdout.on('data', (chunk) => {
45
+ buf += chunk.toString('utf8');
46
+ const lines = buf.split('\n');
47
+ buf = lines.pop() || '';
48
+ for (const line of lines) {
49
+ const trimmed = line.trim();
50
+ if (!trimmed)
51
+ continue;
52
+ let ev;
53
+ try {
54
+ ev = JSON.parse(trimmed);
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ parseGeminiEvent(ev, s, tools, ctx.emit);
60
+ }
61
+ });
62
+ child.stderr.on('data', (c) => { stderr += c.toString('utf8'); });
63
+ child.on('error', (err) => resolve({ ok: false, text: s.text, error: `gemini spawn error: ${err.message}`, stopReason: 'error' }));
64
+ child.on('close', (code) => {
65
+ const usage = { inputTokens: s.input, outputTokens: s.output, cachedInputTokens: s.cached, contextPercent: null };
66
+ if (ctx.signal.aborted) {
67
+ resolve({ ok: false, text: s.text, error: 'Interrupted by user.', stopReason: 'interrupted', sessionId: s.sessionId, usage });
68
+ return;
69
+ }
70
+ const ok = !s.error && code === 0;
71
+ resolve({ ok, text: s.text, error: s.error || (ok ? null : `gemini exited ${code}${stderr ? `: ${stderr.slice(0, 200)}` : ''}`), stopReason: s.stopReason, sessionId: s.sessionId, usage });
72
+ });
73
+ });
74
+ }
75
+ tui(input) {
76
+ const args = [];
77
+ if (input.model)
78
+ args.push('--model', input.model);
79
+ if (input.extraArgs?.length)
80
+ args.push(...input.extraArgs);
81
+ return { command: this.bin, args, cwd: input.workdir, env: input.env };
82
+ }
83
+ }
84
+ export function parseGeminiEvent(ev, s, tools, emit) {
85
+ const t = ev.type || '';
86
+ if (t === 'init') {
87
+ if (ev.session_id && ev.session_id !== s.sessionId) {
88
+ s.sessionId = ev.session_id;
89
+ emit({ type: 'session', sessionId: ev.session_id });
90
+ }
91
+ s.model = ev.model ?? s.model;
92
+ return;
93
+ }
94
+ if (t === 'message' && ev.role === 'assistant') {
95
+ if (ev.delta) {
96
+ const d = ev.content || '';
97
+ if (d) {
98
+ s.text += d;
99
+ emit({ type: 'text', delta: d });
100
+ }
101
+ }
102
+ else if (!s.text.trim() && ev.content) {
103
+ s.text = ev.content;
104
+ emit({ type: 'text', delta: ev.content });
105
+ }
106
+ return;
107
+ }
108
+ if (t === 'tool_use' || t === 'tool_call') {
109
+ const id = String(ev.tool_id || ev.id || '').trim();
110
+ const name = String(ev.tool_name || ev.name || ev.tool || 'Tool');
111
+ if (id && !tools.has(id)) {
112
+ tools.set(id, { name, summary: name });
113
+ emit({ type: 'tool', call: { id, name, summary: name, status: 'running' } });
114
+ }
115
+ return;
116
+ }
117
+ if (t === 'tool_result') {
118
+ const id = String(ev.tool_id || ev.id || '').trim();
119
+ const tool = id ? tools.get(id) : undefined;
120
+ if (tool)
121
+ emit({ type: 'tool', call: { id, name: tool.name, summary: tool.summary, status: ev.is_error ? 'failed' : 'done' } });
122
+ return;
123
+ }
124
+ if (t === 'error' && ev.severity === 'error') {
125
+ s.error = String(ev.message || ev.error || 'Gemini error');
126
+ return;
127
+ }
128
+ if (t === 'result') {
129
+ if (ev.session_id)
130
+ s.sessionId = ev.session_id;
131
+ if (ev.status === 'error' || ev.status === 'failure')
132
+ s.error = String(ev.error || ev.message || `status ${ev.status}`);
133
+ s.stopReason = ev.status === 'success' ? 'end_turn' : ev.status;
134
+ const u = ev.stats;
135
+ if (u) {
136
+ s.input = u.input_tokens ?? u.input ?? s.input;
137
+ s.output = u.output_tokens ?? u.output ?? s.output;
138
+ s.cached = u.cached ?? s.cached;
139
+ emit({ type: 'usage', usage: { inputTokens: s.input, outputTokens: s.output, cachedInputTokens: s.cached, contextPercent: null } });
140
+ }
141
+ return;
142
+ }
143
+ }
@@ -0,0 +1,14 @@
1
+ import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, DriverEvent } from '../contracts/driver.js';
2
+ export declare class HermesDriver implements AgentDriver {
3
+ private readonly bin;
4
+ readonly id = "hermes";
5
+ readonly capabilities: {
6
+ steer: boolean;
7
+ interact: boolean;
8
+ resume: boolean;
9
+ tui: boolean;
10
+ };
11
+ constructor(bin?: string);
12
+ run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
13
+ }
14
+ export declare function applyHermesUpdate(u: any, s: any, tools: Set<string>, emit: (e: DriverEvent) => void): void;