@pikiloom/kernel 0.2.21 → 0.3.2
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 +20 -7
- package/dist/drivers/acp.js +30 -163
- package/dist/drivers/claude.d.ts +2 -21
- package/dist/drivers/claude.js +44 -57
- package/dist/drivers/codex.d.ts +0 -10
- package/dist/drivers/codex.js +47 -144
- package/dist/drivers/gemini.js +9 -27
- package/dist/drivers/hermes.d.ts +1 -2
- package/dist/drivers/hermes.js +1 -4
- package/dist/drivers/index.d.ts +5 -4
- package/dist/drivers/index.js +8 -4
- package/dist/{workspace → drivers}/native.d.ts +0 -1
- package/dist/{workspace → drivers}/native.js +1 -1
- package/dist/drivers/rpc.d.ts +45 -0
- package/dist/drivers/rpc.js +130 -0
- package/dist/drivers/shared.d.ts +21 -0
- package/dist/drivers/shared.js +66 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +10 -2
- package/dist/protocol/index.d.ts +5 -0
- package/dist/protocol/index.js +10 -0
- package/dist/runtime/hub.js +4 -8
- package/dist/workspace/index.d.ts +1 -2
- package/dist/workspace/index.js +3 -2
- package/dist/workspace/mcp.js +6 -16
- package/dist/workspace/npm-search.d.ts +9 -0
- package/dist/workspace/npm-search.js +21 -0
- package/dist/workspace/sessions.js +6 -9
- package/dist/workspace/skills.d.ts +11 -0
- package/dist/workspace/skills.js +14 -20
- package/llms.txt +11 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -205,7 +205,8 @@ interrupts only the current turn and the next queued task promotes.
|
|
|
205
205
|
| `ClaudeDriver` | `claude` | `claude` CLI, stream-json (+ `--effort`, partial messages) | ✓ | — | ✓ | ✓ |
|
|
206
206
|
| `CodexDriver` | `codex` | `codex app-server` JSON-RPC (HITL via `requestUserInput`) | ✓ | via askUser | ✓ | ✓ |
|
|
207
207
|
| `GeminiDriver` | `gemini` | `gemini --output-format stream-json` | — | — | ✓ | ✓ |
|
|
208
|
-
| `
|
|
208
|
+
| `AcpDriver` | *(config)* | generic ACP ndjson JSON-RPC — any ACP CLI: `new AcpDriver({ id, command, args })` | — | via askUser | ✓ | — |
|
|
209
|
+
| `HermesDriver` | `hermes` | ACP preset over `AcpDriver` (`hermes acp`) | — | via askUser | ✓ | — |
|
|
209
210
|
| `EchoDriver` | `echo` | none (hermetic, in-process) | ✓ | ✓ | ✓ | ✓ |
|
|
210
211
|
|
|
211
212
|
Write your own by implementing `AgentDriver` and passing it to `createLoom({ drivers })` (or
|
|
@@ -311,16 +312,28 @@ node-builtins-only and additive — every existing port/default is unchanged.
|
|
|
311
312
|
|
|
312
313
|
## Exports
|
|
313
314
|
|
|
314
|
-
Main entry `@pikiloom/kernel`
|
|
315
|
-
`@pikiloom/kernel/surfaces`, `@pikiloom/kernel/protocol`.
|
|
315
|
+
Main entry `@pikiloom/kernel` is the public API, pinned by `test/api-surface.test.ts`.
|
|
316
|
+
Subpaths: `@pikiloom/kernel/drivers`, `@pikiloom/kernel/surfaces`, `@pikiloom/kernel/protocol`.
|
|
317
|
+
Driver-internal parser/settle helpers are exported only from their modules (for white-box
|
|
318
|
+
tests) and are NOT part of the public surface.
|
|
316
319
|
|
|
317
320
|
- Runtime: `createLoom`, `Loom`, `Hub`, `SessionRunner`, `runTurn`, `PtyBridge`, `ptyAvailable`, `attachTui`
|
|
318
|
-
- Drivers: `EchoDriver`, `ClaudeDriver`, `CodexDriver`, `GeminiDriver`, `HermesDriver`
|
|
321
|
+
- Drivers: `EchoDriver`, `ClaudeDriver`, `CodexDriver`, `GeminiDriver`, `AcpDriver` (+ `AcpDriverConfig`), `HermesDriver`
|
|
322
|
+
- Native discovery (driver-axis): `discover{Claude,Codex,Gemini}NativeSessions`, `encodeClaudeProjectDir` + type `DiscoverOptions`
|
|
319
323
|
- Surfaces: `WebSurface`, `CliSurface`
|
|
320
324
|
- Ports/defaults: `FsSessionStore`, `NullModelResolver`, `NoopToolProvider`, `PassthroughSystemPromptBuilder`, `AutoCancelInteractionHandler`, `DeferToTerminalInteractionHandler`, `NoopCatalog`, `defaultBaseDir`
|
|
321
|
-
- Workspace: `resolveLoomPaths`, `SessionsManager`, `SkillsManager`, `McpRegistry`, `ensureDirSymlink`, `
|
|
322
|
-
-
|
|
323
|
-
-
|
|
325
|
+
- Workspace: `resolveLoomPaths`, `normalizeStateDirName`, `SessionsManager`, `SkillsManager`, `McpRegistry`, `ensureDirSymlink`, `parseSkillMeta` + types `LoomPaths`, `LoomScope`, `ManagedSessionInfo`, `SkillInfo`, `SkillMeta`, `McpCatalogEntry`
|
|
326
|
+
- Multi-account: `accountTokenSupported`, `accountTokenEnvVar`, `accountTokenEnv` — which env var carries an agent's auth token, so an app can inject a selected account's token per spawn (claude: `CLAUDE_CODE_OAUTH_TOKEN`; storage/selection stay app-side)
|
|
327
|
+
- Protocol: `UniversalSnapshot`, `diffSnapshot`, `applySnapshotPatch`, `emptySnapshot`, `PROTOCOL_VERSION`, `makeSessionKey`, `splitSessionKey`, all wire/`Client*`/`Server*` message types
|
|
328
|
+
- Types: `AgentDriver`, `AgentTurnInput`, `DriverContext`, `DriverEvent`, `DriverResult`, `NativeSessionInfo`, `LoomIO`, `PromptInput`, `Surface`, `Plugin`, `SpawnContribution`, `SessionStore`, `ModelResolver`, `ToolProvider`, `SystemPromptBuilder`, `InteractionHandler`, `Catalog`, …
|
|
329
|
+
|
|
330
|
+
### Claude driver tuning (env)
|
|
331
|
+
|
|
332
|
+
The claude driver's background-hold / stall / recovery heuristics ship sane defaults and can
|
|
333
|
+
be tuned per deployment: `PIKILOOM_CLAUDE_BG_HOLD_MS`, `PIKILOOM_CLAUDE_BG_AGENT_HOLD_MS`,
|
|
334
|
+
`PIKILOOM_CLAUDE_BG_HOLD_RECHECK_MS`, `PIKILOOM_CLAUDE_BG_SETTLE_QUIET_MS`,
|
|
335
|
+
`PIKILOOM_CLAUDE_MODEL_STALL_MS`, `PIKILOOM_CLAUDE_TRUNCATED_RECOVERY` (=0 disables),
|
|
336
|
+
`PIKILOOM_CLAUDE_RESUME_NOOP_RETRIES`.
|
|
324
337
|
|
|
325
338
|
---
|
|
326
339
|
|
package/dist/drivers/acp.js
CHANGED
|
@@ -1,132 +1,7 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { createInterface } from 'node:readline';
|
|
3
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { dirname
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
constructor(code, message) {
|
|
8
|
-
super(message);
|
|
9
|
-
this.code = code;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
// ── ACP JSON-RPC client over a child process' stdio (ndjson framing) ───────────
|
|
13
|
-
class AcpClient {
|
|
14
|
-
bin;
|
|
15
|
-
args;
|
|
16
|
-
env;
|
|
17
|
-
cwd;
|
|
18
|
-
proc = null;
|
|
19
|
-
nextId = 1;
|
|
20
|
-
pending = new Map();
|
|
21
|
-
notifyCb;
|
|
22
|
-
requestCb;
|
|
23
|
-
stderrTail = [];
|
|
24
|
-
constructor(bin, args, env, cwd) {
|
|
25
|
-
this.bin = bin;
|
|
26
|
-
this.args = args;
|
|
27
|
-
this.env = env;
|
|
28
|
-
this.cwd = cwd;
|
|
29
|
-
}
|
|
30
|
-
onNotification(cb) { this.notifyCb = cb; }
|
|
31
|
-
onRequest(cb) { this.requestCb = cb; }
|
|
32
|
-
stderrText() { return this.stderrTail.join('\n'); }
|
|
33
|
-
start() {
|
|
34
|
-
try {
|
|
35
|
-
this.proc = spawn(this.bin, this.args, {
|
|
36
|
-
cwd: this.cwd,
|
|
37
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
|
-
env: this.env ? { ...process.env, ...this.env } : process.env,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
const rl = createInterface({ input: this.proc.stdout, crlfDelay: Infinity });
|
|
45
|
-
rl.on('line', (line) => this.onLine(line));
|
|
46
|
-
this.proc.stderr.on('data', (chunk) => {
|
|
47
|
-
for (const ln of chunk.toString('utf8').split('\n')) {
|
|
48
|
-
const t = ln.trim();
|
|
49
|
-
if (!t)
|
|
50
|
-
continue;
|
|
51
|
-
this.stderrTail.push(t.slice(0, 240));
|
|
52
|
-
if (this.stderrTail.length > 20)
|
|
53
|
-
this.stderrTail.shift();
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
this.proc.on('close', () => { for (const cb of this.pending.values())
|
|
57
|
-
cb({ error: { message: 'acp process exited' } }); this.pending.clear(); });
|
|
58
|
-
this.proc.on('error', () => { });
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
onLine(line) {
|
|
62
|
-
const t = line.trim();
|
|
63
|
-
if (!t)
|
|
64
|
-
return;
|
|
65
|
-
let m;
|
|
66
|
-
try {
|
|
67
|
-
m = JSON.parse(t);
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return;
|
|
71
|
-
} // non-JSON stdout noise
|
|
72
|
-
if (m.method && m.id != null) {
|
|
73
|
-
void this.handleRequest(m);
|
|
74
|
-
return;
|
|
75
|
-
} // agent -> client request
|
|
76
|
-
if (m.id != null) { // response to one of our requests
|
|
77
|
-
const cb = this.pending.get(m.id);
|
|
78
|
-
if (cb) {
|
|
79
|
-
this.pending.delete(m.id);
|
|
80
|
-
cb(m);
|
|
81
|
-
}
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (m.method)
|
|
85
|
-
this.notifyCb?.(m.method, m.params ?? {}); // notification (session/update)
|
|
86
|
-
}
|
|
87
|
-
async handleRequest(m) {
|
|
88
|
-
const id = m.id;
|
|
89
|
-
if (!this.requestCb) {
|
|
90
|
-
this.respondError(id, -32601, `Method not implemented: ${m.method}`);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
try {
|
|
94
|
-
const result = await this.requestCb(m.method, m.params ?? {});
|
|
95
|
-
this.respond(id, result ?? null);
|
|
96
|
-
}
|
|
97
|
-
catch (e) {
|
|
98
|
-
const code = e instanceof AcpRpcError ? e.code : -32603;
|
|
99
|
-
this.respondError(id, code, e?.message || 'handler error');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
request(method, params, timeoutMs = 60_000) {
|
|
103
|
-
return new Promise((resolve) => {
|
|
104
|
-
if (!this.proc || this.proc.killed) {
|
|
105
|
-
resolve({ error: { message: 'not connected' } });
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
const id = this.nextId++;
|
|
109
|
-
const timer = setTimeout(() => { this.pending.delete(id); resolve({ error: { message: `ACP '${method}' timed out` } }); }, timeoutMs);
|
|
110
|
-
this.pending.set(id, (m) => { clearTimeout(timer); resolve(m); });
|
|
111
|
-
this.write({ jsonrpc: '2.0', id, method, params });
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
notify(method, params) { this.write({ jsonrpc: '2.0', method, params }); }
|
|
115
|
-
respond(id, result) { this.write({ jsonrpc: '2.0', id, result }); }
|
|
116
|
-
respondError(id, code, message) { this.write({ jsonrpc: '2.0', id, error: { code, message } }); }
|
|
117
|
-
write(msg) {
|
|
118
|
-
if (!this.proc || this.proc.killed)
|
|
119
|
-
return;
|
|
120
|
-
try {
|
|
121
|
-
this.proc.stdin.write(JSON.stringify(msg) + '\n');
|
|
122
|
-
}
|
|
123
|
-
catch { /* stream closed */ }
|
|
124
|
-
}
|
|
125
|
-
kill() { try {
|
|
126
|
-
this.proc?.kill('SIGTERM');
|
|
127
|
-
}
|
|
128
|
-
catch { /* ignore */ } this.proc = null; }
|
|
129
|
-
}
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { RpcError, StdioRpcClient } from './rpc.js';
|
|
4
|
+
import { attachedFileNote, contextPercent, imageMimeForFile, wireAbort } from './shared.js';
|
|
130
5
|
// ── pikiloom McpServerSpec[] -> ACP mcpServers[] ───────────────────────────────
|
|
131
6
|
export function toAcpMcpServers(servers) {
|
|
132
7
|
if (!servers || !servers.length)
|
|
@@ -145,29 +20,19 @@ export function toAcpMcpServers(servers) {
|
|
|
145
20
|
}
|
|
146
21
|
return out;
|
|
147
22
|
}
|
|
148
|
-
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp']);
|
|
149
|
-
function mimeForExt(ext) {
|
|
150
|
-
if (ext === '.jpg' || ext === '.jpeg')
|
|
151
|
-
return 'image/jpeg';
|
|
152
|
-
if (ext === '.gif')
|
|
153
|
-
return 'image/gif';
|
|
154
|
-
if (ext === '.webp')
|
|
155
|
-
return 'image/webp';
|
|
156
|
-
return 'image/png';
|
|
157
|
-
}
|
|
158
23
|
// ACP prompt content blocks: text + inline base64 images; other attachments noted as text.
|
|
159
24
|
export function buildAcpPromptBlocks(prompt, attachments) {
|
|
160
25
|
const blocks = [];
|
|
161
26
|
for (const f of attachments) {
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
27
|
+
const mime = imageMimeForFile(f);
|
|
28
|
+
if (mime) {
|
|
164
29
|
try {
|
|
165
|
-
blocks.push({ type: 'image', mimeType:
|
|
30
|
+
blocks.push({ type: 'image', mimeType: mime, data: readFileSync(f).toString('base64') });
|
|
166
31
|
continue;
|
|
167
32
|
}
|
|
168
33
|
catch { /* fall through to a text note */ }
|
|
169
34
|
}
|
|
170
|
-
blocks.push({ type: 'text', text:
|
|
35
|
+
blocks.push({ type: 'text', text: attachedFileNote(f) });
|
|
171
36
|
}
|
|
172
37
|
blocks.push({ type: 'text', text: prompt });
|
|
173
38
|
return blocks;
|
|
@@ -260,12 +125,13 @@ export function applyAcpUpdate(update, s, tools, emit) {
|
|
|
260
125
|
case 'usage_update': {
|
|
261
126
|
if (typeof update.size === 'number')
|
|
262
127
|
s.contextWindow = update.size;
|
|
263
|
-
if (typeof update.used === 'number')
|
|
264
|
-
s.contextUsed = update.used;
|
|
265
128
|
if (typeof update.used === 'number') {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
129
|
+
s.contextUsed = update.used;
|
|
130
|
+
emit({ type: 'usage', usage: {
|
|
131
|
+
inputTokens: null, outputTokens: null, cachedInputTokens: null,
|
|
132
|
+
contextUsedTokens: update.used,
|
|
133
|
+
contextPercent: contextPercent(update.used > 0 ? update.used : null, s.contextWindow),
|
|
134
|
+
} });
|
|
269
135
|
}
|
|
270
136
|
return;
|
|
271
137
|
}
|
|
@@ -321,7 +187,7 @@ function fallbackOptionId(options, fallback) {
|
|
|
321
187
|
function readAcpTextFile(params) {
|
|
322
188
|
const p = String(params?.path || '');
|
|
323
189
|
if (!p || !existsSync(p))
|
|
324
|
-
throw new
|
|
190
|
+
throw new RpcError(-32602, `file not found: ${p}`);
|
|
325
191
|
let content = readFileSync(p, 'utf8');
|
|
326
192
|
const line = typeof params?.line === 'number' ? params.line : null;
|
|
327
193
|
const limit = typeof params?.limit === 'number' ? params.limit : null;
|
|
@@ -335,7 +201,7 @@ function readAcpTextFile(params) {
|
|
|
335
201
|
function writeAcpTextFile(params) {
|
|
336
202
|
const p = String(params?.path || '');
|
|
337
203
|
if (!p)
|
|
338
|
-
throw new
|
|
204
|
+
throw new RpcError(-32602, 'path required');
|
|
339
205
|
try {
|
|
340
206
|
mkdirSync(dirname(p), { recursive: true });
|
|
341
207
|
}
|
|
@@ -351,9 +217,11 @@ function acpUsage(s, raw) {
|
|
|
351
217
|
output = n(raw.outputTokens ?? raw.output_tokens);
|
|
352
218
|
cached = n(raw.cachedReadTokens ?? raw.cached_read_tokens ?? raw.cachedInputTokens);
|
|
353
219
|
}
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
220
|
+
const used = s.contextUsed;
|
|
221
|
+
return {
|
|
222
|
+
inputTokens: input, outputTokens: output, cachedInputTokens: cached, contextUsedTokens: used,
|
|
223
|
+
contextPercent: contextPercent(used || null, s.contextWindow), turnOutputTokens: output,
|
|
224
|
+
};
|
|
357
225
|
}
|
|
358
226
|
export class AcpDriver {
|
|
359
227
|
id;
|
|
@@ -376,19 +244,18 @@ export class AcpDriver {
|
|
|
376
244
|
}
|
|
377
245
|
async run(input, ctx) {
|
|
378
246
|
const env = { ...(this.cfg.env || {}), ...(input.env || {}) };
|
|
379
|
-
const client = new
|
|
247
|
+
const client = new StdioRpcClient({ command: this.cfg.command, args: this.cfg.args, env, cwd: input.workdir, label: this.id });
|
|
380
248
|
const state = { text: '', reasoning: '', contextWindow: null, contextUsed: null };
|
|
381
249
|
const tools = new Set();
|
|
382
250
|
let sessionId = input.sessionId ?? null;
|
|
383
251
|
let permSeq = 0;
|
|
384
252
|
if (!client.start())
|
|
385
253
|
return { ok: false, text: '', error: `failed to start ${this.cfg.command}`, stopReason: 'error' };
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
254
|
+
const unwireAbort = wireAbort(ctx.signal, () => {
|
|
255
|
+
if (sessionId)
|
|
256
|
+
client.notify('session/cancel', { sessionId });
|
|
257
|
+
client.kill();
|
|
258
|
+
});
|
|
392
259
|
client.onNotification((method, params) => {
|
|
393
260
|
if (method === 'session/update')
|
|
394
261
|
applyAcpUpdate(params?.update ?? params, state, tools, ctx.emit);
|
|
@@ -398,14 +265,14 @@ export class AcpDriver {
|
|
|
398
265
|
case 'session/request_permission': return this.resolvePermission(params, ctx, `${this.id}-perm-${++permSeq}`);
|
|
399
266
|
case 'fs/read_text_file':
|
|
400
267
|
if (!this.cfg.fsAccess)
|
|
401
|
-
throw new
|
|
268
|
+
throw new RpcError(-32601, 'fs/read_text_file not supported');
|
|
402
269
|
return readAcpTextFile(params);
|
|
403
270
|
case 'fs/write_text_file':
|
|
404
271
|
if (!this.cfg.fsAccess)
|
|
405
|
-
throw new
|
|
272
|
+
throw new RpcError(-32601, 'fs/write_text_file not supported');
|
|
406
273
|
return writeAcpTextFile(params);
|
|
407
274
|
default:
|
|
408
|
-
throw new
|
|
275
|
+
throw new RpcError(-32601, `Method not implemented: ${method}`);
|
|
409
276
|
}
|
|
410
277
|
});
|
|
411
278
|
try {
|
|
@@ -447,7 +314,7 @@ export class AcpDriver {
|
|
|
447
314
|
return { ok: true, text: state.text, reasoning: state.reasoning || undefined, error: null, stopReason, sessionId, usage };
|
|
448
315
|
}
|
|
449
316
|
finally {
|
|
450
|
-
|
|
317
|
+
unwireAbort();
|
|
451
318
|
client.kill();
|
|
452
319
|
}
|
|
453
320
|
}
|
package/dist/drivers/claude.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, DriverEvent, TuiInput, TuiSpec, NativeSessionInfo } from '../contracts/driver.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { UniversalPlan } from '../protocol/index.js';
|
|
3
3
|
export declare class ClaudeDriver implements AgentDriver {
|
|
4
4
|
private readonly bin;
|
|
5
5
|
readonly id = "claude";
|
|
@@ -18,19 +18,6 @@ export declare class ClaudeDriver implements AgentDriver {
|
|
|
18
18
|
run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
|
|
19
19
|
private usage;
|
|
20
20
|
}
|
|
21
|
-
export interface ClaudeUsageState {
|
|
22
|
-
input: number | null;
|
|
23
|
-
output: number | null;
|
|
24
|
-
cached: number | null;
|
|
25
|
-
cacheCreation?: number | null;
|
|
26
|
-
contextWindow?: number | null;
|
|
27
|
-
turnOutputTokensBase?: number | null;
|
|
28
|
-
thinkingEstTokens?: number | null;
|
|
29
|
-
}
|
|
30
|
-
export declare function claudeUsageOf(s: ClaudeUsageState): UniversalUsage;
|
|
31
|
-
export declare function applyClaudeThinkingEstimate(s: any, ev: any): boolean;
|
|
32
|
-
export declare function claudeContextWindowFromModel(model: unknown): number | null;
|
|
33
|
-
export declare function claudeEffectiveContextWindow(advertised: number | null): number | null;
|
|
34
21
|
export declare function handleClaudeEvent(ev: any, s: any, emit: (e: DriverEvent) => void): void;
|
|
35
22
|
export declare function claudeBgHoldCapMs(): number;
|
|
36
23
|
export declare function claudeBgAgentHoldCapMs(): number;
|
|
@@ -41,6 +28,7 @@ export declare function claudeModelStallMs(): number;
|
|
|
41
28
|
export declare const CLAUDE_TRUNCATED_RECOVERY_PROMPT: string;
|
|
42
29
|
export declare function claudeTruncatedRecoveryEnabled(): boolean;
|
|
43
30
|
export declare function isClaudeSyntheticResumeNoise(text: string): boolean;
|
|
31
|
+
export declare function isClaudeSlashCommand(prompt: string): boolean;
|
|
44
32
|
export declare function claudeProducedRealOutput(s: any): boolean;
|
|
45
33
|
export declare function claudeResumeNoopRetryLimit(): number;
|
|
46
34
|
export declare function claudeUserEventHasToolResult(ev: any): boolean;
|
|
@@ -56,11 +44,4 @@ export declare function decideClaudeResultSettle(input: {
|
|
|
56
44
|
sawBackground: boolean;
|
|
57
45
|
}): ClaudeResultSettleDecision;
|
|
58
46
|
export declare function claudeUserMessage(text: string, attachments?: string[]): string;
|
|
59
|
-
export declare function emitClaudeImages(blocks: any[], s: any, emit: (e: DriverEvent) => void): void;
|
|
60
47
|
export declare function todoWriteToPlan(input: any): UniversalPlan | null;
|
|
61
|
-
export declare function readClaudeTaskCreateId(ev: any, block: any): string | null;
|
|
62
|
-
export declare function rebuildClaudeTaskPlan(s: any): UniversalPlan | null;
|
|
63
|
-
export declare function applyTaskUpdateToTodoPlan(plan: UniversalPlan | null | undefined, taskId: string, rawStatus: string): UniversalPlan | null;
|
|
64
|
-
export declare function shortToolValue(value: unknown, max?: number): string;
|
|
65
|
-
export declare function summarizeToolUse(name: string, input: any): string;
|
|
66
|
-
export declare function firstResultLine(content: any): string;
|
package/dist/drivers/claude.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { discoverClaudeNativeSessions } from './native.js';
|
|
4
|
+
import { attachedFileNote, contextPercent, createLineBuffer, imageMimeForFile, parseJsonLine, sigterm, wireAbort } from './shared.js';
|
|
5
5
|
// Real driver: shells the local `claude` CLI in stream-json mode and normalizes its
|
|
6
6
|
// events into kernel DriverEvents. Faithful to pikiloom's claude.ts event shapes
|
|
7
7
|
// (system / stream_event{message_start,content_block_delta,message_delta} / assistant / result),
|
|
@@ -71,8 +71,11 @@ export class ClaudeDriver {
|
|
|
71
71
|
taskList: new Map(),
|
|
72
72
|
taskOrder: [],
|
|
73
73
|
pendingTaskCreates: new Map(),
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
todoPlan: null,
|
|
75
|
+
seenImages: new Set(),
|
|
76
|
+
// run_in_background lifecycle: task ids seen as started vs. reached a terminal status;
|
|
77
|
+
// bgAgentTasks marks the sub-agent-backed ones (they earn the longer hold cap).
|
|
78
|
+
bgStarted: new Set(), bgTerminal: new Set(), bgAgentTasks: new Set(),
|
|
76
79
|
};
|
|
77
80
|
return new Promise((resolve) => {
|
|
78
81
|
let child;
|
|
@@ -124,17 +127,10 @@ export class ClaudeDriver {
|
|
|
124
127
|
}
|
|
125
128
|
catch { /* ignore */ }
|
|
126
129
|
if (!child.killed && child.exitCode == null) {
|
|
127
|
-
if (kill)
|
|
128
|
-
|
|
129
|
-
child.kill('SIGTERM');
|
|
130
|
-
}
|
|
131
|
-
catch { /* ignore */ }
|
|
132
|
-
}
|
|
130
|
+
if (kill)
|
|
131
|
+
sigterm(child);
|
|
133
132
|
else {
|
|
134
|
-
const guard = setTimeout(() =>
|
|
135
|
-
child?.kill('SIGTERM');
|
|
136
|
-
}
|
|
137
|
-
catch { /* ignore */ } }, CLAUDE_EXIT_LEAK_GUARD_MS);
|
|
133
|
+
const guard = setTimeout(() => sigterm(child), CLAUDE_EXIT_LEAK_GUARD_MS);
|
|
138
134
|
unref(guard);
|
|
139
135
|
}
|
|
140
136
|
}
|
|
@@ -207,14 +203,7 @@ export class ClaudeDriver {
|
|
|
207
203
|
finish({ ok: false, text: '', error: `spawn failed: ${err?.message || err}`, stopReason: 'error' });
|
|
208
204
|
return;
|
|
209
205
|
}
|
|
210
|
-
|
|
211
|
-
child.kill('SIGTERM');
|
|
212
|
-
}
|
|
213
|
-
catch { /* ignore */ } };
|
|
214
|
-
if (ctx.signal.aborted)
|
|
215
|
-
onAbort();
|
|
216
|
-
else
|
|
217
|
-
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
206
|
+
wireAbort(ctx.signal, () => sigterm(child));
|
|
218
207
|
if (steerable) {
|
|
219
208
|
ctx.registerSteer(async (prompt, attachments) => {
|
|
220
209
|
try {
|
|
@@ -226,27 +215,17 @@ export class ClaudeDriver {
|
|
|
226
215
|
}
|
|
227
216
|
});
|
|
228
217
|
}
|
|
229
|
-
|
|
218
|
+
const nextLines = createLineBuffer();
|
|
230
219
|
let stderr = '';
|
|
231
220
|
child.stdout.on('data', (chunk) => {
|
|
232
221
|
if (settled)
|
|
233
222
|
return; // ignore the process's post-settle shutdown chatter
|
|
234
|
-
|
|
235
|
-
const lines = buf.split('\n');
|
|
236
|
-
buf = lines.pop() || '';
|
|
237
|
-
for (const line of lines) {
|
|
223
|
+
for (const line of nextLines(chunk)) {
|
|
238
224
|
if (settled)
|
|
239
225
|
return;
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
226
|
+
const ev = parseJsonLine(line);
|
|
227
|
+
if (ev === undefined)
|
|
242
228
|
continue;
|
|
243
|
-
let ev;
|
|
244
|
-
try {
|
|
245
|
-
ev = JSON.parse(trimmed);
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
229
|
state.lastEventAt = Date.now();
|
|
251
230
|
handleClaudeEvent(ev, state, ctx.emit);
|
|
252
231
|
const pending = pendingClaudeBackgroundTasks(state);
|
|
@@ -291,7 +270,13 @@ export class ClaudeDriver {
|
|
|
291
270
|
// through its repair to a real answer within this one turn. Bounded (the repair can
|
|
292
271
|
// no-op more than once); the post-tool stall watchdog is the backstop if the CLI never
|
|
293
272
|
// engages. Scoped to resumes (input.sessionId) — a fresh session has no dangling turn.
|
|
273
|
+
// Excludes local slash commands (isClaudeSlashCommand): /compact, /clear & friends are
|
|
274
|
+
// LEGITIMATELY output-empty (their effect is a local action, not an assistant reply), so
|
|
275
|
+
// the emptiness that flags a dropped send is normal for them. Re-issuing one is never a
|
|
276
|
+
// repair — for /compact it just fires a second compaction that reports "Not enough
|
|
277
|
+
// messages to compact." A real dropped send is a plain prompt and still self-heals.
|
|
294
278
|
if (!hasError && !!input.sessionId && !claudeProducedRealOutput(state)
|
|
279
|
+
&& !isClaudeSlashCommand(input.prompt)
|
|
295
280
|
&& noopResumeRetries < claudeResumeNoopRetryLimit()) {
|
|
296
281
|
noopResumeRetries++;
|
|
297
282
|
let injected = false;
|
|
@@ -374,7 +359,7 @@ export class ClaudeDriver {
|
|
|
374
359
|
return claudeUsageOf(s);
|
|
375
360
|
}
|
|
376
361
|
}
|
|
377
|
-
|
|
362
|
+
function claudeUsageOf(s) {
|
|
378
363
|
// While a message is still streaming, the CLI's live thinking estimate (system/thinking_tokens)
|
|
379
364
|
// is often the ONLY output signal — subscription accounts stream no plaintext thinking and no
|
|
380
365
|
// usage until the message settles. Fold it into the derived numbers (never into the raw
|
|
@@ -382,22 +367,20 @@ export function claudeUsageOf(s) {
|
|
|
382
367
|
// output_tokens supersedes it at message_delta.
|
|
383
368
|
const effOutput = Math.max(s.output ?? 0, s.thinkingEstTokens ?? 0);
|
|
384
369
|
const used = (s.input ?? 0) + (s.cached ?? 0) + (s.cacheCreation ?? 0) + effOutput;
|
|
385
|
-
const window = s.contextWindow ?? null;
|
|
386
|
-
const contextPercent = window && used > 0 ? Math.min(99.9, Math.round((used / window) * 1000) / 10) : null;
|
|
387
370
|
const turnOutput = (s.turnOutputTokensBase ?? 0) + effOutput;
|
|
388
371
|
return {
|
|
389
372
|
inputTokens: s.input,
|
|
390
373
|
outputTokens: s.output,
|
|
391
374
|
cachedInputTokens: s.cached,
|
|
392
375
|
contextUsedTokens: used > 0 ? used : null,
|
|
393
|
-
contextPercent,
|
|
376
|
+
contextPercent: contextPercent(used > 0 ? used : null, s.contextWindow ?? null),
|
|
394
377
|
turnOutputTokens: turnOutput > 0 ? turnOutput : null,
|
|
395
378
|
};
|
|
396
379
|
}
|
|
397
380
|
// Accumulate the CLI's live thinking-token estimate onto driver state. Prefer the per-event
|
|
398
381
|
// delta (correct whether the CLI's running total is per-message or per-turn); fall back to a
|
|
399
382
|
// monotonic max of the running total. Returns true when the estimate advanced.
|
|
400
|
-
|
|
383
|
+
function applyClaudeThinkingEstimate(s, ev) {
|
|
401
384
|
const prev = s.thinkingEstTokens ?? 0;
|
|
402
385
|
const delta = Number(ev?.estimated_tokens_delta);
|
|
403
386
|
const total = Number(ev?.estimated_tokens);
|
|
@@ -410,7 +393,7 @@ export function applyClaudeThinkingEstimate(s, ev) {
|
|
|
410
393
|
// Advertised context window by Claude model id (best-effort; unknown -> null so the
|
|
411
394
|
// percent simply stays absent rather than wrong). Anchor-free so vendor-prefixed ids
|
|
412
395
|
// (us.anthropic.claude-…) still match.
|
|
413
|
-
|
|
396
|
+
function claudeContextWindowFromModel(model) {
|
|
414
397
|
const id = String(model ?? '').trim().toLowerCase();
|
|
415
398
|
if (!id)
|
|
416
399
|
return null;
|
|
@@ -424,7 +407,7 @@ export function claudeContextWindowFromModel(model) {
|
|
|
424
407
|
}
|
|
425
408
|
// Usable window = advertised minus Claude's max-output (20k) + autocompact (13k) reserve.
|
|
426
409
|
const CLAUDE_USABLE_WINDOW_RESERVE = 33_000;
|
|
427
|
-
|
|
410
|
+
function claudeEffectiveContextWindow(advertised) {
|
|
428
411
|
if (advertised == null)
|
|
429
412
|
return null;
|
|
430
413
|
return advertised <= CLAUDE_USABLE_WINDOW_RESERVE ? advertised : advertised - CLAUDE_USABLE_WINDOW_RESERVE;
|
|
@@ -832,6 +815,15 @@ export function isClaudeSyntheticResumeNoise(text) {
|
|
|
832
815
|
const t = (text || '').trim().toLowerCase();
|
|
833
816
|
return t === 'no response requested.' || t === 'no response requested';
|
|
834
817
|
}
|
|
818
|
+
// A prompt whose first token is a Claude slash command (/compact, /clear, /cost, /model …, a custom
|
|
819
|
+
// /namespace:command). Such a prompt is a LOCAL action, not a request for a model reply, so an
|
|
820
|
+
// output-empty result is expected — not the dropped-send signature the no-op-resume repair targets.
|
|
821
|
+
// The first token must be a bare command name (letters/digits/_/- with an optional :namespace) ending
|
|
822
|
+
// at whitespace or end-of-string, so a filesystem-style path (/Users/foo) is NOT matched. Pure +
|
|
823
|
+
// exported for hermetic testing.
|
|
824
|
+
export function isClaudeSlashCommand(prompt) {
|
|
825
|
+
return /^\/[a-z0-9][\w-]*(?::[\w-]+)?(?:\s|$)/i.test((prompt || '').trimStart());
|
|
826
|
+
}
|
|
835
827
|
// True once the turn produced ANY real model output — streamed or whole-message text/reasoning, a
|
|
836
828
|
// tool use, or a spawned sub-agent. False for a pure no-op (a synthetic resume-repair result that
|
|
837
829
|
// ran none of the prompt), which is exactly what the no-op-resume recovery keys off. Pure +
|
|
@@ -944,11 +936,6 @@ export function decideClaudeResultSettle(input) {
|
|
|
944
936
|
return 'hold';
|
|
945
937
|
return input.sawBackground ? 'quiet-settle' : 'settle';
|
|
946
938
|
}
|
|
947
|
-
// Claude vision input formats (matches the legacy driver / Anthropic API: png, jpeg, gif, webp).
|
|
948
|
-
const CLAUDE_IMAGE_MIME = {
|
|
949
|
-
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
950
|
-
'.gif': 'image/gif', '.webp': 'image/webp',
|
|
951
|
-
};
|
|
952
939
|
// A stream-json user message (for --input-format stream-json; used to send the prompt and to
|
|
953
940
|
// inject mid-turn steer messages while stdin stays open). Image attachments are inlined as base64
|
|
954
941
|
// image content blocks so the model actually sees them; other files become a text note. Without
|
|
@@ -956,7 +943,7 @@ const CLAUDE_IMAGE_MIME = {
|
|
|
956
943
|
export function claudeUserMessage(text, attachments) {
|
|
957
944
|
const content = [];
|
|
958
945
|
for (const filePath of attachments || []) {
|
|
959
|
-
const mime =
|
|
946
|
+
const mime = imageMimeForFile(filePath);
|
|
960
947
|
if (mime) {
|
|
961
948
|
try {
|
|
962
949
|
content.push({ type: 'image', source: { type: 'base64', media_type: mime, data: readFileSync(filePath).toString('base64') } });
|
|
@@ -964,13 +951,13 @@ export function claudeUserMessage(text, attachments) {
|
|
|
964
951
|
}
|
|
965
952
|
catch { /* unreadable -> fall through to a text note */ }
|
|
966
953
|
}
|
|
967
|
-
content.push({ type: 'text', text:
|
|
954
|
+
content.push({ type: 'text', text: attachedFileNote(filePath) });
|
|
968
955
|
}
|
|
969
956
|
content.push({ type: 'text', text });
|
|
970
957
|
return JSON.stringify({ type: 'user', message: { role: 'user', content } });
|
|
971
958
|
}
|
|
972
959
|
// Surface base64 image content blocks as artifacts (data URLs), deduped per turn.
|
|
973
|
-
|
|
960
|
+
function emitClaudeImages(blocks, s, emit) {
|
|
974
961
|
if (!Array.isArray(blocks))
|
|
975
962
|
return;
|
|
976
963
|
s.seenImages ||= new Set();
|
|
@@ -1014,7 +1001,7 @@ export function todoWriteToPlan(input) {
|
|
|
1014
1001
|
// dashboard's task-list card never renders. Ported from pikiloom's legacy claude driver.
|
|
1015
1002
|
// The assigned task id from a TaskCreate tool_result. Prefer the structured field; fall back
|
|
1016
1003
|
// to parsing "Task #N" from a string result.
|
|
1017
|
-
|
|
1004
|
+
function readClaudeTaskCreateId(ev, block) {
|
|
1018
1005
|
const structured = ev?.toolUseResult?.task?.id;
|
|
1019
1006
|
if (structured != null && String(structured).trim())
|
|
1020
1007
|
return String(structured).trim();
|
|
@@ -1026,7 +1013,7 @@ export function readClaudeTaskCreateId(ev, block) {
|
|
|
1026
1013
|
}
|
|
1027
1014
|
return null;
|
|
1028
1015
|
}
|
|
1029
|
-
|
|
1016
|
+
function rebuildClaudeTaskPlan(s) {
|
|
1030
1017
|
if (!Array.isArray(s?.taskOrder) || !s.taskOrder.length)
|
|
1031
1018
|
return null;
|
|
1032
1019
|
const steps = [];
|
|
@@ -1048,7 +1035,7 @@ export function rebuildClaudeTaskPlan(s) {
|
|
|
1048
1035
|
// Used when the id isn't in the TaskCreate store, so an update issued AFTER a TodoWrite still
|
|
1049
1036
|
// lands on the displayed list — the plan reflects the state after the LAST change, whichever
|
|
1050
1037
|
// mechanism wrote it. Returns a fresh plan (never mutates) or null when inapplicable.
|
|
1051
|
-
|
|
1038
|
+
function applyTaskUpdateToTodoPlan(plan, taskId, rawStatus) {
|
|
1052
1039
|
if (!plan || !Array.isArray(plan.steps) || !plan.steps.length)
|
|
1053
1040
|
return null;
|
|
1054
1041
|
if (!/^\d+$/.test(taskId))
|
|
@@ -1072,7 +1059,7 @@ export function applyTaskUpdateToTodoPlan(plan, taskId, rawStatus) {
|
|
|
1072
1059
|
// Turns a Claude tool_use {name,input} into a one-line human summary. The runtime's
|
|
1073
1060
|
// activity projector joins these into snapshot.activity; the structured form lives in
|
|
1074
1061
|
// toolCalls. Kept driver-local: knowing Claude's tool input shapes is the driver's job.
|
|
1075
|
-
|
|
1062
|
+
function shortToolValue(value, max = 140) {
|
|
1076
1063
|
if (value == null)
|
|
1077
1064
|
return '';
|
|
1078
1065
|
const text = (typeof value === 'string' ? value : String(value)).replace(/\s+/g, ' ').trim();
|
|
@@ -1084,7 +1071,7 @@ function toolInputDetail(name, input) {
|
|
|
1084
1071
|
const i = input || {};
|
|
1085
1072
|
return i.command || i.file_path || i.path || i.pattern || i.query || i.url || i.description || '';
|
|
1086
1073
|
}
|
|
1087
|
-
|
|
1074
|
+
function summarizeToolUse(name, input) {
|
|
1088
1075
|
const tool = String(name || '').trim() || 'Tool';
|
|
1089
1076
|
const i = input || {};
|
|
1090
1077
|
const description = shortToolValue(i.description, 120);
|
|
@@ -1149,7 +1136,7 @@ export function summarizeToolUse(name, input) {
|
|
|
1149
1136
|
}
|
|
1150
1137
|
}
|
|
1151
1138
|
// First non-empty line of a tool_result content (string | block[]), for the "summary -> detail" form.
|
|
1152
|
-
|
|
1139
|
+
function firstResultLine(content) {
|
|
1153
1140
|
let text = '';
|
|
1154
1141
|
if (typeof content === 'string')
|
|
1155
1142
|
text = content;
|
package/dist/drivers/codex.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AgentDriver, AgentTurnInput, DriverContext, DriverEvent, DriverResult, TuiInput, TuiSpec, NativeSessionInfo } from '../contracts/driver.js';
|
|
2
|
-
import type { UniversalUsage } from '../protocol/index.js';
|
|
3
2
|
export declare function codexToolSummary(item: any): {
|
|
4
3
|
id: string;
|
|
5
4
|
name: string;
|
|
@@ -34,12 +33,3 @@ export declare class CodexDriver implements AgentDriver {
|
|
|
34
33
|
limit?: number;
|
|
35
34
|
}): NativeSessionInfo[];
|
|
36
35
|
}
|
|
37
|
-
export interface CodexUsageState {
|
|
38
|
-
input: number | null;
|
|
39
|
-
output: number | null;
|
|
40
|
-
cached: number | null;
|
|
41
|
-
contextUsed?: number | null;
|
|
42
|
-
contextWindow?: number | null;
|
|
43
|
-
}
|
|
44
|
-
export declare function applyCodexTokenUsage(s: CodexUsageState, rawUsage: any): void;
|
|
45
|
-
export declare function codexUsageOf(s: CodexUsageState): UniversalUsage;
|