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