@adecore/agents 0.23.0 → 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,9 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { errorText } from '../error-text.js';
|
|
3
3
|
import { claudeArgs, claudeEnv, promptPrefix } from '../providers/claude.js';
|
|
4
|
-
import { ChatChild } from './chat-process.js';
|
|
4
|
+
import { ChatChild, MAX_LINE_LENGTH, readStreamLines } from './chat-process.js';
|
|
5
5
|
import { ClaudeProtocol } from './claude-protocol.js';
|
|
6
6
|
import { buildUserMessage } from './input.js';
|
|
7
|
+
// Claude may echo a steer only once a slow tool returns, so a running turn sets no deadline; after the turn ended, a message
|
|
8
|
+
// it still holds opens the next turn at once or never arrives.
|
|
9
|
+
const STEER_GRACE_MS = 30_000;
|
|
7
10
|
/*
|
|
8
11
|
* One `claude -p` process on the stream-json protocol: flags from the selection and the modes,
|
|
9
12
|
* frames in over stdout, frames out over stdin. The process stays alive between turns; the session
|
|
@@ -15,17 +18,19 @@ export class ClaudeBackend {
|
|
|
15
18
|
launch;
|
|
16
19
|
host;
|
|
17
20
|
allowedTools;
|
|
18
|
-
protocol
|
|
21
|
+
protocol;
|
|
19
22
|
child = null;
|
|
20
23
|
stdinClosed = false;
|
|
21
24
|
// The person asked to stop; the result frame that follows closes the turn as aborted.
|
|
22
25
|
interrupted = false;
|
|
23
26
|
steers = new Map();
|
|
27
|
+
steerTimer = null;
|
|
24
28
|
active = false;
|
|
25
29
|
constructor(launch, host, options = {}) {
|
|
26
30
|
this.launch = launch;
|
|
27
31
|
this.host = host;
|
|
28
32
|
this.allowedTools = options.allowedTools ?? [];
|
|
33
|
+
this.protocol = new ClaudeProtocol(launch.goal ?? null);
|
|
29
34
|
}
|
|
30
35
|
get running() {
|
|
31
36
|
return this.child !== null;
|
|
@@ -51,7 +56,7 @@ export class ClaudeBackend {
|
|
|
51
56
|
cwd: this.launch.cwd,
|
|
52
57
|
env: { ...this.launch.env, ...claudeEnv(selection) },
|
|
53
58
|
...(this.launch.spawn ? { spawn: this.launch.spawn } : {}),
|
|
54
|
-
onError: (error) => this.
|
|
59
|
+
onError: (error) => this.broken(error),
|
|
55
60
|
onExit: (exitCode, stderr) => this.handleExit(child, exitCode, stderr)
|
|
56
61
|
});
|
|
57
62
|
this.child = child;
|
|
@@ -87,6 +92,7 @@ export class ClaudeBackend {
|
|
|
87
92
|
}
|
|
88
93
|
interrupt() {
|
|
89
94
|
this.interrupted = true;
|
|
95
|
+
this.protocol.noteInterrupt();
|
|
90
96
|
this.write({ type: 'control_request', request_id: `interrupt-${Date.now()}`, request: { subtype: 'interrupt' } });
|
|
91
97
|
}
|
|
92
98
|
stopTask(taskId) {
|
|
@@ -129,35 +135,18 @@ export class ClaudeBackend {
|
|
|
129
135
|
return buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId });
|
|
130
136
|
}
|
|
131
137
|
async readLines(child) {
|
|
132
|
-
const reader = child.process.stdout.getReader();
|
|
133
|
-
const decoder = new TextDecoder();
|
|
134
|
-
let buffered = '';
|
|
135
138
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
break;
|
|
139
|
+
await readStreamLines(child.process.stdout, (line) => this.readLine(child, line), () => {
|
|
140
|
+
if (this.child === child) {
|
|
141
|
+
this.failed(new Error(`Claude wrote a frame of more than ${MAX_LINE_LENGTH / 1_000_000} million characters, which was dropped`));
|
|
140
142
|
}
|
|
141
|
-
|
|
142
|
-
let newline = buffered.indexOf('\n');
|
|
143
|
-
while (newline >= 0) {
|
|
144
|
-
this.readLine(child, buffered.slice(0, newline));
|
|
145
|
-
buffered = buffered.slice(newline + 1);
|
|
146
|
-
newline = buffered.indexOf('\n');
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (buffered.trim() !== '') {
|
|
150
|
-
this.readLine(child, buffered);
|
|
151
|
-
}
|
|
143
|
+
});
|
|
152
144
|
}
|
|
153
145
|
catch (error) {
|
|
154
146
|
if (this.child === child) {
|
|
155
|
-
this.
|
|
147
|
+
this.broken(error);
|
|
156
148
|
}
|
|
157
149
|
}
|
|
158
|
-
finally {
|
|
159
|
-
reader.releaseLock();
|
|
160
|
-
}
|
|
161
150
|
}
|
|
162
151
|
handleLine(child, line) {
|
|
163
152
|
if (this.child !== child || line.trim() === '') {
|
|
@@ -174,10 +163,14 @@ export class ClaudeBackend {
|
|
|
174
163
|
if (echoed?.type === 'user' && typeof echoed.uuid === 'string' && echoed.parent_tool_use_id == null) {
|
|
175
164
|
this.steers.get(echoed.uuid)?.resolve(true);
|
|
176
165
|
this.steers.delete(echoed.uuid);
|
|
166
|
+
if (this.steers.size === 0) {
|
|
167
|
+
this.clearSteerTimer();
|
|
168
|
+
}
|
|
177
169
|
}
|
|
178
170
|
for (const event of this.protocol.handle(frame)) {
|
|
179
171
|
if (event.type === 'turn.done') {
|
|
180
172
|
this.active = false;
|
|
173
|
+
this.awaitSteers();
|
|
181
174
|
}
|
|
182
175
|
if (event.type === 'turn.done' && this.interrupted) {
|
|
183
176
|
this.interrupted = false;
|
|
@@ -195,13 +188,38 @@ export class ClaudeBackend {
|
|
|
195
188
|
this.failed(error);
|
|
196
189
|
}
|
|
197
190
|
}
|
|
191
|
+
awaitSteers() {
|
|
192
|
+
if (this.steers.size === 0 || this.steerTimer !== null) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
this.steerTimer = setTimeout(() => {
|
|
196
|
+
this.steerTimer = null;
|
|
197
|
+
for (const pending of this.steers.values()) {
|
|
198
|
+
pending.reject(new Error('Claude ended its turn without taking your message'));
|
|
199
|
+
}
|
|
200
|
+
this.steers.clear();
|
|
201
|
+
}, STEER_GRACE_MS);
|
|
202
|
+
this.steerTimer.unref?.();
|
|
203
|
+
}
|
|
204
|
+
clearSteerTimer() {
|
|
205
|
+
if (this.steerTimer !== null) {
|
|
206
|
+
clearTimeout(this.steerTimer);
|
|
207
|
+
this.steerTimer = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
198
210
|
rejectSteers(reason) {
|
|
199
211
|
this.active = false;
|
|
212
|
+
this.clearSteerTimer();
|
|
200
213
|
for (const pending of this.steers.values()) {
|
|
201
214
|
pending.reject(new Error(reason));
|
|
202
215
|
}
|
|
203
216
|
this.steers.clear();
|
|
204
217
|
}
|
|
218
|
+
/* A pipe that failed takes no more writes; a frame a consumer choked on leaves it as it was. */
|
|
219
|
+
broken(error) {
|
|
220
|
+
this.stdinClosed = true;
|
|
221
|
+
this.failed(error);
|
|
222
|
+
}
|
|
205
223
|
failed(error) {
|
|
206
224
|
this.rejectSteers(`Claude transport failed: ${errorText(error)}`);
|
|
207
225
|
this.protocol.forgetPending();
|
|
@@ -228,8 +246,14 @@ export class ClaudeBackend {
|
|
|
228
246
|
if (!child || this.stdinClosed) {
|
|
229
247
|
throw new Error('Claude stdin is closed');
|
|
230
248
|
}
|
|
231
|
-
|
|
232
|
-
|
|
249
|
+
try {
|
|
250
|
+
child.process.stdin.write(`${JSON.stringify(frame)}\n`);
|
|
251
|
+
child.process.stdin.flush();
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
this.stdinClosed = true;
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
233
257
|
}
|
|
234
258
|
closeStdin() {
|
|
235
259
|
if (!this.child || this.stdinClosed) {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { ChatGoal } from '@adecore/agent-contracts';
|
|
1
2
|
import type { ApprovalDecision, BackendEvent } from './backend.ts';
|
|
3
|
+
export declare function claudeGoalFromCommand(text: string, current: ChatGoal | null): ChatGoal | null | undefined;
|
|
4
|
+
export declare function claudeGoalCheck(text: string, current: ChatGoal | null): ChatGoal | undefined;
|
|
2
5
|
export declare class ClaudeProtocol {
|
|
3
6
|
private promptId;
|
|
4
7
|
private readonly pending;
|
|
@@ -17,6 +20,13 @@ export declare class ClaudeProtocol {
|
|
|
17
20
|
private readonly refusedWindows;
|
|
18
21
|
private readonly stopRequests;
|
|
19
22
|
private readonly steerIds;
|
|
23
|
+
private goal;
|
|
24
|
+
private goalAnswered;
|
|
25
|
+
private interrupting;
|
|
26
|
+
private readonly backgroundAgents;
|
|
27
|
+
constructor(goal?: ChatGoal | null);
|
|
28
|
+
noteInterrupt(): void;
|
|
29
|
+
private setGoal;
|
|
20
30
|
beginSteer(promptId: string): void;
|
|
21
31
|
beginPrompt(promptId: string): void;
|
|
22
32
|
handle(frame: unknown): BackendEvent[];
|
|
@@ -34,6 +44,8 @@ export declare class ClaudeProtocol {
|
|
|
34
44
|
private handleControlResponse;
|
|
35
45
|
private handleStreamEvent;
|
|
36
46
|
private handleAssistant;
|
|
47
|
+
private trackGoal;
|
|
48
|
+
private settleGoal;
|
|
37
49
|
private handleToolProgress;
|
|
38
50
|
private handleUser;
|
|
39
51
|
private handleResult;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
+
import { isRecord } from '@adecore/agent-contracts';
|
|
1
2
|
import { readClaudeEvent } from '../usage/limits/normalize.js';
|
|
2
3
|
import { claudeRuntimeMode } from '../providers/claude.js';
|
|
3
|
-
function isRecord(value) {
|
|
4
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
|
-
}
|
|
6
4
|
function str(value) {
|
|
7
5
|
return typeof value === 'string' ? value : null;
|
|
8
6
|
}
|
|
@@ -89,6 +87,65 @@ function parseQuestions(input) {
|
|
|
89
87
|
multiSelect: question.multiSelect === true
|
|
90
88
|
}));
|
|
91
89
|
}
|
|
90
|
+
/*
|
|
91
|
+
* What Claude Code 2.1.296 answers a `/goal` with, as a `<synthetic>` assistant message of its own: the goal
|
|
92
|
+
* it set, the one it cleared, or the one that is active with how many turns its check sent back. Undefined
|
|
93
|
+
* for any other text, null where no goal is left.
|
|
94
|
+
*/
|
|
95
|
+
const GOAL_SET = 'Goal set: ';
|
|
96
|
+
const GOAL_ACTIVE = /^Goal active: ([\s\S]+?) \(([^)]*)\)(?:\nLast check: ([\s\S]*))?$/;
|
|
97
|
+
const GOAL_GONE = ['Goal cleared: ', 'Goal cleared after ', 'No goal set'];
|
|
98
|
+
export function claudeGoalFromCommand(text, current) {
|
|
99
|
+
const said = text.trim();
|
|
100
|
+
if (said.startsWith(GOAL_SET)) {
|
|
101
|
+
const objective = said.slice(GOAL_SET.length).trim();
|
|
102
|
+
return objective === '' ? undefined : { objective, status: 'active', checks: 0 };
|
|
103
|
+
}
|
|
104
|
+
if (GOAL_GONE.some((prefix) => said.startsWith(prefix))) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const active = GOAL_ACTIVE.exec(said);
|
|
108
|
+
if (!active) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const objective = active[1];
|
|
112
|
+
const checks = /\d+/.exec(active[2])?.[0];
|
|
113
|
+
const lastCheck = active[3]?.trim();
|
|
114
|
+
return {
|
|
115
|
+
...(current?.objective === objective ? current : {}),
|
|
116
|
+
objective,
|
|
117
|
+
status: 'active',
|
|
118
|
+
checks: checks === undefined ? 0 : Number(checks),
|
|
119
|
+
...(lastCheck ? { lastCheck } : {})
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// The messages whose blocks are still counted. The frames of one message come close together, so only the newest can still grow.
|
|
123
|
+
const COUNTED_MESSAGES = 256;
|
|
124
|
+
/* The ordinal of the next block of `messageId`; a count moves to the back on every block and the oldest goes. */
|
|
125
|
+
function nextOrdinal(counts, messageId) {
|
|
126
|
+
const ordinal = counts.get(messageId) ?? 0;
|
|
127
|
+
counts.delete(messageId);
|
|
128
|
+
counts.set(messageId, ordinal + 1);
|
|
129
|
+
if (counts.size > COUNTED_MESSAGES) {
|
|
130
|
+
counts.delete(counts.keys().next().value);
|
|
131
|
+
}
|
|
132
|
+
return ordinal;
|
|
133
|
+
}
|
|
134
|
+
/*
|
|
135
|
+
* One check of the goal's Stop hook that found it not met yet: a synthetic user message the CLI feeds the
|
|
136
|
+
* agent before it goes on, which names the objective in brackets. Undefined for any other message.
|
|
137
|
+
*/
|
|
138
|
+
export function claudeGoalCheck(text, current) {
|
|
139
|
+
if (current === null) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const prefix = `Stop hook feedback:\n[${current.objective}]: `;
|
|
143
|
+
if (!text.startsWith(prefix)) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const lastCheck = text.slice(prefix.length).trim();
|
|
147
|
+
return { ...current, status: 'active', checks: (current.checks ?? 0) + 1, ...(lastCheck === '' ? {} : { lastCheck }) };
|
|
148
|
+
}
|
|
92
149
|
/*
|
|
93
150
|
* Claude Code's stream-json frames, in and out. Text is keyed by message id plus the ordinal of the
|
|
94
151
|
* text block inside that message, because the streaming events and the final `assistant` frames
|
|
@@ -120,6 +177,25 @@ export class ClaudeProtocol {
|
|
|
120
177
|
// The task each `stop_task` request named, so an error answer finds it again.
|
|
121
178
|
stopRequests = new Map();
|
|
122
179
|
steerIds = new Set();
|
|
180
|
+
// The goal this process works toward, which `--resume` takes up from the transcript; a model answer since it was
|
|
181
|
+
// set or last checked is what lets a turn that ends normally count as its check passing.
|
|
182
|
+
goal;
|
|
183
|
+
goalAnswered = false;
|
|
184
|
+
interrupting = false;
|
|
185
|
+
// Background subagents still at work; Claude Code holds a goal's check until they report.
|
|
186
|
+
backgroundAgents = new Set();
|
|
187
|
+
constructor(goal = null) {
|
|
188
|
+
this.goal = goal;
|
|
189
|
+
}
|
|
190
|
+
/* A stop is on its way; the turn it ends leaves the goal set but nobody working on it. */
|
|
191
|
+
noteInterrupt() {
|
|
192
|
+
this.interrupting = true;
|
|
193
|
+
}
|
|
194
|
+
setGoal(goal, events) {
|
|
195
|
+
this.goal = goal;
|
|
196
|
+
this.goalAnswered = false;
|
|
197
|
+
events.push({ type: 'goal', goal });
|
|
198
|
+
}
|
|
123
199
|
beginSteer(promptId) {
|
|
124
200
|
this.steerIds.add(promptId);
|
|
125
201
|
}
|
|
@@ -313,6 +389,7 @@ export class ClaudeProtocol {
|
|
|
313
389
|
if (taskId) {
|
|
314
390
|
this.endBackground(taskId, events);
|
|
315
391
|
this.workflows.delete(taskId);
|
|
392
|
+
this.backgroundAgents.delete(taskId);
|
|
316
393
|
}
|
|
317
394
|
// The CLI wakes the main agent itself when a background task settles; this frame is the only
|
|
318
395
|
// thing that says what it was about, and it arrives before the turn nobody asked for.
|
|
@@ -335,6 +412,10 @@ export class ClaudeProtocol {
|
|
|
335
412
|
handleTaskStarted(frame, events) {
|
|
336
413
|
const ref = str(frame.tool_use_id);
|
|
337
414
|
if (frame.task_type === 'local_agent') {
|
|
415
|
+
const agentTask = str(frame.task_id);
|
|
416
|
+
if (agentTask && frame.is_backgrounded === true) {
|
|
417
|
+
this.backgroundAgents.add(agentTask);
|
|
418
|
+
}
|
|
338
419
|
if (ref) {
|
|
339
420
|
events.push({
|
|
340
421
|
type: 'task.started',
|
|
@@ -383,6 +464,7 @@ export class ClaudeProtocol {
|
|
|
383
464
|
const status = str(patch.status);
|
|
384
465
|
if (status !== null && ENDED_TASK_STATUSES.has(status)) {
|
|
385
466
|
this.foregroundShells.delete(taskId);
|
|
467
|
+
this.backgroundAgents.delete(taskId);
|
|
386
468
|
this.endBackground(taskId, events);
|
|
387
469
|
this.endWorkflow(taskId, null, status === 'completed', events);
|
|
388
470
|
}
|
|
@@ -485,6 +567,9 @@ export class ClaudeProtocol {
|
|
|
485
567
|
this.apiError = frame.error;
|
|
486
568
|
}
|
|
487
569
|
const model = str(message.model);
|
|
570
|
+
if (!parentRef) {
|
|
571
|
+
this.trackGoal(model, content, events);
|
|
572
|
+
}
|
|
488
573
|
// The CLI answers some frames itself and names them `<synthetic>`; no model wrote those.
|
|
489
574
|
if (parentRef && model && model !== '<synthetic>' && this.subagentModels.get(parentRef) !== model) {
|
|
490
575
|
this.subagentModels.set(parentRef, model);
|
|
@@ -495,14 +580,12 @@ export class ClaudeProtocol {
|
|
|
495
580
|
continue;
|
|
496
581
|
}
|
|
497
582
|
if (block.type === 'text') {
|
|
498
|
-
const ordinal = this.frameTextCount
|
|
499
|
-
this.frameTextCount.set(messageId, ordinal + 1);
|
|
583
|
+
const ordinal = nextOrdinal(this.frameTextCount, messageId);
|
|
500
584
|
// A subagent's text is its own report, not the thread's answer; the parent says whose row it is.
|
|
501
585
|
events.push({ type: 'text.done', ref: this.textRef(messageId, ordinal), text: str(block.text) ?? '', ...(parentRef ? { parentRef } : {}) });
|
|
502
586
|
}
|
|
503
587
|
else if (block.type === 'thinking' && !parentRef) {
|
|
504
|
-
const ordinal = this.frameThinkingCount
|
|
505
|
-
this.frameThinkingCount.set(messageId, ordinal + 1);
|
|
588
|
+
const ordinal = nextOrdinal(this.frameThinkingCount, messageId);
|
|
506
589
|
events.push({ type: 'thinking.done', ref: this.thinkingRef(messageId, ordinal), text: str(block.thinking) ?? '' });
|
|
507
590
|
}
|
|
508
591
|
else if (block.type === 'tool_use') {
|
|
@@ -524,6 +607,44 @@ export class ClaudeProtocol {
|
|
|
524
607
|
events.push({ type: 'note', level: 'error', text: `The request failed: ${error.replaceAll('_', ' ')}` });
|
|
525
608
|
}
|
|
526
609
|
}
|
|
610
|
+
trackGoal(model, content, events) {
|
|
611
|
+
if (model === '<synthetic>') {
|
|
612
|
+
const next = claudeGoalFromCommand(resultText(content), this.goal);
|
|
613
|
+
if (next !== undefined) {
|
|
614
|
+
this.setGoal(next, events);
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (this.goal === null) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
this.goalAnswered = true;
|
|
622
|
+
// A message after a stop takes the goal up again; its hook never went away.
|
|
623
|
+
if (this.goal.status === 'paused') {
|
|
624
|
+
this.goal = { ...this.goal, status: 'active' };
|
|
625
|
+
events.push({ type: 'goal', goal: this.goal });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/*
|
|
629
|
+
* Where a turn leaves the goal. Claude Code ends a turn on its own only once the goal's check passed, so
|
|
630
|
+
* a turn that ends normally after the model answered met it; Claude Code 2.1.296 says nothing more
|
|
631
|
+
* about it in stream-json. A check it defers while background work runs leaves the goal active, and a
|
|
632
|
+
* stopped or failed turn leaves it set with nobody working on it.
|
|
633
|
+
*/
|
|
634
|
+
settleGoal(failed, events) {
|
|
635
|
+
const goal = this.goal;
|
|
636
|
+
const interrupted = this.interrupting;
|
|
637
|
+
this.interrupting = false;
|
|
638
|
+
if (goal?.status !== 'active') {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (failed || interrupted) {
|
|
642
|
+
this.setGoal({ ...goal, status: 'paused' }, events);
|
|
643
|
+
}
|
|
644
|
+
else if (this.goalAnswered && this.backgroundTasks.size === 0 && this.workflows.size === 0 && this.backgroundAgents.size === 0) {
|
|
645
|
+
this.setGoal({ ...goal, status: 'complete' }, events);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
527
648
|
/*
|
|
528
649
|
* `tool_progress` carries how long the call has run (once per 30 s for a Bash under the CLI's
|
|
529
650
|
* remote gate, or a heartbeat for a slow MCP tool); the start is kept so the client can count on.
|
|
@@ -543,6 +664,13 @@ export class ClaudeProtocol {
|
|
|
543
664
|
}
|
|
544
665
|
const message = isRecord(frame.message) ? frame.message : {};
|
|
545
666
|
const content = Array.isArray(message.content) ? message.content : [];
|
|
667
|
+
if (frame.isSynthetic === true && !str(frame.parent_tool_use_id)) {
|
|
668
|
+
const checked = claudeGoalCheck(resultText(message.content), this.goal);
|
|
669
|
+
if (checked !== undefined) {
|
|
670
|
+
this.setGoal(checked, events);
|
|
671
|
+
events.push({ type: 'note', level: 'info', text: `Goal not met yet: ${checked.lastCheck ?? checked.objective}` });
|
|
672
|
+
}
|
|
673
|
+
}
|
|
546
674
|
for (const block of content) {
|
|
547
675
|
if (!isRecord(block) || block.type !== 'tool_result') {
|
|
548
676
|
continue;
|
|
@@ -587,6 +715,8 @@ export class ClaudeProtocol {
|
|
|
587
715
|
const limit = failed ? this.limit() : null;
|
|
588
716
|
this.apiError = null;
|
|
589
717
|
this.refusedWindows.clear();
|
|
718
|
+
// Before the turn ends, so whoever settles work on that end already reads where the goal stands.
|
|
719
|
+
this.settleGoal(failed, events);
|
|
590
720
|
events.push({
|
|
591
721
|
type: 'turn.done',
|
|
592
722
|
...(promptId === null ? {} : { promptId }),
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
export declare class ClaudeTitleReader {
|
|
2
2
|
private readonly progress;
|
|
3
|
-
private readonly found;
|
|
4
3
|
private readonly projectsDir;
|
|
5
4
|
private readonly chunkBytes;
|
|
6
5
|
constructor(projectsDir?: string, chunkBytes?: number);
|
|
7
|
-
forSession(agentSessionId: string, projectsDir?: string): Promise<string | null>;
|
|
6
|
+
forSession(agentSessionId: string, projectsDir?: string, cwd?: string): Promise<string | null>;
|
|
8
7
|
forTranscript(path: string): Promise<string | null>;
|
|
9
8
|
private take;
|
|
10
|
-
private locate;
|
|
11
9
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { join } from 'node:path';
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { isRecord } from '@adecore/agent-contracts';
|
|
4
3
|
import { cleanTitle, readLines } from '../title-file.js';
|
|
5
4
|
import { usageRoots } from '../usage/roots.js';
|
|
5
|
+
import { findClaudeSession } from './claude-transcript.js';
|
|
6
6
|
// The two records carry these words; a line without them is never parsed.
|
|
7
7
|
const MARKERS = ['"ai-title"', '"custom-title"'];
|
|
8
8
|
/*
|
|
@@ -14,16 +14,15 @@ const MARKERS = ['"ai-title"', '"custom-title"'];
|
|
|
14
14
|
*/
|
|
15
15
|
export class ClaudeTitleReader {
|
|
16
16
|
progress = new Map();
|
|
17
|
-
found = new Map();
|
|
18
17
|
projectsDir;
|
|
19
18
|
chunkBytes;
|
|
20
19
|
constructor(projectsDir = usageRoots().find((root) => root.provider === 'claude')?.path ?? '', chunkBytes = 4 * 1024 * 1024) {
|
|
21
20
|
this.projectsDir = projectsDir;
|
|
22
21
|
this.chunkBytes = chunkBytes;
|
|
23
22
|
}
|
|
24
|
-
/* The title of a session found by its id, for a chat, which knows no transcript path; in the projects of its account. */
|
|
25
|
-
async forSession(agentSessionId, projectsDir = this.projectsDir) {
|
|
26
|
-
const path = await
|
|
23
|
+
/* The title of a session found by its id, for a chat, which knows no transcript path; in the projects of its account, its own folder first. */
|
|
24
|
+
async forSession(agentSessionId, projectsDir = this.projectsDir, cwd) {
|
|
25
|
+
const path = await findClaudeSession(projectsDir, agentSessionId, cwd);
|
|
27
26
|
return path === null ? null : await this.forTranscript(path);
|
|
28
27
|
}
|
|
29
28
|
async forTranscript(path) {
|
|
@@ -57,41 +56,14 @@ export class ClaudeTitleReader {
|
|
|
57
56
|
catch {
|
|
58
57
|
return;
|
|
59
58
|
}
|
|
60
|
-
if (
|
|
59
|
+
if (!isRecord(record)) {
|
|
61
60
|
return;
|
|
62
61
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
progress.aiTitle = cleanTitle(entry.aiTitle) ?? progress.aiTitle;
|
|
62
|
+
if (record.type === 'ai-title') {
|
|
63
|
+
progress.aiTitle = cleanTitle(record.aiTitle) ?? progress.aiTitle;
|
|
66
64
|
}
|
|
67
|
-
else if (
|
|
68
|
-
progress.customTitle = cleanTitle(
|
|
65
|
+
else if (record.type === 'custom-title') {
|
|
66
|
+
progress.customTitle = cleanTitle(record.customTitle) ?? progress.customTitle;
|
|
69
67
|
}
|
|
70
68
|
}
|
|
71
|
-
/* The folder a transcript sits in is named after the working directory, so the file name is the only key a session id gives. */
|
|
72
|
-
async locate(agentSessionId, projectsDir) {
|
|
73
|
-
const known = this.found.get(agentSessionId);
|
|
74
|
-
if (known !== undefined && existsSync(known)) {
|
|
75
|
-
return known;
|
|
76
|
-
}
|
|
77
|
-
if (projectsDir === '' || agentSessionId.includes('/')) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
let dirs;
|
|
81
|
-
try {
|
|
82
|
-
dirs = await readdir(projectsDir);
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
const file = `${agentSessionId}.jsonl`;
|
|
88
|
-
for (const dir of dirs) {
|
|
89
|
-
const path = join(projectsDir, dir, file);
|
|
90
|
-
if (existsSync(path)) {
|
|
91
|
-
this.found.set(agentSessionId, path);
|
|
92
|
-
return path;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
69
|
}
|
|
@@ -8,6 +8,7 @@ export interface SubagentMeta {
|
|
|
8
8
|
transcript: string;
|
|
9
9
|
}
|
|
10
10
|
export declare function claudeProjectSlug(cwd: string): string;
|
|
11
|
+
export declare function findClaudeSession(projectsDir: string, sessionId: string, cwd?: string): Promise<string | null>;
|
|
11
12
|
export declare function findSubagentsDir(projectsDir: string, cwd: string, sessionId: string): Promise<string | null>;
|
|
12
13
|
export declare function readSubagentMetas(dir: string): Promise<SubagentMeta[]>;
|
|
13
14
|
export declare function findWorkflowAgent(subagentsDir: string, agentId: string): Promise<SubagentMeta | null>;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { isRecord } from '@adecore/agent-contracts';
|
|
5
|
+
import { fileExists } from '../fs.js';
|
|
4
6
|
import { readLines } from '../title-file.js';
|
|
5
7
|
import { ClaudeProtocol } from './claude-protocol.js';
|
|
6
8
|
import { readingThread, settledReading } from './subagent-projection.js';
|
|
7
9
|
const CHUNK_BYTES = 4 * 1024 * 1024;
|
|
8
|
-
function isRecord(value) {
|
|
9
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
-
}
|
|
11
10
|
function str(value) {
|
|
12
11
|
return typeof value === 'string' && value !== '' ? value : null;
|
|
13
12
|
}
|
|
@@ -26,19 +25,30 @@ export function spokenText(content) {
|
|
|
26
25
|
export function claudeProjectSlug(cwd) {
|
|
27
26
|
return cwd.replace(/[^a-zA-Z0-9]/g, '-');
|
|
28
27
|
}
|
|
28
|
+
// Where each session was found last, by projects folder and session id; a path whose file went is searched for again.
|
|
29
|
+
const sessionPaths = new Map();
|
|
29
30
|
/*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* session
|
|
31
|
+
* The transcript Claude Code keeps of a session, found by its id: in the folder of `cwd` first, then
|
|
32
|
+
* where it was found before, then in every project folder, since the id is unique across all of them
|
|
33
|
+
* and a slug the CLI wrote differently (or a session resumed from elsewhere) sits in another folder.
|
|
34
|
+
* Null when no folder holds it, and for an id that would reach outside the projects folder.
|
|
33
35
|
*/
|
|
34
|
-
export async function
|
|
35
|
-
if (projectsDir === '' || sessionId
|
|
36
|
+
export async function findClaudeSession(projectsDir, sessionId, cwd) {
|
|
37
|
+
if (projectsDir === '' || sessionId === '' || /[/\\]|\.\./.test(sessionId)) {
|
|
36
38
|
return null;
|
|
37
39
|
}
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
+
const file = `${sessionId}.jsonl`;
|
|
41
|
+
const key = `${projectsDir}\n${sessionId}`;
|
|
42
|
+
const own = cwd === undefined ? null : join(projectsDir, claudeProjectSlug(cwd), file);
|
|
43
|
+
if (own !== null && (await fileExists(own))) {
|
|
44
|
+
sessionPaths.set(key, own);
|
|
40
45
|
return own;
|
|
41
46
|
}
|
|
47
|
+
const known = sessionPaths.get(key);
|
|
48
|
+
if (known !== undefined && known !== own && (await fileExists(known))) {
|
|
49
|
+
return known;
|
|
50
|
+
}
|
|
51
|
+
sessionPaths.delete(key);
|
|
42
52
|
let dirs;
|
|
43
53
|
try {
|
|
44
54
|
dirs = await readdir(projectsDir);
|
|
@@ -46,13 +56,22 @@ export async function findSubagentsDir(projectsDir, cwd, sessionId) {
|
|
|
46
56
|
catch {
|
|
47
57
|
return null;
|
|
48
58
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
const candidates = dirs.map((dir) => join(projectsDir, dir, file)).filter((candidate) => candidate !== own);
|
|
60
|
+
const present = await Promise.all(candidates.map(fileExists));
|
|
61
|
+
const found = candidates.find((_candidate, i) => present[i]) ?? null;
|
|
62
|
+
if (found !== null) {
|
|
63
|
+
sessionPaths.set(key, found);
|
|
54
64
|
}
|
|
55
|
-
return
|
|
65
|
+
return found;
|
|
66
|
+
}
|
|
67
|
+
/* Where the subagents of a session keep their transcripts: the folder beside its own transcript, once the CLI made it. */
|
|
68
|
+
export async function findSubagentsDir(projectsDir, cwd, sessionId) {
|
|
69
|
+
const transcript = await findClaudeSession(projectsDir, sessionId, cwd);
|
|
70
|
+
if (transcript === null) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const dir = join(dirname(transcript), sessionId, 'subagents');
|
|
74
|
+
return (await fileExists(dir)) ? dir : null;
|
|
56
75
|
}
|
|
57
76
|
/* Every meta file in a subagents folder that says which agent it is about; one that does not parse is left out. */
|
|
58
77
|
export async function readSubagentMetas(dir) {
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import { type ChatSkill } from '@adecore/agent-contracts';
|
|
1
|
+
import { type ChatGoalAction, type ChatSkill } from '@adecore/agent-contracts';
|
|
2
2
|
import type { ApprovalDecision, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
|
|
3
3
|
import { type CodexClientInfo } from './codex-transport.ts';
|
|
4
4
|
export declare function parseSkillsList(result: unknown): ChatSkill[];
|
|
5
|
+
export type GoalCommand = {
|
|
6
|
+
kind: 'show';
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'clear';
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'pause';
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'resume';
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'set';
|
|
15
|
+
objective: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function parseGoalCommand(text: string): GoalCommand | null;
|
|
5
18
|
export interface CodexBackendOptions {
|
|
6
19
|
client?: CodexClientInfo;
|
|
7
20
|
}
|
|
@@ -20,12 +33,19 @@ export declare class CodexBackend implements ChatBackend {
|
|
|
20
33
|
private imageInputSupported;
|
|
21
34
|
private turnUnnamed;
|
|
22
35
|
private interruptOwed;
|
|
36
|
+
private goalNow;
|
|
23
37
|
constructor(launch: BackendLaunch, host: BackendHost, options?: CodexBackendOptions);
|
|
24
38
|
get running(): boolean;
|
|
25
39
|
get pid(): number | null;
|
|
26
40
|
start(): Promise<void>;
|
|
41
|
+
private handshake;
|
|
42
|
+
private readGoal;
|
|
27
43
|
private turnInput;
|
|
28
44
|
sendTurn(input: TurnInput): void;
|
|
45
|
+
private turnStartParams;
|
|
46
|
+
private runGoalCommand;
|
|
47
|
+
private reply;
|
|
48
|
+
goal(action: ChatGoalAction): Promise<void>;
|
|
29
49
|
steerTurn(input: TurnInput): Promise<boolean>;
|
|
30
50
|
private acceptPrompt;
|
|
31
51
|
private fail;
|