pikiloom 0.4.41 → 0.4.43
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-B8V2eV_D.js +1 -0
- package/dashboard/dist/assets/{ConnectionModal-CAlACYKM.js → ConnectionModal-BOVwXLJB.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-D3KGIof1.js → DirBrowser-DbeAWYiL.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-C_69Y7CR.js → ExtensionsTab-BMS3PW9N.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-B5_6IS4X.js → IMAccessTab-B1sRgJk3.js} +1 -1
- package/dashboard/dist/assets/{Modal-BjZifpga.js → Modal-DCwGz46r.js} +1 -1
- package/dashboard/dist/assets/{Modals-B_V24pNA.js → Modals-Bh_0en5P.js} +1 -1
- package/dashboard/dist/assets/{Select-D7xW38wq.js → Select-DC6zguGC.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-DNlLd-PL.js +1 -0
- package/dashboard/dist/assets/{SystemTab-i5nmYweA.js → SystemTab-uFqZinIZ.js} +1 -1
- package/dashboard/dist/assets/index-CL5H13Cl.js +3 -0
- package/dashboard/dist/assets/index-DooLxbPX.js +23 -0
- package/dashboard/dist/assets/index-DwmXPtDd.css +1 -0
- package/dashboard/dist/assets/{shared-BKwEgcmy.js → shared-Byy2BNLq.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/accounts.js +181 -0
- package/dist/agent/drivers/claude.js +187 -36
- package/dist/agent/drivers/codex.js +30 -2
- package/dist/agent/kernel-bridge.js +63 -3
- package/dist/agent/session.js +6 -2
- package/dist/agent/stream.js +26 -0
- package/dist/bot/bot.js +45 -10
- package/dist/bot/command-ui.js +69 -4
- package/dist/cli/kernel-app.js +4 -2
- package/dist/dashboard/routes/accounts.js +120 -0
- package/dist/dashboard/routes/agents.js +1 -1
- package/dist/dashboard/server.js +2 -0
- package/dist/model/responses-bridge.js +37 -0
- package/package.json +1 -1
- package/packages/kernel/dist/accounts.d.ts +6 -0
- package/packages/kernel/dist/accounts.js +29 -0
- package/packages/kernel/dist/drivers/acp.d.ts +24 -0
- package/packages/kernel/dist/drivers/acp.js +477 -0
- package/packages/kernel/dist/drivers/claude.d.ts +3 -1
- package/packages/kernel/dist/drivers/claude.js +126 -9
- package/packages/kernel/dist/drivers/codex.js +35 -15
- package/packages/kernel/dist/drivers/hermes.d.ts +3 -12
- package/packages/kernel/dist/drivers/hermes.js +8 -191
- package/packages/kernel/dist/drivers/index.d.ts +1 -0
- package/packages/kernel/dist/drivers/index.js +1 -0
- package/packages/kernel/dist/index.d.ts +2 -0
- package/packages/kernel/dist/index.js +3 -0
- package/dashboard/dist/assets/AgentTab-B4ZC9QFL.js +0 -1
- package/dashboard/dist/assets/SessionPanel-BbrfUDLg.js +0 -1
- package/dashboard/dist/assets/index-A_dL4aEz.js +0 -23
- package/dashboard/dist/assets/index-Bthwt6K_.css +0 -1
- package/dashboard/dist/assets/index-D18pCeqv.js +0 -3
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { extname } from 'node:path';
|
|
2
4
|
import { discoverClaudeNativeSessions } from '../workspace/native.js';
|
|
3
5
|
// Real driver: shells the local `claude` CLI in stream-json mode and normalizes its
|
|
4
6
|
// events into kernel DriverEvents. Faithful to pikiloom's claude.ts event shapes
|
|
@@ -28,6 +30,9 @@ export class ClaudeDriver {
|
|
|
28
30
|
}
|
|
29
31
|
run(input, ctx) {
|
|
30
32
|
const steerable = !!input.steerable;
|
|
33
|
+
// Image attachments must ride a stream-json user message (text-only stdin can't carry images),
|
|
34
|
+
// so enable that input mode whenever there are attachments, not just for mid-turn steering.
|
|
35
|
+
const useStreamJson = steerable || !!input.attachments?.length;
|
|
31
36
|
const args = ['-p', '--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
|
|
32
37
|
if (input.model)
|
|
33
38
|
args.push('--model', input.model);
|
|
@@ -41,8 +46,10 @@ export class ClaudeDriver {
|
|
|
41
46
|
args.push('--mcp-config', input.mcpConfigPath);
|
|
42
47
|
if (input.permissionMode)
|
|
43
48
|
args.push('--permission-mode', input.permissionMode); // parity: keep bypass/accept-edits on the kernel path
|
|
49
|
+
if (useStreamJson)
|
|
50
|
+
args.push('--input-format', 'stream-json');
|
|
44
51
|
if (steerable)
|
|
45
|
-
args.push('--
|
|
52
|
+
args.push('--replay-user-messages'); // parity: mid-turn steer
|
|
46
53
|
if (input.extraArgs?.length)
|
|
47
54
|
args.push(...input.extraArgs);
|
|
48
55
|
const state = {
|
|
@@ -54,6 +61,9 @@ export class ClaudeDriver {
|
|
|
54
61
|
contextWindow: null, turnOutputTokensBase: 0,
|
|
55
62
|
subAgents: new Map(),
|
|
56
63
|
tools: new Map(),
|
|
64
|
+
taskList: new Map(),
|
|
65
|
+
taskOrder: [],
|
|
66
|
+
pendingTaskCreates: new Map(),
|
|
57
67
|
};
|
|
58
68
|
return new Promise((resolve) => {
|
|
59
69
|
let child;
|
|
@@ -90,9 +100,9 @@ export class ClaudeDriver {
|
|
|
90
100
|
else
|
|
91
101
|
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
92
102
|
if (steerable) {
|
|
93
|
-
ctx.registerSteer(async (prompt) => {
|
|
103
|
+
ctx.registerSteer(async (prompt, attachments) => {
|
|
94
104
|
try {
|
|
95
|
-
child.stdin.write(claudeUserMessage(prompt) + '\n');
|
|
105
|
+
child.stdin.write(claudeUserMessage(prompt, attachments) + '\n');
|
|
96
106
|
return true;
|
|
97
107
|
}
|
|
98
108
|
catch {
|
|
@@ -136,8 +146,11 @@ export class ClaudeDriver {
|
|
|
136
146
|
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() });
|
|
137
147
|
});
|
|
138
148
|
try {
|
|
139
|
-
if (
|
|
140
|
-
child.stdin.write(claudeUserMessage(input.prompt) + '\n');
|
|
149
|
+
if (useStreamJson) {
|
|
150
|
+
child.stdin.write(claudeUserMessage(input.prompt, input.attachments) + '\n');
|
|
151
|
+
if (!steerable)
|
|
152
|
+
child.stdin.end(); // single turn; steer mode keeps stdin open for replay
|
|
153
|
+
}
|
|
141
154
|
else {
|
|
142
155
|
child.stdin.write(input.prompt);
|
|
143
156
|
child.stdin.end();
|
|
@@ -301,6 +314,34 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
301
314
|
emit({ type: 'plan', plan });
|
|
302
315
|
continue;
|
|
303
316
|
}
|
|
317
|
+
// Task list (current Claude mechanism): stash the subject; the tool_result assigns the id.
|
|
318
|
+
// Plan-only — like the legacy driver these never surface as Activity rows.
|
|
319
|
+
if (name === 'TaskCreate') {
|
|
320
|
+
const subject = typeof b.input?.subject === 'string' ? b.input.subject.trim() : '';
|
|
321
|
+
if (subject)
|
|
322
|
+
(s.pendingTaskCreates ||= new Map()).set(id, { subject });
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (name === 'TaskUpdate') {
|
|
326
|
+
const taskId = String(b.input?.taskId ?? '').trim();
|
|
327
|
+
const rawStatus = String(b.input?.status ?? '').trim().toLowerCase();
|
|
328
|
+
if (taskId) {
|
|
329
|
+
if (rawStatus === 'deleted') {
|
|
330
|
+
s.taskList?.delete(taskId);
|
|
331
|
+
if (Array.isArray(s.taskOrder))
|
|
332
|
+
s.taskOrder = s.taskOrder.filter((x) => x !== taskId);
|
|
333
|
+
}
|
|
334
|
+
else if (rawStatus) {
|
|
335
|
+
const existing = s.taskList?.get(taskId);
|
|
336
|
+
if (existing)
|
|
337
|
+
existing.status = rawStatus;
|
|
338
|
+
}
|
|
339
|
+
const plan = rebuildClaudeTaskPlan(s);
|
|
340
|
+
if (plan)
|
|
341
|
+
emit({ type: 'plan', plan });
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
304
345
|
if (name === 'Task' || name === 'Agent') {
|
|
305
346
|
const input = b.input || {};
|
|
306
347
|
const sub = {
|
|
@@ -348,6 +389,23 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
348
389
|
continue;
|
|
349
390
|
emitClaudeImages(b.content || [], s, emit);
|
|
350
391
|
const id = String(b.tool_use_id || '').trim();
|
|
392
|
+
// TaskCreate result: the assigned task id arrives here; register the task and emit the plan.
|
|
393
|
+
if (id && s.pendingTaskCreates?.has(id)) {
|
|
394
|
+
const pending = s.pendingTaskCreates.get(id);
|
|
395
|
+
const assignedId = readClaudeTaskCreateId(ev, b);
|
|
396
|
+
if (pending && assignedId) {
|
|
397
|
+
s.pendingTaskCreates.delete(id);
|
|
398
|
+
(s.taskList ||= new Map());
|
|
399
|
+
(s.taskOrder ||= []);
|
|
400
|
+
if (!s.taskList.has(assignedId))
|
|
401
|
+
s.taskOrder.push(assignedId);
|
|
402
|
+
s.taskList.set(assignedId, { subject: pending.subject, status: 'pending' });
|
|
403
|
+
const plan = rebuildClaudeTaskPlan(s);
|
|
404
|
+
if (plan)
|
|
405
|
+
emit({ type: 'plan', plan });
|
|
406
|
+
}
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
351
409
|
const tool = id ? s.tools?.get(id) : undefined;
|
|
352
410
|
if (!tool)
|
|
353
411
|
continue;
|
|
@@ -384,10 +442,30 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
384
442
|
return;
|
|
385
443
|
}
|
|
386
444
|
}
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
445
|
+
// Claude vision input formats (matches the legacy driver / Anthropic API: png, jpeg, gif, webp).
|
|
446
|
+
const CLAUDE_IMAGE_MIME = {
|
|
447
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
448
|
+
'.gif': 'image/gif', '.webp': 'image/webp',
|
|
449
|
+
};
|
|
450
|
+
// A stream-json user message (for --input-format stream-json; used to send the prompt and to
|
|
451
|
+
// inject mid-turn steer messages while stdin stays open). Image attachments are inlined as base64
|
|
452
|
+
// image content blocks so the model actually sees them; other files become a text note. Without
|
|
453
|
+
// this the kernel path sent text only and silently dropped pasted/attached images.
|
|
454
|
+
export function claudeUserMessage(text, attachments) {
|
|
455
|
+
const content = [];
|
|
456
|
+
for (const filePath of attachments || []) {
|
|
457
|
+
const mime = CLAUDE_IMAGE_MIME[extname(filePath).toLowerCase()];
|
|
458
|
+
if (mime) {
|
|
459
|
+
try {
|
|
460
|
+
content.push({ type: 'image', source: { type: 'base64', media_type: mime, data: readFileSync(filePath).toString('base64') } });
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
catch { /* unreadable -> fall through to a text note */ }
|
|
464
|
+
}
|
|
465
|
+
content.push({ type: 'text', text: `[Attached file: ${filePath}]` });
|
|
466
|
+
}
|
|
467
|
+
content.push({ type: 'text', text });
|
|
468
|
+
return JSON.stringify({ type: 'user', message: { role: 'user', content } });
|
|
391
469
|
}
|
|
392
470
|
// Surface base64 image content blocks as artifacts (data URLs), deduped per turn.
|
|
393
471
|
export function emitClaudeImages(blocks, s, emit) {
|
|
@@ -425,6 +503,45 @@ export function todoWriteToPlan(input) {
|
|
|
425
503
|
}
|
|
426
504
|
return steps.length ? { explanation: null, steps } : null;
|
|
427
505
|
}
|
|
506
|
+
// ── Claude task-list (TaskCreate / TaskUpdate) -> UniversalPlan ──────────────────────
|
|
507
|
+
// Current Claude Code drives its task list through TaskCreate/TaskUpdate, NOT TodoWrite
|
|
508
|
+
// (TodoWrite is the legacy mechanism). TaskCreate carries the subject; its tool_result then
|
|
509
|
+
// assigns a stable task id (toolUseResult.task.id, or "Task #N" in the text). TaskUpdate flips
|
|
510
|
+
// a task's status by id. We accumulate the list in driver state and re-emit the whole plan on
|
|
511
|
+
// each change. Without this the kernel path never emits a plan event for Claude, so the
|
|
512
|
+
// dashboard's task-list card never renders. Ported from pikiloom's legacy claude driver.
|
|
513
|
+
// The assigned task id from a TaskCreate tool_result. Prefer the structured field; fall back
|
|
514
|
+
// to parsing "Task #N" from a string result.
|
|
515
|
+
export function readClaudeTaskCreateId(ev, block) {
|
|
516
|
+
const structured = ev?.toolUseResult?.task?.id;
|
|
517
|
+
if (structured != null && String(structured).trim())
|
|
518
|
+
return String(structured).trim();
|
|
519
|
+
const content = block?.content;
|
|
520
|
+
if (typeof content === 'string') {
|
|
521
|
+
const m = content.match(/Task #(\d+)/);
|
|
522
|
+
if (m)
|
|
523
|
+
return m[1];
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
export function rebuildClaudeTaskPlan(s) {
|
|
528
|
+
if (!Array.isArray(s?.taskOrder) || !s.taskOrder.length)
|
|
529
|
+
return null;
|
|
530
|
+
const steps = [];
|
|
531
|
+
for (const id of s.taskOrder) {
|
|
532
|
+
const task = s.taskList?.get(id);
|
|
533
|
+
if (!task)
|
|
534
|
+
continue;
|
|
535
|
+
const lowered = String(task.status || '').toLowerCase();
|
|
536
|
+
const status = lowered === 'completed' ? 'completed'
|
|
537
|
+
: (lowered === 'in_progress' || lowered === 'inprogress') ? 'inProgress'
|
|
538
|
+
: 'pending';
|
|
539
|
+
const text = String(task.subject || '').trim();
|
|
540
|
+
if (text)
|
|
541
|
+
steps.push({ text, status });
|
|
542
|
+
}
|
|
543
|
+
return steps.length ? { explanation: null, steps } : null;
|
|
544
|
+
}
|
|
428
545
|
// ── Tool-call summarization (ported from pikiloom's summarizeClaudeToolUse) ──────────
|
|
429
546
|
// Turns a Claude tool_use {name,input} into a one-line human summary. The runtime's
|
|
430
547
|
// activity projector joins these into snapshot.activity; the structured form lives in
|
|
@@ -185,13 +185,11 @@ export function codexReasoningItemText(item) {
|
|
|
185
185
|
];
|
|
186
186
|
return parts.map((p) => (typeof p === 'string' ? p : p?.text || '')).filter(Boolean).join('\n').trim();
|
|
187
187
|
}
|
|
188
|
-
// A completed final_answer
|
|
189
|
-
// deltaItems holds the ids already streamed, so a completed item echoing
|
|
190
|
-
// not double-counted (matches the legacy driver's deltaSeenForItem guard).
|
|
188
|
+
// A completed agentMessage (commentary preamble OR final_answer) that did NOT stream deltas:
|
|
189
|
+
// append + emit it live. deltaItems holds the ids already streamed, so a completed item echoing
|
|
190
|
+
// a streamed one is not double-counted (matches the legacy driver's deltaSeenForItem guard).
|
|
191
|
+
// Both phases are surfaced — codex's commentary preambles are part of the visible "中间过程".
|
|
191
192
|
export function captureCodexAgentMessage(item, s, deltaItems, phases, emit) {
|
|
192
|
-
const phase = item?.phase || (item?.id ? phases.get(item.id) : null) || 'final_answer';
|
|
193
|
-
if (phase !== 'final_answer')
|
|
194
|
-
return;
|
|
195
193
|
const text = typeof item?.text === 'string' ? item.text.trim() : '';
|
|
196
194
|
if (!text)
|
|
197
195
|
return;
|
|
@@ -238,11 +236,26 @@ export class CodexDriver {
|
|
|
238
236
|
const phases = new Map();
|
|
239
237
|
const toolSummaries = new Map();
|
|
240
238
|
const deltaItems = new Set();
|
|
239
|
+
let lastTextItemId = null;
|
|
241
240
|
let steerRegistered = false;
|
|
242
241
|
const ok = await srv.start();
|
|
243
242
|
if (!ok)
|
|
244
243
|
return { ok: false, text: '', error: 'failed to start codex app-server', stopReason: 'error' };
|
|
245
|
-
|
|
244
|
+
let settle = () => { };
|
|
245
|
+
const turnDone = new Promise((res) => { settle = res; });
|
|
246
|
+
// On abort: gracefully interrupt the running turn, then settle turnDone OURSELVES. A bare
|
|
247
|
+
// srv.kill() (SIGTERM) never produces a turn/completed notification, so without this explicit
|
|
248
|
+
// settle() the `await turnDone` below hangs forever — run() never resolves and the task stays
|
|
249
|
+
// "running" in the orchestrator even though the codex process is already dead ("停止不掉,但实际上已经停了").
|
|
250
|
+
const onAbort = () => {
|
|
251
|
+
if (state.sessionId && state.turnId) {
|
|
252
|
+
srv.call('turn/interrupt', { threadId: state.sessionId, turnId: state.turnId }, 5_000).finally(() => settle());
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
srv.kill();
|
|
256
|
+
settle();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
246
259
|
if (ctx.signal.aborted)
|
|
247
260
|
onAbort();
|
|
248
261
|
else
|
|
@@ -265,8 +278,6 @@ export class CodexDriver {
|
|
|
265
278
|
state.sessionId = threadId;
|
|
266
279
|
ctx.emit({ type: 'session', sessionId: threadId });
|
|
267
280
|
}
|
|
268
|
-
let settle = () => { };
|
|
269
|
-
const turnDone = new Promise((res) => { settle = res; });
|
|
270
281
|
srv.onNotification((method, params) => {
|
|
271
282
|
if (params?.threadId && params.threadId !== state.sessionId && method !== 'turn/started')
|
|
272
283
|
return;
|
|
@@ -298,13 +309,22 @@ export class CodexDriver {
|
|
|
298
309
|
break;
|
|
299
310
|
}
|
|
300
311
|
case 'item/agentMessage/delta': {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
312
|
+
if (!params?.delta)
|
|
313
|
+
break;
|
|
314
|
+
// Surface BOTH commentary (preamble) and final_answer messages live. Codex narrates
|
|
315
|
+
// what it is about to do via phase=commentary agentMessages before tool calls;
|
|
316
|
+
// gating on final_answer dropped them, leaving the "中间过程" invisible. Separate
|
|
317
|
+
// distinct message items with a blank line so preamble and answer don't run together.
|
|
318
|
+
if (params.itemId && params.itemId !== lastTextItemId && state.text) {
|
|
319
|
+
state.text += '\n\n';
|
|
320
|
+
ctx.emit({ type: 'text', delta: '\n\n' });
|
|
321
|
+
}
|
|
322
|
+
if (params.itemId) {
|
|
323
|
+
lastTextItemId = params.itemId;
|
|
324
|
+
deltaItems.add(params.itemId);
|
|
307
325
|
}
|
|
326
|
+
state.text += params.delta;
|
|
327
|
+
ctx.emit({ type: 'text', delta: params.delta });
|
|
308
328
|
break;
|
|
309
329
|
}
|
|
310
330
|
case 'item/reasoning/textDelta':
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
export declare class HermesDriver
|
|
3
|
-
private readonly bin;
|
|
4
|
-
readonly id = "hermes";
|
|
5
|
-
readonly capabilities: {
|
|
6
|
-
steer: boolean;
|
|
7
|
-
interact: boolean;
|
|
8
|
-
resume: boolean;
|
|
9
|
-
tui: boolean;
|
|
10
|
-
};
|
|
1
|
+
import { AcpDriver, applyAcpUpdate } from './acp.js';
|
|
2
|
+
export declare class HermesDriver extends AcpDriver {
|
|
11
3
|
constructor(bin?: string);
|
|
12
|
-
run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
|
|
13
4
|
}
|
|
14
|
-
export declare
|
|
5
|
+
export declare const applyHermesUpdate: typeof applyAcpUpdate;
|
|
@@ -1,194 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
class
|
|
5
|
-
bin;
|
|
6
|
-
args;
|
|
7
|
-
env;
|
|
8
|
-
proc = null;
|
|
9
|
-
buf = '';
|
|
10
|
-
nextId = 1;
|
|
11
|
-
pending = new Map();
|
|
12
|
-
notify;
|
|
13
|
-
constructor(bin, args, env) {
|
|
14
|
-
this.bin = bin;
|
|
15
|
-
this.args = args;
|
|
16
|
-
this.env = env;
|
|
17
|
-
}
|
|
18
|
-
onNotification(cb) { this.notify = cb; }
|
|
19
|
-
start() {
|
|
20
|
-
try {
|
|
21
|
-
this.proc = spawn(this.bin, this.args, { stdio: ['pipe', 'pipe', 'pipe'], env: this.env ? { ...process.env, ...this.env } : process.env });
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
this.proc.stdout.on('data', (chunk) => this.onData(chunk));
|
|
27
|
-
this.proc.on('close', () => { for (const cb of this.pending.values())
|
|
28
|
-
cb({ error: { message: 'acp exited' } }); this.pending.clear(); });
|
|
29
|
-
this.proc.on('error', () => { });
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
onData(chunk) {
|
|
33
|
-
this.buf += chunk.toString('utf8');
|
|
34
|
-
const lines = this.buf.split('\n');
|
|
35
|
-
this.buf = lines.pop() || '';
|
|
36
|
-
for (const line of lines) {
|
|
37
|
-
if (!line.trim())
|
|
38
|
-
continue;
|
|
39
|
-
let m;
|
|
40
|
-
try {
|
|
41
|
-
m = JSON.parse(line);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (m.method && m.id != null) {
|
|
47
|
-
this.respond(m.id, {});
|
|
48
|
-
} // agent->client request: ack
|
|
49
|
-
else if (m.id != null) {
|
|
50
|
-
const cb = this.pending.get(m.id);
|
|
51
|
-
if (cb) {
|
|
52
|
-
this.pending.delete(m.id);
|
|
53
|
-
cb(m);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
else if (m.method)
|
|
57
|
-
this.notify?.(m.method, m.params ?? {});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
request(method, params, timeoutMs = 60_000) {
|
|
61
|
-
return new Promise((resolve) => {
|
|
62
|
-
if (!this.proc || this.proc.killed) {
|
|
63
|
-
resolve({ error: { message: 'not connected' } });
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const id = this.nextId++;
|
|
67
|
-
const timer = setTimeout(() => { this.pending.delete(id); resolve({ error: { message: `ACP '${method}' timed out` } }); }, timeoutMs);
|
|
68
|
-
this.pending.set(id, (m) => { clearTimeout(timer); resolve(m); });
|
|
69
|
-
try {
|
|
70
|
-
this.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
clearTimeout(timer);
|
|
74
|
-
this.pending.delete(id);
|
|
75
|
-
resolve({ error: { message: 'write failed' } });
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
respond(id, result) { try {
|
|
80
|
-
this.proc?.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
|
|
81
|
-
}
|
|
82
|
-
catch { /* closed */ } }
|
|
83
|
-
kill() { try {
|
|
84
|
-
this.proc?.kill('SIGTERM');
|
|
85
|
-
}
|
|
86
|
-
catch { /* ignore */ } this.proc = null; }
|
|
87
|
-
}
|
|
88
|
-
export class HermesDriver {
|
|
89
|
-
bin;
|
|
90
|
-
id = 'hermes';
|
|
91
|
-
capabilities = { steer: false, interact: false, resume: true, tui: false };
|
|
1
|
+
import { AcpDriver, applyAcpUpdate } from './acp.js';
|
|
2
|
+
// Hermes = the reference ACP agent, shipped as a thin preset over the generic AcpDriver.
|
|
3
|
+
// Any other ACP CLI (OpenCode, Gemini-ACP, …) is just `new AcpDriver({ id, command, args })`.
|
|
4
|
+
export class HermesDriver extends AcpDriver {
|
|
92
5
|
constructor(bin = 'hermes') {
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
async run(input, ctx) {
|
|
96
|
-
const client = new AcpClient(this.bin, ['acp'], input.env);
|
|
97
|
-
const s = { text: '', reasoning: '', sessionId: input.sessionId ?? null, contextWindow: null, contextUsed: null, error: null };
|
|
98
|
-
const tools = new Set();
|
|
99
|
-
if (!client.start())
|
|
100
|
-
return { ok: false, text: '', error: 'failed to start hermes acp', stopReason: 'error' };
|
|
101
|
-
const onAbort = () => { try {
|
|
102
|
-
client.request('session/cancel', { sessionId: s.sessionId });
|
|
103
|
-
}
|
|
104
|
-
catch { /* ignore */ } client.kill(); };
|
|
105
|
-
if (ctx.signal.aborted)
|
|
106
|
-
onAbort();
|
|
107
|
-
else
|
|
108
|
-
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
109
|
-
client.onNotification((method, params) => {
|
|
110
|
-
if (method !== 'session/update')
|
|
111
|
-
return;
|
|
112
|
-
applyHermesUpdate(params?.update ?? params, s, tools, ctx.emit);
|
|
113
|
-
});
|
|
114
|
-
try {
|
|
115
|
-
const init = await client.request('initialize', { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } } });
|
|
116
|
-
if (init.error)
|
|
117
|
-
return { ok: false, text: '', error: init.error.message || 'initialize failed', stopReason: 'error' };
|
|
118
|
-
if (!s.sessionId) {
|
|
119
|
-
const ns = await client.request('session/new', { cwd: input.workdir, mcpServers: [] });
|
|
120
|
-
if (ns.error)
|
|
121
|
-
return { ok: false, text: '', error: ns.error.message || 'session/new failed', stopReason: 'error' };
|
|
122
|
-
s.sessionId = ns.result?.sessionId || ns.result?.session_id || null;
|
|
123
|
-
if (s.sessionId)
|
|
124
|
-
ctx.emit({ type: 'session', sessionId: s.sessionId });
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
await client.request('session/load', { sessionId: s.sessionId, cwd: input.workdir, mcpServers: [] }, 30_000).catch(() => ({}));
|
|
128
|
-
}
|
|
129
|
-
if (!s.sessionId)
|
|
130
|
-
return { ok: false, text: '', error: 'hermes returned no session id', stopReason: 'error' };
|
|
131
|
-
if (input.model)
|
|
132
|
-
await client.request('session/set_model', { sessionId: s.sessionId, modelId: input.model }, 15_000).catch(() => ({}));
|
|
133
|
-
const promptResp = await client.request('session/prompt', {
|
|
134
|
-
sessionId: s.sessionId,
|
|
135
|
-
prompt: [{ type: 'text', text: input.prompt }],
|
|
136
|
-
}, 7_200_000);
|
|
137
|
-
const usage = { inputTokens: null, outputTokens: null, cachedInputTokens: null, contextUsedTokens: s.contextUsed, contextPercent: null };
|
|
138
|
-
if (ctx.signal.aborted)
|
|
139
|
-
return { ok: false, text: s.text, error: 'Interrupted by user.', stopReason: 'interrupted', sessionId: s.sessionId, usage };
|
|
140
|
-
if (promptResp.error)
|
|
141
|
-
return { ok: false, text: s.text, error: promptResp.error.message || 'session/prompt failed', stopReason: 'error', sessionId: s.sessionId, usage };
|
|
142
|
-
const stopReason = promptResp.result?.stopReason ?? 'end_turn';
|
|
143
|
-
return { ok: !s.error, text: s.text, reasoning: s.reasoning || undefined, error: s.error, stopReason, sessionId: s.sessionId, usage };
|
|
144
|
-
}
|
|
145
|
-
finally {
|
|
146
|
-
client.kill();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
export function applyHermesUpdate(u, s, tools, emit) {
|
|
151
|
-
if (!u)
|
|
152
|
-
return;
|
|
153
|
-
switch (u.sessionUpdate) {
|
|
154
|
-
case 'agent_message_chunk': {
|
|
155
|
-
const t = u.content?.text;
|
|
156
|
-
if (typeof t === 'string' && t) {
|
|
157
|
-
s.text += t;
|
|
158
|
-
emit({ type: 'text', delta: t });
|
|
159
|
-
}
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
case 'agent_thought_chunk': {
|
|
163
|
-
const t = u.content?.text;
|
|
164
|
-
if (typeof t === 'string' && t) {
|
|
165
|
-
s.reasoning += t;
|
|
166
|
-
emit({ type: 'reasoning', delta: t });
|
|
167
|
-
}
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
case 'tool_call': {
|
|
171
|
-
const id = typeof u.toolCallId === 'string' ? u.toolCallId : '';
|
|
172
|
-
const title = (typeof u.title === 'string' && u.title.trim()) || 'tool';
|
|
173
|
-
if (id && !tools.has(id)) {
|
|
174
|
-
tools.add(id);
|
|
175
|
-
emit({ type: 'tool', call: { id, name: title, summary: title, status: 'running' } });
|
|
176
|
-
}
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
case 'tool_call_update': {
|
|
180
|
-
const id = typeof u.toolCallId === 'string' ? u.toolCallId : '';
|
|
181
|
-
const title = (typeof u.title === 'string' && u.title.trim()) || 'tool';
|
|
182
|
-
if (id && (u.status === 'completed' || u.status === 'failed'))
|
|
183
|
-
emit({ type: 'tool', call: { id, name: title, summary: title, status: u.status === 'failed' ? 'failed' : 'done' } });
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
case 'usage_update': {
|
|
187
|
-
if (typeof u.used === 'number') {
|
|
188
|
-
s.contextUsed = u.used;
|
|
189
|
-
emit({ type: 'usage', usage: { inputTokens: null, outputTokens: null, cachedInputTokens: null, contextUsedTokens: u.used, contextPercent: null } });
|
|
190
|
-
}
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
6
|
+
super({ id: 'hermes', command: bin, args: ['acp'] });
|
|
193
7
|
}
|
|
194
8
|
}
|
|
9
|
+
// Back-compat alias: the generic ACP session/update parser handles the identical wire that
|
|
10
|
+
// the original hermes-specific parser did. Kept so existing imports/tests resolve.
|
|
11
|
+
export const applyHermesUpdate = applyAcpUpdate;
|
|
@@ -2,4 +2,5 @@ export { EchoDriver } from './echo.js';
|
|
|
2
2
|
export { ClaudeDriver, handleClaudeEvent, todoWriteToPlan } from './claude.js';
|
|
3
3
|
export { CodexDriver } from './codex.js';
|
|
4
4
|
export { GeminiDriver, parseGeminiEvent } from './gemini.js';
|
|
5
|
+
export { AcpDriver, applyAcpUpdate, toAcpMcpServers, buildAcpPromptBlocks, type AcpDriverConfig } from './acp.js';
|
|
5
6
|
export { HermesDriver, applyHermesUpdate } from './hermes.js';
|
|
@@ -2,4 +2,5 @@ export { EchoDriver } from './echo.js';
|
|
|
2
2
|
export { ClaudeDriver, handleClaudeEvent, todoWriteToPlan } from './claude.js';
|
|
3
3
|
export { CodexDriver } from './codex.js';
|
|
4
4
|
export { GeminiDriver, parseGeminiEvent } from './gemini.js';
|
|
5
|
+
export { AcpDriver, applyAcpUpdate, toAcpMcpServers, buildAcpPromptBlocks } from './acp.js';
|
|
5
6
|
export { HermesDriver, applyHermesUpdate } from './hermes.js';
|
|
@@ -12,8 +12,10 @@ export { EchoDriver } from './drivers/echo.js';
|
|
|
12
12
|
export { ClaudeDriver } from './drivers/claude.js';
|
|
13
13
|
export { CodexDriver } from './drivers/codex.js';
|
|
14
14
|
export { GeminiDriver } from './drivers/gemini.js';
|
|
15
|
+
export { AcpDriver, type AcpDriverConfig } from './drivers/acp.js';
|
|
15
16
|
export { HermesDriver } from './drivers/hermes.js';
|
|
16
17
|
export { WebSurface, type WebSurfaceOptions } from './surfaces/web.js';
|
|
17
18
|
export { CliSurface } from './surfaces/cli.js';
|
|
18
19
|
export * from './workspace/index.js';
|
|
20
|
+
export * from './accounts.js';
|
|
19
21
|
export * from './protocol/index.js';
|
|
@@ -22,10 +22,13 @@ export { EchoDriver } from './drivers/echo.js';
|
|
|
22
22
|
export { ClaudeDriver } from './drivers/claude.js';
|
|
23
23
|
export { CodexDriver } from './drivers/codex.js';
|
|
24
24
|
export { GeminiDriver } from './drivers/gemini.js';
|
|
25
|
+
export { AcpDriver } from './drivers/acp.js';
|
|
25
26
|
export { HermesDriver } from './drivers/hermes.js';
|
|
26
27
|
export { WebSurface } from './surfaces/web.js';
|
|
27
28
|
export { CliSurface } from './surfaces/cli.js';
|
|
28
29
|
// Workspace: unified top-level directory + session/skill/mcp management (loom.paths/sessions/skills/mcp)
|
|
29
30
|
export * from './workspace/index.js';
|
|
31
|
+
// Multi-account: per-account isolated config dirs (CLAUDE_CONFIG_DIR / CODEX_HOME)
|
|
32
|
+
export * from './accounts.js';
|
|
30
33
|
// Protocol (the wire vocabulary; shared with transports)
|
|
31
34
|
export * from './protocol/index.js';
|