@shanepadgett/tau-agent 0.16.0 → 0.18.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 (55) hide show
  1. package/docs/subagents.md +25 -6
  2. package/extensions/attention/README.md +1 -0
  3. package/extensions/attention/index.ts +38 -0
  4. package/extensions/context/README.md +13 -3
  5. package/extensions/context/definitions.ts +3 -15
  6. package/extensions/context/evidence.ts +516 -0
  7. package/extensions/context/index.ts +114 -116
  8. package/extensions/context/panel.ts +60 -0
  9. package/extensions/context/settings.ts +30 -1
  10. package/extensions/context/sync.ts +171 -686
  11. package/extensions/context/validation.ts +4 -1
  12. package/extensions/context/write-scope.ts +109 -0
  13. package/extensions/context-pruning/README.md +22 -0
  14. package/extensions/context-pruning/file-evidence.ts +265 -0
  15. package/extensions/context-pruning/index.ts +379 -0
  16. package/extensions/context-pruning/projection.ts +108 -0
  17. package/extensions/context-pruning/prune.ts +346 -0
  18. package/extensions/context-pruning/render.ts +179 -0
  19. package/extensions/context-pruning/settings.ts +41 -0
  20. package/extensions/explore/README.md +3 -1
  21. package/extensions/explore/autoread.ts +85 -22
  22. package/extensions/explore/full-file-knowledge.ts +234 -0
  23. package/extensions/explore/index.ts +2 -3
  24. package/extensions/explore/read-cache.ts +150 -86
  25. package/extensions/explore/read-snapshots.ts +17 -4
  26. package/extensions/explore/read.ts +57 -38
  27. package/extensions/footer/index.ts +62 -60
  28. package/extensions/run-summary/index.ts +5 -5
  29. package/extensions/silent-command-runner/README.md +1 -1
  30. package/extensions/silent-command-runner/index.ts +45 -28
  31. package/extensions/soul/prompt.ts +3 -1
  32. package/extensions/subagent/README.md +21 -5
  33. package/extensions/subagent/agents/context-sync.md +97 -0
  34. package/extensions/subagent/agents/{generalist.md → dormant/generalist.md} +6 -0
  35. package/extensions/subagent/agents/{scout.md → dormant/scout.md} +6 -0
  36. package/extensions/subagent/agents/review.md +48 -0
  37. package/extensions/subagent/agents/web-research.md +6 -0
  38. package/extensions/subagent/agents.ts +15 -2
  39. package/extensions/subagent/cmux-dashboard.ts +454 -0
  40. package/extensions/subagent/index.ts +181 -234
  41. package/extensions/subagent/render.ts +1 -1
  42. package/extensions/subagent/resume.ts +78 -0
  43. package/extensions/subagent/run.ts +213 -118
  44. package/extensions/subagent/runtime.ts +856 -0
  45. package/extensions/subagent/session-resource.ts +169 -0
  46. package/extensions/tau-help/help.md +6 -2
  47. package/extensions/turn-budget/index.ts +8 -36
  48. package/package.json +2 -2
  49. package/schemas/tau.schema.json +48 -1
  50. package/shared/context-pruning-state.ts +364 -0
  51. package/shared/events.ts +18 -0
  52. package/shared/model-fallback/index.ts +21 -10
  53. package/shared/model-fallback/types.ts +5 -3
  54. package/shared/settings/load.ts +78 -1
  55. package/shared/tool-row-state.ts +21 -1
@@ -1,66 +1,109 @@
1
+ import { rm } from "node:fs/promises";
1
2
  import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
3
  import { Type } from "typebox";
4
+ import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
3
5
  import { createToolRowStateStore } from "../../shared/tool-row-state.js";
4
- import { discoverAgents, type AgentDefinition, type AgentDiscovery } from "./agents.ts";
6
+ import contextSettings from "../context/settings.ts";
7
+ import { discoverAgents, type AgentDiscovery } from "./agents.ts";
8
+ import { createCmuxDashboard, type CmuxDashboard, type DashboardOrphan } from "./cmux-dashboard.ts";
5
9
  import { renderSubagentCall, renderSubagentResult } from "./render.ts";
