@shanepadgett/tau-agent 0.42.3 → 0.44.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 +0 -1
- package/docs/context.md +1 -1
- package/docs/extending-tau-agent.md +1 -2
- package/extensions/attention/README.md +1 -1
- package/extensions/cache-diagnostics/index.ts +4 -0
- package/extensions/context/README.md +1 -24
- package/extensions/context/definitions.ts +1 -52
- package/extensions/context/index.ts +1 -150
- package/extensions/context/panel.ts +0 -72
- package/extensions/explore/guidance.ts +9 -1
- package/extensions/footer/README.md +1 -1
- package/extensions/footer/index.ts +3 -25
- package/extensions/qna/choice-question-body.ts +7 -2
- package/extensions/run-summary/README.md +1 -1
- package/extensions/run-summary/index.ts +18 -25
- package/extensions/runtime-context/README.md +1 -1
- package/extensions/runtime-context/context.ts +1 -1
- package/extensions/runtime-context/index.ts +10 -16
- package/extensions/script-runner/index.ts +21 -29
- package/extensions/silent-command-runner/README.md +0 -2
- package/extensions/silent-command-runner/index.ts +12 -41
- package/extensions/soul/README.md +5 -8
- package/extensions/soul/context.ts +115 -0
- package/extensions/soul/index.ts +141 -7
- package/extensions/soul/prompt.ts +47 -102
- package/extensions/soul/state.ts +114 -0
- package/extensions/soul/tools.ts +31 -0
- package/extensions/tau-help/help.md +6 -18
- package/extensions/tau-help/index.ts +10 -4
- package/extensions/tool-approval/README.md +4 -2
- package/extensions/tool-approval/index.ts +141 -53
- package/extensions/tool-approval/panel.ts +153 -0
- package/extensions/tool-loader/README.md +1 -1
- package/extensions/tool-loader/index.ts +91 -22
- package/package.json +2 -2
- package/schemas/tau.schema.json +0 -104
- package/shared/bounded-text-result.ts +0 -1
- package/shared/events.ts +19 -15
- package/shared/isolated-session.ts +1 -2
- package/shared/model-effort.ts +15 -21
- package/shared/prompt-contributions.ts +24 -0
- package/src/index.ts +1 -1
- package/docs/subagents.md +0 -92
- package/extensions/auto-compact/README.md +0 -9
- package/extensions/auto-compact/index.ts +0 -122
- package/extensions/auto-compact/settings.ts +0 -30
- package/extensions/context/settings.ts +0 -62
- package/extensions/context/sync.ts +0 -276
- package/extensions/context/validation.ts +0 -88
- package/extensions/effort/README.md +0 -7
- package/extensions/effort/index.ts +0 -134
- package/extensions/effort/state.ts +0 -18
- package/extensions/qna/inline-editor-row.ts +0 -56
- package/extensions/soul/overseer.ts +0 -273
- package/extensions/soul/settings.ts +0 -34
- package/extensions/subagent/README.md +0 -67
- package/extensions/subagent/agents/context-sync.md +0 -244
- package/extensions/subagent/agents/dormant/generalist.md +0 -33
- package/extensions/subagent/agents/scout.md +0 -104
- package/extensions/subagent/agents/web-research.md +0 -101
- package/extensions/subagent/agents.ts +0 -261
- package/extensions/subagent/cmux-dashboard.ts +0 -495
- package/extensions/subagent/index.ts +0 -425
- package/extensions/subagent/panel.ts +0 -124
- package/extensions/subagent/render.ts +0 -74
- package/extensions/subagent/resume.ts +0 -78
- package/extensions/subagent/run.ts +0 -641
- package/extensions/subagent/runtime.ts +0 -1296
- package/extensions/subagent/session-resource.ts +0 -61
- package/extensions/subagent/settings.ts +0 -18
|
@@ -1,641 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
5
|
-
import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT_MAX_BYTES,
|
|
8
|
-
DEFAULT_MAX_LINES,
|
|
9
|
-
formatSize,
|
|
10
|
-
truncateHead,
|
|
11
|
-
type AgentSessionEvent,
|
|
12
|
-
type ExtensionAPI,
|
|
13
|
-
type ExtensionContext,
|
|
14
|
-
} from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { createCompleteFileMeta } from "../../shared/full-file-knowledge.ts";
|
|
16
|
-
import { createIsolatedSessionResource, type IsolatedSessionResource } from "../../shared/isolated-session.ts";
|
|
17
|
-
import { FILE_INJECTION_TYPE } from "@shanepadgett/tau-agent";
|
|
18
|
-
import type { AgentDefinition } from "./agents.ts";
|
|
19
|
-
import { emptySubagentResumeState, type RetainedTurnOutcome, type SubagentResumeState } from "./resume.ts";
|
|
20
|
-
import { resolveSubagentSessionInputs, type SubagentSessionInputs } from "./session-resource.ts";
|
|
21
|
-
|
|
22
|
-
const PREVIEW_LIMIT = 600;
|
|
23
|
-
const VALUE_LIMIT = 180;
|
|
24
|
-
|
|
25
|
-
export interface SubagentAction {
|
|
26
|
-
tool: string;
|
|
27
|
-
summary: string;
|
|
28
|
-
error: boolean;
|
|
29
|
-
}
|
|
30
|
-
export interface SubagentUsage {
|
|
31
|
-
input: number;
|
|
32
|
-
output: number;
|
|
33
|
-
cacheRead: number;
|
|
34
|
-
cacheWrite: number;
|
|
35
|
-
cost: number;
|
|
36
|
-
turns: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type SubagentLifecycle = "waiting" | "starting" | "running" | "completed" | "failed" | "aborted";
|
|
40
|
-
export type SubagentPhase = "discovery" | "queue" | "startup" | "run" | "output";
|
|
41
|
-
|
|
42
|
-
export interface SubagentDetails {
|
|
43
|
-
agent: string;
|
|
44
|
-
displayName: string;
|
|
45
|
-
threadId?: string;
|
|
46
|
-
invocationId?: string;
|
|
47
|
-
status: SubagentLifecycle;
|
|
48
|
-
phase: SubagentPhase;
|
|
49
|
-
completionState?: string;
|
|
50
|
-
task: string;
|
|
51
|
-
model: string;
|
|
52
|
-
thinkingLevel: string;
|
|
53
|
-
response?: string;
|
|
54
|
-
error?: string;
|
|
55
|
-
currentActivity?: string;
|
|
56
|
-
toolCalls: number;
|
|
57
|
-
actions: SubagentAction[];
|
|
58
|
-
omittedActions: number;
|
|
59
|
-
omittedErrors: number;
|
|
60
|
-
usage: SubagentUsage;
|
|
61
|
-
contextPercent?: number;
|
|
62
|
-
durationMs: number;
|
|
63
|
-
truncation?: {
|
|
64
|
-
truncated: boolean;
|
|
65
|
-
path?: string;
|
|
66
|
-
outputLines: number;
|
|
67
|
-
totalLines: number;
|
|
68
|
-
outputBytes: number;
|
|
69
|
-
totalBytes: number;
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Immutable observer payload. Distinct invocation IDs for queued turns of one thread. */
|
|
74
|
-
export interface SubagentInvocationSnapshot extends SubagentDetails {
|
|
75
|
-
invocationId: string;
|
|
76
|
-
startedAt: number;
|
|
77
|
-
files: readonly string[];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface SubagentThread {
|
|
81
|
-
id: string;
|
|
82
|
-
displayName: string;
|
|
83
|
-
definition: AgentDefinition;
|
|
84
|
-
sessionInputs: SubagentSessionInputs;
|
|
85
|
-
resource: IsolatedSessionResource;
|
|
86
|
-
cwd: string;
|
|
87
|
-
model: string;
|
|
88
|
-
thinkingLevel: string;
|
|
89
|
-
initialTask: string;
|
|
90
|
-
resumeState: SubagentResumeState;
|
|
91
|
-
lastAssistantMessageAt: number | undefined;
|
|
92
|
-
turns: number;
|
|
93
|
-
turnGate: FifoGate;
|
|
94
|
-
pendingTurns: number;
|
|
95
|
-
lastUsedAt: number;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function extensionPathsForTools(pi: ExtensionAPI, tools: readonly string[]): string[] {
|
|
99
|
-
const selected = new Set(tools);
|
|
100
|
-
return [
|
|
101
|
-
...new Set(
|
|
102
|
-
pi
|
|
103
|
-
.getAllTools()
|
|
104
|
-
.filter((tool) => selected.has(tool.name))
|
|
105
|
-
.map((tool) => tool.sourceInfo.path)
|
|
106
|
-
.filter((path) => path.length > 0 && !path.startsWith("<")),
|
|
107
|
-
),
|
|
108
|
-
].sort();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export class FifoGate {
|
|
112
|
-
private active = 0;
|
|
113
|
-
private readonly waiters: Array<{
|
|
114
|
-
signal: AbortSignal;
|
|
115
|
-
resolve: (release: () => void) => void;
|
|
116
|
-
reject: (error: Error) => void;
|
|
117
|
-
}> = [];
|
|
118
|
-
private readonly limit: number;
|
|
119
|
-
constructor(limit = 4) {
|
|
120
|
-
this.limit = limit;
|
|
121
|
-
}
|
|
122
|
-
acquire(signal: AbortSignal): Promise<() => void> {
|
|
123
|
-
if (signal.aborted) return Promise.reject(new Error("Subagent call aborted while waiting"));
|
|
124
|
-
if (this.active < this.limit) {
|
|
125
|
-
this.active += 1;
|
|
126
|
-
return Promise.resolve(this.releaseOnce());
|
|
127
|
-
}
|
|
128
|
-
return new Promise((resolve, reject) => {
|
|
129
|
-
const waiter = { signal, resolve: (_release: () => void) => {}, reject };
|
|
130
|
-
const abort = () => {
|
|
131
|
-
const index = this.waiters.indexOf(waiter);
|
|
132
|
-
if (index >= 0) this.waiters.splice(index, 1);
|
|
133
|
-
reject(new Error("Subagent call aborted while waiting"));
|
|
134
|
-
};
|
|
135
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
136
|
-
waiter.resolve = (release) => {
|
|
137
|
-
signal.removeEventListener("abort", abort);
|
|
138
|
-
resolve(release);
|
|
139
|
-
};
|
|
140
|
-
this.waiters.push(waiter);
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
private releaseOnce(): () => void {
|
|
144
|
-
let released = false;
|
|
145
|
-
return () => {
|
|
146
|
-
if (released) return;
|
|
147
|
-
released = true;
|
|
148
|
-
const next = this.waiters.shift();
|
|
149
|
-
if (next) next.resolve(this.releaseOnce());
|
|
150
|
-
else this.active -= 1;
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function capped(value: unknown, limit = VALUE_LIMIT): string {
|
|
156
|
-
let text: string;
|
|
157
|
-
try {
|
|
158
|
-
text = typeof value === "string" ? value : (JSON.stringify(value) ?? String(value));
|
|
159
|
-
} catch {
|
|
160
|
-
text = "[unavailable]";
|
|
161
|
-
}
|
|
162
|
-
return text.length <= limit ? text : `${text.slice(0, limit - 1)}…`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Rolling tail so live previews keep changing after the limit. */
|
|
166
|
-
function cappedTail(value: unknown, limit = PREVIEW_LIMIT): string {
|
|
167
|
-
let text: string;
|
|
168
|
-
try {
|
|
169
|
-
text = typeof value === "string" ? value : (JSON.stringify(value) ?? String(value));
|
|
170
|
-
} catch {
|
|
171
|
-
text = "[unavailable]";
|
|
172
|
-
}
|
|
173
|
-
if (text.length <= limit) return text;
|
|
174
|
-
return `…${text.slice(-(limit - 1))}`;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function textOf(message: AssistantMessage): string {
|
|
178
|
-
return message.content
|
|
179
|
-
.filter((part) => part.type === "text")
|
|
180
|
-
.map((part) => part.text)
|
|
181
|
-
.join("");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function cloneSnapshot(details: SubagentDetails): SubagentDetails {
|
|
185
|
-
return structuredClone(details);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function cloneInvocationSnapshot(snapshot: SubagentInvocationSnapshot): SubagentInvocationSnapshot {
|
|
189
|
-
return structuredClone(snapshot);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export async function createSubagentThread(options: {
|
|
193
|
-
id: string;
|
|
194
|
-
displayName: string;
|
|
195
|
-
definition: AgentDefinition;
|
|
196
|
-
extensionPaths: readonly string[];
|
|
197
|
-
initialTask: string;
|
|
198
|
-
ctx: ExtensionContext;
|
|
199
|
-
thinkingLevel: NonNullable<ExtensionContext["thinkingLevel"]>;
|
|
200
|
-
signal: AbortSignal;
|
|
201
|
-
onWarning?: (warning: string) => void;
|
|
202
|
-
}): Promise<SubagentThread> {
|
|
203
|
-
const {
|
|
204
|
-
id,
|
|
205
|
-
displayName,
|
|
206
|
-
definition,
|
|
207
|
-
extensionPaths,
|
|
208
|
-
initialTask,
|
|
209
|
-
ctx,
|
|
210
|
-
thinkingLevel: parentThinkingLevel,
|
|
211
|
-
signal,
|
|
212
|
-
onWarning,
|
|
213
|
-
} = options;
|
|
214
|
-
const sessionInputs = await resolveSubagentSessionInputs({
|
|
215
|
-
definition,
|
|
216
|
-
extensionPaths,
|
|
217
|
-
ctx,
|
|
218
|
-
parentThinkingLevel,
|
|
219
|
-
signal,
|
|
220
|
-
...(onWarning === undefined ? {} : { onWarning }),
|
|
221
|
-
});
|
|
222
|
-
const resource = await createIsolatedSessionResource(sessionInputs, signal);
|
|
223
|
-
try {
|
|
224
|
-
return {
|
|
225
|
-
id,
|
|
226
|
-
displayName,
|
|
227
|
-
definition,
|
|
228
|
-
sessionInputs,
|
|
229
|
-
resource,
|
|
230
|
-
cwd: ctx.cwd,
|
|
231
|
-
model: sessionInputs.modelName,
|
|
232
|
-
thinkingLevel: sessionInputs.thinkingLevel,
|
|
233
|
-
initialTask,
|
|
234
|
-
resumeState: emptySubagentResumeState(),
|
|
235
|
-
lastAssistantMessageAt: undefined,
|
|
236
|
-
turns: 0,
|
|
237
|
-
turnGate: new FifoGate(1),
|
|
238
|
-
pendingTurns: 0,
|
|
239
|
-
lastUsedAt: Date.now(),
|
|
240
|
-
};
|
|
241
|
-
} catch (error) {
|
|
242
|
-
await resource.dispose();
|
|
243
|
-
throw error;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export async function disposeSubagentThread(thread: SubagentThread): Promise<void> {
|
|
248
|
-
await thread.resource.dispose();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function emptyTurnUsage(): { usage: SubagentUsage; toolUsage: Usage } {
|
|
252
|
-
return {
|
|
253
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
|
|
254
|
-
toolUsage: {
|
|
255
|
-
input: 0,
|
|
256
|
-
output: 0,
|
|
257
|
-
cacheRead: 0,
|
|
258
|
-
cacheWrite: 0,
|
|
259
|
-
totalTokens: 0,
|
|
260
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function accumulateAssistantUsage(usage: SubagentUsage, toolUsage: Usage, message: AssistantMessage): void {
|
|
266
|
-
usage.input += message.usage.input;
|
|
267
|
-
usage.output += message.usage.output;
|
|
268
|
-
usage.cacheRead += message.usage.cacheRead;
|
|
269
|
-
usage.cacheWrite += message.usage.cacheWrite;
|
|
270
|
-
usage.cost += message.usage.cost.total;
|
|
271
|
-
usage.turns += 1;
|
|
272
|
-
toolUsage.input += message.usage.input;
|
|
273
|
-
toolUsage.output += message.usage.output;
|
|
274
|
-
toolUsage.cacheRead += message.usage.cacheRead;
|
|
275
|
-
toolUsage.cacheWrite += message.usage.cacheWrite;
|
|
276
|
-
toolUsage.totalTokens += message.usage.totalTokens;
|
|
277
|
-
toolUsage.cost.input += message.usage.cost.input;
|
|
278
|
-
toolUsage.cost.output += message.usage.cost.output;
|
|
279
|
-
toolUsage.cost.cacheRead += message.usage.cost.cacheRead;
|
|
280
|
-
toolUsage.cost.cacheWrite += message.usage.cost.cacheWrite;
|
|
281
|
-
toolUsage.cost.total += message.usage.cost.total;
|
|
282
|
-
if (message.usage.cacheWrite1h !== undefined)
|
|
283
|
-
toolUsage.cacheWrite1h = (toolUsage.cacheWrite1h ?? 0) + message.usage.cacheWrite1h;
|
|
284
|
-
if (message.usage.reasoning !== undefined)
|
|
285
|
-
toolUsage.reasoning = (toolUsage.reasoning ?? 0) + message.usage.reasoning;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function pushBoundedAction(details: SubagentDetails, action: SubagentAction): void {
|
|
289
|
-
details.actions.push(action);
|
|
290
|
-
if (details.actions.length <= 20) return;
|
|
291
|
-
const removable = details.actions.findIndex((item) => !item.error);
|
|
292
|
-
const [removed] = details.actions.splice(removable >= 0 ? removable : 0, 1);
|
|
293
|
-
details.omittedActions += 1;
|
|
294
|
-
if (removed?.error) details.omittedErrors += 1;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function subscribeTurnEvents(options: {
|
|
298
|
-
session: SubagentThread["resource"]["session"];
|
|
299
|
-
details: SubagentDetails;
|
|
300
|
-
usage: SubagentUsage;
|
|
301
|
-
toolUsage: Usage;
|
|
302
|
-
turnMessages: AssistantMessage[];
|
|
303
|
-
assistantMessageEndAt: number[];
|
|
304
|
-
publish: (force?: boolean) => void;
|
|
305
|
-
}): () => void {
|
|
306
|
-
const { session, details, usage, toolUsage, turnMessages, assistantMessageEndAt, publish } = options;
|
|
307
|
-
const actionById = new Map<string, string>();
|
|
308
|
-
return session.subscribe((event: AgentSessionEvent) => {
|
|
309
|
-
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
310
|
-
details.response = "";
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
314
|
-
details.response = cappedTail(details.response + event.assistantMessageEvent.delta, PREVIEW_LIMIT);
|
|
315
|
-
publish();
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
319
|
-
turnMessages.push(event.message);
|
|
320
|
-
assistantMessageEndAt.push(Date.now());
|
|
321
|
-
accumulateAssistantUsage(usage, toolUsage, event.message);
|
|
322
|
-
details.response = cappedTail(textOf(event.message), PREVIEW_LIMIT);
|
|
323
|
-
details.currentActivity = undefined;
|
|
324
|
-
publish(true);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (event.type === "tool_execution_start") {
|
|
328
|
-
details.toolCalls += 1;
|
|
329
|
-
const summary = `${event.toolName} ${capped(event.args)}`.trim();
|
|
330
|
-
actionById.set(event.toolCallId, summary);
|
|
331
|
-
details.currentActivity = summary;
|
|
332
|
-
publish(true);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
if (event.type === "tool_execution_end") {
|
|
336
|
-
pushBoundedAction(details, {
|
|
337
|
-
tool: event.toolName,
|
|
338
|
-
summary: actionById.get(event.toolCallId) ?? event.toolName,
|
|
339
|
-
error: event.isError,
|
|
340
|
-
});
|
|
341
|
-
details.currentActivity = undefined;
|
|
342
|
-
publish(true);
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async function buildAutoreadMessages(options: { files: readonly string[]; cwd: string; invocationId: string }): Promise<
|
|
348
|
-
Array<{
|
|
349
|
-
customType: typeof FILE_INJECTION_TYPE;
|
|
350
|
-
content: string;
|
|
351
|
-
display: false;
|
|
352
|
-
details: Record<string, unknown>;
|
|
353
|
-
}>
|
|
354
|
-
> {
|
|
355
|
-
const { files, cwd, invocationId } = options;
|
|
356
|
-
return Promise.all(
|
|
357
|
-
files.map(async (file, index) => {
|
|
358
|
-
const pathKey = resolve(cwd, file);
|
|
359
|
-
const common = {
|
|
360
|
-
v: 1 as const,
|
|
361
|
-
rowId: `${invocationId}:${index}`,
|
|
362
|
-
path: file,
|
|
363
|
-
cwd,
|
|
364
|
-
source: "subagent",
|
|
365
|
-
batchId: invocationId,
|
|
366
|
-
};
|
|
367
|
-
try {
|
|
368
|
-
const bytes = await readFile(pathKey);
|
|
369
|
-
const content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
370
|
-
const lines = content.split("\n");
|
|
371
|
-
const numbered = lines.map((line, lineIndex) => `${lineIndex + 1}: ${line}`).join("\n");
|
|
372
|
-
return {
|
|
373
|
-
customType: FILE_INJECTION_TYPE,
|
|
374
|
-
content: `${file}\n${numbered}`,
|
|
375
|
-
display: false as const,
|
|
376
|
-
details: {
|
|
377
|
-
...common,
|
|
378
|
-
kind: "full",
|
|
379
|
-
status: "injected",
|
|
380
|
-
readCache: createCompleteFileMeta({
|
|
381
|
-
pathKey,
|
|
382
|
-
presentation: "line-numbered",
|
|
383
|
-
servedHash: createHash("sha256").update(bytes).digest("hex"),
|
|
384
|
-
mode: "baseline",
|
|
385
|
-
sourceText: content,
|
|
386
|
-
returnedText: `${file}\n${numbered}`,
|
|
387
|
-
totalLines: lines.length,
|
|
388
|
-
summary: `${lines.length} lines`,
|
|
389
|
-
}),
|
|
390
|
-
},
|
|
391
|
-
};
|
|
392
|
-
} catch (error) {
|
|
393
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
394
|
-
return {
|
|
395
|
-
customType: FILE_INJECTION_TYPE,
|
|
396
|
-
content: `${file}\nInjection failed: ${message}`,
|
|
397
|
-
display: false as const,
|
|
398
|
-
details: { ...common, kind: "full", status: "failed", error: message },
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
}),
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function buildTurnPrompt(options: {
|
|
406
|
-
definition: AgentDefinition;
|
|
407
|
-
task: string;
|
|
408
|
-
files: readonly string[];
|
|
409
|
-
initial: boolean;
|
|
410
|
-
resumePrompt: string | undefined;
|
|
411
|
-
}): string {
|
|
412
|
-
const { definition, task, files, initial, resumePrompt } = options;
|
|
413
|
-
if (resumePrompt !== undefined) return resumePrompt;
|
|
414
|
-
const fileNote = files.length
|
|
415
|
-
? " Parent-supplied autoread files are included as line-numbered context for this turn."
|
|
416
|
-
: "";
|
|
417
|
-
if (initial) {
|
|
418
|
-
return `You are an isolated delegated child agent. Stay within the delegated task and return only the requested result.${fileNote}\n\n## Agent instructions\n${definition.prompt}\n\n## Delegated task\n${task}`;
|
|
419
|
-
}
|
|
420
|
-
return `Continue the existing delegated work using the context already in this thread. Return only the requested result.${fileNote}\n\n## Parent follow-up\n${task}`;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async function applyTerminalOutcome(
|
|
424
|
-
details: SubagentDetails,
|
|
425
|
-
terminal: AssistantMessage,
|
|
426
|
-
definitionName: string,
|
|
427
|
-
): Promise<void> {
|
|
428
|
-
const response = textOf(terminal);
|
|
429
|
-
if (terminal.stopReason === "aborted") {
|
|
430
|
-
details.status = "aborted";
|
|
431
|
-
details.completionState = "aborted";
|
|
432
|
-
details.error = capped(terminal.errorMessage ?? "Child aborted");
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
if (terminal.stopReason === "error") {
|
|
436
|
-
details.status = "failed";
|
|
437
|
-
details.completionState = "error";
|
|
438
|
-
details.error = capped(terminal.errorMessage ?? "Child terminal error");
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
if ((terminal.stopReason === "stop" || terminal.stopReason === "length") && response.trim()) {
|
|
442
|
-
details.status = "completed";
|
|
443
|
-
details.completionState = terminal.stopReason;
|
|
444
|
-
details.phase = "output";
|
|
445
|
-
const truncation = truncateHead(response, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES });
|
|
446
|
-
let returned = truncation.content;
|
|
447
|
-
let path: string | undefined;
|
|
448
|
-
if (truncation.truncated) {
|
|
449
|
-
const directory = await mkdtemp(join(tmpdir(), "tau-subagent-"));
|
|
450
|
-
path = join(directory, "output.md");
|
|
451
|
-
await writeFile(path, response, { encoding: "utf8", mode: 0o600 });
|
|
452
|
-
returned += `\n\n[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full output saved to: ${path}]`;
|
|
453
|
-
}
|
|
454
|
-
details.response = returned;
|
|
455
|
-
details.truncation = {
|
|
456
|
-
truncated: truncation.truncated,
|
|
457
|
-
path,
|
|
458
|
-
outputLines: truncation.outputLines,
|
|
459
|
-
totalLines: truncation.totalLines,
|
|
460
|
-
outputBytes: truncation.outputBytes,
|
|
461
|
-
totalBytes: truncation.totalBytes,
|
|
462
|
-
};
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
details.status = "failed";
|
|
466
|
-
details.completionState = terminal.stopReason;
|
|
467
|
-
details.error = `Agent ${definitionName} run failed: empty or non-terminal response`;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
async function deliverTurnPrompt(options: {
|
|
471
|
-
session: SubagentThread["resource"]["session"];
|
|
472
|
-
thread: SubagentThread;
|
|
473
|
-
task: string;
|
|
474
|
-
files: readonly string[];
|
|
475
|
-
invocationId: string;
|
|
476
|
-
initial: boolean;
|
|
477
|
-
resumePrompt: string | undefined;
|
|
478
|
-
signal: AbortSignal;
|
|
479
|
-
}): Promise<void> {
|
|
480
|
-
const { session, thread, task, files, invocationId, initial, resumePrompt, signal } = options;
|
|
481
|
-
const abort = () => {
|
|
482
|
-
void session.abort().catch(() => undefined);
|
|
483
|
-
};
|
|
484
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
485
|
-
try {
|
|
486
|
-
const autoreadMessages = await buildAutoreadMessages({
|
|
487
|
-
files,
|
|
488
|
-
cwd: thread.cwd,
|
|
489
|
-
invocationId,
|
|
490
|
-
});
|
|
491
|
-
if (signal.aborted) throw new Error("Subagent call aborted before prompt");
|
|
492
|
-
for (const message of autoreadMessages) {
|
|
493
|
-
if (signal.aborted) throw new Error("Subagent call aborted during autoread delivery");
|
|
494
|
-
await session.sendCustomMessage<unknown>(message, { deliverAs: "nextTurn" });
|
|
495
|
-
}
|
|
496
|
-
if (signal.aborted) throw new Error("Subagent call aborted before prompt");
|
|
497
|
-
await session.prompt(buildTurnPrompt({ definition: thread.definition, task, files, initial, resumePrompt }), {
|
|
498
|
-
expandPromptTemplates: false,
|
|
499
|
-
});
|
|
500
|
-
} finally {
|
|
501
|
-
signal.removeEventListener("abort", abort);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function turnResult(
|
|
506
|
-
details: SubagentDetails,
|
|
507
|
-
retainable: boolean,
|
|
508
|
-
assistantMessageEndAt: readonly number[],
|
|
509
|
-
usage: SubagentUsage,
|
|
510
|
-
toolUsage: Usage,
|
|
511
|
-
definitionName: string,
|
|
512
|
-
): {
|
|
513
|
-
content: string;
|
|
514
|
-
details: SubagentDetails;
|
|
515
|
-
retainable: boolean;
|
|
516
|
-
terminalOutcome: RetainedTurnOutcome;
|
|
517
|
-
assistantMessageEndAt: readonly number[];
|
|
518
|
-
usage?: Usage;
|
|
519
|
-
} {
|
|
520
|
-
return {
|
|
521
|
-
content:
|
|
522
|
-
details.status === "completed"
|
|
523
|
-
? (details.response ?? "")
|
|
524
|
-
: (details.error ?? `Agent ${definitionName} failed`),
|
|
525
|
-
details,
|
|
526
|
-
retainable,
|
|
527
|
-
terminalOutcome:
|
|
528
|
-
details.status === "completed" ? "completed" : details.status === "aborted" ? "aborted" : "failed",
|
|
529
|
-
assistantMessageEndAt,
|
|
530
|
-
...(usage.turns === 0 ? {} : { usage: toolUsage }),
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
export async function runSubagentTurn(options: {
|
|
535
|
-
thread: SubagentThread;
|
|
536
|
-
task: string;
|
|
537
|
-
files?: readonly string[];
|
|
538
|
-
invocationId?: string;
|
|
539
|
-
initial: boolean;
|
|
540
|
-
resumePrompt?: string;
|
|
541
|
-
signal: AbortSignal;
|
|
542
|
-
onUpdate?: (details: SubagentDetails) => void | Promise<void>;
|
|
543
|
-
}): Promise<{
|
|
544
|
-
content: string;
|
|
545
|
-
details: SubagentDetails;
|
|
546
|
-
retainable: boolean;
|
|
547
|
-
terminalOutcome: RetainedTurnOutcome;
|
|
548
|
-
assistantMessageEndAt: readonly number[];
|
|
549
|
-
usage?: Usage;
|
|
550
|
-
}> {
|
|
551
|
-
const { thread, task, files = [], invocationId = thread.id, initial, resumePrompt, signal, onUpdate } = options;
|
|
552
|
-
const { definition } = thread;
|
|
553
|
-
const { session } = thread.resource;
|
|
554
|
-
const started = Date.now();
|
|
555
|
-
const { usage, toolUsage } = emptyTurnUsage();
|
|
556
|
-
const details: SubagentDetails = {
|
|
557
|
-
agent: definition.name,
|
|
558
|
-
displayName: thread.displayName,
|
|
559
|
-
threadId: thread.id,
|
|
560
|
-
status: "running",
|
|
561
|
-
phase: "run",
|
|
562
|
-
task,
|
|
563
|
-
model: thread.model,
|
|
564
|
-
thinkingLevel: thread.thinkingLevel,
|
|
565
|
-
toolCalls: 0,
|
|
566
|
-
actions: [],
|
|
567
|
-
omittedActions: 0,
|
|
568
|
-
omittedErrors: 0,
|
|
569
|
-
usage,
|
|
570
|
-
durationMs: 0,
|
|
571
|
-
};
|
|
572
|
-
let unsubscribe: (() => void) | undefined;
|
|
573
|
-
let lastTextUpdate = 0;
|
|
574
|
-
const turnMessages: AssistantMessage[] = [];
|
|
575
|
-
const assistantMessageEndAt: number[] = [];
|
|
576
|
-
let retainable = false;
|
|
577
|
-
const publish = (force = false) => {
|
|
578
|
-
const now = Date.now();
|
|
579
|
-
if (!force && now - lastTextUpdate < 100) return;
|
|
580
|
-
lastTextUpdate = now;
|
|
581
|
-
details.durationMs = now - started;
|
|
582
|
-
const context = session.getContextUsage();
|
|
583
|
-
if (typeof context?.percent === "number") details.contextPercent = context.percent;
|
|
584
|
-
else delete details.contextPercent;
|
|
585
|
-
const snapshot = cloneSnapshot(details);
|
|
586
|
-
snapshot.actions = snapshot.actions.slice(-5);
|
|
587
|
-
// Detached observer chain — must not delay prompt completion.
|
|
588
|
-
void Promise.resolve()
|
|
589
|
-
.then(() => onUpdate?.(snapshot))
|
|
590
|
-
.catch(() => undefined);
|
|
591
|
-
};
|
|
592
|
-
try {
|
|
593
|
-
if (signal.aborted) {
|
|
594
|
-
details.status = "aborted";
|
|
595
|
-
details.error = "Subagent call aborted before prompt";
|
|
596
|
-
details.durationMs = Date.now() - started;
|
|
597
|
-
publish(true);
|
|
598
|
-
return turnResult(details, false, assistantMessageEndAt, usage, toolUsage, definition.name);
|
|
599
|
-
}
|
|
600
|
-
unsubscribe = subscribeTurnEvents({
|
|
601
|
-
session,
|
|
602
|
-
details,
|
|
603
|
-
usage,
|
|
604
|
-
toolUsage,
|
|
605
|
-
turnMessages,
|
|
606
|
-
assistantMessageEndAt,
|
|
607
|
-
publish,
|
|
608
|
-
});
|
|
609
|
-
await deliverTurnPrompt({
|
|
610
|
-
session,
|
|
611
|
-
thread,
|
|
612
|
-
task,
|
|
613
|
-
files,
|
|
614
|
-
invocationId,
|
|
615
|
-
initial,
|
|
616
|
-
resumePrompt,
|
|
617
|
-
signal,
|
|
618
|
-
});
|
|
619
|
-
// Prompt returned without throw — session remains usable for retention decisions.
|
|
620
|
-
retainable = true;
|
|
621
|
-
const terminal = turnMessages.at(-1);
|
|
622
|
-
if (!terminal) throw new Error(`Agent ${definition.name} run failed: no terminal assistant response`);
|
|
623
|
-
await applyTerminalOutcome(details, terminal, definition.name);
|
|
624
|
-
details.durationMs = Date.now() - started;
|
|
625
|
-
publish(true);
|
|
626
|
-
return turnResult(details, retainable, assistantMessageEndAt, usage, toolUsage, definition.name);
|
|
627
|
-
} catch (error) {
|
|
628
|
-
details.status = signal.aborted ? "aborted" : "failed";
|
|
629
|
-
details.error = capped(error instanceof Error ? error.message : "Subagent failed");
|
|
630
|
-
details.durationMs = Date.now() - started;
|
|
631
|
-
// Prompt/session failures leave the child unsafe for reuse.
|
|
632
|
-
retainable = false;
|
|
633
|
-
publish(true);
|
|
634
|
-
return turnResult(details, retainable, assistantMessageEndAt, usage, toolUsage, definition.name);
|
|
635
|
-
} finally {
|
|
636
|
-
unsubscribe?.();
|
|
637
|
-
thread.turns += 1;
|
|
638
|
-
thread.lastUsedAt = Date.now();
|
|
639
|
-
if (session.isStreaming) await session.abort().catch(() => undefined);
|
|
640
|
-
}
|
|
641
|
-
}
|