@shanepadgett/tau-agent 0.33.1 → 0.35.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.
Files changed (203) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/bash-approval/README.md +20 -0
  8. package/extensions/bash-approval/index.ts +235 -0
  9. package/extensions/bash-approval/settings.ts +24 -0
  10. package/extensions/branch/index.ts +38 -21
  11. package/extensions/cache-diagnostics/index.ts +70 -58
  12. package/extensions/checkpoint/README.md +9 -0
  13. package/extensions/checkpoint/checkpoint-budget.ts +66 -0
  14. package/extensions/checkpoint/checkpoint.ts +279 -0
  15. package/extensions/checkpoint/index.ts +102 -0
  16. package/extensions/checkpoint/messages.ts +170 -0
  17. package/extensions/checkpoint/prompt.ts +24 -0
  18. package/extensions/checkpoint/settings.ts +27 -0
  19. package/extensions/commit/commit-effort.ts +3 -3
  20. package/extensions/commit/commit-plan.ts +2 -2
  21. package/extensions/context/README.md +12 -8
  22. package/extensions/context/definitions.ts +178 -55
  23. package/extensions/context/index.ts +192 -160
  24. package/extensions/context/panel.ts +57 -37
  25. package/extensions/context/sync.ts +139 -89
  26. package/extensions/context/validation.ts +37 -25
  27. package/extensions/effort/README.md +1 -1
  28. package/extensions/effort/index.ts +3 -3
  29. package/extensions/effort/state.ts +3 -18
  30. package/extensions/explore/guidance.ts +6 -47
  31. package/extensions/explore/index.ts +38 -53
  32. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  33. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  34. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  35. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  36. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  37. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  38. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  39. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  40. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  41. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  42. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  43. package/extensions/explore/tools/target-params.ts +9 -0
  44. package/extensions/footer/index.ts +75 -56
  45. package/extensions/handoff/index.ts +8 -30
  46. package/extensions/image-gen/index.ts +46 -40
  47. package/extensions/patch/executor.ts +190 -135
  48. package/extensions/patch/matcher.ts +82 -48
  49. package/extensions/patch/parser.ts +149 -105
  50. package/extensions/patch/render.ts +32 -39
  51. package/extensions/patch/summary.ts +30 -26
  52. package/extensions/ready/README.md +7 -0
  53. package/extensions/ready/index.ts +58 -0
  54. package/extensions/ready/model.ts +128 -0
  55. package/extensions/ready/packs.ts +272 -0
  56. package/extensions/ready/render.ts +445 -0
  57. package/extensions/ready/scan.ts +760 -0
  58. package/extensions/reference/index.ts +2 -0
  59. package/extensions/reference/panel.ts +94 -31
  60. package/extensions/review/README.md +14 -6
  61. package/extensions/review/index.ts +58 -99
  62. package/extensions/review/model.ts +13 -33
  63. package/extensions/review/session.ts +9 -23
  64. package/extensions/runtime-context/context.ts +30 -26
  65. package/extensions/script-runner/README.md +2 -2
  66. package/extensions/script-runner/index.ts +122 -82
  67. package/extensions/silent-command-runner/index.ts +38 -23
  68. package/extensions/soul/README.md +2 -2
  69. package/extensions/soul/index.ts +4 -4
  70. package/extensions/soul/prompt.ts +6 -10
  71. package/extensions/soul/settings.ts +6 -3
  72. package/extensions/subagent/agents/context-sync.md +185 -33
  73. package/extensions/subagent/agents/scout.md +2 -2
  74. package/extensions/subagent/agents/web-research.md +2 -2
  75. package/extensions/subagent/agents.ts +137 -84
  76. package/extensions/subagent/cmux-dashboard.ts +39 -30
  77. package/extensions/subagent/index.ts +79 -59
  78. package/extensions/subagent/resume.ts +1 -1
  79. package/extensions/subagent/run.ts +308 -193
  80. package/extensions/subagent/runtime.ts +781 -343
  81. package/extensions/tau/index.ts +47 -38
  82. package/extensions/tau-help/help.md +20 -8
  83. package/extensions/tool-loader/README.md +4 -2
  84. package/extensions/tool-loader/index.ts +77 -52
  85. package/extensions/web/index.ts +6 -3
  86. package/extensions/web/webfetch.ts +114 -86
  87. package/package.json +3 -3
  88. package/schemas/tau.schema.json +38 -35
  89. package/shared/bounded-text-result.ts +1 -0
  90. package/shared/checkpoint-visibility.ts +9 -0
  91. package/shared/events.ts +28 -10
  92. package/shared/isolated-session.ts +72 -30
  93. package/shared/model-effort.ts +23 -22
  94. package/shared/model-fallback/index.ts +66 -41
  95. package/shared/settings/load.ts +45 -38
  96. package/shared/temporary-output-store.ts +16 -10
  97. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  98. package/{extensions/explore → src}/ast/engine.ts +54 -38
  99. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  100. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  101. package/src/ast/format/deps.ts +94 -0
  102. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  103. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  104. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  105. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  106. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  107. package/src/ast/format/targeted-result.ts +20 -0
  108. package/src/ast/grammars/c_sharp.wasm +0 -0
  109. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  110. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  111. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  112. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  113. package/{extensions/explore → src}/ast/ir.ts +6 -0
  114. package/src/ast/languages/csharp-file-deps.ts +90 -0
  115. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  116. package/src/ast/languages/dotted-index.ts +131 -0
  117. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  118. package/src/ast/languages/fixtures/sample.cs +53 -0
  119. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  120. package/src/ast/languages/fixtures/sample.go +37 -0
  121. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  122. package/src/ast/languages/fixtures/sample.java +40 -0
  123. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  124. package/src/ast/languages/fixtures/sample.kt +31 -0
  125. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  126. package/src/ast/languages/fixtures/sample.md +23 -0
  127. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  128. package/src/ast/languages/fixtures/sample.rs +54 -0
  129. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  130. package/src/ast/languages/fixtures/sample.swift +45 -0
  131. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  132. package/src/ast/languages/fixtures/sample.ts +45 -0
  133. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  134. package/src/ast/languages/fixtures/sample.tsx +36 -0
  135. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  136. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  137. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  138. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  139. package/src/ast/languages/jvm-file-deps.ts +135 -0
  140. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  141. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  142. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  143. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  144. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  145. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  146. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  147. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  148. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  149. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  150. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  151. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  152. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  153. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  154. package/src/ast/queries/impact.ts +291 -0
  155. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  156. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  157. package/{extensions/explore → src}/ast/query.ts +1 -1
  158. package/{extensions/explore → src}/ast/registry.ts +1 -2
  159. package/{extensions/explore → src}/ast/scan.ts +1 -1
  160. package/src/ast/session.ts +50 -0
  161. package/{extensions/explore → src}/ast/slice.ts +8 -0
  162. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  163. package/src/file-injection/index.ts +547 -0
  164. package/src/image-generation/client.ts +50 -31
  165. package/src/image-generation/index.ts +177 -134
  166. package/src/index.ts +2 -0
  167. package/src/tool-loading/index.ts +58 -0
  168. package/extensions/context/evidence.ts +0 -517
  169. package/extensions/context/projection.ts +0 -141
  170. package/extensions/context/state.ts +0 -30
  171. package/extensions/context/write-scope.ts +0 -109
  172. package/extensions/explore/ast/format/deps.ts +0 -86
  173. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  174. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  175. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  176. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  177. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  178. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  179. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  180. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  181. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  182. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  183. package/extensions/explore/ast/languages/odin.ts +0 -407
  184. package/extensions/explore/ast/queries/impact.ts +0 -250
  185. package/extensions/explore/outline-injection.ts +0 -151
  186. package/extensions/explore/read/autoread.ts +0 -118
  187. package/extensions/review/panel.ts +0 -128
  188. package/extensions/working-memory/README.md +0 -17
  189. package/extensions/working-memory/checkpoint.ts +0 -242
  190. package/extensions/working-memory/index.ts +0 -341
  191. package/extensions/working-memory/memory.ts +0 -235
  192. package/extensions/working-memory/render.ts +0 -185
  193. package/extensions/working-memory/settings.ts +0 -38
  194. package/extensions/working-memory/state.ts +0 -158
  195. package/shared/autoread.ts +0 -234
  196. package/shared/context-messages.ts +0 -19
  197. package/shared/outline-injection.ts +0 -56
  198. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  199. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  200. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  201. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  202. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  203. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -14,13 +14,13 @@ import {
14
14
  } from "@earendil-works/pi-coding-agent";