6
- import {
7
- createSubagentThread,
8
- disposeSubagentThread,
9
- extensionPathsForTools,
10
- FifoGate,
11
- runSubagentTurn,
12
- type SubagentDetails,
13
- type SubagentThread,
14
- } from "./run.ts";
10
+ import type { SubagentDetails } from "./run.ts";
11
+ import { failedToolResult, SubagentRuntime } from "./runtime.ts";
15
12
 
16
13
  const params = Type.Union([
17
14
  Type.Object(
18
- { agent: Type.String({ minLength: 1 }), task: Type.String({ minLength: 1 }) },
15
+ {
16
+ agent: Type.String({ minLength: 1 }),
17
+ task: Type.String({ minLength: 1 }),
18
+ files: Type.Optional(
19
+ Type.Array(Type.String({ minLength: 1 }), {
20
+ description: "Files to autoread into the child's context before this turn",
21
+ }),
22
+ ),
23
+ },
19
24
  { additionalProperties: false },
20
25
  ),
21
26
  Type.Object(
22
- { thread: Type.String({ minLength: 1 }), task: Type.String({ minLength: 1 }) },
27
+ {
28
+ thread: Type.String({ minLength: 1 }),
29
+ task: Type.String({ minLength: 1 }),
30
+ files: Type.Optional(
31
+ Type.Array(Type.String({ minLength: 1 }), {
32
+ description: "Files to autoread into the child's context before this turn",
33
+ }),
34
+ ),
35
+ },
23
36
  { additionalProperties: false },
24
37
  ),
25
38
  ]);
26
39
 
