@adecore/agents 0.21.1 → 0.22.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.js +3 -8
- package/dist/chat/backend.d.ts +4 -0
- package/dist/chat/chat-core.d.ts +5 -2
- package/dist/chat/chat-core.js +8 -7
- package/dist/chat/chat-log.js +2 -2
- package/dist/chat/chat-session.d.ts +11 -2
- package/dist/chat/chat-session.js +141 -13
- package/dist/chat/chat-store.js +4 -7
- package/dist/chat/chat-title.js +4 -8
- package/dist/chat/claude-backend.d.ts +1 -0
- package/dist/chat/claude-backend.js +8 -6
- package/dist/chat/claude-protocol.d.ts +2 -0
- package/dist/chat/claude-protocol.js +21 -1
- package/dist/chat/claude-title.js +1 -1
- package/dist/chat/claude-transcript.d.ts +1 -0
- package/dist/chat/claude-transcript.js +1 -1
- package/dist/chat/codex-backend.d.ts +2 -2
- package/dist/chat/codex-backend.js +5 -8
- package/dist/chat/codex-protocol.js +1 -1
- package/dist/chat/codex-thread.js +4 -5
- package/dist/chat/composer-preferences.js +2 -3
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/external-sessions.d.ts +26 -0
- package/dist/chat/external-sessions.js +136 -0
- package/dist/chat/fake-claude.js +73 -0
- package/dist/chat/limit-resume.js +2 -7
- package/dist/chat/thread-search.d.ts +14 -0
- package/dist/chat/thread-search.js +64 -0
- package/dist/chat/visual-store.js +8 -6
- package/dist/context/refusal.js +3 -7
- package/dist/context/verb.js +1 -5
- package/dist/host/agent-host.js +2 -2
- package/dist/host/handlers.js +1 -2
- package/dist/host/wiring.js +1 -1
- package/dist/lineage.js +2 -3
- package/dist/messages/deliver-message.js +6 -11
- package/dist/messages/deliver-notice.js +8 -12
- package/dist/messages/notice-store.js +3 -5
- package/dist/outbox/outbox-worker.js +5 -2
- package/dist/providers/accounts/service.d.ts +2 -1
- package/dist/providers/accounts/service.js +11 -13
- package/dist/providers/catalog.js +2 -1
- package/dist/providers/claude.js +0 -1
- package/dist/providers/detect.js +2 -10
- package/dist/record-directory.js +0 -1
- package/dist/tasks/task-coordinator.d.ts +1 -0
- package/dist/tasks/task-coordinator.js +34 -34
- package/dist/tasks/task-store.js +1 -1
- package/dist/tasks/task-work.js +2 -2
- package/dist/tasks/waiting-child.d.ts +1 -0
- package/dist/tasks/waiting-child.js +8 -6
- package/dist/tasks/wake-parent.js +15 -10
- package/dist/tasks/wiring.js +32 -37
- package/dist/usage/aggregate.js +62 -46
- package/dist/usage/exchange.js +1 -1
- package/dist/usage/limits/monitor.d.ts +2 -2
- package/dist/usage/limits/monitor.js +2 -2
- package/dist/usage/limits/probe.js +1 -1
- package/dist/usage/pricing.js +1 -1
- package/dist/usage/readers/codex.d.ts +1 -0
- package/dist/usage/readers/codex.js +1 -1
- package/dist/usage/roots.js +1 -2
- package/dist/usage/scanner.js +7 -9
- package/dist/usage/usage-service.d.ts +1 -2
- package/dist/usage/usage-service.js +3 -6
- package/package.json +3 -3
- package/src/async.ts +3 -9
- package/src/chat/backend.ts +2 -0
- package/src/chat/chat-core.ts +8 -7
- package/src/chat/chat-log.ts +2 -2
- package/src/chat/chat-session.ts +148 -13
- package/src/chat/chat-store.ts +4 -8
- package/src/chat/chat-title.ts +4 -8
- package/src/chat/claude-backend.ts +9 -6
- package/src/chat/claude-protocol.ts +23 -1
- package/src/chat/claude-title.ts +1 -1
- package/src/chat/claude-transcript.ts +2 -2
- package/src/chat/codex-backend.ts +5 -8
- package/src/chat/codex-protocol.ts +1 -1
- package/src/chat/codex-thread.ts +6 -5
- package/src/chat/composer-preferences.ts +2 -3
- package/src/chat/errors.ts +2 -0
- package/src/chat/external-sessions.ts +177 -0
- package/src/chat/fake-claude.ts +75 -1
- package/src/chat/limit-resume.ts +2 -7
- package/src/chat/thread-search.ts +87 -0
- package/src/chat/thread.ts +1 -1
- package/src/chat/visual-store.ts +9 -6
- package/src/chat/wake-chat.ts +4 -1
- package/src/context/refusal.ts +3 -7
- package/src/context/verb.ts +1 -5
- package/src/host/agent-host.ts +2 -2
- package/src/host/handlers.ts +1 -2
- package/src/host/wiring.ts +1 -1
- package/src/lineage.ts +2 -3
- package/src/messages/deliver-message.ts +6 -11
- package/src/messages/deliver-notice.ts +8 -12
- package/src/messages/notice-store.ts +3 -5
- package/src/outbox/outbox-worker.ts +5 -2
- package/src/providers/accounts/service.ts +12 -13
- package/src/providers/accounts/variables.ts +1 -0
- package/src/providers/catalog.ts +2 -1
- package/src/providers/claude.ts +0 -1
- package/src/providers/detect.ts +2 -11
- package/src/record-directory.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/tasks/task-coordinator.ts +35 -33
- package/src/tasks/task-store.ts +1 -1
- package/src/tasks/task-work.ts +2 -2
- package/src/tasks/waiting-child.ts +10 -7
- package/src/tasks/wake-parent.ts +21 -16
- package/src/tasks/wiring.ts +33 -41
- package/src/usage/aggregate.ts +80 -50
- package/src/usage/exchange.ts +1 -1
- package/src/usage/limits/monitor.ts +4 -4
- package/src/usage/limits/probe.ts +2 -2
- package/src/usage/pricing.ts +1 -1
- package/src/usage/readers/codex.ts +1 -1
- package/src/usage/roots.ts +1 -2
- package/src/usage/scanner.ts +8 -10
- package/src/usage/usage-service.ts +5 -9
package/src/chat/chat-session.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ChatInfo,
|
|
8
8
|
ChatItem,
|
|
9
9
|
ChatQueuedMessage,
|
|
10
|
+
ChatQuoteSource,
|
|
10
11
|
ChatSkill,
|
|
11
12
|
ChatSubagentItem,
|
|
12
13
|
ChatTurnItem,
|
|
@@ -42,6 +43,8 @@ export interface TurnCheckpoints {
|
|
|
42
43
|
diff(cwd: string, tree: string): Promise<ChatCheckpointDiff | null>;
|
|
43
44
|
/* The diff of a turn that ended, with the tree of the folder it was taken against: where that turn left the files. */
|
|
44
45
|
settle(cwd: string, tree: string): Promise<{ diff: ChatCheckpointDiff; after: string } | null>;
|
|
46
|
+
/* The commit HEAD points at, or null when unborn or outside a repository. A host without it records no heads. */
|
|
47
|
+
head?(cwd: string): Promise<string | null>;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
/* What a host says in front of the next real prompt, beside the preambles a chat keeps itself. */
|
|
@@ -123,6 +126,7 @@ export interface ChatSendExtras {
|
|
|
123
126
|
mentions?: string[];
|
|
124
127
|
skills?: string[];
|
|
125
128
|
chats?: string[];
|
|
129
|
+
quoteSource?: ChatQuoteSource;
|
|
126
130
|
attachments?: ChatAttachment[];
|
|
127
131
|
}
|
|
128
132
|
|
|
@@ -193,6 +197,10 @@ export class ChatSession {
|
|
|
193
197
|
private naming = false;
|
|
194
198
|
// Set while the host goes down: a CLI dying with it must not end the turn a restart takes up again.
|
|
195
199
|
private frozen = false;
|
|
200
|
+
// Which CLI process (generation) announced each subagent of its own, by call id; only that one can stop it.
|
|
201
|
+
private readonly subagentOwners = new Map<string, number>();
|
|
202
|
+
// Subagents a stop_task went out for, as task id to call id, until their notification settles them.
|
|
203
|
+
private readonly stopping = new Map<string, string>();
|
|
196
204
|
// Background subagents a load found running whose transcript did not show an end yet; no CLI here will report them.
|
|
197
205
|
private readonly orphans = new Set<string>();
|
|
198
206
|
private pendingPreambles: readonly string[];
|
|
@@ -302,7 +310,8 @@ export class ChatSession {
|
|
|
302
310
|
/*
|
|
303
311
|
* Internal follow-ups wait for a new turn. User input goes through `sendInput`, which can steer native work.
|
|
304
312
|
*/
|
|
305
|
-
send(text: string,
|
|
313
|
+
send(text: string, sent: ChatSendExtras = {}): { queued: boolean; turnId: string } {
|
|
314
|
+
const extras = this.vetted(sent);
|
|
306
315
|
const turnId = newId('turn');
|
|
307
316
|
// Behind whatever still waits (a CLI that crashed leaves its queue), so everything goes out in the order it was sent.
|
|
308
317
|
if (this.busy || this.queue.length > 0 || this.replacementWaits()) {
|
|
@@ -318,6 +327,7 @@ export class ChatSession {
|
|
|
318
327
|
...(extras.mentions?.length ? { mentions: extras.mentions } : {}),
|
|
319
328
|
...(extras.skills?.length ? { skills: extras.skills } : {}),
|
|
320
329
|
...(extras.chats?.length ? { chats: extras.chats } : {}),
|
|
330
|
+
...(extras.quoteSource ? { quoteSource: extras.quoteSource } : {}),
|
|
321
331
|
...(extras.attachments?.length ? { attachments: extras.attachments } : {})
|
|
322
332
|
};
|
|
323
333
|
this.setQueue([...this.queue, message]);
|
|
@@ -424,11 +434,22 @@ export class ChatSession {
|
|
|
424
434
|
this.options.persist();
|
|
425
435
|
}
|
|
426
436
|
|
|
437
|
+
/* A quote whose source is no reply of this thread is sent as the plain text it is; the message itself is never refused for it. */
|
|
438
|
+
private vetted(extras: ChatSendExtras): ChatSendExtras {
|
|
439
|
+
if (extras.quoteSource === undefined || this.thread.get(extras.quoteSource.itemId)?.kind === 'assistant') {
|
|
440
|
+
return extras;
|
|
441
|
+
}
|
|
442
|
+
const plain = { ...extras };
|
|
443
|
+
delete plain.quoteSource;
|
|
444
|
+
return plain;
|
|
445
|
+
}
|
|
446
|
+
|
|
427
447
|
sendInput(text: string, extras: ChatSendExtras = {}): Promise<{ queued: boolean; turnId: string }> {
|
|
428
448
|
return this.submitInput(text, extras);
|
|
429
449
|
}
|
|
430
450
|
|
|
431
|
-
private async submitInput(text: string,
|
|
451
|
+
private async submitInput(text: string, sent: ChatSendExtras): Promise<{ queued: boolean; turnId: string }> {
|
|
452
|
+
const extras = this.vetted(sent);
|
|
432
453
|
const backend = this.backend;
|
|
433
454
|
const turnId = this.info.activeTurnId;
|
|
434
455
|
if (
|
|
@@ -472,7 +493,8 @@ export class ChatSession {
|
|
|
472
493
|
mentions: extras.mentions,
|
|
473
494
|
skills: extras.skills,
|
|
474
495
|
chats: references.ids,
|
|
475
|
-
attachments: extras.attachments
|
|
496
|
+
attachments: extras.attachments,
|
|
497
|
+
...(extras.quoteSource ? { quoteSource: extras.quoteSource } : {})
|
|
476
498
|
}),
|
|
477
499
|
this.thread.patchInfo({ queue: this.queue.filter((entry) => entry.id !== message.id) })
|
|
478
500
|
]);
|
|
@@ -562,7 +584,14 @@ export class ChatSession {
|
|
|
562
584
|
this.setQueue(rest);
|
|
563
585
|
this.dispatch(
|
|
564
586
|
next.text,
|
|
565
|
-
{
|
|
587
|
+
{
|
|
588
|
+
mentions: next.mentions,
|
|
589
|
+
skills: next.skills,
|
|
590
|
+
chats: next.chats,
|
|
591
|
+
attachments: next.attachments,
|
|
592
|
+
uiChoice: next.uiChoice,
|
|
593
|
+
quoteSource: next.quoteSource
|
|
594
|
+
},
|
|
566
595
|
next.turnId
|
|
567
596
|
);
|
|
568
597
|
}
|
|
@@ -1212,6 +1241,98 @@ export class ChatSession {
|
|
|
1212
1241
|
this.deliverNote({ noteId: `lost-background-${item.id}`, note: text, preamble: text });
|
|
1213
1242
|
}
|
|
1214
1243
|
|
|
1244
|
+
/*
|
|
1245
|
+
* Stops one subagent of the CLI's own. The process that announced it ends it with `stop_task` and the
|
|
1246
|
+
* parent goes on; a process that did not start it has nothing to address, so while a turn runs that
|
|
1247
|
+
* is refused, and otherwise the row is only marked.
|
|
1248
|
+
*/
|
|
1249
|
+
stopSubagent(toolUseId: string): 'stopping' | 'marked' {
|
|
1250
|
+
const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
1251
|
+
const agentId = row?.native?.agentId;
|
|
1252
|
+
const owned = this.subagentOwners.get(toolUseId) === this.generation;
|
|
1253
|
+
if (agentId !== undefined && owned && this.backend?.stopTask) {
|
|
1254
|
+
this.stopping.set(agentId, toolUseId);
|
|
1255
|
+
this.backend.stopTask(agentId);
|
|
1256
|
+
return 'stopping';
|
|
1257
|
+
}
|
|
1258
|
+
if (agentId !== undefined && !owned && this.thread.info.activeTurnId !== null) {
|
|
1259
|
+
throw new ChatError('subagent-stale', 'This sub-agent belongs to a process that has ended');
|
|
1260
|
+
}
|
|
1261
|
+
this.markSubagentStopped(toolUseId);
|
|
1262
|
+
return 'marked';
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
private trackSubagentOwner(generation: number, event: BackendEvent): void {
|
|
1266
|
+
if (event.type === 'task.started' && event.taskId) {
|
|
1267
|
+
this.subagentOwners.set(event.ref, generation);
|
|
1268
|
+
} else if (event.type === 'task.done') {
|
|
1269
|
+
this.noteStopped(event);
|
|
1270
|
+
if (event.ref !== null) {
|
|
1271
|
+
this.subagentOwners.delete(event.ref);
|
|
1272
|
+
}
|
|
1273
|
+
} else if (event.type === 'exit') {
|
|
1274
|
+
this.stopping.clear();
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/* The CLI would not stop it: with no turn running the row is only marked, and while one runs a note says it still goes on. */
|
|
1279
|
+
private refuseStop(agentId: string, message: string): void {
|
|
1280
|
+
const toolUseId = this.stopping.get(agentId);
|
|
1281
|
+
if (toolUseId === undefined) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
this.stopping.delete(agentId);
|
|
1285
|
+
const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
1286
|
+
if (row?.status !== 'running') {
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (this.thread.info.activeTurnId === null) {
|
|
1290
|
+
this.markSubagentStopped(toolUseId);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
this.emit([
|
|
1294
|
+
this.thread.upsert({
|
|
1295
|
+
id: `stop-refused-${toolUseId}-${Date.now()}`,
|
|
1296
|
+
kind: 'note',
|
|
1297
|
+
createdAt: Date.now(),
|
|
1298
|
+
turnId: null,
|
|
1299
|
+
level: 'warning',
|
|
1300
|
+
text: `"${row.description || row.subagentType || 'Sub-agent'}" could not be stopped: ${message}`
|
|
1301
|
+
})
|
|
1302
|
+
]);
|
|
1303
|
+
this.options.persist();
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/* The line under a subagent that `stop_task` ended, once the CLI reported it settled. */
|
|
1307
|
+
private noteStopped(event: Extract<BackendEvent, { type: 'task.done' }>): void {
|
|
1308
|
+
const stopped = [...this.stopping].find(([agentId, toolUseId]) => agentId === event.taskId || toolUseId === event.ref);
|
|
1309
|
+
if (stopped === undefined) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
this.stopping.delete(stopped[0]);
|
|
1313
|
+
// It finished on its own just before the stop arrived; the row already says so.
|
|
1314
|
+
if (event.ok) {
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const row = this.thread.find('subagent', (item) => item.toolUseId === stopped[1]);
|
|
1318
|
+
if (!row) {
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const now = Date.now();
|
|
1322
|
+
this.emit([
|
|
1323
|
+
...(row.status === 'running' ? [this.thread.upsert({ ...row, status: 'failed' as const, finishedAt: now })] : []),
|
|
1324
|
+
this.thread.upsert({
|
|
1325
|
+
id: `stopped-${row.toolUseId}`,
|
|
1326
|
+
kind: 'note',
|
|
1327
|
+
createdAt: now,
|
|
1328
|
+
turnId: null,
|
|
1329
|
+
level: 'info',
|
|
1330
|
+
text: `"${row.description || row.subagentType || 'Sub-agent'}" was stopped.`
|
|
1331
|
+
})
|
|
1332
|
+
]);
|
|
1333
|
+
this.options.persist();
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1215
1336
|
/*
|
|
1216
1337
|
* Marks a subagent of the CLI's own as stopped: `failed` with a note that says what it means, since
|
|
1217
1338
|
* no CLI stops one subagent on its own. Refused while a turn runs, which may still be waiting on it.
|
|
@@ -1325,7 +1446,8 @@ export class ChatSession {
|
|
|
1325
1446
|
skills,
|
|
1326
1447
|
chats,
|
|
1327
1448
|
attachments,
|
|
1328
|
-
...(extras.uiChoice ? { uiChoice: extras.uiChoice } : {})
|
|
1449
|
+
...(extras.uiChoice ? { uiChoice: extras.uiChoice } : {}),
|
|
1450
|
+
...(extras.quoteSource ? { quoteSource: extras.quoteSource } : {})
|
|
1329
1451
|
})
|
|
1330
1452
|
);
|
|
1331
1453
|
}
|
|
@@ -1362,14 +1484,14 @@ export class ChatSession {
|
|
|
1362
1484
|
if (!checkpoints) {
|
|
1363
1485
|
return Promise.resolve();
|
|
1364
1486
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
.then((tree) => {
|
|
1487
|
+
const cwd = this.thread.info.cwd;
|
|
1488
|
+
return Promise.all([checkpoints.take(cwd), checkpoints.head?.(cwd) ?? null])
|
|
1489
|
+
.then(([tree, head]) => {
|
|
1368
1490
|
const turn = this.thread.get(turnId);
|
|
1369
1491
|
if (tree === null || turn?.kind !== 'turn' || turn.state !== 'running' || this.info.activeTurnId !== turnId) {
|
|
1370
1492
|
return;
|
|
1371
1493
|
}
|
|
1372
|
-
this.emit([this.thread.upsert({ ...turn, checkpoint: tree })]);
|
|
1494
|
+
this.emit([this.thread.upsert({ ...turn, checkpoint: tree, ...(head === null ? {} : { checkpointHead: head }) })]);
|
|
1373
1495
|
})
|
|
1374
1496
|
.catch(() => undefined);
|
|
1375
1497
|
}
|
|
@@ -1380,14 +1502,22 @@ export class ChatSession {
|
|
|
1380
1502
|
if (turn?.kind !== 'turn' || turn.checkpoint === undefined || turn.checkpointDiff || !this.options.checkpoints) {
|
|
1381
1503
|
return;
|
|
1382
1504
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1505
|
+
const { checkpoints } = this.options;
|
|
1506
|
+
const cwd = this.thread.info.cwd;
|
|
1507
|
+
void Promise.all([checkpoints.settle(cwd, turn.checkpoint), checkpoints.head?.(cwd) ?? null])
|
|
1508
|
+
.then(([answer, head]) => {
|
|
1386
1509
|
const settled = this.thread.get(turnId);
|
|
1387
1510
|
if (answer === null || settled?.kind !== 'turn') {
|
|
1388
1511
|
return;
|
|
1389
1512
|
}
|
|
1390
|
-
this.emit([
|
|
1513
|
+
this.emit([
|
|
1514
|
+
this.thread.upsert({
|
|
1515
|
+
...settled,
|
|
1516
|
+
checkpointDiff: answer.diff,
|
|
1517
|
+
checkpointAfter: answer.after,
|
|
1518
|
+
...(head === null ? {} : { checkpointHeadAfter: head })
|
|
1519
|
+
})
|
|
1520
|
+
]);
|
|
1391
1521
|
this.options.persist();
|
|
1392
1522
|
})
|
|
1393
1523
|
.catch(() => undefined);
|
|
@@ -1631,6 +1761,10 @@ export class ChatSession {
|
|
|
1631
1761
|
this.applyTitle(event.title);
|
|
1632
1762
|
return;
|
|
1633
1763
|
}
|
|
1764
|
+
if (event.type === 'stop.refused') {
|
|
1765
|
+
this.refuseStop(event.taskId, event.message);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1634
1768
|
/*
|
|
1635
1769
|
* A CLI reports the window it was started on. A pick made since waits for the restart the next
|
|
1636
1770
|
* send does, so its report may not put the old size back under a meter that already reads the new one.
|
|
@@ -1687,6 +1821,7 @@ export class ChatSession {
|
|
|
1687
1821
|
this.awaitingBackgroundReport = true;
|
|
1688
1822
|
}
|
|
1689
1823
|
this.emit(projected);
|
|
1824
|
+
this.trackSubagentOwner(generation, event);
|
|
1690
1825
|
this.watchWorkflows(event);
|
|
1691
1826
|
if (isMainAgentOutput(event)) {
|
|
1692
1827
|
this.spokenTurnId = this.thread.info.activeTurnId;
|
package/src/chat/chat-store.ts
CHANGED
|
@@ -4,12 +4,12 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { ChatInfoSchema, ChatItemSchema, type ChatInfo, type ChatItem } from '@adecore/agent-contracts';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { isNotFound, writeAtomic, writeAtomicSync } from '../fs.ts';
|
|
7
|
-
import {
|
|
7
|
+
import { recordFileName } from '../record-directory.ts';
|
|
8
8
|
import { migrateInlineAttachments, type AttachmentStore } from './attachment-store.ts';
|
|
9
|
+
import { isBookmarkFileName } from './bookmark-store.ts';
|
|
9
10
|
import { parseLog, type ChatLogLine } from './chat-log.ts';
|
|
10
11
|
import { ChatThread } from './thread.ts';
|
|
11
12
|
import { isVisualFileName } from './visual-store.ts';
|
|
12
|
-
import { recordFileName } from '../record-directory.ts';
|
|
13
13
|
|
|
14
14
|
// zod strips what it does not know inside the info and the items, so a file written by an older build
|
|
15
15
|
// (with `interactionMode`, say) still parses and loses only the dropped field. A record without `seq`
|
|
@@ -30,7 +30,7 @@ const OWN_KEYS = new Set(['info', 'items', 'seq', 'resetSeq', 'preambles', 'uiAc
|
|
|
30
30
|
/* What a host keeps in a chat's record beside the thread; it never reaches the wire. */
|
|
31
31
|
export type ChatRecordExtras = Record<string, unknown>;
|
|
32
32
|
|
|
33
|
-
function
|
|
33
|
+
function withoutOwnKeys(record: Record<string, unknown>): ChatRecordExtras {
|
|
34
34
|
return Object.fromEntries(Object.entries(record).filter(([key]) => !OWN_KEYS.has(key)));
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -90,10 +90,6 @@ function liftUiAccess(record: unknown): unknown {
|
|
|
90
90
|
return Object.keys(uiAccess).length === 0 ? record : { ...record, uiAccess };
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function withoutOwnKeys(extras: ChatRecordExtras): ChatRecordExtras {
|
|
94
|
-
return Object.fromEntries(Object.entries(extras).filter(([key]) => !OWN_KEYS.has(key)));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
93
|
export interface ChatStoreOptions {
|
|
98
94
|
attachments?: AttachmentStore | null;
|
|
99
95
|
// Files a host keeps beside a record under names that also end in .json (a chat's plans), which are no chats.
|
|
@@ -214,7 +210,7 @@ export class ChatStore {
|
|
|
214
210
|
resetSeq,
|
|
215
211
|
preambles: snapshot.preambles ?? [],
|
|
216
212
|
uiAccess: snapshot.uiAccess ?? {},
|
|
217
|
-
extras:
|
|
213
|
+
extras: withoutOwnKeys(snapshot),
|
|
218
214
|
lines
|
|
219
215
|
};
|
|
220
216
|
}
|
package/src/chat/chat-title.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentKind } from '@adecore/agent-contracts';
|
|
2
|
-
import { cleanTitle } from '../title-file.ts';
|
|
3
2
|
import type { ProviderRegistry } from '../providers/registry.ts';
|
|
4
3
|
import { runProcess } from '../run-process.ts';
|
|
4
|
+
import { cleanTitle } from '../title-file.ts';
|
|
5
5
|
|
|
6
6
|
// Enough of a conversation to name it; the rest of a long prompt or answer says nothing a title needs.
|
|
7
7
|
const MAX_PROMPT_CHARS = 2000;
|
|
@@ -19,10 +19,7 @@ function cap(text: string, limit: number): string {
|
|
|
19
19
|
return text.length <= limit ? text : `${text.slice(0, limit)}...`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/*
|
|
23
|
-
* What the CLI is asked. JSON again, as for a commit message: a model left to write freely opens
|
|
24
|
-
* with a sentence, and that sentence would become the name of the node.
|
|
25
|
-
*/
|
|
22
|
+
/* JSON, since a model left to write freely opens with a sentence, and that sentence would become the name. */
|
|
26
23
|
export function buildTitlePrompt(prompt: string, answer: string): string {
|
|
27
24
|
return [
|
|
28
25
|
'Write a short title for the conversation below, as a person would name it in a list of chats.',
|
|
@@ -42,9 +39,8 @@ export function buildTitlePrompt(prompt: string, answer: string): string {
|
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
/*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* is better than a stray line of it.
|
|
42
|
+
* Strict on purpose: a run that holds no `{"title": "..."}` object printed something other than a
|
|
43
|
+
* title, and a derived name is better than a stray line of it.
|
|
48
44
|
*/
|
|
49
45
|
export function parseTitle(output: string): string | null {
|
|
50
46
|
const start = [...output.matchAll(/\{\s*"title"/g)].at(-1)?.index;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { errorText } from '../error-text.ts';
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { errorText } from '../error-text.ts';
|
|
3
3
|
import { claudeArgs, claudeEnv, promptPrefix } from '../providers/claude.ts';
|
|
4
4
|
import type { ApprovalDecision, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
|
|
5
5
|
import { ChatChild } from './chat-process.ts';
|
|
@@ -71,12 +71,10 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
sendTurn(input: TurnInput): void {
|
|
74
|
-
// The prefix is what the CLI must see first (ultrathink), then the note about the links, then what was typed.
|
|
75
|
-
const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
|
|
76
74
|
const promptId = input.promptId ?? randomUUID();
|
|
77
75
|
this.active = true;
|
|
78
76
|
this.protocol.beginPrompt(promptId);
|
|
79
|
-
this.write(
|
|
77
|
+
this.write(this.userMessage(input, promptId));
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
steerTurn(input: TurnInput): Promise<boolean> {
|
|
@@ -88,8 +86,7 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
88
86
|
this.steers.set(promptId, { resolve, reject });
|
|
89
87
|
this.protocol.beginSteer(promptId);
|
|
90
88
|
try {
|
|
91
|
-
|
|
92
|
-
this.write(buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId }));
|
|
89
|
+
this.write(this.userMessage(input, promptId));
|
|
93
90
|
} catch (error) {
|
|
94
91
|
this.steers.delete(promptId);
|
|
95
92
|
reject(error instanceof Error ? error : new Error(errorText(error)));
|
|
@@ -147,6 +144,12 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
147
144
|
return child?.end() ?? Promise.resolve();
|
|
148
145
|
}
|
|
149
146
|
|
|
147
|
+
private userMessage(input: TurnInput, promptId: string): Record<string, unknown> {
|
|
148
|
+
// What the CLI must see first (ultrathink), then the preamble, then what was typed.
|
|
149
|
+
const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
|
|
150
|
+
return buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId });
|
|
151
|
+
}
|
|
152
|
+
|
|
150
153
|
private async readLines(child: ChatChild): Promise<void> {
|
|
151
154
|
const reader = child.process.stdout.getReader();
|
|
152
155
|
const decoder = new TextDecoder();
|
|
@@ -147,6 +147,9 @@ export class ClaudeProtocol {
|
|
|
147
147
|
private apiError: string | null = null;
|
|
148
148
|
private readonly refusedWindows = new Map<string, number | null>();
|
|
149
149
|
|
|
150
|
+
// The task each `stop_task` request named, so an error answer finds it again.
|
|
151
|
+
private readonly stopRequests = new Map<string, string>();
|
|
152
|
+
|
|
150
153
|
private readonly steerIds = new Set<string>();
|
|
151
154
|
|
|
152
155
|
beginSteer(promptId: string): void {
|
|
@@ -185,6 +188,9 @@ export class ClaudeProtocol {
|
|
|
185
188
|
case 'control_request':
|
|
186
189
|
this.handleControlRequest(frame, events);
|
|
187
190
|
break;
|
|
191
|
+
case 'control_response':
|
|
192
|
+
this.handleControlResponse(frame, events);
|
|
193
|
+
break;
|
|
188
194
|
case 'control_cancel_request': {
|
|
189
195
|
const requestId = str(frame.request_id);
|
|
190
196
|
if (requestId) {
|
|
@@ -445,7 +451,23 @@ export class ClaudeProtocol {
|
|
|
445
451
|
|
|
446
452
|
/* The control request that ends one task the CLI runs beside its turns. */
|
|
447
453
|
stopTaskRequest(taskId: string): unknown {
|
|
448
|
-
|
|
454
|
+
const requestId = `stop-task-${taskId}-${Date.now()}`;
|
|
455
|
+
this.stopRequests.set(requestId, taskId);
|
|
456
|
+
return { type: 'control_request', request_id: requestId, request: { subtype: 'stop_task', task_id: taskId } };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/* Only a refused `stop_task` matters; every other answer is the CLI saying it took the request. */
|
|
460
|
+
private handleControlResponse(frame: Frame, events: BackendEvent[]): void {
|
|
461
|
+
const response = isRecord(frame.response) ? frame.response : null;
|
|
462
|
+
const requestId = str(response?.request_id);
|
|
463
|
+
const taskId = requestId === null ? undefined : this.stopRequests.get(requestId);
|
|
464
|
+
if (response === null || requestId === null || taskId === undefined) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
this.stopRequests.delete(requestId);
|
|
468
|
+
if (response.subtype === 'error') {
|
|
469
|
+
events.push({ type: 'stop.refused', taskId, message: str(response.error) ?? 'The CLI refused to stop it' });
|
|
470
|
+
}
|
|
449
471
|
}
|
|
450
472
|
|
|
451
473
|
private handleStreamEvent(frame: Frame, events: BackendEvent[]): void {
|
package/src/chat/claude-title.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { usageRoots } from '../usage/roots.ts';
|
|
5
4
|
import { cleanTitle, readLines } from '../title-file.ts';
|
|
5
|
+
import { usageRoots } from '../usage/roots.ts';
|
|
6
6
|
|
|
7
7
|
interface Progress {
|
|
8
8
|
offset: number;
|
|
@@ -5,8 +5,8 @@ import type { ChatHistoryResult, ChatItem } from '@adecore/agent-contracts';
|
|
|
5
5
|
import { readLines } from '../title-file.ts';
|
|
6
6
|
import { ClaudeProtocol } from './claude-protocol.ts';
|
|
7
7
|
import { readingThread, settledReading } from './subagent-projection.ts';
|
|
8
|
-
import type { ChatThread } from './thread.ts';
|
|
9
8
|
import type { ThreadProjector } from './projector.ts';
|
|
9
|
+
import type { ChatThread } from './thread.ts';
|
|
10
10
|
|
|
11
11
|
const CHUNK_BYTES = 4 * 1024 * 1024;
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@ function str(value: unknown): string | null {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/* The words of a user line that somebody said, or null for a line that carries tool results. */
|
|
22
|
-
function spokenText(content: unknown): string | null {
|
|
22
|
+
export function spokenText(content: unknown): string | null {
|
|
23
23
|
if (typeof content === 'string') {
|
|
24
24
|
return content;
|
|
25
25
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { accessSync, constants } from 'node:fs';
|
|
2
2
|
import { attachmentImageMime, type ChatSkill } from '@adecore/agent-contracts';
|
|
3
|
+
import { errorText } from '../error-text.ts';
|
|
3
4
|
import { codexServiceTier, codexThreadOptions } from '../providers/codex.ts';
|
|
4
5
|
import type { ApprovalDecision, BackendEvent, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
|
|
5
6
|
import { CodexProtocol } from './codex-protocol.ts';
|
|
6
|
-
import { attachmentNote } from './input.ts';
|
|
7
7
|
import { CodexTransport, DEFAULT_CODEX_CLIENT, type CodexClientInfo, type CodexFrame } from './codex-transport.ts';
|
|
8
|
-
import {
|
|
8
|
+
import { attachmentNote } from './input.ts';
|
|
9
9
|
|
|
10
10
|
function textInput(text: string) {
|
|
11
11
|
return [{ type: 'text', text, text_elements: [] }];
|
|
@@ -46,18 +46,15 @@ export interface CodexBackendOptions {
|
|
|
46
46
|
*/
|
|
47
47
|
export class CodexBackend implements ChatBackend {
|
|
48
48
|
readonly acknowledgesTurns = true;
|
|
49
|
-
private promptId: string | null = null;
|
|
50
49
|
private readonly launch: BackendLaunch;
|
|
51
|
-
private imageWrites: Promise<void> | null = null;
|
|
52
50
|
private readonly host: BackendHost;
|
|
53
51
|
private readonly protocol: CodexProtocol;
|
|
54
52
|
private readonly client: CodexClientInfo;
|
|
53
|
+
private promptId: string | null = null;
|
|
54
|
+
private imageWrites: Promise<void> | null = null;
|
|
55
55
|
private transport: CodexTransport | null = null;
|
|
56
56
|
private threadId = '';
|
|
57
|
-
|
|
58
|
-
* Codex 0.154 ignores developer instructions on `thread/resume` (measured), so a thread started
|
|
59
|
-
* before anything was linked would never hear of its links; the first prompt of a resumed process says so instead.
|
|
60
|
-
*/
|
|
57
|
+
// Codex 0.154 ignores developer instructions on `thread/resume` (measured), so the first prompt of a resumed process carries them.
|
|
61
58
|
private contextPending: string | null = null;
|
|
62
59
|
private imageInputSupported: boolean | null = null;
|
|
63
60
|
/* A turn asked for that Codex has not named yet; `turn/interrupt` needs that name. */
|
|
@@ -172,7 +172,7 @@ function elicitationInput(meta: Frame): Frame {
|
|
|
172
172
|
return input;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
//
|
|
175
|
+
// The latest reset of every spent window; one without a known reset leaves resuming to a person.
|
|
176
176
|
function spentUntil(windows: readonly UsageWindow[]): number | null {
|
|
177
177
|
const spent = windows.filter((window) => window.used >= 1);
|
|
178
178
|
if (spent.length === 0 || spent.some((window) => window.resetsAt === null)) {
|
package/src/chat/codex-thread.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ChatItem } from '@adecore/agent-contracts';
|
|
2
|
+
import { errorText } from '../error-text.ts';
|
|
2
3
|
import type { SpawnChatProcess } from './chat-process.ts';
|
|
3
4
|
import { CodexProtocol } from './codex-protocol.ts';
|
|
4
5
|
import { CodexTransport, DEFAULT_CODEX_CLIENT, type CodexClientInfo } from './codex-transport.ts';
|
|
5
6
|
import { ChatError } from './errors.ts';
|
|
6
7
|
import { readingThread, settledReading } from './subagent-projection.ts';
|
|
7
|
-
import { errorText } from '../error-text.ts';
|
|
8
8
|
|
|
9
9
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
10
10
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -116,14 +116,15 @@ async function nthTurnId(transport: CodexTransport, threadId: string, turns: num
|
|
|
116
116
|
let seen = 0;
|
|
117
117
|
let cursor: string | null = null;
|
|
118
118
|
do {
|
|
119
|
-
const page =
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
const page = parseThreadItemsPage(
|
|
120
|
+
await transport.request('thread/turns/list', { threadId, limit: 100, sortDirection: 'asc', ...(cursor === null ? {} : { cursor }) })
|
|
121
|
+
);
|
|
122
|
+
for (const turn of page.entries) {
|
|
122
123
|
if (++seen === turns) {
|
|
123
124
|
return typeof turn.id === 'string' ? turn.id : null;
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
|
-
cursor =
|
|
127
|
+
cursor = page.nextCursor;
|
|
127
128
|
} while (cursor !== null);
|
|
128
129
|
return null;
|
|
129
130
|
}
|
|
@@ -15,9 +15,8 @@ interface Held {
|
|
|
15
15
|
|
|
16
16
|
/*
|
|
17
17
|
* What each connected client says a new chat starts with, for the chats the host starts with no
|
|
18
|
-
* client mounting them. Held per
|
|
19
|
-
*
|
|
20
|
-
* host's own defaults.
|
|
18
|
+
* client mounting them. Held per client only, so with nobody connected a chat gets the host's own
|
|
19
|
+
* defaults rather than the pick of a person who left.
|
|
21
20
|
*/
|
|
22
21
|
export class ComposerPreferences {
|
|
23
22
|
private readonly byClient = new Map<string, Held>();
|
package/src/chat/errors.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type ChatErrorCode =
|
|
|
12
12
|
| 'chat-unsupported'
|
|
13
13
|
| 'invalid-attachments'
|
|
14
14
|
| 'subagent-not-found'
|
|
15
|
+
// The sub-agent was started by a CLI process that has ended, so no process here can stop it.
|
|
16
|
+
| 'subagent-stale'
|
|
15
17
|
| 'task-not-found'
|
|
16
18
|
// What reading or forking a CLI's own conversation is refused with.
|
|
17
19
|
| 'transcript-missing'
|