15
15
  import { createCompleteFileMeta } from "../../shared/full-file-knowledge.ts";
16
16
  import { createIsolatedSessionResource, type IsolatedSessionResource } from "../../shared/isolated-session.ts";
17
+ import { FILE_INJECTION_TYPE } from "@shanepadgett/tau-agent";
17
18
  import type { AgentDefinition } from "./agents.ts";
18
19
  import { emptySubagentResumeState, type RetainedTurnOutcome, type SubagentResumeState } from "./resume.ts";
19
20
  import { resolveSubagentSessionInputs, type SubagentSessionInputs } from "./session-resource.ts";
20
21
 
21
22
  const PREVIEW_LIMIT = 600;
22
23
  const VALUE_LIMIT = 180;
23
- const AUTOREAD_MESSAGE_TYPE = "tau.autoread";
24
24
 
25
25
  export interface SubagentAction {
26
26
  tool: string;
@@ -163,7 +163,7 @@ function capped(value: unknown, limit = VALUE_LIMIT): string {
163
163
  }
164
164
 
165
165
  /** Rolling tail so live previews keep changing after the limit. */
166
- export function cappedTail(value: unknown, limit = PREVIEW_LIMIT): string {
166
+ function cappedTail(value: unknown, limit = PREVIEW_LIMIT): string {
167
167
  let text: string;
168
168
  try {
169
169
  text = typeof value === "string" ? value : (JSON.stringify(value) ?? String(value));
@@ -248,6 +248,289 @@ export async function disposeSubagentThread(thread: SubagentThread): Promise<voi
248
248
  await thread.resource.dispose();
249
249
  }
250
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
+
251
534
  export async function runSubagentTurn(options: {
252
535
  thread: SubagentThread;
253
536
  task: string;
@@ -269,15 +552,7 @@ export async function runSubagentTurn(options: {
269
552
  const { definition } = thread;
270
553
  const { session } = thread.resource;
271
554
  const started = Date.now();
272
- const usage: SubagentUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
273
- const toolUsage: Usage = {
274
- input: 0,
275
- output: 0,
276
- cacheRead: 0,
277
- cacheWrite: 0,
278
- totalTokens: 0,
279
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
280
- };
555
+ const { usage, toolUsage } = emptyTurnUsage();
281
556
  const details: SubagentDetails = {
282
557
  agent: definition.name,
283
558
  displayName: thread.displayName,
@@ -320,188 +595,35 @@ export async function runSubagentTurn(options: {
320
595
  details.error = "Subagent call aborted before prompt";
321
596
  details.durationMs = Date.now() - started;
322
597
  publish(true);
323
- return {
324
- content: details.error,
325
- details,
326
- retainable: false,
327
- terminalOutcome: "aborted",
328
- assistantMessageEndAt,
329
- };
598
+ return turnResult(details, false, assistantMessageEndAt, usage, toolUsage, definition.name);
330
599
  }
331
- const actionById = new Map<string, string>();
332
- unsubscribe = session.subscribe((event: AgentSessionEvent) => {
333
- if (event.type === "message_update" && event.message.role === "assistant") {
334
- details.response = cappedTail(textOf(event.message), PREVIEW_LIMIT);
335
- publish();
336
- } else if (event.type === "message_end" && event.message.role === "assistant") {
337
- turnMessages.push(event.message);
338
- assistantMessageEndAt.push(Date.now());
339
- usage.input += event.message.usage.input;
340
- usage.output += event.message.usage.output;
341
- usage.cacheRead += event.message.usage.cacheRead;
342
- usage.cacheWrite += event.message.usage.cacheWrite;
343
- usage.cost += event.message.usage.cost.total;
344
- usage.turns += 1;
345
- toolUsage.input += event.message.usage.input;
346
- toolUsage.output += event.message.usage.output;
347
- toolUsage.cacheRead += event.message.usage.cacheRead;
348
- toolUsage.cacheWrite += event.message.usage.cacheWrite;
349
- toolUsage.totalTokens += event.message.usage.totalTokens;
350
- toolUsage.cost.input += event.message.usage.cost.input;
351
- toolUsage.cost.output += event.message.usage.cost.output;
352
- toolUsage.cost.cacheRead += event.message.usage.cost.cacheRead;
353
- toolUsage.cost.cacheWrite += event.message.usage.cost.cacheWrite;
354
- toolUsage.cost.total += event.message.usage.cost.total;
355
- if (event.message.usage.cacheWrite1h !== undefined)
356
- toolUsage.cacheWrite1h = (toolUsage.cacheWrite1h ?? 0) + event.message.usage.cacheWrite1h;
357
- if (event.message.usage.reasoning !== undefined)
358
- toolUsage.reasoning = (toolUsage.reasoning ?? 0) + event.message.usage.reasoning;
359
- details.response = cappedTail(textOf(event.message), PREVIEW_LIMIT);
360
- details.currentActivity = undefined;
361
- publish(true);
362
- } else if (event.type === "tool_execution_start") {
363
- details.toolCalls += 1;
364
- const summary = `${event.toolName} ${capped(event.args)}`.trim();
365
- actionById.set(event.toolCallId, summary);
366
- details.currentActivity = summary;
367
- publish(true);
368
- } else if (event.type === "tool_execution_end") {
369
- const action = {
370
- tool: event.toolName,
371
- summary: actionById.get(event.toolCallId) ?? event.toolName,
372
- error: event.isError,
373
- };
374
- details.actions.push(action);
375
- if (details.actions.length > 20) {
376
- const removable = details.actions.findIndex((item) => !item.error);
377
- const [removed] = details.actions.splice(removable >= 0 ? removable : 0, 1);
378
- details.omittedActions += 1;
379
- if (removed?.error) details.omittedErrors += 1;
380
- }
381
- details.currentActivity = undefined;
382
- publish(true);
383
- }
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,
384
618
  });
385
- const abort = () => {
386
- void session.abort().catch(() => undefined);
387
- };
388
- signal.addEventListener("abort", abort, { once: true });
389
- try {
390
- const autoreadMessages = await Promise.all(
391
- files.map(async (file, index) => {
392
- const pathKey = resolve(thread.cwd, file);
393
- const common = {
394
- rowId: `${invocationId}:${index}`,
395
- path: file,
396
- cwd: thread.cwd,
397
- source: "subagent",
398
- batchId: invocationId,
399
- };
400
- try {
401
- const bytes = await readFile(pathKey);
402
- const content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
403
- const lines = content.split("\n");
404
- const numbered = lines.map((line, lineIndex) => `${lineIndex + 1}: ${line}`).join("\n");
405
- return {
406
- customType: AUTOREAD_MESSAGE_TYPE,
407
- content: `${file}\n${numbered}`,
408
- display: false,
409
- details: {
410
- ...common,
411
- status: "read",
412
- readCache: createCompleteFileMeta({
413
- pathKey,
414
- presentation: "line-numbered",
415
- servedHash: createHash("sha256").update(bytes).digest("hex"),
416
- mode: "baseline",
417
- sourceText: content,
418
- returnedText: `${file}\n${numbered}`,
419
- totalLines: lines.length,
420
- summary: `${lines.length} lines`,
421
- }),
422
- },
423
- };
424
- } catch (error) {
425
- const message = error instanceof Error ? error.message : String(error);
426
- return {
427
- customType: AUTOREAD_MESSAGE_TYPE,
428
- content: `${file}\nAutoread failed: ${message}`,
429
- display: false,
430
- details: { ...common, status: "failed", error: message },
431
- };
432
- }
433
- }),
434
- );
435
- if (signal.aborted) throw new Error("Subagent call aborted before prompt");
436
- for (const message of autoreadMessages) {
437
- if (signal.aborted) throw new Error("Subagent call aborted during autoread delivery");
438
- await session.sendCustomMessage<unknown>(message, { deliverAs: "nextTurn" });
439
- }
440
- if (signal.aborted) throw new Error("Subagent call aborted before prompt");
441
- await session.prompt(
442
- resumePrompt ??
443
- (initial
444
- ? `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}`
445
- : `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}`),
446
- { expandPromptTemplates: false },
447
- );
448
- } finally {
449
- signal.removeEventListener("abort", abort);
450
- }
451
619
  // Prompt returned without throw — session remains usable for retention decisions.
452
620
  retainable = true;
453
621
  const terminal = turnMessages.at(-1);
454
622
  if (!terminal) throw new Error(`Agent ${definition.name} run failed: no terminal assistant response`);
455
- const response = textOf(terminal);
456
- if (terminal.stopReason === "aborted") {
457
- details.status = "aborted";
458
- details.completionState = "aborted";
459
- details.error = capped(terminal.errorMessage ?? "Child aborted");
460
- } else if (terminal.stopReason === "error") {
461
- details.status = "failed";
462
- details.completionState = "error";
463
- details.error = capped(terminal.errorMessage ?? "Child terminal error");
464
- } else if ((terminal.stopReason === "stop" || terminal.stopReason === "length") && response.trim()) {
465
- details.status = "completed";
466
- details.completionState = terminal.stopReason;
467
- details.phase = "output";
468
- const truncation = truncateHead(response, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES });
469
- let returned = truncation.content;
470
- let path: string | undefined;
471
- if (truncation.truncated) {
472
- const directory = await mkdtemp(join(tmpdir(), "tau-subagent-"));
473
- path = join(directory, "output.md");
474
- await writeFile(path, response, { encoding: "utf8", mode: 0o600 });
475
- returned += `\n\n[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full output saved to: ${path}]`;
476
- }
477
- details.response = returned;
478
- details.truncation = {
479
- truncated: truncation.truncated,
480
- path,
481
- outputLines: truncation.outputLines,
482
- totalLines: truncation.totalLines,
483
- outputBytes: truncation.outputBytes,
484
- totalBytes: truncation.totalBytes,
485
- };
486
- } else {
487
- details.status = "failed";
488
- details.completionState = terminal.stopReason;
489
- details.error = `Agent ${definition.name} run failed: empty or non-terminal response`;
490
- }
623
+ await applyTerminalOutcome(details, terminal, definition.name);
491
624
  details.durationMs = Date.now() - started;
492
625
  publish(true);
493
- return {
494
- content:
495
- details.status === "completed"
496
- ? (details.response ?? "")
497
- : (details.error ?? `Agent ${definition.name} failed`),
498
- details,
499
- retainable,
500
- terminalOutcome:
501
- details.status === "completed" ? "completed" : details.status === "aborted" ? "aborted" : "failed",
502
- assistantMessageEndAt,
503
- ...(usage.turns === 0 ? {} : { usage: toolUsage }),
504
- };
626
+ return turnResult(details, retainable, assistantMessageEndAt, usage, toolUsage, definition.name);
505
627
  } catch (error) {
506
628
  details.status = signal.aborted ? "aborted" : "failed";
507
629
  details.error = capped(error instanceof Error ? error.message : "Subagent failed");
@@ -509,14 +631,7 @@ export async function runSubagentTurn(options: {
509
631
  // Prompt/session failures leave the child unsafe for reuse.
510
632
  retainable = false;
511
633
  publish(true);
512
- return {
513
- content: details.error,
514
- details,
515
- retainable,
516
- terminalOutcome: details.status === "aborted" ? "aborted" : "failed",
517
- assistantMessageEndAt,
518
- ...(usage.turns === 0 ? {} : { usage: toolUsage }),
519
- };
634
+ return turnResult(details, retainable, assistantMessageEndAt, usage, toolUsage, definition.name);
520
635
  } finally {
521
636
  unsubscribe?.();
522
637
  thread.turns += 1;