@adecore/agents 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/async.d.ts +5 -0
- package/dist/async.js +13 -5
- package/dist/chat/acp-protocol.js +3 -5
- package/dist/chat/chat-core.d.ts +1 -0
- package/dist/chat/chat-core.js +9 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.d.ts +2 -0
- package/dist/chat/chat-session.js +9 -10
- package/dist/chat/claude-protocol.js +1 -6
- package/dist/chat/claude-transcript.js +1 -3
- package/dist/chat/codex-protocol.js +1 -6
- package/dist/chat/projector.js +1 -3
- package/dist/chat/request-summary.js +1 -3
- package/dist/coded-error.js +1 -5
- package/dist/context/argv.js +1 -2
- package/dist/context/verb.js +1 -3
- package/dist/error-text.d.ts +1 -0
- package/dist/error-text.js +4 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.js +12 -0
- package/dist/fs.d.ts +1 -0
- package/dist/fs.js +8 -4
- package/dist/lineage.js +11 -5
- package/dist/messages/notice-store.js +3 -4
- package/dist/providers/accounts/acp-account.js +3 -20
- package/dist/providers/accounts/service.d.ts +1 -2
- package/dist/providers/accounts/service.js +5 -9
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/acp-agent-state.js +1 -3
- package/dist/providers/claude.js +0 -1
- package/dist/providers/codex.js +0 -1
- package/dist/providers/detect.js +1 -1
- package/dist/providers/shell-commands.js +1 -2
- package/dist/run-process.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -8
- package/dist/tasks/background-limit.js +3 -2
- package/dist/tasks/task-coordinator.js +11 -17
- package/dist/testing/watch.js +0 -1
- package/dist/usage/limits/monitor.js +1 -2
- package/dist/usage/readers/claude.js +3 -10
- package/dist/usage/readers/codex.js +2 -9
- package/dist/usage/readers/grok.js +3 -10
- package/dist/usage/readers/host-log.js +3 -10
- package/dist/usage/record.d.ts +2 -0
- package/dist/usage/record.js +13 -0
- package/package.json +6 -6
- package/src/async.ts +23 -5
- package/src/chat/acp-backend.ts +121 -0
- package/src/chat/acp-protocol.ts +172 -6
- package/src/chat/attachment-store.ts +21 -0
- package/src/chat/backend.ts +0 -2
- package/src/chat/bookmark-store.ts +23 -0
- package/src/chat/chat-core.ts +218 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +414 -13
- package/src/chat/chat-store.ts +50 -0
- package/src/chat/chat-title.ts +9 -0
- package/src/chat/claude-backend.ts +50 -0
- package/src/chat/claude-protocol.ts +155 -8
- package/src/chat/claude-title.ts +10 -0
- package/src/chat/claude-transcript.ts +47 -4
- package/src/chat/codex-backend.ts +131 -0
- package/src/chat/codex-protocol.ts +180 -8
- package/src/chat/codex-thread.ts +23 -0
- package/src/chat/composer-preferences.ts +6 -0
- package/src/chat/context-breakdown.ts +11 -0
- package/src/chat/delta-coalescer.ts +8 -0
- package/src/chat/external-sessions.ts +28 -0
- package/src/chat/generated-image.ts +25 -0
- package/src/chat/index.ts +5 -0
- package/src/chat/input.ts +13 -0
- package/src/chat/json-rpc-transport.ts +28 -0
- package/src/chat/limit-resume.ts +13 -0
- package/src/chat/projector.ts +160 -4
- package/src/chat/readable-text.ts +3 -0
- package/src/chat/request-summary.ts +18 -4
- package/src/chat/subagent-projection.ts +3 -0
- package/src/chat/subagent-reader.ts +76 -0
- package/src/chat/subagent-settlement.ts +24 -0
- package/src/chat/thread-search.ts +13 -0
- package/src/chat/thread.ts +31 -0
- package/src/chat/ui-queries.ts +122 -0
- package/src/chat/ui-replies.ts +34 -0
- package/src/chat/visual-store.ts +49 -0
- package/src/chat/wake-chat.ts +3 -0
- package/src/client-sinks.ts +2 -0
- package/src/coded-error.ts +1 -5
- package/src/context/argv.ts +18 -2
- package/src/context/index.ts +1 -0
- package/src/context/refusal.ts +3 -0
- package/src/context/verb.ts +32 -4
- package/src/coordination/index.ts +10 -0
- package/src/error-text.ts +7 -0
- package/src/fields.ts +14 -0
- package/src/fs.ts +20 -4
- package/src/host/agent-host.ts +20 -0
- package/src/host/environment.ts +6 -0
- package/src/host/handlers.ts +18 -0
- package/src/host/memory-port.ts +4 -0
- package/src/host/wiring.ts +10 -0
- package/src/index.ts +1 -0
- package/src/lineage.ts +24 -5
- package/src/messages/deliver-message.ts +6 -0
- package/src/messages/deliver-notice.ts +13 -0
- package/src/messages/notice-store.ts +16 -4
- package/src/modes.ts +1 -0
- package/src/outbox/outbox-worker.ts +31 -0
- package/src/outbox/outbox.ts +5 -0
- package/src/providers/accounts/accounts.ts +36 -0
- package/src/providers/accounts/acp-account.ts +22 -27
- package/src/providers/accounts/launch.ts +4 -0
- package/src/providers/accounts/service.ts +110 -12
- package/src/providers/accounts/shadow-home.ts +10 -0
- package/src/providers/accounts/status.ts +13 -4
- package/src/providers/accounts/store.ts +25 -0
- package/src/providers/accounts/variables.ts +11 -1
- package/src/providers/acp-agent-state.ts +24 -3
- package/src/providers/acp-provider.ts +5 -0
- package/src/providers/acp-usage-log.ts +2 -0
- package/src/providers/catalog.ts +17 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-rules.ts +3 -0
- package/src/providers/codex.ts +2 -1
- package/src/providers/detect.ts +6 -1
- package/src/providers/provider.ts +1 -1
- package/src/providers/registry.ts +18 -2
- package/src/providers/shell-commands.ts +37 -2
- package/src/record-directory.ts +19 -0
- package/src/run-process.ts +24 -3
- package/src/serializer.ts +3 -0
- package/src/skills.ts +73 -10
- package/src/tasks/background-limit.ts +9 -2
- package/src/tasks/end-children.ts +9 -0
- package/src/tasks/give-task.ts +8 -0
- package/src/tasks/task-coordinator.ts +81 -17
- package/src/tasks/task-store.ts +28 -0
- package/src/tasks/waiting-child.ts +21 -0
- package/src/tasks/wake-parent.ts +26 -0
- package/src/tasks/wiring.ts +22 -0
- package/src/testing/accounts.ts +2 -0
- package/src/testing/fake-acp-capture.ts +16 -0
- package/src/testing/fake-acp.ts +66 -0
- package/src/testing/fake-claude.ts +177 -0
- package/src/testing/fake-cli.ts +29 -0
- package/src/testing/fake-codex.ts +122 -0
- package/src/testing/manual-clock.ts +2 -0
- package/src/testing/watch.ts +7 -1
- package/src/title-file.ts +12 -0
- package/src/usage/accounts.ts +6 -0
- package/src/usage/aggregate.ts +19 -0
- package/src/usage/exchange.ts +12 -0
- package/src/usage/index-file.ts +27 -0
- package/src/usage/limits/monitor.ts +31 -3
- package/src/usage/limits/normalize.ts +30 -0
- package/src/usage/limits/probe.ts +17 -0
- package/src/usage/pricing.ts +35 -0
- package/src/usage/projects.ts +8 -0
- package/src/usage/readers/claude.ts +12 -9
- package/src/usage/readers/codex.ts +40 -8
- package/src/usage/readers/grok.ts +17 -9
- package/src/usage/readers/host-log.ts +9 -9
- package/src/usage/record.ts +22 -0
- package/src/usage/roots.ts +6 -0
- package/src/usage/scanner.ts +61 -0
- package/src/usage/usage-service.ts +14 -0
- package/src/watch-seam.ts +8 -0
- package/src/windows-command.ts +12 -0
|
@@ -10,9 +10,12 @@ export function readableAssistantText(item: Pick<ChatAssistantItem, 'text' | 'ui
|
|
|
10
10
|
if (!item.ui?.length) {
|
|
11
11
|
return item.text;
|
|
12
12
|
}
|
|
13
|
+
|
|
13
14
|
const blocks = item.ui.map((block) => {
|
|
14
15
|
const frozen = item.uiQueries?.blocks[block.id];
|
|
16
|
+
|
|
15
17
|
return frozen && frozen.revision === block.revision ? { ...block, fallback: frozen.fallback } : block;
|
|
16
18
|
});
|
|
19
|
+
|
|
17
20
|
return uiFallbackText(item.text, blocks);
|
|
18
21
|
}
|
|
@@ -6,10 +6,7 @@ import {
|
|
|
6
6
|
type ChatRequestApproval,
|
|
7
7
|
type ChatRequestSummary
|
|
8
8
|
} from '@adecore/agent-contracts';
|
|
9
|
-
|
|
10
|
-
function str(value: unknown): string | null {
|
|
11
|
-
return typeof value === 'string' ? value : null;
|
|
12
|
-
}
|
|
9
|
+
import { str } from '../fields.ts';
|
|
13
10
|
|
|
14
11
|
// The keys a tool's input names its subject under, in the order a person would look for one.
|
|
15
12
|
const SUBJECT_KEYS = ['file_path', 'notebook_path', 'path', 'url', 'query', 'pattern', 'skill', 'description'];
|
|
@@ -19,7 +16,9 @@ function cut(text: string, max: number): string {
|
|
|
19
16
|
if (text.length <= max) {
|
|
20
17
|
return text;
|
|
21
18
|
}
|
|
19
|
+
|
|
22
20
|
const end = /[\uD800-\uDBFF]/.test(text[max - 1] ?? '') ? max - 1 : max;
|
|
21
|
+
|
|
23
22
|
return text.slice(0, end);
|
|
24
23
|
}
|
|
25
24
|
|
|
@@ -32,7 +31,9 @@ function clipPath(path: string, max: number): string {
|
|
|
32
31
|
if (path.length <= max) {
|
|
33
32
|
return path;
|
|
34
33
|
}
|
|
34
|
+
|
|
35
35
|
const start = path.length - (max - 1);
|
|
36
|
+
|
|
36
37
|
return `…${path.slice(/[\uDC00-\uDFFF]/.test(path[start] ?? '') ? start + 1 : start)}`;
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -44,16 +45,20 @@ function firstLine(text: string): string {
|
|
|
44
45
|
function clipLines(lines: readonly string[], maxLines: number, maxLength: number): { text: string; truncated: boolean } {
|
|
45
46
|
const kept: string[] = [];
|
|
46
47
|
let length = 0;
|
|
48
|
+
|
|
47
49
|
for (const line of lines) {
|
|
48
50
|
if (kept.length === maxLines || length + line.length + (kept.length > 0 ? 1 : 0) > maxLength) {
|
|
49
51
|
if (kept.length === 0) {
|
|
50
52
|
return { text: cut(line, maxLength), truncated: true };
|
|
51
53
|
}
|
|
54
|
+
|
|
52
55
|
return { text: kept.join('\n'), truncated: true };
|
|
53
56
|
}
|
|
57
|
+
|
|
54
58
|
length += line.length + (kept.length > 0 ? 1 : 0);
|
|
55
59
|
kept.push(line);
|
|
56
60
|
}
|
|
61
|
+
|
|
57
62
|
return { text: kept.join('\n'), truncated: false };
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -84,19 +89,25 @@ function fileChange(toolName: string, input: Record<string, unknown>): FileChang
|
|
|
84
89
|
(change): change is { path: string; diff: string } => isRecord(change) && typeof change.path === 'string' && typeof change.diff === 'string'
|
|
85
90
|
)
|
|
86
91
|
: [];
|
|
92
|
+
|
|
87
93
|
if (patches.length > 0) {
|
|
88
94
|
return { path: patches[0]!.path, files: patches.length, lines: changedLines(patches[0]!.diff) };
|
|
89
95
|
}
|
|
96
|
+
|
|
90
97
|
const path = str(input.file_path);
|
|
98
|
+
|
|
91
99
|
if (path === null) {
|
|
92
100
|
return null;
|
|
93
101
|
}
|
|
102
|
+
|
|
94
103
|
if (toolName === 'Edit') {
|
|
95
104
|
return { path, files: 1, lines: replacementLines(str(input.old_string) ?? '', str(input.new_string) ?? '') };
|
|
96
105
|
}
|
|
106
|
+
|
|
97
107
|
if (toolName === 'Write') {
|
|
98
108
|
return { path, files: 1, lines: replacementLines('', str(input.content) ?? '') };
|
|
99
109
|
}
|
|
110
|
+
|
|
100
111
|
if (toolName === 'MultiEdit' && Array.isArray(input.edits)) {
|
|
101
112
|
return {
|
|
102
113
|
path,
|
|
@@ -104,12 +115,14 @@ function fileChange(toolName: string, input: Record<string, unknown>): FileChang
|
|
|
104
115
|
lines: input.edits.filter(isRecord).flatMap((edit) => replacementLines(str(edit.old_string) ?? '', str(edit.new_string) ?? ''))
|
|
105
116
|
};
|
|
106
117
|
}
|
|
118
|
+
|
|
107
119
|
return null;
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
function subjectOf(input: Record<string, unknown>): string {
|
|
111
123
|
const named = SUBJECT_KEYS.map((key) => str(input[key])).find((value) => value !== null && value.trim() !== '');
|
|
112
124
|
const first = named ?? Object.values(input).find((value): value is string => typeof value === 'string' && value.trim() !== '') ?? '';
|
|
125
|
+
|
|
113
126
|
return clip(firstLine(first), CHAT_REQUEST_LIMITS.subject);
|
|
114
127
|
}
|
|
115
128
|
|
|
@@ -120,6 +133,7 @@ export function approvalSummary(item: ChatApprovalItem): ChatRequestApproval {
|
|
|
120
133
|
const diff = change === null ? null : clipLines(change.lines, CHAT_REQUEST_LIMITS.diffLines, CHAT_REQUEST_LIMITS.diff);
|
|
121
134
|
const shell = command === null ? null : clipLines(splitLines(command), CHAT_REQUEST_LIMITS.commandLines, CHAT_REQUEST_LIMITS.command);
|
|
122
135
|
const truncated = diff?.truncated === true || shell?.truncated === true;
|
|
136
|
+
|
|
123
137
|
return {
|
|
124
138
|
toolName: clip(item.toolName, CHAT_REQUEST_LIMITS.subject),
|
|
125
139
|
subject:
|
|
@@ -27,6 +27,7 @@ function readingInfo(): ChatInfo {
|
|
|
27
27
|
/* A thread and the projector that writes it, for a conversation read back rather than streamed. */
|
|
28
28
|
export function readingThread(providerName: string, now: () => number): { thread: ChatThread; projector: ThreadProjector } {
|
|
29
29
|
const thread = new ChatThread(readingInfo());
|
|
30
|
+
|
|
30
31
|
return { thread, projector: new ThreadProjector(thread, { providerName, now }) };
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -38,8 +39,10 @@ export function settledReading(item: ChatItem): ChatItem {
|
|
|
38
39
|
if (item.kind === 'assistant') {
|
|
39
40
|
return { ...item, turnId: null, streaming: false };
|
|
40
41
|
}
|
|
42
|
+
|
|
41
43
|
if (item.kind === 'thinking') {
|
|
42
44
|
return { ...item, turnId: null, streaming: false, endedAt: item.endedAt ?? item.createdAt };
|
|
43
45
|
}
|
|
46
|
+
|
|
44
47
|
return { ...item, turnId: null };
|
|
45
48
|
}
|
|
@@ -110,22 +110,27 @@ export class SubagentReader {
|
|
|
110
110
|
|
|
111
111
|
async read(chatId: string, toolUseId: string, cursor?: string, limit = DEFAULT_PAGE): Promise<ChatSubagentResult> {
|
|
112
112
|
const chat = this.require(chatId);
|
|
113
|
+
|
|
113
114
|
return chat.info.provider === 'codex' ? this.readCodex(chat, toolUseId, cursor, limit) : this.readClaude(chat, toolUseId, cursor, limit);
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/* Every item, for an agent reading the conversation as text; a Codex thread is paged through to its start. */
|
|
117
118
|
async readAll(chatId: string, toolUseId: string): Promise<ChatItem[]> {
|
|
118
119
|
const chat = this.require(chatId);
|
|
120
|
+
|
|
119
121
|
if (chat.info.provider !== 'codex') {
|
|
120
122
|
return (await this.claudeProjection(chat, toolUseId)).items();
|
|
121
123
|
}
|
|
124
|
+
|
|
122
125
|
const pages: ChatItem[][] = [];
|
|
123
126
|
let cursor: string | undefined;
|
|
127
|
+
|
|
124
128
|
do {
|
|
125
129
|
const page = await this.readCodex(chat, toolUseId, cursor, 100);
|
|
126
130
|
pages.unshift(page.items);
|
|
127
131
|
cursor = page.history.cursor ?? undefined;
|
|
128
132
|
} while (cursor !== undefined);
|
|
133
|
+
|
|
129
134
|
return pages.flat();
|
|
130
135
|
}
|
|
131
136
|
|
|
@@ -135,9 +140,11 @@ export class SubagentReader {
|
|
|
135
140
|
*/
|
|
136
141
|
async claudeSettlement(chatId: string, toolUseId: string): Promise<SubagentSettlement | null> {
|
|
137
142
|
const chat = this.options.chat(chatId);
|
|
143
|
+
|
|
138
144
|
if (!chat || chat.info.provider !== 'claude') {
|
|
139
145
|
return null;
|
|
140
146
|
}
|
|
147
|
+
|
|
141
148
|
try {
|
|
142
149
|
return await readSubagentSettlement((await this.claudeFile(chat, toolUseId)).path);
|
|
143
150
|
} catch {
|
|
@@ -149,14 +156,19 @@ export class SubagentReader {
|
|
|
149
156
|
hold(clientId: string, chatId: string, toolUseId: string): void {
|
|
150
157
|
const key = holdKey(chatId, toolUseId);
|
|
151
158
|
const existing = this.holds.get(key);
|
|
159
|
+
|
|
152
160
|
if (existing) {
|
|
153
161
|
existing.clients.add(clientId);
|
|
162
|
+
|
|
154
163
|
return;
|
|
155
164
|
}
|
|
165
|
+
|
|
156
166
|
const chat = this.options.chat(chatId);
|
|
167
|
+
|
|
157
168
|
if (!chat) {
|
|
158
169
|
return;
|
|
159
170
|
}
|
|
171
|
+
|
|
160
172
|
const stop = chat.info.provider === 'codex' ? this.pollCodex(key, chatId, toolUseId) : this.watchClaude(key);
|
|
161
173
|
this.holds.set(key, { chatId, toolUseId, clients: new Set([clientId]), stop });
|
|
162
174
|
}
|
|
@@ -164,10 +176,13 @@ export class SubagentReader {
|
|
|
164
176
|
release(clientId: string, chatId: string, toolUseId: string): void {
|
|
165
177
|
const key = holdKey(chatId, toolUseId);
|
|
166
178
|
const hold = this.holds.get(key);
|
|
179
|
+
|
|
167
180
|
if (!hold) {
|
|
168
181
|
return;
|
|
169
182
|
}
|
|
183
|
+
|
|
170
184
|
hold.clients.delete(clientId);
|
|
185
|
+
|
|
171
186
|
if (hold.clients.size === 0) {
|
|
172
187
|
this.drop(key, hold);
|
|
173
188
|
}
|
|
@@ -182,17 +197,20 @@ export class SubagentReader {
|
|
|
182
197
|
/* Forgets everything about a chat that is gone. */
|
|
183
198
|
releaseChat(chatId: string): void {
|
|
184
199
|
const ofChat = (key: string): boolean => key.startsWith(`${chatId}\n`);
|
|
200
|
+
|
|
185
201
|
for (const [key, hold] of [...this.holds]) {
|
|
186
202
|
if (ofChat(key)) {
|
|
187
203
|
this.drop(key, hold);
|
|
188
204
|
}
|
|
189
205
|
}
|
|
206
|
+
|
|
190
207
|
for (const [key, file] of [...this.claudeFiles]) {
|
|
191
208
|
if (ofChat(key)) {
|
|
192
209
|
this.claudeFiles.delete(key);
|
|
193
210
|
this.projections.delete(file.path);
|
|
194
211
|
}
|
|
195
212
|
}
|
|
213
|
+
|
|
196
214
|
for (const map of [this.codexRows, this.codexSeen]) {
|
|
197
215
|
for (const key of [...map.keys()]) {
|
|
198
216
|
if (ofChat(key)) {
|
|
@@ -209,9 +227,11 @@ export class SubagentReader {
|
|
|
209
227
|
|
|
210
228
|
private require(chatId: string): SubagentChat {
|
|
211
229
|
const chat = this.options.chat(chatId);
|
|
230
|
+
|
|
212
231
|
if (!chat) {
|
|
213
232
|
throw new ChatError('chat-not-found', `No chat ${chatId}`);
|
|
214
233
|
}
|
|
234
|
+
|
|
215
235
|
return chat;
|
|
216
236
|
}
|
|
217
237
|
|
|
@@ -224,10 +244,13 @@ export class SubagentReader {
|
|
|
224
244
|
/* Running while its row says so, whether that row is in the chat's thread or in a conversation read before. */
|
|
225
245
|
private live(chat: SubagentChat, toolUseId: string, inside: ChatItem[]): boolean {
|
|
226
246
|
const workflowAgent = workflowAgentIdOf(toolUseId);
|
|
247
|
+
|
|
227
248
|
if (workflowAgent !== null) {
|
|
228
249
|
return workflowAgentRunning(chat.items(), workflowAgent);
|
|
229
250
|
}
|
|
251
|
+
|
|
230
252
|
const row = findSubagent(chat.items(), toolUseId) ?? findSubagent(inside, toolUseId);
|
|
253
|
+
|
|
231
254
|
return row?.kind === 'subagent' && row.status === 'running';
|
|
232
255
|
}
|
|
233
256
|
|
|
@@ -235,6 +258,7 @@ export class SubagentReader {
|
|
|
235
258
|
const projection = await this.claudeProjection(chat, toolUseId);
|
|
236
259
|
const page = projection.page(limit, cursor);
|
|
237
260
|
const seen = [...this.projections.values()].filter((entry) => entry.chatId === chat.info.chatId).flatMap((entry) => entry.projection.items());
|
|
261
|
+
|
|
238
262
|
return {
|
|
239
263
|
items: page.items,
|
|
240
264
|
history: page.history,
|
|
@@ -247,31 +271,39 @@ export class SubagentReader {
|
|
|
247
271
|
private async claudeProjection(chat: SubagentChat, toolUseId: string): Promise<TranscriptProjection> {
|
|
248
272
|
const file = await this.claudeFile(chat, toolUseId);
|
|
249
273
|
let entry = this.projections.get(file.path);
|
|
274
|
+
|
|
250
275
|
if (!entry) {
|
|
251
276
|
entry = { chatId: chat.info.chatId, projection: new TranscriptProjection(file.path) };
|
|
252
277
|
this.projections.set(file.path, entry);
|
|
253
278
|
this.trimProjections();
|
|
254
279
|
}
|
|
280
|
+
|
|
255
281
|
if (!(await entry.projection.refresh())) {
|
|
256
282
|
throw new ChatError('subagent-not-found', 'Claude has not written a transcript for this subagent');
|
|
257
283
|
}
|
|
284
|
+
|
|
258
285
|
return entry.projection;
|
|
259
286
|
}
|
|
260
287
|
|
|
261
288
|
private async claudeFile(chat: SubagentChat, toolUseId: string): Promise<ClaudeFile> {
|
|
262
289
|
const key = holdKey(chat.info.chatId, toolUseId);
|
|
263
290
|
const known = this.claudeFiles.get(key);
|
|
291
|
+
|
|
264
292
|
if (known) {
|
|
265
293
|
return known;
|
|
266
294
|
}
|
|
295
|
+
|
|
267
296
|
const found = await this.claudeSubagentOf(chat.info, toolUseId);
|
|
297
|
+
|
|
268
298
|
if (found === null) {
|
|
269
299
|
throw new ChatError('subagent-not-found', 'Claude has not written a transcript for this subagent');
|
|
270
300
|
}
|
|
301
|
+
|
|
271
302
|
const { dir, meta } = found;
|
|
272
303
|
const file = { dir, path: meta.transcript };
|
|
273
304
|
this.claudeFiles.set(key, file);
|
|
274
305
|
chat.noteNative(toolUseId, { agentId: meta.agentId });
|
|
306
|
+
|
|
275
307
|
return file;
|
|
276
308
|
}
|
|
277
309
|
|
|
@@ -279,6 +311,7 @@ export class SubagentReader {
|
|
|
279
311
|
private trimProjections(): void {
|
|
280
312
|
const held = new Set([...this.holds.values()].map((hold) => this.claudeFiles.get(holdKey(hold.chatId, hold.toolUseId))?.path));
|
|
281
313
|
const idle = [...this.projections.keys()].filter((path) => !held.has(path));
|
|
314
|
+
|
|
282
315
|
for (const path of idle.slice(0, Math.max(0, idle.length - MAX_IDLE_PROJECTIONS))) {
|
|
283
316
|
this.projections.delete(path);
|
|
284
317
|
}
|
|
@@ -292,53 +325,69 @@ export class SubagentReader {
|
|
|
292
325
|
private async claudeSubagentOf(info: ChatInfo, toolUseId: string): Promise<{ dir: string; meta: SubagentMeta } | null> {
|
|
293
326
|
const seen = new Set<string>();
|
|
294
327
|
let current: ChatInfo | null = info;
|
|
328
|
+
|
|
295
329
|
while (current !== null && !seen.has(current.chatId)) {
|
|
296
330
|
seen.add(current.chatId);
|
|
297
331
|
const sessionId = current.agentSessionId;
|
|
332
|
+
|
|
298
333
|
const dir =
|
|
299
334
|
sessionId === null
|
|
300
335
|
? null
|
|
301
336
|
: await findSubagentsDir(this.options.claudeProjectsDirOf?.(current) ?? this.options.claudeProjectsDir, current.cwd, sessionId);
|
|
337
|
+
|
|
302
338
|
const workflowAgent = workflowAgentIdOf(toolUseId);
|
|
339
|
+
|
|
303
340
|
if (dir !== null && workflowAgent !== null) {
|
|
304
341
|
const meta = await findWorkflowAgent(dir, workflowAgent);
|
|
342
|
+
|
|
305
343
|
if (meta !== null) {
|
|
306
344
|
return { dir: dirname(meta.transcript), meta };
|
|
307
345
|
}
|
|
308
346
|
}
|
|
347
|
+
|
|
309
348
|
// A grandchild has a file of its own in the same folder, found by the call that opened it like any other.
|
|
310
349
|
const meta = dir === null ? undefined : (await readSubagentMetas(dir)).find((candidate) => candidate.toolUseId === toolUseId);
|
|
350
|
+
|
|
311
351
|
if (dir !== null && meta !== undefined) {
|
|
312
352
|
return { dir, meta };
|
|
313
353
|
}
|
|
354
|
+
|
|
314
355
|
const origin: string | undefined = current.forkOf?.chatId;
|
|
315
356
|
current = origin === undefined || !this.options.chatInfo ? null : await this.options.chatInfo(origin);
|
|
316
357
|
}
|
|
358
|
+
|
|
317
359
|
return null;
|
|
318
360
|
}
|
|
319
361
|
|
|
320
362
|
private async readCodex(chat: SubagentChat, toolUseId: string, cursor: string | undefined, limit: number): Promise<ChatSubagentResult> {
|
|
321
363
|
const threadId = this.codexThread(chat, toolUseId);
|
|
364
|
+
|
|
322
365
|
if (cursor !== undefined && !cursor.startsWith(CODEX_CURSOR)) {
|
|
323
366
|
throw new ChatError('history-expired', 'The conversation changed. Reload its history.');
|
|
324
367
|
}
|
|
368
|
+
|
|
325
369
|
const params: ThreadItemsParams = {
|
|
326
370
|
threadId,
|
|
327
371
|
limit,
|
|
328
372
|
sortDirection: 'desc',
|
|
329
373
|
...(cursor === undefined ? {} : { cursor: cursor.slice(CODEX_CURSOR.length) })
|
|
330
374
|
};
|
|
375
|
+
|
|
331
376
|
const { entries, nextCursor } = parseThreadItemsPage(await this.codexPage(chat, params));
|
|
377
|
+
|
|
332
378
|
if (cursor === undefined) {
|
|
333
379
|
// What a poll that starts after this read compares against, so a change in between is not lost.
|
|
334
380
|
this.codexSeen.set(holdKey(chat.info.chatId, toolUseId), JSON.stringify(entries.slice(0, CODEX_POLL_ITEMS)));
|
|
335
381
|
}
|
|
382
|
+
|
|
336
383
|
const items = projectCodexItems([...entries].reverse(), this.now());
|
|
384
|
+
|
|
337
385
|
for (const item of items) {
|
|
338
386
|
if (item.kind === 'subagent') {
|
|
339
387
|
this.codexRows.set(holdKey(chat.info.chatId, item.toolUseId), item);
|
|
340
388
|
}
|
|
341
389
|
}
|
|
390
|
+
|
|
342
391
|
return {
|
|
343
392
|
items,
|
|
344
393
|
history: { cursor: nextCursor === null ? null : `${CODEX_CURSOR}${nextCursor}` },
|
|
@@ -350,45 +399,58 @@ export class SubagentReader {
|
|
|
350
399
|
|
|
351
400
|
private codexThreadOf(chat: SubagentChat, toolUseId: string): string | null {
|
|
352
401
|
const row = findSubagent(chat.items(), toolUseId) ?? this.codexRows.get(holdKey(chat.info.chatId, toolUseId));
|
|
402
|
+
|
|
353
403
|
return row?.kind === 'subagent' ? (row.native?.threadId ?? null) : null;
|
|
354
404
|
}
|
|
355
405
|
|
|
356
406
|
private codexThread(chat: SubagentChat, toolUseId: string): string {
|
|
357
407
|
const threadId = this.codexThreadOf(chat, toolUseId);
|
|
408
|
+
|
|
358
409
|
if (threadId === null) {
|
|
359
410
|
throw new ChatError('subagent-not-found', 'Codex has not said which thread this subagent works in');
|
|
360
411
|
}
|
|
412
|
+
|
|
361
413
|
return threadId;
|
|
362
414
|
}
|
|
363
415
|
|
|
364
416
|
private async codexPage(chat: SubagentChat, params: ThreadItemsParams): Promise<unknown> {
|
|
365
417
|
const running = chat.listThreadItems(params);
|
|
418
|
+
|
|
366
419
|
if (running !== null) {
|
|
367
420
|
return running;
|
|
368
421
|
}
|
|
422
|
+
|
|
369
423
|
const key = JSON.stringify(params);
|
|
370
424
|
const now = this.now();
|
|
371
425
|
const cached = this.codexCache.get(key);
|
|
426
|
+
|
|
372
427
|
if (cached && now - cached.at < CODEX_CACHE_MS) {
|
|
373
428
|
return cached.result;
|
|
374
429
|
}
|
|
430
|
+
|
|
375
431
|
const result = await this.listOnce(this.options.codexProcess(chat.info), params);
|
|
432
|
+
|
|
376
433
|
for (const [stale, entry] of [...this.codexCache]) {
|
|
377
434
|
if (now - entry.at >= CODEX_CACHE_MS) {
|
|
378
435
|
this.codexCache.delete(stale);
|
|
379
436
|
}
|
|
380
437
|
}
|
|
438
|
+
|
|
381
439
|
this.codexCache.set(key, { at: now, result });
|
|
440
|
+
|
|
382
441
|
return result;
|
|
383
442
|
}
|
|
384
443
|
|
|
385
444
|
/* One watch per subagents folder, however many of its transcripts are held. */
|
|
386
445
|
private watchClaude(key: string): () => void {
|
|
387
446
|
const file = this.claudeFiles.get(key);
|
|
447
|
+
|
|
388
448
|
if (!file) {
|
|
389
449
|
return () => undefined;
|
|
390
450
|
}
|
|
451
|
+
|
|
391
452
|
let shared = this.dirWatches.get(file.dir);
|
|
453
|
+
|
|
392
454
|
if (!shared) {
|
|
393
455
|
const watcher = this.seams.watch(file.dir, { recursive: false }, (_event, filename) => this.onClaudeChange(file.dir, filename));
|
|
394
456
|
// A folder that disappears ends the growing, not the host.
|
|
@@ -396,10 +458,13 @@ export class SubagentReader {
|
|
|
396
458
|
shared = { watcher, keys: new Set() };
|
|
397
459
|
this.dirWatches.set(file.dir, shared);
|
|
398
460
|
}
|
|
461
|
+
|
|
399
462
|
shared.keys.add(key);
|
|
463
|
+
|
|
400
464
|
return () => {
|
|
401
465
|
const current = this.dirWatches.get(file.dir);
|
|
402
466
|
current?.keys.delete(key);
|
|
467
|
+
|
|
403
468
|
if (current && current.keys.size === 0) {
|
|
404
469
|
current.watcher.close();
|
|
405
470
|
this.dirWatches.delete(file.dir);
|
|
@@ -410,6 +475,7 @@ export class SubagentReader {
|
|
|
410
475
|
private onClaudeChange(dir: string, filename: string | null): void {
|
|
411
476
|
for (const key of this.dirWatches.get(dir)?.keys ?? []) {
|
|
412
477
|
const path = this.claudeFiles.get(key)?.path;
|
|
478
|
+
|
|
413
479
|
// A platform that names no file could mean any of them.
|
|
414
480
|
if (filename === null || (path !== undefined && basename(path) === filename)) {
|
|
415
481
|
this.markChanged(key);
|
|
@@ -420,14 +486,17 @@ export class SubagentReader {
|
|
|
420
486
|
private pollCodex(key: string, chatId: string, toolUseId: string): () => void {
|
|
421
487
|
let cancel: (() => void) | null = null;
|
|
422
488
|
let stopped = false;
|
|
489
|
+
|
|
423
490
|
const tick = async (): Promise<void> => {
|
|
424
491
|
cancel = null;
|
|
425
492
|
const chat = this.options.chat(chatId);
|
|
426
493
|
const threadId = chat ? this.codexThreadOf(chat, toolUseId) : null;
|
|
427
494
|
const asked = chat && threadId ? chat.listThreadItems({ threadId, limit: CODEX_POLL_ITEMS, sortDirection: 'desc' }) : null;
|
|
495
|
+
|
|
428
496
|
if (asked !== null) {
|
|
429
497
|
try {
|
|
430
498
|
const seen = JSON.stringify(parseThreadItemsPage(await asked).entries);
|
|
499
|
+
|
|
431
500
|
if (!stopped && this.codexSeen.get(key) !== seen) {
|
|
432
501
|
this.codexSeen.set(key, seen);
|
|
433
502
|
this.markChanged(key);
|
|
@@ -436,11 +505,14 @@ export class SubagentReader {
|
|
|
436
505
|
// The process went between the question and the answer; the next tick asks again.
|
|
437
506
|
}
|
|
438
507
|
}
|
|
508
|
+
|
|
439
509
|
if (!stopped) {
|
|
440
510
|
cancel = this.seams.schedule(() => tick(), CODEX_POLL_MS);
|
|
441
511
|
}
|
|
442
512
|
};
|
|
513
|
+
|
|
443
514
|
cancel = this.seams.schedule(() => tick(), CODEX_POLL_MS);
|
|
515
|
+
|
|
444
516
|
return () => {
|
|
445
517
|
stopped = true;
|
|
446
518
|
cancel?.();
|
|
@@ -449,9 +521,11 @@ export class SubagentReader {
|
|
|
449
521
|
|
|
450
522
|
private markChanged(key: string): void {
|
|
451
523
|
this.changed.add(key);
|
|
524
|
+
|
|
452
525
|
if (this.cancelSettle !== null) {
|
|
453
526
|
return;
|
|
454
527
|
}
|
|
528
|
+
|
|
455
529
|
this.cancelSettle = this.seams.schedule(() => this.flushChanged(), CHANGE_SETTLE_MS);
|
|
456
530
|
}
|
|
457
531
|
|
|
@@ -459,8 +533,10 @@ export class SubagentReader {
|
|
|
459
533
|
this.cancelSettle = null;
|
|
460
534
|
const keys = [...this.changed];
|
|
461
535
|
this.changed.clear();
|
|
536
|
+
|
|
462
537
|
for (const key of keys) {
|
|
463
538
|
const hold = this.holds.get(key);
|
|
539
|
+
|
|
464
540
|
for (const clientId of hold?.clients ?? []) {
|
|
465
541
|
this.options.notify(clientId, { chatId: hold!.chatId, toolUseId: hold!.toolUseId });
|
|
466
542
|
}
|
|
@@ -14,20 +14,25 @@ export interface SubagentSettlement {
|
|
|
14
14
|
function handbackBefore(lines: string[], end: number): string | null {
|
|
15
15
|
for (let i = end - 1; i >= 0; i--) {
|
|
16
16
|
let entry: unknown;
|
|
17
|
+
|
|
17
18
|
try {
|
|
18
19
|
entry = JSON.parse(lines[i]!);
|
|
19
20
|
} catch {
|
|
20
21
|
continue;
|
|
21
22
|
}
|
|
23
|
+
|
|
22
24
|
const message = isRecord(entry) && entry.type === 'assistant' && isRecord(entry.message) ? entry.message : null;
|
|
23
25
|
const content = message && Array.isArray(message.content) ? message.content.filter(isRecord) : [];
|
|
26
|
+
|
|
24
27
|
for (const block of content) {
|
|
25
28
|
const input = block.type === 'tool_use' && block.name === 'SubagentHandback' && isRecord(block.input) ? block.input : null;
|
|
29
|
+
|
|
26
30
|
if (input && typeof input.message === 'string' && input.message.trim() !== '') {
|
|
27
31
|
return input.message;
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
}
|
|
35
|
+
|
|
31
36
|
return null;
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -42,36 +47,48 @@ export function settlementOf(text: string): SubagentSettlement | null {
|
|
|
42
47
|
if (!text.endsWith('\n')) {
|
|
43
48
|
return null;
|
|
44
49
|
}
|
|
50
|
+
|
|
45
51
|
const lines = text.split('\n');
|
|
52
|
+
|
|
46
53
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
47
54
|
const line = lines[i]!.trim();
|
|
55
|
+
|
|
48
56
|
if (line === '') {
|
|
49
57
|
continue;
|
|
50
58
|
}
|
|
59
|
+
|
|
51
60
|
let entry: unknown;
|
|
61
|
+
|
|
52
62
|
try {
|
|
53
63
|
entry = JSON.parse(line);
|
|
54
64
|
} catch {
|
|
55
65
|
return null;
|
|
56
66
|
}
|
|
67
|
+
|
|
57
68
|
if (!isRecord(entry) || (entry.type !== 'user' && entry.type !== 'assistant')) {
|
|
58
69
|
continue;
|
|
59
70
|
}
|
|
71
|
+
|
|
60
72
|
const message = isRecord(entry.message) ? entry.message : {};
|
|
61
73
|
const content = Array.isArray(message.content) ? message.content.filter(isRecord) : [];
|
|
74
|
+
|
|
62
75
|
if (entry.type !== 'assistant' || message.stop_reason !== 'end_turn' || content.some((block) => block.type === 'tool_use')) {
|
|
63
76
|
return null;
|
|
64
77
|
}
|
|
78
|
+
|
|
65
79
|
const time = typeof entry.timestamp === 'string' ? Date.parse(entry.timestamp) : Number.NaN;
|
|
66
80
|
const handedBack = handbackBefore(lines, i);
|
|
81
|
+
|
|
67
82
|
const report =
|
|
68
83
|
handedBack ??
|
|
69
84
|
content
|
|
70
85
|
.map((block) => (block.type === 'text' && typeof block.text === 'string' ? block.text : ''))
|
|
71
86
|
.filter((part) => part.trim() !== '')
|
|
72
87
|
.join('\n');
|
|
88
|
+
|
|
73
89
|
return { finishedAt: Number.isFinite(time) ? time : null, report: report === '' ? null : report };
|
|
74
90
|
}
|
|
91
|
+
|
|
75
92
|
return null;
|
|
76
93
|
}
|
|
77
94
|
|
|
@@ -81,14 +98,17 @@ export function settlementOf(text: string): SubagentSettlement | null {
|
|
|
81
98
|
*/
|
|
82
99
|
export async function readSubagentSettlement(path: string): Promise<SubagentSettlement | null> {
|
|
83
100
|
let before: { size: number; mtimeMs: number };
|
|
101
|
+
|
|
84
102
|
try {
|
|
85
103
|
before = await stat(path);
|
|
86
104
|
} catch {
|
|
87
105
|
return null;
|
|
88
106
|
}
|
|
107
|
+
|
|
89
108
|
const from = Math.max(0, before.size - TAIL_BYTES);
|
|
90
109
|
const handle = await open(path, 'r');
|
|
91
110
|
let text: string;
|
|
111
|
+
|
|
92
112
|
try {
|
|
93
113
|
const bytes = Buffer.alloc(before.size - from);
|
|
94
114
|
const { bytesRead } = await handle.read(bytes, 0, bytes.length, from);
|
|
@@ -96,13 +116,17 @@ export async function readSubagentSettlement(path: string): Promise<SubagentSett
|
|
|
96
116
|
} finally {
|
|
97
117
|
await handle.close();
|
|
98
118
|
}
|
|
119
|
+
|
|
99
120
|
const after = await stat(path).catch(() => null);
|
|
121
|
+
|
|
100
122
|
if (after === null || after.size !== before.size || after.mtimeMs !== before.mtimeMs) {
|
|
101
123
|
return null;
|
|
102
124
|
}
|
|
125
|
+
|
|
103
126
|
if (from > 0) {
|
|
104
127
|
const start = text.indexOf('\n');
|
|
105
128
|
text = start < 0 ? '' : text.slice(start + 1);
|
|
106
129
|
}
|
|
130
|
+
|
|
107
131
|
return text === '' ? null : settlementOf(text);
|
|
108
132
|
}
|
|
@@ -29,7 +29,9 @@ function wordStart(text: string, from: number, limit: number): number {
|
|
|
29
29
|
if (from === 0 || /\s/.test(text[from - 1]!) || /\s/.test(text[from]!)) {
|
|
30
30
|
return from;
|
|
31
31
|
}
|
|
32
|
+
|
|
32
33
|
const space = text.slice(from, limit).search(/\s/);
|
|
34
|
+
|
|
33
35
|
return space === -1 ? from : from + space + 1;
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -38,7 +40,9 @@ function wordEnd(text: string, to: number, limit: number): number {
|
|
|
38
40
|
if (to === text.length || /\s/.test(text[to - 1]!) || /\s/.test(text[to]!)) {
|
|
39
41
|
return to;
|
|
40
42
|
}
|
|
43
|
+
|
|
41
44
|
const space = text.slice(limit, to).search(/\s\S*$/);
|
|
45
|
+
|
|
42
46
|
return space === -1 ? to : limit + space;
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -48,6 +52,7 @@ function snippetOf(text: string, index: number, length: number): { snippet: stri
|
|
|
48
52
|
const before = `${from > 0 ? '…' : ''}${plain(text.slice(from, index))}`;
|
|
49
53
|
const match = plain(text.slice(index, index + length));
|
|
50
54
|
const after = `${plain(text.slice(index + length, to))}${to < text.length ? '…' : ''}`;
|
|
55
|
+
|
|
51
56
|
return { snippet: `${before}${match}${after}`, matchStart: before.length, matchLength: match.length };
|
|
52
57
|
}
|
|
53
58
|
|
|
@@ -59,19 +64,25 @@ export function searchThreadItems(items: readonly ChatItem[], query: string, { l
|
|
|
59
64
|
if (query === '' || limit <= 0) {
|
|
60
65
|
return [];
|
|
61
66
|
}
|
|
67
|
+
|
|
62
68
|
const pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
|
63
69
|
const hits: ThreadHit[] = [];
|
|
70
|
+
|
|
64
71
|
for (const item of items) {
|
|
65
72
|
if (item.kind !== 'user' && item.kind !== 'assistant') {
|
|
66
73
|
continue;
|
|
67
74
|
}
|
|
75
|
+
|
|
68
76
|
if (item.kind === 'assistant' && item.parentToolUseId) {
|
|
69
77
|
continue;
|
|
70
78
|
}
|
|
79
|
+
|
|
71
80
|
const found = pattern.exec(item.text);
|
|
81
|
+
|
|
72
82
|
if (found === null) {
|
|
73
83
|
continue;
|
|
74
84
|
}
|
|
85
|
+
|
|
75
86
|
hits.push({
|
|
76
87
|
itemId: item.id,
|
|
77
88
|
author: item.kind === 'user' ? 'person' : 'agent',
|
|
@@ -79,9 +90,11 @@ export function searchThreadItems(items: readonly ChatItem[], query: string, { l
|
|
|
79
90
|
createdAt: item.createdAt,
|
|
80
91
|
turnId: item.turnId
|
|
81
92
|
});
|
|
93
|
+
|
|
82
94
|
if (hits.length >= limit) {
|
|
83
95
|
break;
|
|
84
96
|
}
|
|
85
97
|
}
|
|
98
|
+
|
|
86
99
|
return hits;
|
|
87
100
|
}
|