@shanepadgett/tau-agent 0.16.0 → 0.17.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/docs/subagents.md +25 -6
- package/extensions/attention/README.md +1 -0
- package/extensions/attention/index.ts +38 -0
- package/extensions/context/README.md +13 -3
- package/extensions/context/definitions.ts +3 -15
- package/extensions/context/evidence.ts +516 -0
- package/extensions/context/index.ts +114 -116
- package/extensions/context/panel.ts +60 -0
- package/extensions/context/settings.ts +30 -1
- package/extensions/context/sync.ts +171 -686
- package/extensions/context/validation.ts +4 -1
- package/extensions/context/write-scope.ts +109 -0
- package/extensions/context-pruning/README.md +22 -0
- package/extensions/context-pruning/file-evidence.ts +265 -0
- package/extensions/context-pruning/index.ts +379 -0
- package/extensions/context-pruning/projection.ts +108 -0
- package/extensions/context-pruning/prune.ts +346 -0
- package/extensions/context-pruning/render.ts +179 -0
- package/extensions/context-pruning/settings.ts +41 -0
- package/extensions/explore/README.md +3 -1
- package/extensions/explore/autoread.ts +85 -22
- package/extensions/explore/full-file-knowledge.ts +234 -0
- package/extensions/explore/index.ts +2 -3
- package/extensions/explore/read-cache.ts +150 -86
- package/extensions/explore/read-snapshots.ts +17 -4
- package/extensions/explore/read.ts +57 -38
- package/extensions/footer/index.ts +62 -60
- package/extensions/run-summary/index.ts +5 -5
- package/extensions/silent-command-runner/README.md +1 -1
- package/extensions/silent-command-runner/index.ts +45 -28
- package/extensions/soul/prompt.ts +3 -1
- package/extensions/subagent/README.md +21 -5
- package/extensions/subagent/agents/context-sync.md +90 -0
- package/extensions/subagent/agents/{generalist.md → dormant/generalist.md} +6 -0
- package/extensions/subagent/agents/{scout.md → dormant/scout.md} +6 -0
- package/extensions/subagent/agents/review.md +48 -0
- package/extensions/subagent/agents/web-research.md +6 -0
- package/extensions/subagent/agents.ts +15 -2
- package/extensions/subagent/cmux-dashboard.ts +454 -0
- package/extensions/subagent/index.ts +181 -234
- package/extensions/subagent/render.ts +1 -1
- package/extensions/subagent/resume.ts +78 -0
- package/extensions/subagent/run.ts +213 -118
- package/extensions/subagent/runtime.ts +856 -0
- package/extensions/subagent/session-resource.ts +169 -0
- package/extensions/tau-help/help.md +6 -2
- package/extensions/turn-budget/index.ts +8 -36
- package/package.json +2 -2
- package/schemas/tau.schema.json +48 -1
- package/shared/context-pruning-state.ts +364 -0
- package/shared/events.ts +18 -0
- package/shared/model-fallback/index.ts +21 -10
- package/shared/model-fallback/types.ts +5 -3
- package/shared/settings/load.ts +78 -1
- package/shared/tool-row-state.ts +21 -1
|
@@ -1,44 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
|
|
5
6
|
import {
|
|
6
|
-
createAgentSession,
|
|
7
7
|
DEFAULT_MAX_BYTES,
|
|
8
8
|
DEFAULT_MAX_LINES,
|
|
9
|
-
DefaultResourceLoader,
|
|
10
9
|
formatSize,
|
|
11
|
-
getAgentDir,
|
|
12
|
-
SessionManager,
|
|
13
10
|
truncateHead,
|
|
14
|
-
type AgentSession,
|
|
15
11
|
type AgentSessionEvent,
|
|
16
12
|
type ExtensionAPI,
|
|
17
13
|
type ExtensionContext,
|
|
18
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { createCompleteFileMeta } from "../explore/full-file-knowledge.ts";
|
|
19
16
|
import type { AgentDefinition } from "./agents.ts";
|
|
17
|
+
import { emptySubagentResumeState, type RetainedTurnOutcome, type SubagentResumeState } from "./resume.ts";
|
|
18
|
+
import {
|
|
19
|
+
createSubagentSessionResource,
|
|
20
|
+
resolveSubagentSessionInputs,
|
|
21
|
+
type SubagentSessionInputs,
|
|
22
|
+
type SubagentSessionResource,
|
|
23
|
+
} from "./session-resource.ts";
|
|
20
24
|
|
|
21
25
|
const PREVIEW_LIMIT = 600;
|
|
22
26
|
const VALUE_LIMIT = 180;
|
|
23
|
-
|
|
24
|
-
const CHILD_UI_BLOCKED_METHODS = new Set([
|
|
25
|
-
"setEditorComponent",
|
|
26
|
-
"setFooter",
|
|
27
|
-
"setStatus",
|
|
28
|
-
"setTitle",
|
|
29
|
-
"setWidget",
|
|
30
|
-
"setWorkingIndicator",
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
function childUiContext(ui: ExtensionContext["ui"]): ExtensionContext["ui"] {
|
|
34
|
-
return new Proxy(ui, {
|
|
35
|
-
get(target, property, receiver) {
|
|
36
|
-
if (typeof property === "string" && CHILD_UI_BLOCKED_METHODS.has(property)) return () => undefined;
|
|
37
|
-
const value = Reflect.get(target, property, receiver) as unknown;
|
|
38
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
}
|
|
27
|
+
const AUTOREAD_MESSAGE_TYPE = "tau.autoread";
|
|
42
28
|
|
|
43
29
|
export interface SubagentAction {
|
|
44
30
|
tool: string;
|
|
@@ -53,11 +39,17 @@ export interface SubagentUsage {
|
|
|
53
39
|
cost: number;
|
|
54
40
|
turns: number;
|
|
55
41
|
}
|
|
42
|
+
|
|
43
|
+
export type SubagentLifecycle = "waiting" | "starting" | "running" | "completed" | "failed" | "aborted";
|
|
44
|
+
export type SubagentPhase = "discovery" | "queue" | "startup" | "run" | "output";
|
|
45
|
+
|
|
56
46
|
export interface SubagentDetails {
|
|
57
47
|
agent: string;
|
|
48
|
+
displayName: string;
|
|
58
49
|
threadId?: string;
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
invocationId?: string;
|
|
51
|
+
status: SubagentLifecycle;
|
|
52
|
+
phase: SubagentPhase;
|
|
61
53
|
completionState?: string;
|
|
62
54
|
task: string;
|
|
63
55
|
model: string;
|
|
@@ -81,14 +73,24 @@ export interface SubagentDetails {
|
|
|
81
73
|
};
|
|
82
74
|
}
|
|
83
75
|
|
|
76
|
+
/** Immutable observer payload. Distinct invocation IDs for queued turns of one thread. */
|
|
77
|
+
export interface SubagentInvocationSnapshot extends SubagentDetails {
|
|
78
|
+
invocationId: string;
|
|
79
|
+
startedAt: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
84
82
|
export interface SubagentThread {
|
|
85
83
|
id: string;
|
|
84
|
+
displayName: string;
|
|
86
85
|
definition: AgentDefinition;
|
|
87
|
-
|
|
86
|
+
sessionInputs: SubagentSessionInputs;
|
|
87
|
+
resource: SubagentSessionResource;
|
|
88
88
|
cwd: string;
|
|
89
89
|
model: string;
|
|
90
90
|
thinkingLevel: string;
|
|
91
91
|
initialTask: string;
|
|
92
|
+
resumeState: SubagentResumeState;
|
|
93
|
+
lastAssistantMessageAt: number | undefined;
|
|
92
94
|
turns: number;
|
|
93
95
|
turnGate: FifoGate;
|
|
94
96
|
pendingTurns: number;
|
|
@@ -162,6 +164,18 @@ function capped(value: unknown, limit = VALUE_LIMIT): string {
|
|
|
162
164
|
return text.length <= limit ? text : `${text.slice(0, limit - 1)}…`;
|
|
163
165
|
}
|
|
164
166
|
|
|
167
|
+
/** Rolling tail so live previews keep changing after the limit. */
|
|
168
|
+
export function cappedTail(value: unknown, limit = PREVIEW_LIMIT): string {
|
|
169
|
+
let text: string;
|
|
170
|
+
try {
|
|
171
|
+
text = typeof value === "string" ? value : (JSON.stringify(value) ?? String(value));
|
|
172
|
+
} catch {
|
|
173
|
+
text = "[unavailable]";
|
|
174
|
+
}
|
|
175
|
+
if (text.length <= limit) return text;
|
|
176
|
+
return `…${text.slice(-(limit - 1))}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
165
179
|
function textOf(message: AssistantMessage): string {
|
|
166
180
|
return message.content
|
|
167
181
|
.filter((part) => part.type === "text")
|
|
@@ -169,8 +183,17 @@ function textOf(message: AssistantMessage): string {
|
|
|
169
183
|
.join("");
|
|
170
184
|
}
|
|
171
185
|
|
|
186
|
+
function cloneSnapshot(details: SubagentDetails): SubagentDetails {
|
|
187
|
+
return structuredClone(details);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function cloneInvocationSnapshot(snapshot: SubagentInvocationSnapshot): SubagentInvocationSnapshot {
|
|
191
|
+
return structuredClone(snapshot);
|
|
192
|
+
}
|
|
193
|
+
|
|
172
194
|
export async function createSubagentThread(options: {
|
|
173
195
|
id: string;
|
|
196
|
+
displayName: string;
|
|
174
197
|
definition: AgentDefinition;
|
|
175
198
|
extensionPaths: readonly string[];
|
|
176
199
|
initialTask: string;
|
|
@@ -181,6 +204,7 @@ export async function createSubagentThread(options: {
|
|
|
181
204
|
}): Promise<SubagentThread> {
|
|
182
205
|
const {
|
|
183
206
|
id,
|
|
207
|
+
displayName,
|
|
184
208
|
definition,
|
|
185
209
|
extensionPaths,
|
|
186
210
|
initialTask,
|
|
@@ -189,104 +213,76 @@ export async function createSubagentThread(options: {
|
|
|
189
213
|
signal,
|
|
190
214
|
onWarning,
|
|
191
215
|
} = options;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
else {
|
|
202
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(configured);
|
|
203
|
-
if (!auth.ok) onWarning?.(`model ${definition.model} is unavailable: ${auth.error}; using parent model`);
|
|
204
|
-
else model = configured;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (definition.thinking) {
|
|
208
|
-
const mapped = model?.thinkingLevelMap?.[definition.thinking];
|
|
209
|
-
const unsupported =
|
|
210
|
-
!model?.reasoning ||
|
|
211
|
-
mapped === null ||
|
|
212
|
-
((definition.thinking === "xhigh" || definition.thinking === "max") && mapped === undefined);
|
|
213
|
-
if (unsupported)
|
|
214
|
-
onWarning?.(`thinking ${definition.thinking} is unavailable for the selected model; using parent thinking`);
|
|
215
|
-
else thinkingLevel = definition.thinking;
|
|
216
|
-
}
|
|
217
|
-
const modelName = model ? `${model.provider}/${model.id}` : "unavailable";
|
|
218
|
-
let session: AgentSession | undefined;
|
|
216
|
+
const sessionInputs = await resolveSubagentSessionInputs({
|
|
217
|
+
definition,
|
|
218
|
+
extensionPaths,
|
|
219
|
+
ctx,
|
|
220
|
+
parentThinkingLevel,
|
|
221
|
+
signal,
|
|
222
|
+
...(onWarning === undefined ? {} : { onWarning }),
|
|
223
|
+
});
|
|
224
|
+
const resource = await createSubagentSessionResource(sessionInputs, signal);
|
|
219
225
|
try {
|
|
220
|
-
if (!model) throw new Error(`Agent ${definition.name} startup failed: parent has no model`);
|
|
221
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
222
|
-
if (!auth.ok) throw new Error(`Agent ${definition.name} startup failed: ${auth.error}`);
|
|
223
|
-
if (signal.aborted) throw new Error(`Agent ${definition.name} startup aborted`);
|
|
224
|
-
const resourceLoader = new DefaultResourceLoader({
|
|
225
|
-
cwd: ctx.cwd,
|
|
226
|
-
agentDir: getAgentDir(),
|
|
227
|
-
noExtensions: true,
|
|
228
|
-
additionalExtensionPaths: [...extensionPaths],
|
|
229
|
-
});
|
|
230
|
-
await resourceLoader.reload();
|
|
231
|
-
const created = await createAgentSession({
|
|
232
|
-
cwd: ctx.cwd,
|
|
233
|
-
model,
|
|
234
|
-
thinkingLevel: thinkingLevel as "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max",
|
|
235
|
-
tools: definition.tools,
|
|
236
|
-
excludeTools: ["subagent"],
|
|
237
|
-
resourceLoader,
|
|
238
|
-
sessionManager: SessionManager.inMemory(ctx.cwd),
|
|
239
|
-
});
|
|
240
|
-
session = created.session;
|
|
241
|
-
await session.bindExtensions(
|
|
242
|
-
ctx.mode === "tui" && ctx.hasUI ? { mode: "tui", uiContext: childUiContext(ctx.ui) } : { mode: "print" },
|
|
243
|
-
);
|
|
244
|
-
const active = session.getActiveToolNames().sort();
|
|
245
|
-
const expected = [...definition.tools].sort();
|
|
246
|
-
if (active.join("\0") !== expected.join("\0") || active.includes("subagent")) {
|
|
247
|
-
const missing = expected.filter((tool) => !active.includes(tool));
|
|
248
|
-
throw new Error(
|
|
249
|
-
`Agent ${definition.name} startup failed: unavailable tools: ${missing.join(", ") || "active tool mismatch"}`,
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
226
|
return {
|
|
253
227
|
id,
|
|
228
|
+
displayName,
|
|
254
229
|
definition,
|
|
255
|
-
|
|
230
|
+
sessionInputs,
|
|
231
|
+
resource,
|
|
256
232
|
cwd: ctx.cwd,
|
|
257
|
-
model: modelName,
|
|
258
|
-
thinkingLevel,
|
|
233
|
+
model: sessionInputs.modelName,
|
|
234
|
+
thinkingLevel: sessionInputs.thinkingLevel,
|
|
259
235
|
initialTask,
|
|
236
|
+
resumeState: emptySubagentResumeState(),
|
|
237
|
+
lastAssistantMessageAt: undefined,
|
|
260
238
|
turns: 0,
|
|
261
239
|
turnGate: new FifoGate(1),
|
|
262
240
|
pendingTurns: 0,
|
|
263
241
|
lastUsedAt: Date.now(),
|
|
264
242
|
};
|
|
265
243
|
} catch (error) {
|
|
266
|
-
|
|
267
|
-
session?.dispose();
|
|
244
|
+
await resource.dispose();
|
|
268
245
|
throw error;
|
|
269
246
|
}
|
|
270
247
|
}
|
|
271
248
|
|
|
272
249
|
export async function disposeSubagentThread(thread: SubagentThread): Promise<void> {
|
|
273
|
-
|
|
274
|
-
thread.session.dispose();
|
|
250
|
+
await thread.resource.dispose();
|
|
275
251
|
}
|
|
276
252
|
|
|
277
253
|
export async function runSubagentTurn(options: {
|
|
278
254
|
thread: SubagentThread;
|
|
279
255
|
task: string;
|
|
256
|
+
files?: readonly string[];
|
|
257
|
+
invocationId?: string;
|
|
280
258
|
initial: boolean;
|
|
259
|
+
resumePrompt?: string;
|
|
281
260
|
signal: AbortSignal;
|
|
282
261
|
onUpdate?: (details: SubagentDetails) => void | Promise<void>;
|
|
283
|
-
}): Promise<{
|
|
284
|
-
|
|
285
|
-
|
|
262
|
+
}): Promise<{
|
|
263
|
+
content: string;
|
|
264
|
+
details: SubagentDetails;
|
|
265
|
+
retainable: boolean;
|
|
266
|
+
terminalOutcome: RetainedTurnOutcome;
|
|
267
|
+
assistantMessageEndAt: readonly number[];
|
|
268
|
+
usage?: Usage;
|
|
269
|
+
}> {
|
|
270
|
+
const { thread, task, files = [], invocationId = thread.id, initial, resumePrompt, signal, onUpdate } = options;
|
|
271
|
+
const { definition } = thread;
|
|
272
|
+
const { session } = thread.resource;
|
|
286
273
|
const started = Date.now();
|
|
287
274
|
const usage: SubagentUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
275
|
+
const toolUsage: Usage = {
|
|
276
|
+
input: 0,
|
|
277
|
+
output: 0,
|
|
278
|
+
cacheRead: 0,
|
|
279
|
+
cacheWrite: 0,
|
|
280
|
+
totalTokens: 0,
|
|
281
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
282
|
+
};
|
|
288
283
|
const details: SubagentDetails = {
|
|
289
284
|
agent: definition.name,
|
|
285
|
+
displayName: thread.displayName,
|
|
290
286
|
threadId: thread.id,
|
|
291
287
|
status: "running",
|
|
292
288
|
phase: "run",
|
|
@@ -301,26 +297,67 @@ export async function runSubagentTurn(options: {
|
|
|
301
297
|
durationMs: 0,
|
|
302
298
|
};
|
|
303
299
|
let unsubscribe: (() => void) | undefined;
|
|
304
|
-
let updateChain = Promise.resolve();
|
|
305
300
|
let lastTextUpdate = 0;
|
|
306
301
|
const turnMessages: AssistantMessage[] = [];
|
|
302
|
+
const assistantMessageEndAt: number[] = [];
|
|
303
|
+
let retainable = false;
|
|
307
304
|
const publish = (force = false) => {
|
|
308
305
|
const now = Date.now();
|
|
309
306
|
if (!force && now - lastTextUpdate < 100) return;
|
|
310
307
|
lastTextUpdate = now;
|
|
311
308
|
details.durationMs = now - started;
|
|
312
|
-
const snapshot =
|
|
309
|
+
const snapshot = cloneSnapshot(details);
|
|
313
310
|
snapshot.actions = snapshot.actions.slice(-5);
|
|
314
|
-
|
|
311
|
+
// Detached observer chain — must not delay prompt completion.
|
|
312
|
+
void Promise.resolve()
|
|
313
|
+
.then(() => onUpdate?.(snapshot))
|
|
314
|
+
.catch(() => undefined);
|
|
315
315
|
};
|
|
316
316
|
try {
|
|
317
|
+
if (signal.aborted) {
|
|
318
|
+
details.status = "aborted";
|
|
319
|
+
details.error = "Subagent call aborted before prompt";
|
|
320
|
+
details.durationMs = Date.now() - started;
|
|
321
|
+
publish(true);
|
|
322
|
+
return {
|
|
323
|
+
content: details.error,
|
|
324
|
+
details,
|
|
325
|
+
retainable: false,
|
|
326
|
+
terminalOutcome: "aborted",
|
|
327
|
+
assistantMessageEndAt,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
317
330
|
const actionById = new Map<string, string>();
|
|
318
331
|
unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
319
332
|
if (event.type === "message_update" && event.message.role === "assistant") {
|
|
320
|
-
details.response =
|
|
333
|
+
details.response = cappedTail(textOf(event.message), PREVIEW_LIMIT);
|
|
321
334
|
publish();
|
|
322
335
|
} else if (event.type === "message_end" && event.message.role === "assistant") {
|
|
323
336
|
turnMessages.push(event.message);
|
|
337
|
+
assistantMessageEndAt.push(Date.now());
|
|
338
|
+
usage.input += event.message.usage.input;
|
|
339
|
+
usage.output += event.message.usage.output;
|
|
340
|
+
usage.cacheRead += event.message.usage.cacheRead;
|
|
341
|
+
usage.cacheWrite += event.message.usage.cacheWrite;
|
|
342
|
+
usage.cost += event.message.usage.cost.total;
|
|
343
|
+
usage.turns += 1;
|
|
344
|
+
toolUsage.input += event.message.usage.input;
|
|
345
|
+
toolUsage.output += event.message.usage.output;
|
|
346
|
+
toolUsage.cacheRead += event.message.usage.cacheRead;
|
|
347
|
+
toolUsage.cacheWrite += event.message.usage.cacheWrite;
|
|
348
|
+
toolUsage.totalTokens += event.message.usage.totalTokens;
|
|
349
|
+
toolUsage.cost.input += event.message.usage.cost.input;
|
|
350
|
+
toolUsage.cost.output += event.message.usage.cost.output;
|
|
351
|
+
toolUsage.cost.cacheRead += event.message.usage.cost.cacheRead;
|
|
352
|
+
toolUsage.cost.cacheWrite += event.message.usage.cost.cacheWrite;
|
|
353
|
+
toolUsage.cost.total += event.message.usage.cost.total;
|
|
354
|
+
if (event.message.usage.cacheWrite1h !== undefined)
|
|
355
|
+
toolUsage.cacheWrite1h = (toolUsage.cacheWrite1h ?? 0) + event.message.usage.cacheWrite1h;
|
|
356
|
+
if (event.message.usage.reasoning !== undefined)
|
|
357
|
+
toolUsage.reasoning = (toolUsage.reasoning ?? 0) + event.message.usage.reasoning;
|
|
358
|
+
details.response = cappedTail(textOf(event.message), PREVIEW_LIMIT);
|
|
359
|
+
details.currentActivity = undefined;
|
|
360
|
+
publish(true);
|
|
324
361
|
} else if (event.type === "tool_execution_start") {
|
|
325
362
|
details.toolCalls += 1;
|
|
326
363
|
const summary = `${event.toolName} ${capped(event.args)}`.trim();
|
|
@@ -345,27 +382,73 @@ export async function runSubagentTurn(options: {
|
|
|
345
382
|
}
|
|
346
383
|
});
|
|
347
384
|
const abort = () => {
|
|
348
|
-
void session
|
|
385
|
+
void session.abort().catch(() => undefined);
|
|
349
386
|
};
|
|
350
387
|
signal.addEventListener("abort", abort, { once: true });
|
|
351
388
|
try {
|
|
389
|
+
const autoreadMessages = await Promise.all(
|
|
390
|
+
files.map(async (file, index) => {
|
|
391
|
+
const pathKey = resolve(thread.cwd, file);
|
|
392
|
+
const common = {
|
|
393
|
+
rowId: `${invocationId}:${index}`,
|
|
394
|
+
path: file,
|
|
395
|
+
cwd: thread.cwd,
|
|
396
|
+
source: "subagent",
|
|
397
|
+
batchId: invocationId,
|
|
398
|
+
};
|
|
399
|
+
try {
|
|
400
|
+
const bytes = await readFile(pathKey);
|
|
401
|
+
const content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
402
|
+
const lines = content.split("\n");
|
|
403
|
+
const numbered = lines.map((line, lineIndex) => `${lineIndex + 1}: ${line}`).join("\n");
|
|
404
|
+
return {
|
|
405
|
+
customType: AUTOREAD_MESSAGE_TYPE,
|
|
406
|
+
content: `${file}\n${numbered}`,
|
|
407
|
+
display: false,
|
|
408
|
+
details: {
|
|
409
|
+
...common,
|
|
410
|
+
status: "read",
|
|
411
|
+
readCache: createCompleteFileMeta({
|
|
412
|
+
pathKey,
|
|
413
|
+
presentation: "line-numbered",
|
|
414
|
+
servedHash: createHash("sha256").update(bytes).digest("hex"),
|
|
415
|
+
mode: "baseline",
|
|
416
|
+
sourceText: content,
|
|
417
|
+
returnedText: `${file}\n${numbered}`,
|
|
418
|
+
totalLines: lines.length,
|
|
419
|
+
summary: `${lines.length} lines`,
|
|
420
|
+
}),
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
} catch (error) {
|
|
424
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
425
|
+
return {
|
|
426
|
+
customType: AUTOREAD_MESSAGE_TYPE,
|
|
427
|
+
content: `${file}\nAutoread failed: ${message}`,
|
|
428
|
+
display: false,
|
|
429
|
+
details: { ...common, status: "failed", error: message },
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}),
|
|
433
|
+
);
|
|
434
|
+
if (signal.aborted) throw new Error("Subagent call aborted before prompt");
|
|
435
|
+
for (const message of autoreadMessages) {
|
|
436
|
+
if (signal.aborted) throw new Error("Subagent call aborted during autoread delivery");
|
|
437
|
+
await session.sendCustomMessage<unknown>(message, { deliverAs: "nextTurn" });
|
|
438
|
+
}
|
|
439
|
+
if (signal.aborted) throw new Error("Subagent call aborted before prompt");
|
|
352
440
|
await session.prompt(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
441
|
+
resumePrompt ??
|
|
442
|
+
(initial
|
|
443
|
+
? `You are an isolated delegated child agent. Stay within the delegated task and return only the requested result.${files.length ? " Parent-supplied autoread files are included as line-numbered context for this turn." : ""}\n\n## Agent instructions\n${definition.prompt}\n\n## Delegated task\n${task}`
|
|
444
|
+
: `Continue the existing delegated work using the context already in this thread. Return only the requested result.${files.length ? " Parent-supplied autoread files are included as line-numbered context for this turn." : ""}\n\n## Parent follow-up\n${task}`),
|
|
356
445
|
{ expandPromptTemplates: false },
|
|
357
446
|
);
|
|
358
447
|
} finally {
|
|
359
448
|
signal.removeEventListener("abort", abort);
|
|
360
449
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
usage.output += message.usage.output;
|
|
364
|
-
usage.cacheRead += message.usage.cacheRead;
|
|
365
|
-
usage.cacheWrite += message.usage.cacheWrite;
|
|
366
|
-
usage.cost += message.usage.cost.total;
|
|
367
|
-
usage.turns += 1;
|
|
368
|
-
}
|
|
450
|
+
// Prompt returned without throw — session remains usable for retention decisions.
|
|
451
|
+
retainable = true;
|
|
369
452
|
const terminal = turnMessages.at(-1);
|
|
370
453
|
if (!terminal) throw new Error(`Agent ${definition.name} run failed: no terminal assistant response`);
|
|
371
454
|
const response = textOf(terminal);
|
|
@@ -406,21 +489,33 @@ export async function runSubagentTurn(options: {
|
|
|
406
489
|
}
|
|
407
490
|
details.durationMs = Date.now() - started;
|
|
408
491
|
publish(true);
|
|
409
|
-
await updateChain;
|
|
410
492
|
return {
|
|
411
493
|
content:
|
|
412
494
|
details.status === "completed"
|
|
413
495
|
? (details.response ?? "")
|
|
414
496
|
: (details.error ?? `Agent ${definition.name} failed`),
|
|
415
497
|
details,
|
|
498
|
+
retainable,
|
|
499
|
+
terminalOutcome:
|
|
500
|
+
details.status === "completed" ? "completed" : details.status === "aborted" ? "aborted" : "failed",
|
|
501
|
+
assistantMessageEndAt,
|
|
502
|
+
...(usage.turns === 0 ? {} : { usage: toolUsage }),
|
|
416
503
|
};
|
|
417
504
|
} catch (error) {
|
|
418
505
|
details.status = signal.aborted ? "aborted" : "failed";
|
|
419
506
|
details.error = capped(error instanceof Error ? error.message : "Subagent failed");
|
|
420
507
|
details.durationMs = Date.now() - started;
|
|
508
|
+
// Prompt/session failures leave the child unsafe for reuse.
|
|
509
|
+
retainable = false;
|
|
421
510
|
publish(true);
|
|
422
|
-
|
|
423
|
-
|
|
511
|
+
return {
|
|
512
|
+
content: details.error,
|
|
513
|
+
details,
|
|
514
|
+
retainable,
|
|
515
|
+
terminalOutcome: details.status === "aborted" ? "aborted" : "failed",
|
|
516
|
+
assistantMessageEndAt,
|
|
517
|
+
...(usage.turns === 0 ? {} : { usage: toolUsage }),
|
|
518
|
+
};
|
|
424
519
|
} finally {
|
|
425
520
|
unsubscribe?.();
|
|
426
521
|
thread.turns += 1;
|