@adecore/agents 0.23.1 → 0.24.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/README.md +3 -3
- package/dist/chat/attachment-store.js +4 -5
- package/dist/chat/backend.d.ts +6 -1
- package/dist/chat/background-work.d.ts +1 -1
- package/dist/chat/background-work.js +2 -1
- package/dist/chat/chat-core.d.ts +53 -161
- package/dist/chat/chat-core.js +146 -112
- package/dist/chat/chat-log.js +3 -3
- package/dist/chat/chat-process.d.ts +21 -0
- package/dist/chat/chat-process.js +109 -9
- package/dist/chat/chat-session.d.ts +10 -17
- package/dist/chat/chat-session.js +141 -182
- package/dist/chat/chat-store.d.ts +1 -0
- package/dist/chat/chat-store.js +105 -1
- package/dist/chat/claude-backend.d.ts +4 -0
- package/dist/chat/claude-backend.js +51 -27
- package/dist/chat/claude-protocol.d.ts +12 -0
- package/dist/chat/claude-protocol.js +137 -7
- package/dist/chat/claude-title.d.ts +1 -3
- package/dist/chat/claude-title.js +11 -39
- package/dist/chat/claude-transcript.d.ts +1 -0
- package/dist/chat/claude-transcript.js +36 -17
- package/dist/chat/codex-backend.d.ts +21 -1
- package/dist/chat/codex-backend.js +168 -10
- package/dist/chat/codex-protocol.d.ts +2 -0
- package/dist/chat/codex-protocol.js +37 -3
- package/dist/chat/codex-thread.js +1 -3
- package/dist/chat/codex-transport.d.ts +3 -0
- package/dist/chat/codex-transport.js +38 -35
- package/dist/chat/context-breakdown.js +2 -2
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/external-sessions.js +2 -2
- package/dist/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/dist/chat/index.d.ts +29 -0
- package/dist/chat/index.js +23 -0
- package/dist/chat/projector.d.ts +2 -1
- package/dist/chat/projector.js +12 -3
- package/dist/chat/request-summary.js +1 -4
- package/dist/chat/subagent-settlement.js +1 -3
- package/dist/chat/thread.d.ts +3 -0
- package/dist/chat/thread.js +20 -3
- package/dist/chat/ui-queries.d.ts +2 -0
- package/dist/chat/ui-queries.js +40 -5
- package/dist/chat/ui-replies.d.ts +33 -0
- package/dist/chat/ui-replies.js +152 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +3 -0
- package/dist/coordination/index.d.ts +15 -0
- package/dist/coordination/index.js +16 -0
- package/dist/host/handlers.js +8 -0
- package/dist/host/index.d.ts +7 -0
- package/dist/host/index.js +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/claude.js +3 -1
- package/dist/providers/codex.js +3 -1
- package/dist/providers/index.d.ts +14 -0
- package/dist/providers/index.js +12 -0
- package/dist/run-process.js +54 -14
- package/dist/skills.d.ts +1 -0
- package/dist/skills.js +4 -0
- package/dist/tasks/task-coordinator.d.ts +4 -0
- package/dist/tasks/task-coordinator.js +16 -2
- package/dist/tasks/wiring.js +7 -1
- package/dist/{providers/accounts/test-accounts.d.ts → testing/accounts.d.ts} +2 -2
- package/dist/{providers/accounts/test-accounts.js → testing/accounts.js} +3 -3
- package/dist/testing/bin/fake-claude.d.ts +1 -0
- package/dist/testing/bin/fake-claude.js +6 -0
- package/dist/testing/bin/fake-codex.d.ts +1 -0
- package/dist/testing/bin/fake-codex.js +6 -0
- package/dist/{chat → testing}/fake-claude.d.ts +1 -1
- package/dist/{chat → testing}/fake-claude.js +0 -4
- package/dist/{chat → testing}/fake-cli.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.js +70 -8
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +7 -0
- package/dist/{outbox → testing}/manual-clock.d.ts +1 -1
- package/dist/{watch-test-helpers.d.ts → testing/watch.d.ts} +1 -1
- package/dist/usage/exchange.d.ts +5 -3
- package/dist/usage/exchange.js +25 -16
- package/dist/usage/index-file.d.ts +1 -0
- package/dist/usage/index-file.js +20 -2
- package/dist/usage/index.d.ts +9 -0
- package/dist/usage/index.js +6 -0
- package/dist/usage/limits/normalize.js +1 -3
- package/dist/usage/pricing.d.ts +5 -3
- package/dist/usage/pricing.js +28 -19
- package/dist/usage/scanner.js +39 -11
- package/dist/usage/usage-service.js +8 -3
- package/package.json +72 -11
- package/src/chat/attachment-store.ts +4 -6
- package/src/chat/backend.ts +8 -0
- package/src/chat/background-work.ts +2 -2
- package/src/chat/chat-core.ts +221 -143
- package/src/chat/chat-log.ts +3 -3
- package/src/chat/chat-process.ts +128 -8
- package/src/chat/chat-session.ts +153 -204
- package/src/chat/chat-store.ts +104 -1
- package/src/chat/claude-backend.ts +58 -26
- package/src/chat/claude-protocol.ts +147 -8
- package/src/chat/claude-title.ts +11 -39
- package/src/chat/claude-transcript.ts +38 -18
- package/src/chat/codex-backend.ts +182 -20
- package/src/chat/codex-protocol.ts +40 -5
- package/src/chat/codex-thread.ts +1 -4
- package/src/chat/codex-transport.ts +45 -35
- package/src/chat/context-breakdown.ts +2 -2
- package/src/chat/errors.ts +1 -0
- package/src/chat/external-sessions.ts +2 -2
- package/src/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/src/chat/index.ts +49 -0
- package/src/chat/projector.ts +13 -4
- package/src/chat/request-summary.ts +8 -5
- package/src/chat/subagent-settlement.ts +1 -4
- package/src/chat/thread.ts +22 -3
- package/src/chat/ui-queries.ts +46 -5
- package/src/chat/ui-replies.ts +187 -0
- package/src/context/index.ts +26 -0
- package/src/coordination/index.ts +67 -0
- package/src/host/handlers.ts +8 -0
- package/src/host/index.ts +8 -0
- package/src/index.ts +20 -0
- package/src/providers/accounts/status.ts +1 -4
- package/src/providers/claude.ts +3 -1
- package/src/providers/codex.ts +3 -1
- package/src/providers/index.ts +15 -0
- package/src/run-process.ts +56 -16
- package/src/skills.ts +5 -0
- package/src/tasks/task-coordinator.ts +22 -2
- package/src/tasks/wiring.ts +7 -1
- package/src/{providers/accounts/test-accounts.ts → testing/accounts.ts} +4 -4
- package/src/testing/bin/fake-claude.ts +7 -0
- package/src/testing/bin/fake-codex.ts +7 -0
- package/src/{chat → testing}/fake-claude.ts +1 -5
- package/src/{chat → testing}/fake-cli.ts +1 -1
- package/src/{chat → testing}/fake-codex.ts +75 -6
- package/src/testing/index.ts +7 -0
- package/src/{outbox → testing}/manual-clock.ts +1 -1
- package/src/{watch-test-helpers.ts → testing/watch.ts} +1 -1
- package/src/usage/exchange.ts +31 -20
- package/src/usage/index-file.ts +23 -2
- package/src/usage/index.ts +10 -0
- package/src/usage/limits/normalize.ts +1 -4
- package/src/usage/pricing.ts +34 -23
- package/src/usage/scanner.ts +48 -11
- package/src/usage/usage-service.ts +8 -3
- /package/dist/{chat → testing}/fake-cli.js +0 -0
- /package/dist/{outbox → testing}/manual-clock.js +0 -0
- /package/dist/{watch-test-helpers.js → testing/watch.js} +0 -0
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { resolveUiChoice, UI_FENCE_LANGUAGE, UiFailure
|
|
4
|
-
import { UiStream } from '@adecore/intelligent-ui/stream';
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { HOST_TASK_ORIGIN, notResumedNote } from '@adecore/agent-contracts';
|
|
3
|
+
import { resolveUiChoice, UI_FENCE_LANGUAGE, UiFailure } from '@adecore/intelligent-ui';
|
|
5
4
|
import { commandLabel, isBackgroundWork, reportsOnBackgroundWork, runningInBackground } from './background-work.js';
|
|
6
5
|
import { ChatError } from './errors.js';
|
|
7
6
|
import { limitedTurn, limitResumeAt, limitResumeWake } from './limit-resume.js';
|
|
8
7
|
import { isMainAgentOutput, ThreadProjector } from './projector.js';
|
|
9
8
|
import { readableAssistantText } from './readable-text.js';
|
|
10
9
|
import { ChatThread } from './thread.js';
|
|
10
|
+
import { UiReplies } from './ui-replies.js';
|
|
11
11
|
import { errorText } from '../error-text.js';
|
|
12
12
|
import { systemClock } from '../outbox/outbox-worker.js';
|
|
13
13
|
// Claude Code names a session about six seconds after its first prompt, and a first turn can run for minutes.
|
|
14
14
|
const TITLE_RECHECK_MS = 10_000;
|
|
15
|
-
// Longer than any fence line or the name of a tag a scan looks for, so a match never falls between two deltas.
|
|
16
|
-
const UI_SCAN_OVERLAP = 64;
|
|
17
15
|
/* One per limited turn, so a second fork of the same turn writes no second note. */
|
|
18
16
|
function continuedNoteId(turnId) {
|
|
19
17
|
return `continued-${turnId}`;
|
|
@@ -79,37 +77,32 @@ export class ChatSession {
|
|
|
79
77
|
pendingPreambles;
|
|
80
78
|
submission = null;
|
|
81
79
|
steering = new Set();
|
|
82
|
-
uiStreams = new Map();
|
|
83
|
-
uiCompiled = new Map();
|
|
84
80
|
capturedUiAccess;
|
|
85
|
-
|
|
86
|
-
uiScanned = new Map();
|
|
87
|
-
uiReferencing = new Set();
|
|
88
|
-
uiAttachments = new Map();
|
|
89
|
-
uiFenceLanguage;
|
|
81
|
+
uiReplies;
|
|
90
82
|
constructor(options) {
|
|
91
83
|
this.options = options;
|
|
92
|
-
this.uiFenceLanguage = options.uiFenceLanguage ?? UI_FENCE_LANGUAGE;
|
|
93
84
|
this.pendingPreambles = options.preambles ?? [];
|
|
94
85
|
this.capturedUiAccess = new Map(Object.entries(options.uiAccess ?? {}));
|
|
95
86
|
this.thread = new ChatThread(options.info, options.items);
|
|
96
87
|
this.projector = new ThreadProjector(this.thread, { providerName: options.provider.name });
|
|
88
|
+
this.uiReplies = new UiReplies({
|
|
89
|
+
thread: this.thread,
|
|
90
|
+
fenceLanguage: options.uiFenceLanguage ?? UI_FENCE_LANGUAGE,
|
|
91
|
+
clock: options.clock ?? systemClock,
|
|
92
|
+
...(options.ui ? { host: options.ui } : {}),
|
|
93
|
+
emit: (event) => options.emit(event),
|
|
94
|
+
repair: (note) => {
|
|
95
|
+
if (!this.pendingPreambles.includes(note)) {
|
|
96
|
+
this.pendingPreambles = [...this.pendingPreambles, note];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
97
100
|
}
|
|
98
101
|
get uiPreviews() {
|
|
99
|
-
|
|
100
|
-
for (const [itemId, preview] of this.uiCompiled) {
|
|
101
|
-
const item = this.thread.get(itemId);
|
|
102
|
-
if (item?.kind === 'assistant' && item.streaming && item.text.length === preview.textLength) {
|
|
103
|
-
events.push({ type: 'delta', itemId, text: '', ui: preview.blocks, textLength: preview.textLength });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return events;
|
|
102
|
+
return this.uiReplies.previews();
|
|
107
103
|
}
|
|
108
104
|
previewItems(items) {
|
|
109
|
-
return
|
|
110
|
-
const preview = this.uiCompiled.get(item.id);
|
|
111
|
-
return item.kind === 'assistant' && preview?.textLength === item.text.length ? { ...item, ui: preview.blocks } : item;
|
|
112
|
-
});
|
|
105
|
+
return this.uiReplies.previewItems(items);
|
|
113
106
|
}
|
|
114
107
|
get id() {
|
|
115
108
|
return this.thread.info.chatId;
|
|
@@ -131,7 +124,35 @@ export class ChatSession {
|
|
|
131
124
|
get busy() {
|
|
132
125
|
const turnId = this.thread.info.activeTurnId;
|
|
133
126
|
const turn = turnId ? this.thread.get(turnId) : undefined;
|
|
134
|
-
|
|
127
|
+
// A turn the CLI opened toward a goal is the goal's own: a message waits for it like for any turn of the person's.
|
|
128
|
+
return turnId !== null && (turn?.kind !== 'turn' || turn.origin !== 'agent' || this.goalWorks());
|
|
129
|
+
}
|
|
130
|
+
/* Whether the CLI works toward a goal, which goes on in turns of its own until the goal is met, paused or cleared. */
|
|
131
|
+
goalWorks() {
|
|
132
|
+
return this.thread.info.goal?.status === 'active';
|
|
133
|
+
}
|
|
134
|
+
/*
|
|
135
|
+
* Pauses, resumes or clears the chat's goal. A CLI with calls for it takes them at once, even while a goal
|
|
136
|
+
* turn runs; resuming an idle goal and clearing a goal of a CLI without them go out as `/goal` in a turn of
|
|
137
|
+
* their own, which a goal turn still running is in the way of.
|
|
138
|
+
*/
|
|
139
|
+
async goal(action) {
|
|
140
|
+
const supported = this.options.provider.capabilities.goals;
|
|
141
|
+
if (supported === undefined || (supported === 'command' && action !== 'clear')) {
|
|
142
|
+
throw new ChatError('chat-unsupported', `${this.options.provider.name} cannot ${action} a goal`);
|
|
143
|
+
}
|
|
144
|
+
if (this.thread.info.goal === undefined) {
|
|
145
|
+
throw new ChatError('goal-not-found', 'This chat has no goal');
|
|
146
|
+
}
|
|
147
|
+
const backend = this.backend;
|
|
148
|
+
if (supported === 'native' && backend?.goal && backend.running && (action !== 'resume' || this.busy)) {
|
|
149
|
+
await backend.goal(action);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (this.busy) {
|
|
153
|
+
throw new ChatError('chat-busy', 'Stop the turn that works toward the goal first');
|
|
154
|
+
}
|
|
155
|
+
this.send(`/goal ${action}`);
|
|
135
156
|
}
|
|
136
157
|
/* The next process uses these settings; live work keeps the process it started in. */
|
|
137
158
|
configure(patch) {
|
|
@@ -303,6 +324,8 @@ export class ChatSession {
|
|
|
303
324
|
if (extras.delivery !== 'steer' ||
|
|
304
325
|
this.info.queuePaused ||
|
|
305
326
|
text.startsWith('/') ||
|
|
327
|
+
// A goal runs as turns of its own; a message waits for the one running instead of joining it.
|
|
328
|
+
this.goalWorks() ||
|
|
306
329
|
!this.busy ||
|
|
307
330
|
!turnId ||
|
|
308
331
|
!backend?.steerTurn ||
|
|
@@ -435,6 +458,10 @@ export class ChatSession {
|
|
|
435
458
|
dispatch(text, extras, requestedTurnId) {
|
|
436
459
|
this.pauseQueue(false);
|
|
437
460
|
this.settleAgentTurn();
|
|
461
|
+
// A goal that was met has said what it had to; the next message of the person leaves it behind.
|
|
462
|
+
if (this.thread.info.goal?.status === 'complete') {
|
|
463
|
+
this.emit([this.thread.patchInfo({ goal: undefined })]);
|
|
464
|
+
}
|
|
438
465
|
const { preamble, note } = this.contextNote(text);
|
|
439
466
|
// A slash command must stay the first thing the CLI reads, as `contextNote` keeps it.
|
|
440
467
|
const references = text.startsWith('/') || !this.options.references ? { ids: [], note: null } : this.options.references(extras.chats);
|
|
@@ -552,6 +579,20 @@ export class ChatSession {
|
|
|
552
579
|
this.emit([this.thread.upsert({ ...item, state: 'dismissed' }), this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId))]);
|
|
553
580
|
return true;
|
|
554
581
|
}
|
|
582
|
+
/*
|
|
583
|
+
* Marks the chat as waiting on a person for something the host asks beside the CLI, such as a
|
|
584
|
+
* card a verb holds its call for. The chat reads `needs-you` until every key is released.
|
|
585
|
+
*/
|
|
586
|
+
holdForPerson(key) {
|
|
587
|
+
if (this.thread.hold(key)) {
|
|
588
|
+
this.emit([this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId))]);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
releaseHold(key) {
|
|
592
|
+
if (this.thread.release(key) && this.thread.info.status === 'needs-you') {
|
|
593
|
+
this.emit([this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId))]);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
555
596
|
/*
|
|
556
597
|
* Starts the chat over: the CLI goes, and the next send starts one without a session to resume.
|
|
557
598
|
* The thread resets at once; the returned promise lets the host wait for pending attachment
|
|
@@ -588,6 +629,7 @@ export class ChatSession {
|
|
|
588
629
|
queuePaused: false,
|
|
589
630
|
background: [],
|
|
590
631
|
delegating: undefined,
|
|
632
|
+
goal: undefined,
|
|
591
633
|
effectiveRuntimeMode: undefined,
|
|
592
634
|
permissionMode: undefined,
|
|
593
635
|
slashCommands: [],
|
|
@@ -957,7 +999,7 @@ export class ChatSession {
|
|
|
957
999
|
for (const item of this.thread.list()) {
|
|
958
1000
|
const settled = item.kind === 'turn' && item.state === 'running'
|
|
959
1001
|
? { ...item, state: 'aborted', endedAt: now }
|
|
960
|
-
: item.kind === 'subagent' && item.origin !==
|
|
1002
|
+
: item.kind === 'subagent' && item.origin !== HOST_TASK_ORIGIN && item.status === 'running'
|
|
961
1003
|
? { ...item, status: 'failed', finishedAt: now }
|
|
962
1004
|
: settleStoredItem(item, null);
|
|
963
1005
|
if (settled !== item) {
|
|
@@ -979,9 +1021,29 @@ export class ChatSession {
|
|
|
979
1021
|
this.emit(events);
|
|
980
1022
|
this.options.persist();
|
|
981
1023
|
}
|
|
1024
|
+
/*
|
|
1025
|
+
* Ends the CLI and keeps the conversation: the next message starts a fresh process that resumes it,
|
|
1026
|
+
* and with it the skills, plugins and servers the CLI reads at its start. Refused while a turn runs or
|
|
1027
|
+
* the process still holds work, a request or a background command, since those would end with it.
|
|
1028
|
+
*/
|
|
1029
|
+
restart() {
|
|
1030
|
+
if (this.thread.info.activeTurnId !== null || this.processInUse()) {
|
|
1031
|
+
throw new ChatError('chat-busy', `Chat ${this.id} is still working; stop it before restarting its agent session`);
|
|
1032
|
+
}
|
|
1033
|
+
this.clearStartWarning();
|
|
1034
|
+
this.clearWorkflowWarnings();
|
|
1035
|
+
const backend = this.backend;
|
|
1036
|
+
this.backend = null;
|
|
1037
|
+
this.starting = null;
|
|
1038
|
+
this.launchedSelection = null;
|
|
1039
|
+
// What the init frame of the process that goes announced; a skill added since would be filtered out by it.
|
|
1040
|
+
this.emit([this.thread.patchInfo({ running: false, effectiveRuntimeMode: undefined, permissionMode: undefined, skills: undefined })]);
|
|
1041
|
+
this.options.persist();
|
|
1042
|
+
return backend?.dispose() ?? Promise.resolve();
|
|
1043
|
+
}
|
|
982
1044
|
/* Ends the process and stops listening to it, as the host goes down; the thread stays as it is. */
|
|
983
1045
|
freeze() {
|
|
984
|
-
this.
|
|
1046
|
+
this.uiReplies.clear();
|
|
985
1047
|
this.clearStartWarning();
|
|
986
1048
|
this.clearWorkflowWarnings();
|
|
987
1049
|
this.frozen = true;
|
|
@@ -989,7 +1051,7 @@ export class ChatSession {
|
|
|
989
1051
|
}
|
|
990
1052
|
/* Ends the CLI and everything it started; settles once it exited or was sent the SIGKILL. */
|
|
991
1053
|
dispose() {
|
|
992
|
-
this.
|
|
1054
|
+
this.uiReplies.clear();
|
|
993
1055
|
this.clearStartWarning();
|
|
994
1056
|
this.clearWorkflowWarnings();
|
|
995
1057
|
if (this.titleTimer !== null) {
|
|
@@ -1146,7 +1208,9 @@ export class ChatSession {
|
|
|
1146
1208
|
}
|
|
1147
1209
|
/* Marks every running subagent of the CLI's own as stopped, for a person stopping the turn together with them. */
|
|
1148
1210
|
markSubagentsStopped() {
|
|
1149
|
-
this.markStopped(this.thread
|
|
1211
|
+
this.markStopped(this.thread
|
|
1212
|
+
.list()
|
|
1213
|
+
.filter((item) => item.kind === 'subagent' && item.status === 'running' && item.origin !== HOST_TASK_ORIGIN));
|
|
1150
1214
|
}
|
|
1151
1215
|
markStopped(rows) {
|
|
1152
1216
|
if (rows.length === 0) {
|
|
@@ -1424,7 +1488,7 @@ export class ChatSession {
|
|
|
1424
1488
|
return runningInBackground(this.thread.list()).length > 0 || (this.info.background?.length ?? 0) > 0;
|
|
1425
1489
|
}
|
|
1426
1490
|
processInUse() {
|
|
1427
|
-
return this.hasNativeWork() || this.thread.pending().length > 0 || this.awaitingBackgroundReport;
|
|
1491
|
+
return this.hasNativeWork() || this.thread.pending().length > 0 || this.awaitingBackgroundReport || this.goalWorks();
|
|
1428
1492
|
}
|
|
1429
1493
|
replacementWaits() {
|
|
1430
1494
|
return this.requiresReplacement() && (this.info.activeTurnId !== null || this.processInUse());
|
|
@@ -1469,6 +1533,7 @@ export class ChatSession {
|
|
|
1469
1533
|
saveGeneratedImage: this.options.saveGeneratedImage,
|
|
1470
1534
|
instructions: this.options.instructions?.() ?? null,
|
|
1471
1535
|
resumeNote: this.options.resumeNote?.() ?? null,
|
|
1536
|
+
...(info.goal === undefined ? {} : { goal: info.goal }),
|
|
1472
1537
|
...(folders.length > 0 ? { folders } : {}),
|
|
1473
1538
|
...(this.options.spawn ? { spawn: this.options.spawn } : {})
|
|
1474
1539
|
};
|
|
@@ -1594,7 +1659,7 @@ export class ChatSession {
|
|
|
1594
1659
|
}
|
|
1595
1660
|
this.emit(projected);
|
|
1596
1661
|
this.trackSubagentOwner(generation, event);
|
|
1597
|
-
this.watchWorkflows(event);
|
|
1662
|
+
this.watchWorkflows(generation, event);
|
|
1598
1663
|
if (isMainAgentOutput(event)) {
|
|
1599
1664
|
this.spokenTurnId = this.thread.info.activeTurnId;
|
|
1600
1665
|
}
|
|
@@ -1660,48 +1725,54 @@ export class ChatSession {
|
|
|
1660
1725
|
}
|
|
1661
1726
|
this.workflowWarnings.clear();
|
|
1662
1727
|
}
|
|
1663
|
-
watchWorkflows(event) {
|
|
1664
|
-
if (
|
|
1728
|
+
watchWorkflows(generation, event) {
|
|
1729
|
+
if (event.type === 'tool.output' || event.type === 'tool.progress') {
|
|
1730
|
+
// These come by the thousand, and only the call they name can be a workflow they move.
|
|
1731
|
+
const item = this.projector.itemOf(generation, event.ref);
|
|
1732
|
+
if (item?.kind === 'tool' && item.name === 'Workflow') {
|
|
1733
|
+
this.watchWorkflow(item, true);
|
|
1734
|
+
}
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
if (!['tool.started', 'tool.done', 'workflow.progress', 'task.started', 'task.done', 'exit', 'failed'].includes(event.type)) {
|
|
1665
1738
|
return;
|
|
1666
1739
|
}
|
|
1667
|
-
const clock = this.options.clock ?? systemClock;
|
|
1668
1740
|
const ref = 'ref' in event ? event.ref : null;
|
|
1669
1741
|
for (const item of this.thread.list()) {
|
|
1670
|
-
if (item.kind
|
|
1671
|
-
|
|
1742
|
+
if (item.kind === 'tool' && item.name === 'Workflow') {
|
|
1743
|
+
this.watchWorkflow(item, ref === item.toolUseId && ['workflow.progress', 'tool.started', 'tool.done', 'task.started'].includes(event.type));
|
|
1672
1744
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
}
|
|
1682
|
-
if (item.state !== 'running' || (!progress && existing !== undefined) || (!progress && item.workflow?.stalledAt !== undefined)) {
|
|
1683
|
-
continue;
|
|
1684
|
-
}
|
|
1685
|
-
if (progress) {
|
|
1686
|
-
this.emit([
|
|
1687
|
-
this.thread.upsert({
|
|
1688
|
-
...item,
|
|
1689
|
-
workflow: { name: null, phases: [], agents: [], ...item.workflow, lastProgressAt: clock.now(), stalledAt: undefined }
|
|
1690
|
-
})
|
|
1691
|
-
]);
|
|
1692
|
-
}
|
|
1693
|
-
this.workflowWarnings.set(item.id, clock.setTimeout(() => {
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
watchWorkflow(item, progress) {
|
|
1748
|
+
const clock = this.options.clock ?? systemClock;
|
|
1749
|
+
const existing = this.workflowWarnings.get(item.id);
|
|
1750
|
+
if (item.state !== 'running' || progress) {
|
|
1751
|
+
if (existing !== undefined) {
|
|
1752
|
+
clock.clearTimeout(existing);
|
|
1694
1753
|
this.workflowWarnings.delete(item.id);
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
this.emit([
|
|
1700
|
-
this.thread.upsert({ ...current, workflow: { name: null, phases: [], agents: [], ...current.workflow, stalledAt: clock.now() } })
|
|
1701
|
-
]);
|
|
1702
|
-
this.options.persist();
|
|
1703
|
-
}, 5 * 60_000));
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
if (item.state !== 'running' || (!progress && existing !== undefined) || (!progress && item.workflow?.stalledAt !== undefined)) {
|
|
1757
|
+
return;
|
|
1704
1758
|
}
|
|
1759
|
+
if (progress) {
|
|
1760
|
+
this.emit([
|
|
1761
|
+
this.thread.upsert({
|
|
1762
|
+
...item,
|
|
1763
|
+
workflow: { name: null, phases: [], agents: [], ...item.workflow, lastProgressAt: clock.now(), stalledAt: undefined }
|
|
1764
|
+
})
|
|
1765
|
+
]);
|
|
1766
|
+
}
|
|
1767
|
+
this.workflowWarnings.set(item.id, clock.setTimeout(() => {
|
|
1768
|
+
this.workflowWarnings.delete(item.id);
|
|
1769
|
+
const current = this.thread.get(item.id);
|
|
1770
|
+
if (this.frozen || current?.kind !== 'tool' || current.state !== 'running') {
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
this.emit([this.thread.upsert({ ...current, workflow: { name: null, phases: [], agents: [], ...current.workflow, stalledAt: clock.now() } })]);
|
|
1774
|
+
this.options.persist();
|
|
1775
|
+
}, 5 * 60_000));
|
|
1705
1776
|
}
|
|
1706
1777
|
/* Whether the CLI that reports was started on a pick standing for the same window as the chat's own. */
|
|
1707
1778
|
reportsCurrentWindow() {
|
|
@@ -1802,121 +1873,9 @@ export class ChatSession {
|
|
|
1802
1873
|
}
|
|
1803
1874
|
emit(events) {
|
|
1804
1875
|
const delegating = this.delegatingChange(events);
|
|
1805
|
-
for (
|
|
1806
|
-
|
|
1807
|
-
this.clearUi();
|
|
1808
|
-
}
|
|
1809
|
-
if (event.type === 'item' && event.item.kind === 'tool') {
|
|
1810
|
-
this.uiAttachments.delete(event.item.turnId);
|
|
1811
|
-
}
|
|
1812
|
-
const item = event.type === 'item' ? event.item : event.type === 'delta' ? this.thread.get(event.itemId) : null;
|
|
1813
|
-
if (item?.kind !== 'assistant' || item.parentToolUseId) {
|
|
1814
|
-
this.options.emit(event);
|
|
1815
|
-
continue;
|
|
1816
|
-
}
|
|
1817
|
-
const tail = this.unscannedUiText(item, event.type === 'item');
|
|
1818
|
-
if (uiMayReferenceHost(tail)) {
|
|
1819
|
-
this.uiReferencing.add(item.id);
|
|
1820
|
-
}
|
|
1821
|
-
let stream = this.uiStreams.get(item.id);
|
|
1822
|
-
if (!stream && !uiHasFence(tail, this.uiFenceLanguage)) {
|
|
1823
|
-
this.forgetUiScan(item);
|
|
1824
|
-
this.options.emit(event);
|
|
1825
|
-
continue;
|
|
1826
|
-
}
|
|
1827
|
-
if (!stream) {
|
|
1828
|
-
stream = new UiStream({
|
|
1829
|
-
querySchemas: this.options.ui?.schemas,
|
|
1830
|
-
fenceLanguage: this.uiFenceLanguage,
|
|
1831
|
-
id: item.id,
|
|
1832
|
-
clock: this.options.clock ?? systemClock,
|
|
1833
|
-
emit: (preview) => {
|
|
1834
|
-
this.uiCompiled.set(item.id, preview);
|
|
1835
|
-
this.options.emit({ type: 'delta', itemId: item.id, text: '', ui: preview.blocks, textLength: preview.textLength });
|
|
1836
|
-
}
|
|
1837
|
-
});
|
|
1838
|
-
this.uiStreams.set(item.id, stream);
|
|
1839
|
-
}
|
|
1840
|
-
if (this.uiReferencing.has(item.id)) {
|
|
1841
|
-
this.options.ui?.observe(item, false);
|
|
1842
|
-
}
|
|
1843
|
-
this.forgetUiScan(item);
|
|
1844
|
-
const latestAttachment = this.latestUiAttachment(item.turnId);
|
|
1845
|
-
if (event.type === 'item' && !item.streaming) {
|
|
1846
|
-
const ui = stream
|
|
1847
|
-
.finish(item.text, latestAttachment)
|
|
1848
|
-
.map((block) => ({ ...block, revision: createHash('sha256').update(JSON.stringify(block)).digest('hex') }));
|
|
1849
|
-
this.uiStreams.delete(item.id);
|
|
1850
|
-
this.uiCompiled.delete(item.id);
|
|
1851
|
-
const diagnostics = [...new Set(ui.flatMap((block) => block.diagnostics.map((diagnostic) => diagnostic.message)))].slice(0, 6);
|
|
1852
|
-
if (diagnostics.length > 0) {
|
|
1853
|
-
const repair = `Your last ${this.uiFenceLanguage} block had these diagnostics: ${diagnostics.join(' ')} Use the catalog's supported props and components.`.slice(0, 1200);
|
|
1854
|
-
if (!this.pendingPreambles.includes(repair)) {
|
|
1855
|
-
this.pendingPreambles = [...this.pendingPreambles, repair];
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
const current = this.thread.get(item.id);
|
|
1859
|
-
const completed = { ...item, ui, ...(current?.kind === 'assistant' && current.uiQueries ? { uiQueries: current.uiQueries } : {}) };
|
|
1860
|
-
event = this.thread.upsert(completed);
|
|
1861
|
-
this.options.emit(event);
|
|
1862
|
-
this.options.ui?.observe(completed, true);
|
|
1863
|
-
}
|
|
1864
|
-
else {
|
|
1865
|
-
this.options.emit(event);
|
|
1866
|
-
stream.update(item.text, latestAttachment);
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
/*
|
|
1871
|
-
* The text a scan has not covered yet, from the start of the line the last scan ended in, since a fence
|
|
1872
|
-
* is one line; a long line is cut short, behind a character no fence or tag can follow.
|
|
1873
|
-
*/
|
|
1874
|
-
unscannedUiText(item, whole) {
|
|
1875
|
-
const scanned = whole ? 0 : (this.uiScanned.get(item.id) ?? 0);
|
|
1876
|
-
this.uiScanned.set(item.id, item.text.length);
|
|
1877
|
-
if (scanned === 0 || scanned > item.text.length) {
|
|
1878
|
-
return item.text;
|
|
1879
|
-
}
|
|
1880
|
-
const line = item.text.lastIndexOf('\n', scanned - 1) + 1;
|
|
1881
|
-
const from = Math.max(line, scanned - UI_SCAN_OVERLAP);
|
|
1882
|
-
return from === line ? item.text.slice(from) : `\0${item.text.slice(from)}`;
|
|
1883
|
-
}
|
|
1884
|
-
forgetUiScan(item) {
|
|
1885
|
-
if (!item.streaming) {
|
|
1886
|
-
this.uiScanned.delete(item.id);
|
|
1887
|
-
this.uiReferencing.delete(item.id);
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
/* Looked up once per turn, and again only after one of its tool calls changed. */
|
|
1891
|
-
latestUiAttachment(turnId) {
|
|
1892
|
-
if (this.uiAttachments.has(turnId)) {
|
|
1893
|
-
return this.uiAttachments.get(turnId);
|
|
1894
|
-
}
|
|
1895
|
-
const attachment = this.findUiAttachment(turnId);
|
|
1896
|
-
this.uiAttachments.set(turnId, attachment);
|
|
1897
|
-
return attachment;
|
|
1898
|
-
}
|
|
1899
|
-
findUiAttachment(turnId) {
|
|
1900
|
-
for (const item of this.thread.list().toReversed()) {
|
|
1901
|
-
if (item.kind !== 'tool' || item.turnId !== turnId || item.name !== 'ImageGeneration' || item.state !== 'done') {
|
|
1902
|
-
continue;
|
|
1903
|
-
}
|
|
1904
|
-
const input = GeneratedImageInputSchema.safeParse(item.input);
|
|
1905
|
-
if (input.success && input.data.attachment?.mime.startsWith('image/')) {
|
|
1906
|
-
return input.data.attachment.id;
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
return undefined;
|
|
1910
|
-
}
|
|
1911
|
-
clearUi() {
|
|
1912
|
-
for (const stream of this.uiStreams.values()) {
|
|
1913
|
-
stream.dispose();
|
|
1876
|
+
for (const event of delegating === null ? events : [...events, delegating]) {
|
|
1877
|
+
this.uiReplies.pass(event);
|
|
1914
1878
|
}
|
|
1915
|
-
this.uiStreams.clear();
|
|
1916
|
-
this.uiCompiled.clear();
|
|
1917
|
-
this.uiScanned.clear();
|
|
1918
|
-
this.uiReferencing.clear();
|
|
1919
|
-
this.uiAttachments.clear();
|
|
1920
1879
|
}
|
|
1921
1880
|
/* Said on the info as well as by the rows, since a client with the thread closed sees only the info. */
|
|
1922
1881
|
delegatingChange(events) {
|
|
@@ -33,6 +33,7 @@ export declare class ChatStore {
|
|
|
33
33
|
writeSync(chatId: string, info: ChatInfo, items: ChatItem[], at?: ChatSeq, preambles?: readonly string[], extras?: ChatRecordExtras, uiAccess?: Readonly<Record<string, unknown>>): void;
|
|
34
34
|
read(chatId: string): Promise<ChatRecord | null>;
|
|
35
35
|
unreadable(chatId: string): Promise<boolean>;
|
|
36
|
+
settled(chatId: string): Promise<boolean>;
|
|
36
37
|
private readSnapshot;
|
|
37
38
|
delete(chatId: string): Promise<void>;
|
|
38
39
|
}
|
package/dist/chat/chat-store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdirSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, open, readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { ChatInfoSchema, ChatItemSchema } from '@adecore/agent-contracts';
|
|
5
5
|
import { z } from 'zod';
|
|
@@ -30,6 +30,7 @@ function withoutOwnKeys(record) {
|
|
|
30
30
|
function logName(chatId) {
|
|
31
31
|
return `${encodeURIComponent(chatId)}.log`;
|
|
32
32
|
}
|
|
33
|
+
// `settled` reads the info from the head of the record, so it stays the first key.
|
|
33
34
|
function recordBody(info, items, at, preambles, extras, uiAccess) {
|
|
34
35
|
return JSON.stringify({
|
|
35
36
|
info,
|
|
@@ -154,6 +155,18 @@ export class ChatStore {
|
|
|
154
155
|
async unreadable(chatId) {
|
|
155
156
|
return (await this.readSnapshot(chatId)) === UNREADABLE;
|
|
156
157
|
}
|
|
158
|
+
/*
|
|
159
|
+
* Whether the chat has neither a turn in flight nor a resume after a limit, by the info its record
|
|
160
|
+
* starts with and the last one its log wrote, without parsing the thread. False whenever that
|
|
161
|
+
* cannot be told so cheaply, which leaves the decision to `read`.
|
|
162
|
+
*/
|
|
163
|
+
async settled(chatId) {
|
|
164
|
+
if (!settledInfo(await leadingInfo(join(this.dir, recordFileName(chatId))))) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const last = lastLoggedInfo((await readOrNull(this.logPath(chatId))) ?? '');
|
|
168
|
+
return last === undefined || settledInfo(last);
|
|
169
|
+
}
|
|
157
170
|
async readSnapshot(chatId) {
|
|
158
171
|
const raw = await readOrNull(join(this.dir, recordFileName(chatId)));
|
|
159
172
|
if (raw === null) {
|
|
@@ -182,6 +195,97 @@ export class ChatStore {
|
|
|
182
195
|
}
|
|
183
196
|
}
|
|
184
197
|
const UNREADABLE = Symbol('unreadable');
|
|
198
|
+
const RECORD_HEAD = Buffer.from('{"info":{');
|
|
199
|
+
const HEAD_CHUNK_BYTES = 16384;
|
|
200
|
+
const QUOTE = 0x22;
|
|
201
|
+
const BACKSLASH = 0x5c;
|
|
202
|
+
const OPEN_BRACE = 0x7b;
|
|
203
|
+
const CLOSE_BRACE = 0x7d;
|
|
204
|
+
/*
|
|
205
|
+
* The info a record starts with, read only as far as it goes; undefined for a record that is missing,
|
|
206
|
+
* starts otherwise (an older or hand-edited one) or ends before its info does. Scanning bytes is safe
|
|
207
|
+
* in UTF-8, where no byte of a multibyte character equals a quote, a backslash or a brace.
|
|
208
|
+
*/
|
|
209
|
+
async function leadingInfo(path) {
|
|
210
|
+
let handle;
|
|
211
|
+
try {
|
|
212
|
+
handle = await open(path, 'r');
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
if (isNotFound(e)) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
throw e;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
let head = Buffer.alloc(0);
|
|
222
|
+
let at = RECORD_HEAD.length;
|
|
223
|
+
let depth = 1;
|
|
224
|
+
let inString = false;
|
|
225
|
+
let escaped = false;
|
|
226
|
+
const chunk = Buffer.alloc(HEAD_CHUNK_BYTES);
|
|
227
|
+
let { bytesRead } = await handle.read(chunk, 0, chunk.length, 0);
|
|
228
|
+
while (bytesRead > 0) {
|
|
229
|
+
head = Buffer.concat([head, chunk.subarray(0, bytesRead)]);
|
|
230
|
+
if (!head.subarray(0, RECORD_HEAD.length).equals(RECORD_HEAD.subarray(0, Math.min(head.length, RECORD_HEAD.length)))) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
for (; at < head.length; at++) {
|
|
234
|
+
const byte = head[at];
|
|
235
|
+
if (escaped) {
|
|
236
|
+
escaped = false;
|
|
237
|
+
}
|
|
238
|
+
else if (inString) {
|
|
239
|
+
escaped = byte === BACKSLASH;
|
|
240
|
+
inString = byte !== QUOTE;
|
|
241
|
+
}
|
|
242
|
+
else if (byte === QUOTE) {
|
|
243
|
+
inString = true;
|
|
244
|
+
}
|
|
245
|
+
else if (byte === OPEN_BRACE) {
|
|
246
|
+
depth += 1;
|
|
247
|
+
}
|
|
248
|
+
else if (byte === CLOSE_BRACE) {
|
|
249
|
+
depth -= 1;
|
|
250
|
+
if (depth === 0) {
|
|
251
|
+
return parseOrUndefined(head.subarray(RECORD_HEAD.length - 1, at + 1).toString('utf8'));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
({ bytesRead } = await handle.read(chunk, 0, chunk.length, head.length));
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
await handle.close();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/* The info of the last info or reset line in a log, unvalidated; undefined when no line carries one. */
|
|
264
|
+
function lastLoggedInfo(raw) {
|
|
265
|
+
const lines = raw.split('\n');
|
|
266
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
267
|
+
// Most lines are items and deltas, which are larger and never carry this key.
|
|
268
|
+
if (!lines[i].includes('"info"')) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const event = parseOrUndefined(lines[i])?.event;
|
|
272
|
+
if (event?.type === 'info' || event?.type === 'reset') {
|
|
273
|
+
return event.info;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
function settledInfo(info) {
|
|
279
|
+
return typeof info === 'object' && info !== null && info.activeTurnId === null && !('resumeAt' in info);
|
|
280
|
+
}
|
|
281
|
+
function parseOrUndefined(text) {
|
|
282
|
+
try {
|
|
283
|
+
return JSON.parse(text);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
185
289
|
async function exists(path) {
|
|
186
290
|
try {
|
|
187
291
|
await stat(path);
|
|
@@ -12,6 +12,7 @@ export declare class ClaudeBackend implements ChatBackend {
|
|
|
12
12
|
private stdinClosed;
|
|
13
13
|
private interrupted;
|
|
14
14
|
private readonly steers;
|
|
15
|
+
private steerTimer;
|
|
15
16
|
private active;
|
|
16
17
|
constructor(launch: BackendLaunch, host: BackendHost, options?: ClaudeBackendOptions);
|
|
17
18
|
get running(): boolean;
|
|
@@ -31,7 +32,10 @@ export declare class ClaudeBackend implements ChatBackend {
|
|
|
31
32
|
private readLines;
|
|
32
33
|
private handleLine;
|
|
33
34
|
private readLine;
|
|
35
|
+
private awaitSteers;
|
|
36
|
+
private clearSteerTimer;
|
|
34
37
|
private rejectSteers;
|
|
38
|
+
private broken;
|
|
35
39
|
private failed;
|
|
36
40
|
private respond;
|
|
37
41
|
private write;
|