@adecore/agents 0.39.0 → 0.40.0
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/dist/async.d.ts +5 -0
- package/dist/async.js +13 -5
- package/dist/chat/acp-protocol.js +3 -5
- package/dist/chat/chat-core.js +0 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.js +6 -8
- package/dist/chat/claude-protocol.js +1 -6
- package/dist/chat/claude-transcript.js +1 -3
- package/dist/chat/codex-protocol.js +1 -6
- package/dist/chat/projector.js +1 -3
- package/dist/chat/request-summary.js +1 -3
- package/dist/coded-error.js +1 -5
- package/dist/context/argv.js +1 -2
- package/dist/context/verb.js +1 -3
- package/dist/error-text.d.ts +1 -0
- package/dist/error-text.js +4 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.js +12 -0
- package/dist/fs.d.ts +1 -0
- package/dist/fs.js +8 -4
- package/dist/lineage.js +11 -5
- package/dist/messages/notice-store.js +3 -4
- package/dist/providers/accounts/acp-account.js +3 -20
- package/dist/providers/accounts/service.d.ts +1 -2
- package/dist/providers/accounts/service.js +5 -9
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/acp-agent-state.js +1 -3
- package/dist/providers/claude.js +0 -1
- package/dist/providers/codex.js +0 -1
- package/dist/providers/detect.js +1 -1
- package/dist/providers/shell-commands.js +1 -2
- package/dist/run-process.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -8
- package/dist/tasks/background-limit.js +3 -2
- package/dist/tasks/task-coordinator.js +11 -17
- package/dist/testing/watch.js +0 -1
- package/dist/usage/limits/monitor.js +1 -2
- package/dist/usage/readers/claude.js +3 -10
- package/dist/usage/readers/codex.js +2 -9
- package/dist/usage/readers/grok.js +3 -10
- package/dist/usage/readers/host-log.js +3 -10
- package/dist/usage/record.d.ts +2 -0
- package/dist/usage/record.js +13 -0
- package/package.json +6 -6
- package/src/async.ts +23 -5
- package/src/chat/acp-backend.ts +121 -0
- package/src/chat/acp-protocol.ts +172 -6
- package/src/chat/attachment-store.ts +21 -0
- package/src/chat/backend.ts +0 -2
- package/src/chat/bookmark-store.ts +23 -0
- package/src/chat/chat-core.ts +208 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +401 -10
- package/src/chat/chat-store.ts +50 -0
- package/src/chat/chat-title.ts +9 -0
- package/src/chat/claude-backend.ts +50 -0
- package/src/chat/claude-protocol.ts +155 -8
- package/src/chat/claude-title.ts +10 -0
- package/src/chat/claude-transcript.ts +47 -4
- package/src/chat/codex-backend.ts +131 -0
- package/src/chat/codex-protocol.ts +180 -8
- package/src/chat/codex-thread.ts +23 -0
- package/src/chat/composer-preferences.ts +6 -0
- package/src/chat/context-breakdown.ts +11 -0
- package/src/chat/delta-coalescer.ts +8 -0
- package/src/chat/external-sessions.ts +28 -0
- package/src/chat/generated-image.ts +25 -0
- package/src/chat/index.ts +5 -0
- package/src/chat/input.ts +13 -0
- package/src/chat/json-rpc-transport.ts +28 -0
- package/src/chat/limit-resume.ts +13 -0
- package/src/chat/projector.ts +160 -4
- package/src/chat/readable-text.ts +3 -0
- package/src/chat/request-summary.ts +18 -4
- package/src/chat/subagent-projection.ts +3 -0
- package/src/chat/subagent-reader.ts +76 -0
- package/src/chat/subagent-settlement.ts +24 -0
- package/src/chat/thread-search.ts +13 -0
- package/src/chat/thread.ts +31 -0
- package/src/chat/ui-queries.ts +122 -0
- package/src/chat/ui-replies.ts +34 -0
- package/src/chat/visual-store.ts +49 -0
- package/src/chat/wake-chat.ts +3 -0
- package/src/client-sinks.ts +2 -0
- package/src/coded-error.ts +1 -5
- package/src/context/argv.ts +18 -2
- package/src/context/index.ts +1 -0
- package/src/context/refusal.ts +3 -0
- package/src/context/verb.ts +32 -4
- package/src/coordination/index.ts +10 -0
- package/src/error-text.ts +7 -0
- package/src/fields.ts +14 -0
- package/src/fs.ts +20 -4
- package/src/host/agent-host.ts +20 -0
- package/src/host/environment.ts +6 -0
- package/src/host/handlers.ts +18 -0
- package/src/host/memory-port.ts +4 -0
- package/src/host/wiring.ts +10 -0
- package/src/index.ts +1 -0
- package/src/lineage.ts +24 -5
- package/src/messages/deliver-message.ts +6 -0
- package/src/messages/deliver-notice.ts +13 -0
- package/src/messages/notice-store.ts +16 -4
- package/src/modes.ts +1 -0
- package/src/outbox/outbox-worker.ts +31 -0
- package/src/outbox/outbox.ts +5 -0
- package/src/providers/accounts/accounts.ts +36 -0
- package/src/providers/accounts/acp-account.ts +22 -27
- package/src/providers/accounts/launch.ts +4 -0
- package/src/providers/accounts/service.ts +110 -12
- package/src/providers/accounts/shadow-home.ts +10 -0
- package/src/providers/accounts/status.ts +13 -4
- package/src/providers/accounts/store.ts +25 -0
- package/src/providers/accounts/variables.ts +11 -1
- package/src/providers/acp-agent-state.ts +24 -3
- package/src/providers/acp-provider.ts +5 -0
- package/src/providers/acp-usage-log.ts +2 -0
- package/src/providers/catalog.ts +17 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-rules.ts +3 -0
- package/src/providers/codex.ts +2 -1
- package/src/providers/detect.ts +6 -1
- package/src/providers/provider.ts +1 -1
- package/src/providers/registry.ts +18 -2
- package/src/providers/shell-commands.ts +37 -2
- package/src/record-directory.ts +19 -0
- package/src/run-process.ts +24 -3
- package/src/serializer.ts +3 -0
- package/src/skills.ts +73 -10
- package/src/tasks/background-limit.ts +9 -2
- package/src/tasks/end-children.ts +9 -0
- package/src/tasks/give-task.ts +8 -0
- package/src/tasks/task-coordinator.ts +81 -17
- package/src/tasks/task-store.ts +28 -0
- package/src/tasks/waiting-child.ts +21 -0
- package/src/tasks/wake-parent.ts +26 -0
- package/src/tasks/wiring.ts +22 -0
- package/src/testing/accounts.ts +2 -0
- package/src/testing/fake-acp-capture.ts +16 -0
- package/src/testing/fake-acp.ts +66 -0
- package/src/testing/fake-claude.ts +177 -0
- package/src/testing/fake-cli.ts +29 -0
- package/src/testing/fake-codex.ts +122 -0
- package/src/testing/manual-clock.ts +2 -0
- package/src/testing/watch.ts +7 -1
- package/src/title-file.ts +12 -0
- package/src/usage/accounts.ts +6 -0
- package/src/usage/aggregate.ts +19 -0
- package/src/usage/exchange.ts +12 -0
- package/src/usage/index-file.ts +27 -0
- package/src/usage/limits/monitor.ts +31 -3
- package/src/usage/limits/normalize.ts +30 -0
- package/src/usage/limits/probe.ts +17 -0
- package/src/usage/pricing.ts +35 -0
- package/src/usage/projects.ts +8 -0
- package/src/usage/readers/claude.ts +12 -9
- package/src/usage/readers/codex.ts +40 -8
- package/src/usage/readers/grok.ts +17 -9
- package/src/usage/readers/host-log.ts +9 -9
- package/src/usage/record.ts +22 -0
- package/src/usage/roots.ts +6 -0
- package/src/usage/scanner.ts +61 -0
- package/src/usage/usage-service.ts +14 -0
- package/src/watch-seam.ts +8 -0
- package/src/windows-command.ts +12 -0
package/src/chat/chat-store.ts
CHANGED
|
@@ -81,13 +81,17 @@ function liftUiAccess(record: unknown): unknown {
|
|
|
81
81
|
if (typeof record !== 'object' || record === null || 'uiAccess' in record || !Array.isArray((record as { items?: unknown }).items)) {
|
|
82
82
|
return record;
|
|
83
83
|
}
|
|
84
|
+
|
|
84
85
|
const uiAccess: Record<string, unknown> = {};
|
|
86
|
+
|
|
85
87
|
for (const item of (record as { items: unknown[] }).items) {
|
|
86
88
|
const queries = (item as { uiQueries?: unknown } | null)?.uiQueries;
|
|
89
|
+
|
|
87
90
|
if (typeof queries === 'object' && queries !== null && 'access' in queries) {
|
|
88
91
|
uiAccess[String((item as { id: unknown }).id)] = queries.access;
|
|
89
92
|
}
|
|
90
93
|
}
|
|
94
|
+
|
|
91
95
|
return Object.keys(uiAccess).length === 0 ? record : { ...record, uiAccess };
|
|
92
96
|
}
|
|
93
97
|
|
|
@@ -119,26 +123,31 @@ export class ChatStore {
|
|
|
119
123
|
/* Every chat with a record or a log, for the scan at start. */
|
|
120
124
|
async list(): Promise<string[]> {
|
|
121
125
|
let names: string[];
|
|
126
|
+
|
|
122
127
|
try {
|
|
123
128
|
names = await readdir(this.dir);
|
|
124
129
|
} catch (e) {
|
|
125
130
|
if (isNotFound(e)) {
|
|
126
131
|
return [];
|
|
127
132
|
}
|
|
133
|
+
|
|
128
134
|
throw e;
|
|
129
135
|
}
|
|
136
|
+
|
|
130
137
|
const ids = names
|
|
131
138
|
// A chat's bookmarks and visuals, and whatever a host keeps per chat, sit beside its record under names that also end in .json.
|
|
132
139
|
.filter(
|
|
133
140
|
(name) => (name.endsWith('.json') && !isBookmarkFileName(name) && !isVisualFileName(name) && !this.isSidecar(name)) || name.endsWith('.log')
|
|
134
141
|
)
|
|
135
142
|
.map((name) => decodeURIComponent(name.replace(/\.(json|log)$/, '')));
|
|
143
|
+
|
|
136
144
|
return [...new Set(ids)];
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
/* Whether anything of the chat is on disk, without reading it: a record or a log. */
|
|
140
148
|
async has(chatId: string): Promise<boolean> {
|
|
141
149
|
const found = await Promise.all([exists(join(this.dir, recordFileName(chatId))), exists(this.logPath(chatId))]);
|
|
150
|
+
|
|
142
151
|
return found.includes(true);
|
|
143
152
|
}
|
|
144
153
|
|
|
@@ -146,6 +155,7 @@ export class ChatStore {
|
|
|
146
155
|
async discardLog(chatId: string): Promise<number> {
|
|
147
156
|
const lines = parseLog((await readOrNull(this.logPath(chatId))) ?? '');
|
|
148
157
|
await rm(this.logPath(chatId), { force: true });
|
|
158
|
+
|
|
149
159
|
return lines.at(-1)?.seq ?? 0;
|
|
150
160
|
}
|
|
151
161
|
|
|
@@ -162,6 +172,7 @@ export class ChatStore {
|
|
|
162
172
|
const body = recordBody(info, items, at, preambles, extras, uiAccess);
|
|
163
173
|
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
164
174
|
await writeAtomic(join(this.dir, recordFileName(chatId)), body);
|
|
175
|
+
|
|
165
176
|
return body.length;
|
|
166
177
|
}
|
|
167
178
|
|
|
@@ -186,25 +197,32 @@ export class ChatStore {
|
|
|
186
197
|
async read(chatId: string): Promise<ChatRecord | null> {
|
|
187
198
|
const lines = parseLog((await readOrNull(this.logPath(chatId))) ?? '');
|
|
188
199
|
const snapshot = await this.readSnapshot(chatId);
|
|
200
|
+
|
|
189
201
|
if (snapshot === UNREADABLE) {
|
|
190
202
|
return null;
|
|
191
203
|
}
|
|
204
|
+
|
|
192
205
|
if (snapshot === null) {
|
|
193
206
|
return fromLogAlone(lines);
|
|
194
207
|
}
|
|
208
|
+
|
|
195
209
|
const seq = snapshot.seq ?? 0;
|
|
196
210
|
const thread = new ChatThread(snapshot.info, snapshot.items);
|
|
197
211
|
let resetSeq = snapshot.resetSeq ?? 0;
|
|
212
|
+
|
|
198
213
|
for (const line of lines) {
|
|
199
214
|
// A crash between writing a snapshot and folding the log into it leaves lines the snapshot already holds.
|
|
200
215
|
if (line.seq <= seq) {
|
|
201
216
|
continue;
|
|
202
217
|
}
|
|
218
|
+
|
|
203
219
|
thread.apply(line.event);
|
|
220
|
+
|
|
204
221
|
if (line.event.type === 'reset') {
|
|
205
222
|
resetSeq = line.seq;
|
|
206
223
|
}
|
|
207
224
|
}
|
|
225
|
+
|
|
208
226
|
return {
|
|
209
227
|
...thread.snapshot(),
|
|
210
228
|
seq,
|
|
@@ -233,30 +251,39 @@ export class ChatStore {
|
|
|
233
251
|
if (!settledInfo(await leadingInfo(join(this.dir, recordFileName(chatId))))) {
|
|
234
252
|
return false;
|
|
235
253
|
}
|
|
254
|
+
|
|
236
255
|
const last = lastLoggedInfo((await readOrNull(this.logPath(chatId))) ?? '');
|
|
256
|
+
|
|
237
257
|
return last === undefined || settledInfo(last);
|
|
238
258
|
}
|
|
239
259
|
|
|
240
260
|
private async readSnapshot(chatId: string): Promise<z.infer<typeof RecordSchema> | typeof UNREADABLE | null> {
|
|
241
261
|
const raw = await readOrNull(join(this.dir, recordFileName(chatId)));
|
|
262
|
+
|
|
242
263
|
if (raw === null) {
|
|
243
264
|
return null;
|
|
244
265
|
}
|
|
266
|
+
|
|
245
267
|
let record: unknown;
|
|
268
|
+
|
|
246
269
|
try {
|
|
247
270
|
record = JSON.parse(raw);
|
|
248
271
|
} catch {
|
|
249
272
|
return UNREADABLE;
|
|
250
273
|
}
|
|
274
|
+
|
|
251
275
|
// Before the schema sees it: an image written inline no longer has a shape the schema knows.
|
|
252
276
|
if (this.attachments) {
|
|
253
277
|
const migrated = await migrateInlineAttachments(chatId, record, this.attachments).catch(() => null);
|
|
278
|
+
|
|
254
279
|
if (migrated !== null) {
|
|
255
280
|
record = migrated;
|
|
256
281
|
await writeAtomic(join(this.dir, recordFileName(chatId)), JSON.stringify(migrated));
|
|
257
282
|
}
|
|
258
283
|
}
|
|
284
|
+
|
|
259
285
|
const parsed = RecordSchema.safeParse(liftUiAccess(record));
|
|
286
|
+
|
|
260
287
|
return parsed.success ? parsed.data : UNREADABLE;
|
|
261
288
|
}
|
|
262
289
|
|
|
@@ -281,14 +308,17 @@ const CLOSE_BRACE = 0x7d;
|
|
|
281
308
|
*/
|
|
282
309
|
async function leadingInfo(path: string): Promise<unknown> {
|
|
283
310
|
let handle: Awaited<ReturnType<typeof open>>;
|
|
311
|
+
|
|
284
312
|
try {
|
|
285
313
|
handle = await open(path, 'r');
|
|
286
314
|
} catch (e) {
|
|
287
315
|
if (isNotFound(e)) {
|
|
288
316
|
return undefined;
|
|
289
317
|
}
|
|
318
|
+
|
|
290
319
|
throw e;
|
|
291
320
|
}
|
|
321
|
+
|
|
292
322
|
try {
|
|
293
323
|
let head = Buffer.alloc(0);
|
|
294
324
|
let at = RECORD_HEAD.length;
|
|
@@ -297,13 +327,17 @@ async function leadingInfo(path: string): Promise<unknown> {
|
|
|
297
327
|
let escaped = false;
|
|
298
328
|
const chunk = Buffer.alloc(HEAD_CHUNK_BYTES);
|
|
299
329
|
let { bytesRead } = await handle.read(chunk, 0, chunk.length, 0);
|
|
330
|
+
|
|
300
331
|
while (bytesRead > 0) {
|
|
301
332
|
head = Buffer.concat([head, chunk.subarray(0, bytesRead)]);
|
|
333
|
+
|
|
302
334
|
if (!head.subarray(0, RECORD_HEAD.length).equals(RECORD_HEAD.subarray(0, Math.min(head.length, RECORD_HEAD.length)))) {
|
|
303
335
|
return undefined;
|
|
304
336
|
}
|
|
337
|
+
|
|
305
338
|
for (; at < head.length; at++) {
|
|
306
339
|
const byte = head[at]!;
|
|
340
|
+
|
|
307
341
|
if (escaped) {
|
|
308
342
|
escaped = false;
|
|
309
343
|
} else if (inString) {
|
|
@@ -315,13 +349,16 @@ async function leadingInfo(path: string): Promise<unknown> {
|
|
|
315
349
|
depth += 1;
|
|
316
350
|
} else if (byte === CLOSE_BRACE) {
|
|
317
351
|
depth -= 1;
|
|
352
|
+
|
|
318
353
|
if (depth === 0) {
|
|
319
354
|
return parseOrUndefined(head.subarray(RECORD_HEAD.length - 1, at + 1).toString('utf8'));
|
|
320
355
|
}
|
|
321
356
|
}
|
|
322
357
|
}
|
|
358
|
+
|
|
323
359
|
({ bytesRead } = await handle.read(chunk, 0, chunk.length, head.length));
|
|
324
360
|
}
|
|
361
|
+
|
|
325
362
|
return undefined;
|
|
326
363
|
} finally {
|
|
327
364
|
await handle.close();
|
|
@@ -331,16 +368,20 @@ async function leadingInfo(path: string): Promise<unknown> {
|
|
|
331
368
|
/* The info of the last info or reset line in a log, unvalidated; undefined when no line carries one. */
|
|
332
369
|
function lastLoggedInfo(raw: string): unknown {
|
|
333
370
|
const lines = raw.split('\n');
|
|
371
|
+
|
|
334
372
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
335
373
|
// Most lines are items and deltas, which are larger and never carry this key.
|
|
336
374
|
if (!lines[i]!.includes('"info"')) {
|
|
337
375
|
continue;
|
|
338
376
|
}
|
|
377
|
+
|
|
339
378
|
const event = (parseOrUndefined(lines[i]!) as { event?: { type?: unknown; info?: unknown } } | null | undefined)?.event;
|
|
379
|
+
|
|
340
380
|
if (event?.type === 'info' || event?.type === 'reset') {
|
|
341
381
|
return event.info;
|
|
342
382
|
}
|
|
343
383
|
}
|
|
384
|
+
|
|
344
385
|
return undefined;
|
|
345
386
|
}
|
|
346
387
|
|
|
@@ -359,11 +400,13 @@ function parseOrUndefined(text: string): unknown {
|
|
|
359
400
|
async function exists(path: string): Promise<boolean> {
|
|
360
401
|
try {
|
|
361
402
|
await stat(path);
|
|
403
|
+
|
|
362
404
|
return true;
|
|
363
405
|
} catch (e) {
|
|
364
406
|
if (isNotFound(e)) {
|
|
365
407
|
return false;
|
|
366
408
|
}
|
|
409
|
+
|
|
367
410
|
throw e;
|
|
368
411
|
}
|
|
369
412
|
}
|
|
@@ -375,6 +418,7 @@ async function readOrNull(path: string): Promise<string | null> {
|
|
|
375
418
|
if (isNotFound(e)) {
|
|
376
419
|
return null;
|
|
377
420
|
}
|
|
421
|
+
|
|
378
422
|
throw e;
|
|
379
423
|
}
|
|
380
424
|
}
|
|
@@ -389,17 +433,23 @@ function fromLogAlone(lines: ChatLogLine[]): ChatRecord | null {
|
|
|
389
433
|
if (lines.length === 0) {
|
|
390
434
|
return null;
|
|
391
435
|
}
|
|
436
|
+
|
|
392
437
|
const first = lines.find((line) => line.event.type === 'info' || line.event.type === 'reset')?.event;
|
|
438
|
+
|
|
393
439
|
if (lines[0]!.seq !== 1 || (first?.type !== 'info' && first?.type !== 'reset')) {
|
|
394
440
|
return null;
|
|
395
441
|
}
|
|
442
|
+
|
|
396
443
|
const thread = new ChatThread(first.info);
|
|
397
444
|
let resetSeq = 0;
|
|
445
|
+
|
|
398
446
|
for (const line of lines) {
|
|
399
447
|
thread.apply(line.event);
|
|
448
|
+
|
|
400
449
|
if (line.event.type === 'reset') {
|
|
401
450
|
resetSeq = line.seq;
|
|
402
451
|
}
|
|
403
452
|
}
|
|
453
|
+
|
|
404
454
|
return { ...thread.snapshot(), seq: 0, resetSeq, preambles: [], uiAccess: {}, extras: {}, lines };
|
|
405
455
|
}
|
package/src/chat/chat-title.ts
CHANGED
|
@@ -45,19 +45,25 @@ export function buildTitlePrompt(prompt: string, answer: string): string {
|
|
|
45
45
|
export function parseTitle(output: string): string | null {
|
|
46
46
|
const start = [...output.matchAll(/\{\s*"title"/g)].at(-1)?.index;
|
|
47
47
|
const end = output.lastIndexOf('}');
|
|
48
|
+
|
|
48
49
|
if (start === undefined || end < start) {
|
|
49
50
|
return null;
|
|
50
51
|
}
|
|
52
|
+
|
|
51
53
|
let parsed: unknown;
|
|
54
|
+
|
|
52
55
|
try {
|
|
53
56
|
parsed = JSON.parse(output.slice(start, end + 1));
|
|
54
57
|
} catch {
|
|
55
58
|
return null;
|
|
56
59
|
}
|
|
60
|
+
|
|
57
61
|
if (typeof parsed !== 'object' || parsed === null || !('title' in parsed)) {
|
|
58
62
|
return null;
|
|
59
63
|
}
|
|
64
|
+
|
|
60
65
|
const title = cleanTitle(parsed.title);
|
|
66
|
+
|
|
61
67
|
return title === null ? null : cleanTitle(title.replace(/^["'`]+|["'`.]+$/g, ''));
|
|
62
68
|
}
|
|
63
69
|
|
|
@@ -74,11 +80,14 @@ export async function suggestChatTitle(
|
|
|
74
80
|
): Promise<string | null> {
|
|
75
81
|
const provider = await registry.oneShotProvider(preferred);
|
|
76
82
|
const args = provider?.oneShotArgs?.(buildTitlePrompt(input.prompt, input.answer)) ?? null;
|
|
83
|
+
|
|
77
84
|
if (provider === null || args === null) {
|
|
78
85
|
return null;
|
|
79
86
|
}
|
|
87
|
+
|
|
80
88
|
try {
|
|
81
89
|
const result = await runProcess([provider.command[0]!, ...args], { cwd: input.cwd, timeoutMs: TIMEOUT_MS, ...(env === undefined ? {} : { env }) });
|
|
90
|
+
|
|
82
91
|
return result.exitCode === 0 ? parseTitle(result.stdout) : null;
|
|
83
92
|
} catch {
|
|
84
93
|
return null;
|
|
@@ -56,7 +56,9 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
56
56
|
if (this.child) {
|
|
57
57
|
return Promise.resolve();
|
|
58
58
|
}
|
|
59
|
+
|
|
59
60
|
const { selection, runtimeMode, resume, folders, projectConfig } = this.launch;
|
|
61
|
+
|
|
60
62
|
const args = [
|
|
61
63
|
...this.launch.command,
|
|
62
64
|
...claudeArgs({
|
|
@@ -68,10 +70,13 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
68
70
|
...(projectConfig === undefined ? {} : { projectConfig })
|
|
69
71
|
})
|
|
70
72
|
];
|
|
73
|
+
|
|
71
74
|
if (this.launch.instructions !== null) {
|
|
72
75
|
args.push('--append-system-prompt', this.launch.instructions);
|
|
73
76
|
}
|
|
77
|
+
|
|
74
78
|
this.stdinClosed = false;
|
|
79
|
+
|
|
75
80
|
const child: ChatChild = new ChatChild({
|
|
76
81
|
command: args,
|
|
77
82
|
cwd: this.launch.cwd,
|
|
@@ -80,8 +85,10 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
80
85
|
onError: (error) => this.broken(error),
|
|
81
86
|
onExit: (exitCode, stderr) => this.handleExit(child, exitCode, stderr)
|
|
82
87
|
});
|
|
88
|
+
|
|
83
89
|
this.child = child;
|
|
84
90
|
void this.readLines(child);
|
|
91
|
+
|
|
85
92
|
return Promise.resolve();
|
|
86
93
|
}
|
|
87
94
|
|
|
@@ -96,10 +103,13 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
96
103
|
if (!this.child || !this.active || this.stdinClosed) {
|
|
97
104
|
return Promise.resolve(false);
|
|
98
105
|
}
|
|
106
|
+
|
|
99
107
|
const promptId = input.promptId ?? randomUUID();
|
|
108
|
+
|
|
100
109
|
return new Promise((resolve, reject) => {
|
|
101
110
|
this.steers.set(promptId, { resolve, reject });
|
|
102
111
|
this.protocol.beginSteer(promptId);
|
|
112
|
+
|
|
103
113
|
try {
|
|
104
114
|
this.write(this.userMessage(input, promptId));
|
|
105
115
|
} catch (error) {
|
|
@@ -118,6 +128,7 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
118
128
|
this.interrupted = true;
|
|
119
129
|
this.protocol.noteInterrupt();
|
|
120
130
|
this.write({ type: 'control_request', request_id: `interrupt-${Date.now()}`, request: { subtype: 'interrupt' } });
|
|
131
|
+
|
|
121
132
|
// A steer the CLI has not taken in survives a plain interrupt and would run after the Stop; it goes back to the queue instead.
|
|
122
133
|
for (const promptId of this.steers.keys()) {
|
|
123
134
|
this.write({
|
|
@@ -134,25 +145,31 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
134
145
|
|
|
135
146
|
respondApproval(requestId: string, decision: ApprovalDecision, message?: string): boolean {
|
|
136
147
|
const frame = this.protocol.approvalResponse(requestId, decision, message);
|
|
148
|
+
|
|
137
149
|
if (frame === null) {
|
|
138
150
|
return false;
|
|
139
151
|
}
|
|
152
|
+
|
|
140
153
|
return this.respond(requestId, frame);
|
|
141
154
|
}
|
|
142
155
|
|
|
143
156
|
respondQuestion(requestId: string, answers: Record<string, string>): boolean {
|
|
144
157
|
const frame = this.protocol.questionResponse(requestId, answers);
|
|
158
|
+
|
|
145
159
|
if (frame === null) {
|
|
146
160
|
return false;
|
|
147
161
|
}
|
|
162
|
+
|
|
148
163
|
return this.respond(requestId, frame);
|
|
149
164
|
}
|
|
150
165
|
|
|
151
166
|
declineRequest(requestId: string, message: string): boolean {
|
|
152
167
|
const frame = this.protocol.declineResponse(requestId, message);
|
|
168
|
+
|
|
153
169
|
if (frame === null) {
|
|
154
170
|
return false;
|
|
155
171
|
}
|
|
172
|
+
|
|
156
173
|
return this.respond(requestId, frame);
|
|
157
174
|
}
|
|
158
175
|
|
|
@@ -165,12 +182,14 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
165
182
|
this.rejectSteers('Claude stopped before acknowledging your message');
|
|
166
183
|
const child = this.child;
|
|
167
184
|
this.child = null;
|
|
185
|
+
|
|
168
186
|
return child?.end() ?? Promise.resolve();
|
|
169
187
|
}
|
|
170
188
|
|
|
171
189
|
private userMessage(input: TurnInput, promptId: string): Record<string, unknown> {
|
|
172
190
|
// What the CLI must see first (ultrathink), then the preamble, then what was typed.
|
|
173
191
|
const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
|
|
192
|
+
|
|
174
193
|
return buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId });
|
|
175
194
|
}
|
|
176
195
|
|
|
@@ -196,33 +215,43 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
196
215
|
if (this.child !== child || line.trim() === '') {
|
|
197
216
|
return;
|
|
198
217
|
}
|
|
218
|
+
|
|
199
219
|
let frame: unknown;
|
|
220
|
+
|
|
200
221
|
try {
|
|
201
222
|
frame = JSON.parse(line);
|
|
202
223
|
} catch {
|
|
203
224
|
return;
|
|
204
225
|
}
|
|
226
|
+
|
|
205
227
|
const echoed = typeof frame === 'object' && frame !== null ? (frame as Record<string, unknown>) : null;
|
|
228
|
+
|
|
206
229
|
if (echoed?.type === 'control_response' && this.cancelledSteer(echoed.response)) {
|
|
207
230
|
return;
|
|
208
231
|
}
|
|
232
|
+
|
|
209
233
|
if (echoed?.type === 'user' && typeof echoed.uuid === 'string' && echoed.parent_tool_use_id == null) {
|
|
210
234
|
this.steers.get(echoed.uuid)?.resolve(true);
|
|
211
235
|
this.steers.delete(echoed.uuid);
|
|
236
|
+
|
|
212
237
|
if (this.steers.size === 0) {
|
|
213
238
|
this.clearSteerTimer();
|
|
214
239
|
}
|
|
215
240
|
}
|
|
241
|
+
|
|
216
242
|
for (const event of this.protocol.handle(frame)) {
|
|
217
243
|
if (event.type === 'turn.done') {
|
|
218
244
|
this.active = false;
|
|
219
245
|
this.awaitSteers();
|
|
220
246
|
}
|
|
247
|
+
|
|
221
248
|
if (event.type === 'turn.done' && this.interrupted) {
|
|
222
249
|
this.interrupted = false;
|
|
223
250
|
this.host.onEvent({ ...event, state: 'aborted' });
|
|
251
|
+
|
|
224
252
|
continue;
|
|
225
253
|
}
|
|
254
|
+
|
|
226
255
|
this.host.onEvent(event);
|
|
227
256
|
}
|
|
228
257
|
}
|
|
@@ -240,18 +269,24 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
240
269
|
if (typeof response !== 'object' || response === null) {
|
|
241
270
|
return false;
|
|
242
271
|
}
|
|
272
|
+
|
|
243
273
|
const { request_id: requestId, response: body } = response as { request_id?: unknown; response?: unknown };
|
|
274
|
+
|
|
244
275
|
if (typeof requestId !== 'string' || !requestId.startsWith(CANCEL_STEER)) {
|
|
245
276
|
return false;
|
|
246
277
|
}
|
|
278
|
+
|
|
247
279
|
const promptId = requestId.slice(CANCEL_STEER.length);
|
|
280
|
+
|
|
248
281
|
if (typeof body === 'object' && body !== null && (body as { cancelled?: unknown }).cancelled === true) {
|
|
249
282
|
this.steers.get(promptId)?.resolve(false);
|
|
250
283
|
this.steers.delete(promptId);
|
|
284
|
+
|
|
251
285
|
if (this.steers.size === 0) {
|
|
252
286
|
this.clearSteerTimer();
|
|
253
287
|
}
|
|
254
288
|
}
|
|
289
|
+
|
|
255
290
|
return true;
|
|
256
291
|
}
|
|
257
292
|
|
|
@@ -259,13 +294,17 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
259
294
|
if (this.steers.size === 0 || this.steerTimer !== null) {
|
|
260
295
|
return;
|
|
261
296
|
}
|
|
297
|
+
|
|
262
298
|
this.steerTimer = setTimeout(() => {
|
|
263
299
|
this.steerTimer = null;
|
|
300
|
+
|
|
264
301
|
for (const pending of this.steers.values()) {
|
|
265
302
|
pending.reject(new Error('Claude ended its turn without taking your message'));
|
|
266
303
|
}
|
|
304
|
+
|
|
267
305
|
this.steers.clear();
|
|
268
306
|
}, STEER_GRACE_MS);
|
|
307
|
+
|
|
269
308
|
this.steerTimer.unref?.();
|
|
270
309
|
}
|
|
271
310
|
|
|
@@ -279,9 +318,11 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
279
318
|
private rejectSteers(reason: string): void {
|
|
280
319
|
this.active = false;
|
|
281
320
|
this.clearSteerTimer();
|
|
321
|
+
|
|
282
322
|
for (const pending of this.steers.values()) {
|
|
283
323
|
pending.reject(new Error(reason));
|
|
284
324
|
}
|
|
325
|
+
|
|
285
326
|
this.steers.clear();
|
|
286
327
|
}
|
|
287
328
|
|
|
@@ -294,6 +335,7 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
294
335
|
private failed(error: unknown): void {
|
|
295
336
|
this.rejectSteers(`Lost the connection to Claude: ${errorText(error)}`);
|
|
296
337
|
this.protocol.forgetPending();
|
|
338
|
+
|
|
297
339
|
try {
|
|
298
340
|
this.host.onEvent({ type: 'failed', message: `Lost the connection to Claude: ${errorText(error)}`, processAlive: this.running });
|
|
299
341
|
} catch (reportError) {
|
|
@@ -304,24 +346,29 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
304
346
|
private respond(requestId: string, frame: unknown): boolean {
|
|
305
347
|
try {
|
|
306
348
|
this.write(frame);
|
|
349
|
+
|
|
307
350
|
return true;
|
|
308
351
|
} catch (error) {
|
|
309
352
|
this.host.onEvent({ type: 'request.withdrawn', requestId });
|
|
310
353
|
this.failed(error);
|
|
354
|
+
|
|
311
355
|
return false;
|
|
312
356
|
}
|
|
313
357
|
}
|
|
314
358
|
|
|
315
359
|
private write(frame: unknown): void {
|
|
316
360
|
const child = this.child;
|
|
361
|
+
|
|
317
362
|
if (!child || this.stdinClosed) {
|
|
318
363
|
throw new Error('Claude stdin is closed');
|
|
319
364
|
}
|
|
365
|
+
|
|
320
366
|
try {
|
|
321
367
|
child.process.stdin.write(`${JSON.stringify(frame)}\n`);
|
|
322
368
|
child.process.stdin.flush();
|
|
323
369
|
} catch (error) {
|
|
324
370
|
this.stdinClosed = true;
|
|
371
|
+
|
|
325
372
|
throw error;
|
|
326
373
|
}
|
|
327
374
|
}
|
|
@@ -330,7 +377,9 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
330
377
|
if (!this.child || this.stdinClosed) {
|
|
331
378
|
return;
|
|
332
379
|
}
|
|
380
|
+
|
|
333
381
|
this.stdinClosed = true;
|
|
382
|
+
|
|
334
383
|
try {
|
|
335
384
|
this.child.process.stdin.end();
|
|
336
385
|
} catch {
|
|
@@ -342,6 +391,7 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
342
391
|
if (this.child !== child) {
|
|
343
392
|
return;
|
|
344
393
|
}
|
|
394
|
+
|
|
345
395
|
this.child = null;
|
|
346
396
|
this.rejectSteers('Claude stopped before it got your message');
|
|
347
397
|
this.protocol.forgetPending();
|