27
- const MAX_RETAINED_THREADS = 16;
28
-
29
- function emptyDetails(
30
- agent: string,
31
- task: string,
32
- status: "waiting" | "failed" | "aborted",
33
- phase: "discovery" | "queue" | "startup",
34
- model: string,
35
- thinkingLevel: string,
36
- threadId: string | undefined,
37
- error: string | undefined,
38
- ): SubagentDetails {
39
- return {
40
- agent,
41
- ...(threadId === undefined ? {} : { threadId }),
42
- status,
43
- phase,
44
- task,
45
- model,
46
- thinkingLevel,
47
- toolCalls: 0,
48
- actions: [],
49
- omittedActions: 0,
50
- omittedErrors: 0,
51
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
52
- durationMs: 0,
53
- ...(error === undefined ? {} : { error }),
54
- };
55
- }
56
-
57
40
  export default function subagentExtension(pi: ExtensionAPI): void {
58
- const gate = new FifoGate(4);
59
- const controllers = new Set<AbortController>();
60
- const threads = new Map<string, SubagentThread>();
41
+ const runtime = new SubagentRuntime(pi);
42
+ let failureNotify: ((message: string) => void) | undefined;
43
+ const orphans: DashboardOrphan[] = [];
44
+ const retryOrphans = async () => {
45
+ if (orphans.length === 0) return;
46
+ const pending = orphans.splice(0, orphans.length);
47
+ for (const orphan of pending) {
48
+ if (orphan.surfaceId && orphan.workspaceId) {
49
+ const result = await pi
50
+ .exec(
51
+ "cmux",
52
+ [
53
+ "--json",
54
+ "--id-format",
55
+ "both",
56
+ "rpc",
57
+ "surface.close",
58
+ JSON.stringify({ workspace_id: orphan.workspaceId, surface_id: orphan.surfaceId }),
59
+ ],
60
+ { timeout: 2500 },
61
+ )
62
+ .catch(() => ({ stdout: "", stderr: "close failed", code: 1, killed: false }));
63
+ const closed =
64
+ (result.code === 0 && !result.killed) ||
65
+ /not found|unknown surface|no such/i.test(`${result.stderr}\n${result.stdout}`);
66
+ if (!closed) {
67
+ orphans.push(orphan);
68
+ continue;
69
+ }
70
+ }
71
+ // Known-closed or never had a surface id after ambiguous open: drop directory only when closed/absent.
72
+ // Ambiguous no-id orphans stay on disk; keep tracking so we do not open more without awareness.
73
+ if (orphan.surfaceId) {
74
+ await rm(orphan.directory, { recursive: true, force: true }).catch(() => undefined);
75
+ } else {
76
+ orphans.push(orphan);
77
+ }
78
+ }
79
+ };
80
+ const makeDashboard = () =>
81
+ createCmuxDashboard({
82
+ exec: (command, args, options) => pi.exec(command, args, options),
83
+ canOpen: () => orphans.length === 0,
84
+ notify: (message) => {
85
+ try {
86
+ failureNotify?.(message);
87
+ } catch {
88
+ // ignore
89
+ }
90
+ },
91
+ });
92
+ let dashboard: CmuxDashboard = makeDashboard();
93
+ let unsubscribeDashboard = runtime.subscribe((snapshot) => {
94
+ dashboard.onSnapshot(snapshot);
95
+ });
96
+ const replaceDashboard = async () => {
97
+ unsubscribeDashboard();
98
+ const leftover = await dashboard.shutdown();
99
+ orphans.push(...leftover);
100
+ await retryOrphans();
101
+ dashboard = makeDashboard();
102
+ unsubscribeDashboard = runtime.subscribe((snapshot) => {
103
+ dashboard.onSnapshot(snapshot);
104
+ });
105
+ };
61
106
  const fingerprints = new Map<string, string>();
62
- const runtimeWarnings = new Set<string>();
63
- let nextThreadId = 1;
64
107
  const rowState = createToolRowStateStore(pi, "subagent.tool-row-state");
65
108
  const warn = (discovery: AgentDiscovery, ctx: ExtensionContext) => {
66
109
  const current = new Set<string>();
@@ -79,25 +122,23 @@ export default function subagentExtension(pi: ExtensionAPI): void {
79
122
  }
80
123
  for (const path of fingerprints.keys()) if (!current.has(path)) fingerprints.delete(path);
81
124
  };
82
- const disposeThreads = async () => {
83
- const retained = [...threads.values()];
84
- threads.clear();
85
- await Promise.all(retained.map((thread) => disposeSubagentThread(thread)));
125
+ const parentVisibleAgents = async (ctx: ExtensionContext, discovery: AgentDiscovery) => {
126
+ const sync = (await loadTauExtensionSettings(ctx, contextSettings)).sync;
127
+ const parentVisible = sync.enabled && sync.automation;
128
+ return [...discovery.agents.values()]
129
+ .filter((agent) => agent.name !== "context-sync" || parentVisible)
130
+ .sort((a, b) => a.name.localeCompare(b.name));
86
131
  };
132
+
87
133
  pi.on("before_agent_start", async (event, ctx) => {
88
134
  if (!pi.getActiveTools().includes("subagent")) return undefined;
89
135
  const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
90
136
  warn(discovery, ctx);
91
- const lines = [...discovery.agents.values()]
92
- .sort((a, b) => a.name.localeCompare(b.name))
93
- .map((agent) => `- ${agent.name}: ${agent.description}`);
94
- const activeThreads = [...threads.values()]
95
- .filter((thread) => thread.cwd === ctx.cwd)
96
- .sort((a, b) => a.id.localeCompare(b.id))
97
- .map((thread) => {
98
- const task = thread.initialTask.replace(/\s+/g, " ").trim();
99
- return `- ${thread.id} (${thread.definition.name}): ${task.length <= 160 ? task : `${task.slice(0, 159)}…`}`;
100
- });
137
+ const lines = (await parentVisibleAgents(ctx, discovery)).map((agent) => `- ${agent.name}: ${agent.description}`);
138
+ const activeThreads = runtime.listThreads(ctx.cwd).map((thread) => {
139
+ const task = thread.initialTask.replace(/\s+/g, " ").trim();
140
+ return `- ${thread.id} (${thread.displayName} · ${thread.definition.name}): ${task.length <= 160 ? task : `${task.slice(0, 159)}…`}`;
141
+ });
101
142
  const threadSection = activeThreads.length ? `\n\nActive reusable threads:\n${activeThreads.join("\n")}` : "";
102
143
  const prompt = `## Subagents
103
144
  Use \`subagent\` when an available agent matches a focused part of the task.
@@ -107,6 +148,8 @@ ${lines.join("\n")}
107
148
 
108
149
  Start a fresh thread with \`agent\` and \`task\`. Continue an existing thread with \`thread\` and \`task\`. Reuse a thread when feedback or follow-up work depends on its prior reads and reasoning. Start fresh for unrelated work or when its context is stale or oversized.${threadSection}
109
150
 
151
+ Pass \`files\` when exact relevant files are already known. Tau autoreads current line-numbered snapshots into that child turn before it starts.
152
+
110
153
  Delegate one focused task per call. Children do not inherit parent messages. Include exact absolute reference paths when a child must inspect a repository outside the current working directory.`;
111
154
  return { systemPrompt: `${event.systemPrompt}\n\n${prompt}` };
112
155
  });
@@ -115,182 +158,82 @@ Delegate one focused task per call. Children do not inherit parent messages. Inc
115
158
  name: "subagent",
116
159
  label: "Subagent",
117
160
  description:
118
- "Start a focused isolated child agent or continue a retained child thread. Output is limited to 50KB or 2,000 lines.",
161
+ "Start a focused isolated child agent or continue a retained child thread. Known files can be autoread into the child turn. Output is limited to 50KB or 2,000 lines.",
119
162
  promptSnippet: "Start a focused child agent or continue a retained child thread",
120
163
  parameters: params,
121
164
  executionMode: "parallel",
122
165
  async execute(_id, raw, signal, onUpdate, ctx) {
123
166
  const task = raw.task.trim();
167
+ const files = [...new Set((raw.files ?? []).map((path) => path.trim()))];
124
168
  const continuing = "thread" in raw;
125
169
  const parentModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "unavailable";
126
170
  const parentThinking = pi.getThinkingLevel();
127
- let thread: SubagentThread | undefined;
128
- let definition: AgentDefinition | undefined;
129
- let agent = continuing ? raw.thread.trim() : raw.agent.trim();
130
- let threadId = continuing ? raw.thread.trim() : undefined;
131
- if (!task || !agent) {
171
+ const agent = continuing ? raw.thread.trim() : raw.agent.trim();
172
+ const threadKey = continuing ? raw.thread.trim() : undefined;
173
+
174
+ failureNotify = (message) => {
175
+ ctx.ui.notify(message, "warning");
176
+ };
177
+ dashboard.setInteractive(ctx.mode === "tui" && ctx.hasUI);
178
+
179
+ if (!task || !agent || files.some((path) => !path)) {
132
180
  const error = continuing
133
- ? "Subagent continuation requires non-empty thread and task"
134
- : "Subagent input requires non-empty agent and task";
135
- return {
136
- content: [{ type: "text", text: error }],
137
- details: emptyDetails(agent, task, "failed", "queue", parentModel, parentThinking, threadId, error),
138
- };
139
- }
140
- if (continuing) {
141
- thread = threads.get(agent);
142
- if (!thread || thread.cwd !== ctx.cwd) {
143
- const names =
144
- [...threads.values()]
145
- .filter((item) => item.cwd === ctx.cwd)
146
- .map((item) => item.id)
147
- .sort()
148
- .join(", ") || "none";
149
- const error = `Subagent thread ${agent} is unavailable. Active threads: ${names}`;
150
- return {
151
- content: [{ type: "text", text: error }],
152
- details: emptyDetails(
153
- agent,
154
- task,
155
- "failed",
156
- "discovery",
157
- parentModel,
158
- parentThinking,
159
- agent,
160
- error,
161
- ),
162
- };
163
- }
164
- agent = thread.definition.name;
165
- threadId = thread.id;
166
- definition = thread.definition;
167
- } else {
168
- const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
169
- warn(discovery, ctx);
170
- definition = discovery.agents.get(agent);
171
- const invalid = discovery.invalid.get(agent);
172
- if (!definition) {
173
- const reason = invalid?.map((item) => item.reason).join("; ") ?? "unknown agent";
174
- const names = [...discovery.agents.keys()].sort().join(", ") || "none";
175
- const error = `Agent ${agent} discovery failed: ${reason}. Runnable agents: ${names}`;
176
- return {
177
- content: [{ type: "text", text: error }],
178
- details: emptyDetails(
179
- agent,
180
- task,
181
- "failed",
182
- "discovery",
183
- parentModel,
184
- parentThinking,
185
- undefined,
186
- error,
187
- ),
188
- };
189
- }
190
- threadId = `thread-${nextThreadId++}`;
181
+ ? "Subagent continuation requires non-empty thread, task, and file paths"
182
+ : "Subagent input requires non-empty agent, task, and file paths";
183
+ return failedToolResult(agent, task, "queue", parentModel, parentThinking, error, threadKey);
191
184
  }
192
- const controller = new AbortController();
193
- controllers.add(controller);
194
- const combined = AbortSignal.any([controller.signal, ...(signal ? [signal] : [])]);
195
- let releaseThread: (() => void) | undefined;
196
- let releaseGlobal: (() => void) | undefined;
197
- let reservedThread: SubagentThread | undefined;
198
- let phase: "queue" | "startup" = "queue";
199
- try {
200
- const waiting = emptyDetails(
201
- agent,
202
- task,
203
- "waiting",
204
- "queue",
205
- thread?.model ?? parentModel,
206
- thread?.thinkingLevel ?? parentThinking,
207
- threadId,
208
- undefined,
209
- );
210
- await onUpdate?.({ content: [{ type: "text", text: `${agent}: waiting` }], details: waiting });
211
- if (thread) {
212
- reservedThread = thread;
213
- thread.pendingTurns += 1;
214
- releaseThread = await thread.turnGate.acquire(combined);
215
- }
216
- releaseGlobal = await gate.acquire(combined);
217
- if (!thread) {
218
- phase = "startup";
219
- if (!threadId || !definition) throw new Error("Subagent startup state is incomplete");
220
- const selectedDefinition = definition;
221
- if (threads.size >= MAX_RETAINED_THREADS) {
222
- const evicted = [...threads.values()]
223
- .filter((item) => item.pendingTurns === 0)
224
- .sort((a, b) => a.lastUsedAt - b.lastUsedAt)[0];
225
- if (!evicted) throw new Error("Subagent thread limit reached while all retained threads are busy");
226
- threads.delete(evicted.id);
227
- await disposeSubagentThread(evicted);
185
+
186
+ return runtime.execute({
187
+ agent,
188
+ task,
189
+ files,
190
+ continuing,
191
+ threadKey,
192
+ ctx,
193
+ parentModel,
194
+ parentThinking,
195
+ signal,
196
+ onUpdate: (details) =>
197
+ onUpdate?.({
198
+ content: [
199
+ {
200
+ type: "text",
201
+ text: details.currentActivity ?? details.response ?? `${details.agent}: ${details.status}`,
202
+ },
203
+ ],
204
+ details,
205
+ }),
206
+ resolveFreshDefinition: async () => {
207
+ const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
208
+ warn(discovery, ctx);
209
+ const definition = discovery.agents.get(agent);
210
+ const invalid = discovery.invalid.get(agent);
211
+ if (!definition) {
212
+ const reason = invalid?.map((item) => item.reason).join("; ") ?? "unknown agent";
213
+ const names =
214
+ (await parentVisibleAgents(ctx, discovery)).map((item) => item.name).join(", ") || "none";
215
+ return {
216
+ ok: false,
217
+ phase: "discovery",
218
+ error: `Agent ${agent} discovery failed: ${reason}. Runnable agents: ${names}`,
219
+ };
228
220
  }
229
- thread = await createSubagentThread({
230
- id: threadId,
231
- definition: selectedDefinition,
232
- extensionPaths: extensionPathsForTools(pi, selectedDefinition.tools),
233
- initialTask: task,
234
- ctx,
235
- thinkingLevel: parentThinking,
236
- signal: combined,
237
- onWarning: (warning) => {
238
- const message = `Subagent definition ${selectedDefinition.path}: ${warning}`;
239
- if (runtimeWarnings.has(message)) return;
240
- runtimeWarnings.add(message);
241
- ctx.ui.notify(message, "warning");
242
- },
243
- });
244
- threads.set(thread.id, thread);
245
- reservedThread = thread;
246
- thread.pendingTurns += 1;
247
- }
248
- if (!thread) throw new Error("Subagent thread startup failed");
249
- const result = await runSubagentTurn({
250
- thread,
251
- task,
252
- initial: thread.turns === 0,
253
- signal: combined,
254
- onUpdate: (details) =>
255
- onUpdate?.({
256
- content: [
257
- {
258
- type: "text",
259
- text: details.currentActivity ?? details.response ?? `${agent}: ${details.status}`,
260
- },
261
- ],
262
- details,
263
- }),
264
- });
265
- return {
266
- content: [
267
- {
268
- type: "text",
269
- text: `Thread: ${thread.id}\nReuse with subagent({ thread: "${thread.id}", task: "..." })\n\n${result.content}`,
270
- },
271
- ],
272
- details: result.details,
273
- };
274
- } catch (error) {
275
- const message = error instanceof Error ? error.message : `Agent ${agent} ${phase} failed`;
276
- const status = combined.aborted ? "aborted" : "failed";
277
- const details = emptyDetails(
278
- agent,
279
- task,
280
- status,
281
- phase,
282
- thread?.model ?? parentModel,
283
- thread?.thinkingLevel ?? parentThinking,
284
- thread?.id ?? threadId,
285
- message,
286
- );
287
- return { content: [{ type: "text", text: message }], details };
288
- } finally {
289
- if (reservedThread) reservedThread.pendingTurns -= 1;
290
- releaseGlobal?.();
291
- releaseThread?.();
292
- controllers.delete(controller);
293
- }
221
+ if (definition.name === "context-sync") {
222
+ const sync = (await loadTauExtensionSettings(ctx, contextSettings)).sync;
223
+ if (!sync.enabled) {
224
+ return { ok: false, phase: "discovery", error: "Context sync is disabled in settings." };
225
+ }
226
+ if (!sync.automation) {
227
+ return {
228
+ ok: false,
229
+ phase: "discovery",
230
+ error: "Context-sync automation is disabled. Use /context-sync or enable extensions.context.sync.automation.",
231
+ };
232
+ }
233
+ }
234
+ return { ok: true, definition };
235
+ },
236
+ });
294
237
  },
295
238
  renderCall(args, theme, context) {
296
239
  return renderSubagentCall(args, theme, {
@@ -318,16 +261,20 @@ Delegate one focused task per call. Children do not inherit parent messages. Inc
318
261
  if (details?.status === "failed" || details?.status === "aborted") return { isError: true };
319
262
  });
320
263
  pi.on("session_start", async () => {
321
- for (const controller of controllers) controller.abort();
322
- controllers.clear();
323
- await disposeThreads();
324
- nextThreadId = 1;
264
+ await runtime.reset();
265
+ await replaceDashboard();
325
266
  rowState.clear();
326
- runtimeWarnings.clear();
267
+ fingerprints.clear();
268
+ failureNotify = undefined;
327
269
  });
328
270
  pi.on("session_shutdown", async () => {
329
- for (const controller of controllers) controller.abort();
330
- controllers.clear();
331
- await disposeThreads();
271
+ unsubscribeDashboard();
272
+ await runtime.shutdown();
273
+ const leftover = await dashboard.shutdown();
274
+ orphans.push(...leftover);
275
+ await retryOrphans();
276
+ rowState.clear();
277
+ fingerprints.clear();
278
+ failureNotify = undefined;
332
279
  });
333
280
  }
@@ -47,7 +47,7 @@ export function renderSubagentResult(
47
47
  text.setText("");
48
48
  return text;
49
49
  }
50
- const identity = details.threadId ? `${details.agent} · ${details.threadId}` : details.agent;
50
+ const identity = `${details.displayName} (${details.agent})`;
51
51
  const header = `${title(theme, context.rowState, context.rowId, context.invalidate)} ${theme.fg("accent", identity)} ${theme.fg("muted", `$${details.usage.cost.toFixed(4)} · ${(details.durationMs / 1000).toFixed(1)}s · ${details.toolCalls} tools`)}`;
52
52
  if (!expanded) {
53
53
  text.setText(`${header} ${theme.fg("muted", details.task.replace(/\s+/g, " ").trim())}`);
@@ -0,0 +1,78 @@
1
+ import type { AgentDefinition } from "./agents.ts";
2
+
3
+ const RESUME_OPTIONAL_HISTORY_TOKENS = 20_000;
4
+
5
+ export type RetainedTurnOutcome = "completed" | "failed" | "aborted";
6
+
7
+ export interface RetainedTurnRecord {
8
+ task: string;
9
+ outcome: RetainedTurnOutcome;
10
+ terminalText: string;
11
+ files: readonly string[];
12
+ }
13
+
14
+ export interface SubagentResumeState {
15
+ records: readonly RetainedTurnRecord[];
16
+ relevantPaths: readonly string[];
17
+ }
18
+
19
+ export function emptySubagentResumeState(): SubagentResumeState {
20
+ return { records: [], relevantPaths: [] };
21
+ }
22
+
23
+ export function retainSubagentTurn(state: SubagentResumeState, record: RetainedTurnRecord): SubagentResumeState {
24
+ const paths = new Set(state.relevantPaths);
25
+ for (const path of record.files) paths.add(path);
26
+ return {
27
+ records: [...state.records, { ...record, files: [...record.files] }],
28
+ relevantPaths: [...paths],
29
+ };
30
+ }
31
+
32
+ export function selectRetainedTurns(
33
+ state: SubagentResumeState,
34
+ optionalTokenBudget = RESUME_OPTIONAL_HISTORY_TOKENS,
35
+ ): { records: readonly RetainedTurnRecord[]; omitted: number } {
36
+ const initial = state.records[0];
37
+ if (!initial) return { records: [], omitted: 0 };
38
+ let used = 0;
39
+ const selectedIndexes = new Set<number>();
40
+ for (let index = state.records.length - 1; index >= 1; index -= 1) {
41
+ const record = state.records[index];
42
+ if (!record) continue;
43
+ const tokens = Math.ceil(JSON.stringify(record).length / 4);
44
+ if (used + tokens > optionalTokenBudget) continue;
45
+ used += tokens;
46
+ selectedIndexes.add(index);
47
+ }
48
+ return {
49
+ records: [initial, ...state.records.slice(1).filter((_record, index) => selectedIndexes.has(index + 1))],
50
+ omitted: state.records.length - 1 - selectedIndexes.size,
51
+ };
52
+ }
53
+
54
+ export function buildColdResumePrompt(options: {
55
+ definition: AgentDefinition;
56
+ state: SubagentResumeState;
57
+ followUp: string;
58
+ hasAutoreadFiles: boolean;
59
+ }): string {
60
+ const selected = selectRetainedTurns(options.state);
61
+ const data = JSON.stringify({
62
+ version: 1,
63
+ retainedTurns: selected.records,
64
+ omittedRetainedTurns: selected.omitted,
65
+ relevantPaths: options.state.relevantPaths,
66
+ parentFollowUp: options.followUp,
67
+ });
68
+ return `You are an isolated delegated child agent resuming prior delegated work. Stay within the delegated task and return only the requested result.${options.hasAutoreadFiles ? " Parent-supplied autoread files are included as line-numbered context for this turn." : ""}
69
+
70
+ ## Agent instructions
71
+ ${options.definition.prompt}
72
+
73
+ The JSON object between the fixed markers is prior work data. Its retained terminal text is exact. Earlier file contents, tool calls, tool results, intermediate responses, and thinking are absent. Relevant paths are a manifest only; read current source before relying on it.
74
+
75
+ <tau-subagent-resume-json>
76
+ ${data}
77
+ </tau-subagent-resume-json>`;
78
+ }