@shanepadgett/tau-agent 0.42.3 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +0 -1
  2. package/docs/context.md +1 -1
  3. package/docs/extending-tau-agent.md +1 -2
  4. package/extensions/attention/README.md +1 -1
  5. package/extensions/cache-diagnostics/index.ts +4 -0
  6. package/extensions/context/README.md +1 -24
  7. package/extensions/context/definitions.ts +1 -52
  8. package/extensions/context/index.ts +1 -150
  9. package/extensions/context/panel.ts +0 -72
  10. package/extensions/explore/guidance.ts +9 -1
  11. package/extensions/footer/README.md +1 -1
  12. package/extensions/footer/index.ts +3 -25
  13. package/extensions/qna/choice-question-body.ts +7 -2
  14. package/extensions/run-summary/README.md +1 -1
  15. package/extensions/run-summary/index.ts +18 -25
  16. package/extensions/runtime-context/README.md +1 -1
  17. package/extensions/runtime-context/context.ts +1 -1
  18. package/extensions/runtime-context/index.ts +10 -16
  19. package/extensions/script-runner/index.ts +21 -29
  20. package/extensions/silent-command-runner/README.md +0 -2
  21. package/extensions/silent-command-runner/index.ts +12 -41
  22. package/extensions/soul/README.md +5 -8
  23. package/extensions/soul/context.ts +115 -0
  24. package/extensions/soul/index.ts +141 -7
  25. package/extensions/soul/prompt.ts +47 -102
  26. package/extensions/soul/state.ts +114 -0
  27. package/extensions/soul/tools.ts +31 -0
  28. package/extensions/tau-help/help.md +6 -18
  29. package/extensions/tau-help/index.ts +10 -4
  30. package/extensions/tool-approval/README.md +4 -2
  31. package/extensions/tool-approval/index.ts +141 -53
  32. package/extensions/tool-approval/panel.ts +153 -0
  33. package/extensions/tool-loader/README.md +1 -1
  34. package/extensions/tool-loader/index.ts +91 -22
  35. package/package.json +2 -2
  36. package/schemas/tau.schema.json +0 -104
  37. package/shared/bounded-text-result.ts +0 -1
  38. package/shared/events.ts +19 -15
  39. package/shared/isolated-session.ts +1 -2
  40. package/shared/model-effort.ts +15 -21
  41. package/shared/prompt-contributions.ts +24 -0
  42. package/src/index.ts +1 -1
  43. package/docs/subagents.md +0 -92
  44. package/extensions/auto-compact/README.md +0 -9
  45. package/extensions/auto-compact/index.ts +0 -122
  46. package/extensions/auto-compact/settings.ts +0 -30
  47. package/extensions/context/settings.ts +0 -62
  48. package/extensions/context/sync.ts +0 -276
  49. package/extensions/context/validation.ts +0 -88
  50. package/extensions/effort/README.md +0 -7
  51. package/extensions/effort/index.ts +0 -134
  52. package/extensions/effort/state.ts +0 -18
  53. package/extensions/qna/inline-editor-row.ts +0 -56
  54. package/extensions/soul/overseer.ts +0 -273
  55. package/extensions/soul/settings.ts +0 -34
  56. package/extensions/subagent/README.md +0 -67
  57. package/extensions/subagent/agents/context-sync.md +0 -244
  58. package/extensions/subagent/agents/dormant/generalist.md +0 -33
  59. package/extensions/subagent/agents/scout.md +0 -104
  60. package/extensions/subagent/agents/web-research.md +0 -101
  61. package/extensions/subagent/agents.ts +0 -261
  62. package/extensions/subagent/cmux-dashboard.ts +0 -495
  63. package/extensions/subagent/index.ts +0 -425
  64. package/extensions/subagent/panel.ts +0 -124
  65. package/extensions/subagent/render.ts +0 -74
  66. package/extensions/subagent/resume.ts +0 -78
  67. package/extensions/subagent/run.ts +0 -641
  68. package/extensions/subagent/runtime.ts +0 -1296
  69. package/extensions/subagent/session-resource.ts +0 -61
  70. package/extensions/subagent/settings.ts +0 -18
@@ -1,425 +0,0 @@
1
- import { rm } from "node:fs/promises";
2
- import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { Type } from "typebox";
4
- import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
5
- import { createToolRowStateStore } from "../../shared/tool-row-state.js";
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";
9
- import { createAgentsPanel } from "./panel.ts";
10
- import { renderSubagentCall, renderSubagentResult } from "./render.ts";
11
- import type { SubagentDetails } from "./run.ts";
12
- import { failedToolResult, SubagentRuntime } from "./runtime.ts";
13
- import subagentSettings from "./settings.ts";
14
-
15
- interface SubagentSessionState {
16
- disabled: string[];
17
- }
18
-
19
- const SUBAGENT_SESSION_STATE_TYPE = "tau.subagent.disabled";
20
-
21
- const params = Type.Object(
22
- {
23
- agent: Type.Optional(
24
- Type.String({
25
- minLength: 1,
26
- description: "Fresh agent name. Pass agent or thread, never both.",
27
- }),
28
- ),
29
- thread: Type.Optional(
30
- Type.String({
31
- minLength: 1,
32
- description: "Retained thread id. Pass thread or agent, never both.",
33
- }),
34
- ),
35
- task: Type.String({ minLength: 1 }),
36
- files: Type.Optional(
37
- Type.Array(Type.String({ minLength: 1 }), {
38
- description: "Files to autoread into the child's context before this turn",
39
- }),
40
- ),
41
- },
42
- { additionalProperties: false },
43
- );
44
-
45
- export default function subagentExtension(pi: ExtensionAPI): void {
46
- const runtime = new SubagentRuntime(pi);
47
- let sessionDisabled = new Set<string>();
48
- let failureNotify: ((message: string) => void) | undefined;
49
- const orphans: DashboardOrphan[] = [];
50
- const retryOrphans = async () => {
51
- if (orphans.length === 0) return;
52
- const pending = orphans.splice(0, orphans.length);
53
- for (const orphan of pending) {
54
- if (orphan.surfaceId && orphan.workspaceId) {
55
- const result = await pi
56
- .exec(
57
- "cmux",
58
- [
59
- "--json",
60
- "--id-format",
61
- "both",
62
- "rpc",
63
- "surface.close",
64
- JSON.stringify({ workspace_id: orphan.workspaceId, surface_id: orphan.surfaceId }),
65
- ],
66
- { timeout: 2500 },
67
- )
68
- .catch(() => ({ stdout: "", stderr: "close failed", code: 1, killed: false }));
69
- const closed =
70
- (result.code === 0 && !result.killed) ||
71
- /not found|unknown surface|no such/i.test(`${result.stderr}\n${result.stdout}`);
72
- if (!closed) {
73
- orphans.push(orphan);
74
- continue;
75
- }
76
- }
77
- // Known-closed or never had a surface id after ambiguous open: drop directory only when closed/absent.
78
- // Ambiguous no-id orphans stay on disk; keep tracking so we do not open more without awareness.
79
- if (orphan.surfaceId) {
80
- await rm(orphan.directory, { recursive: true, force: true }).catch(() => undefined);
81
- } else {
82
- orphans.push(orphan);
83
- }
84
- }
85
- };
86
- const makeDashboard = () =>
87
- createCmuxDashboard({
88
- exec: (command, args, options) => pi.exec(command, args, options),
89
- canOpen: () => orphans.length === 0,
90
- notify: (message) => {
91
- try {
92
- failureNotify?.(message);
93
- } catch {
94
- // ignore
95
- }
96
- },
97
- });
98
- let dashboard: CmuxDashboard = makeDashboard();
99
- let unsubscribeDashboard = runtime.subscribe((snapshot) => {
100
- dashboard.onSnapshot(snapshot);
101
- });
102
- const replaceDashboard = async () => {
103
- unsubscribeDashboard();
104
- const leftover = await dashboard.shutdown();
105
- orphans.push(...leftover);
106
- await retryOrphans();
107
- dashboard = makeDashboard();
108
- unsubscribeDashboard = runtime.subscribe((snapshot) => {
109
- dashboard.onSnapshot(snapshot);
110
- });
111
- };
112
- const fingerprints = new Map<string, string>();
113
- const rowState = createToolRowStateStore(pi, "subagent.tool-row-state");
114
- const warn = (discovery: AgentDiscovery, ctx: ExtensionContext) => {
115
- const current = new Set<string>();
116
- const grouped = new Map<string, string[]>();
117
- for (const diagnostic of discovery.diagnostics) {
118
- const reasons = grouped.get(diagnostic.path) ?? [];
119
- reasons.push(diagnostic.reason);
120
- grouped.set(diagnostic.path, reasons);
121
- }
122
- for (const [path, reasons] of grouped) {
123
- current.add(path);
124
- const fingerprint = [...reasons].sort().join("\0");
125
- if (fingerprints.get(path) !== fingerprint)
126
- ctx.ui.notify(`Invalid subagent definition ${path}: ${reasons.join("; ").slice(0, 500)}`, "warning");
127
- fingerprints.set(path, fingerprint);
128
- }
129
- for (const path of fingerprints.keys()) if (!current.has(path)) fingerprints.delete(path);
130
- };
131
- const disabledAgentNames = async (ctx: ExtensionContext) => {
132
- const configured = new Set((await loadTauExtensionSettings(ctx, subagentSettings)).disabled);
133
- return { configured, effective: new Set([...configured, ...sessionDisabled]) };
134
- };
135
- const parentVisibleAgents = async (ctx: ExtensionContext, discovery: AgentDiscovery) => {
136
- const [sync, disabled] = await Promise.all([
137
- loadTauExtensionSettings(ctx, contextSettings).then((settings) => settings.sync),
138
- disabledAgentNames(ctx),
139
- ]);
140
- const parentVisible = sync.enabled && sync.automation;
141
- return [...discovery.agents.values()]
142
- .filter((agent) => !disabled.effective.has(agent.name) && (agent.name !== "context-sync" || parentVisible))
143
- .sort((a, b) => a.name.localeCompare(b.name));
144
- };
145
- const restoreSessionState = (ctx: ExtensionContext) => {
146
- let saved: string[] | undefined;
147
- for (const entry of ctx.sessionManager.getBranch()) {
148
- if (entry.type !== "custom" || entry.customType !== SUBAGENT_SESSION_STATE_TYPE) continue;
149
- const data = entry.data as { disabled?: unknown } | undefined;
150
- if (Array.isArray(data?.disabled)) {
151
- saved = data.disabled.filter((name): name is string => typeof name === "string" && name.length > 0);
152
- }
153
- }
154
- sessionDisabled = new Set(saved ?? []);
155
- };
156
-
157
- pi.registerCommand("agents", {
158
- description: "Enable/disable subagents for this session",
159
- handler: async (_args, ctx) => {
160
- if (ctx.mode !== "tui") {
161
- ctx.ui.notify("/agents requires TUI mode", "error");
162
- return;
163
- }
164
-
165
- const [discovery, disabled] = await Promise.all([
166
- discoverAgents(ctx.cwd, ctx.isProjectTrusted()),
167
- disabledAgentNames(ctx),
168
- ]);
169
- warn(discovery, ctx);
170
- const agents = [...discovery.agents.values()].sort((a, b) => a.name.localeCompare(b.name));
171
- if (agents.length === 0) {
172
- ctx.ui.notify("No valid subagents found", "warning");
173
- return;
174
- }
175
-
176
- await ctx.ui.custom((tui, theme, _keybindings, done) =>
177
- createAgentsPanel(
178
- tui,
179
- theme,
180
- agents.map((agent) => ({
181
- id: agent.name,
182
- disabled: disabled.effective.has(agent.name),
183
- configured: disabled.configured.has(agent.name),
184
- })),
185
- (disabledNames) => {
186
- for (const agent of agents) {
187
- if (!disabled.configured.has(agent.name)) sessionDisabled.delete(agent.name);
188
- }
189
- for (const name of disabledNames) sessionDisabled.add(name);
190
- pi.appendEntry<SubagentSessionState>(SUBAGENT_SESSION_STATE_TYPE, {
191
- disabled: [...sessionDisabled].sort(),
192
- });
193
- },
194
- () => done(undefined),
195
- ),
196
- );
197
- },
198
- });
199
-
200
- pi.on("before_agent_start", async (event, ctx) => {
201
- if (!pi.getActiveTools().includes("subagent")) return undefined;
202
- const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
203
- warn(discovery, ctx);
204
- const agents = await parentVisibleAgents(ctx, discovery);
205
- const lines = agents.map((agent) => `- ${agent.name}: ${agent.description}`);
206
- const scoutGuidance = agents.some((agent) => agent.name === "scout")
207
- ? "\n\nScout only for substantial multi-hop lookup that would flood parent context. Skip few known-path reads, single declaration lookups, or small digs with most evidence already in hand; use tools directly. When uncertain, dig yourself. Do not send code review, diagnosis, design, or other judgment work to scout."
208
- : "";
209
- const prompt = `## Subagents
210
- Use \`subagent\` only when an available agent's listed purpose matches the delegated work. Stay inside that purpose. Off-purpose calls are denied — for example, do not send code review to a lookup-only agent.
211
-
212
- Available agents for this turn:
213
- ${lines.join("\n") || "none"}
214
-
215
- Start a fresh thread with \`agent\` and \`task\`. Continue an existing thread with \`thread\` and \`task\`. Pass exactly one of \`agent\` or \`thread\`, never both and never neither. 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.
216
-
217
- Pass \`files\` when exact relevant files are already known. Tau autoreads current line-numbered snapshots into that child turn before it starts.
218
-
219
- 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.${scoutGuidance}`;
220
- return { systemPrompt: `${event.systemPrompt}\n\n${prompt}` };
221
- });
222
- const resolveFreshSubagentDefinition = async (ctx: ExtensionContext, agent: string) => {
223
- const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
224
- warn(discovery, ctx);
225
- const definition = discovery.agents.get(agent);
226
- const invalid = discovery.invalid.get(agent);
227
- if (!definition) {
228
- const reason = invalid?.map((item) => item.reason).join("; ") ?? "unknown agent";
229
- const names = (await parentVisibleAgents(ctx, discovery)).map((item) => item.name).join(", ") || "none";
230
- return {
231
- ok: false as const,
232
- phase: "discovery" as const,
233
- error: `Agent ${agent} discovery failed: ${reason}. Runnable agents: ${names}`,
234
- };
235
- }
236
- const disabled = await disabledAgentNames(ctx);
237
- if (disabled.effective.has(definition.name)) {
238
- const source = disabled.configured.has(definition.name) ? "in Tau settings" : "for this session";
239
- return {
240
- ok: false as const,
241
- phase: "discovery" as const,
242
- error: `Agent ${definition.name} is disabled ${source}.`,
243
- };
244
- }
245
- if (definition.name === "context-sync") {
246
- const sync = (await loadTauExtensionSettings(ctx, contextSettings)).sync;
247
- if (!sync.enabled) {
248
- return { ok: false as const, phase: "discovery" as const, error: "Context sync is disabled in settings." };
249
- }
250
- if (!sync.automation) {
251
- return {
252
- ok: false as const,
253
- phase: "discovery" as const,
254
- error: "Context-sync automation is disabled. Use /context-sync or enable extensions.context.sync.automation.",
255
- };
256
- }
257
- }
258
- return { ok: true as const, definition };
259
- };
260
-
261
- const disabledContinuationResult = async (
262
- ctx: ExtensionContext,
263
- threadKey: string | undefined,
264
- task: string,
265
- parentModel: string,
266
- parentThinking: ReturnType<ExtensionAPI["getThinkingLevel"]>,
267
- ) => {
268
- const thread = runtime.listThreads(ctx.cwd).find((item) => item.id === threadKey);
269
- if (!thread) return undefined;
270
- const disabled = await disabledAgentNames(ctx);
271
- if (!disabled.effective.has(thread.definition.name)) return undefined;
272
- const source = disabled.configured.has(thread.definition.name) ? "in Tau settings" : "for this session";
273
- return failedToolResult(
274
- thread.definition.name,
275
- task,
276
- "discovery",
277
- parentModel,
278
- parentThinking,
279
- `Agent ${thread.definition.name} is disabled ${source}.`,
280
- threadKey,
281
- );
282
- };
283
-
284
- pi.registerTool(
285
- defineTool<typeof params, SubagentDetails>({
286
- name: "subagent",
287
- label: "Subagent",
288
- description:
289
- "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.",
290
- promptSnippet: "Start a focused child agent or continue a retained child thread",
291
- parameters: params,
292
- executionMode: "parallel",
293
- async execute(_id, raw, signal, onUpdate, ctx) {
294
- const task = raw.task.trim();
295
- const files = [...new Set((raw.files ?? []).map((path) => path.trim()))];
296
- const agentName = raw.agent?.trim() || undefined;
297
- const threadKey = raw.thread?.trim() || undefined;
298
- const parentModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "unavailable";
299
- const parentThinking = pi.getThinkingLevel();
300
-
301
- failureNotify = (message) => {
302
- ctx.ui.notify(message, "warning");
303
- };
304
- dashboard.setInteractive(ctx.mode === "tui" && ctx.hasUI);
305
-
306
- const failQueue = (agent: string, error: string, thread?: string) =>
307
- failedToolResult(agent, task, "queue", parentModel, parentThinking, error, thread);
308
-
309
- if (!task || files.some((path) => !path)) {
310
- return failQueue(
311
- agentName ?? threadKey ?? "",
312
- threadKey !== undefined
313
- ? "Subagent continuation requires non-empty thread, task, and file paths"
314
- : "Subagent input requires non-empty agent, task, and file paths",
315
- threadKey,
316
- );
317
- }
318
- if (agentName !== undefined && threadKey !== undefined) {
319
- return failQueue(
320
- agentName,
321
- "Subagent input requires exactly one of agent or thread, not both",
322
- threadKey,
323
- );
324
- }
325
-
326
- const onUpdateDetails = (details: SubagentDetails) =>
327
- onUpdate?.({
328
- content: [
329
- {
330
- type: "text",
331
- text: details.currentActivity ?? details.response ?? `${details.agent}: ${details.status}`,
332
- },
333
- ],
334
- details,
335
- });
336
-
337
- if (threadKey !== undefined) {
338
- const disabledContinuation = await disabledContinuationResult(
339
- ctx,
340
- threadKey,
341
- task,
342
- parentModel,
343
- parentThinking,
344
- );
345
- if (disabledContinuation) return disabledContinuation;
346
- return runtime.execute({
347
- agent: threadKey,
348
- task,
349
- files,
350
- continuing: true,
351
- threadKey,
352
- ctx,
353
- parentModel,
354
- parentThinking,
355
- signal,
356
- onUpdate: onUpdateDetails,
357
- resolveFreshDefinition: () => resolveFreshSubagentDefinition(ctx, threadKey),
358
- });
359
- }
360
-
361
- if (agentName === undefined) {
362
- return failQueue("", "Subagent input requires exactly one of agent or thread");
363
- }
364
-
365
- return runtime.execute({
366
- agent: agentName,
367
- task,
368
- files,
369
- continuing: false,
370
- ctx,
371
- parentModel,
372
- parentThinking,
373
- signal,
374
- onUpdate: onUpdateDetails,
375
- resolveFreshDefinition: () => resolveFreshSubagentDefinition(ctx, agentName),
376
- });
377
- },
378
- renderCall(args, theme, context) {
379
- return renderSubagentCall(args, theme, {
380
- executionStarted: context.executionStarted,
381
- isPartial: context.isPartial,
382
- lastComponent: context.lastComponent,
383
- rowState,
384
- rowId: context.toolCallId,
385
- invalidate: context.invalidate,
386
- });
387
- },
388
- renderResult(result, options, theme, context) {
389
- return renderSubagentResult(result, options.expanded, theme, {
390
- lastComponent: context.lastComponent,
391
- rowState,
392
- rowId: context.toolCallId,
393
- invalidate: context.invalidate,
394
- });
395
- },
396
- }),
397
- );
398
- pi.on("tool_result", (event) => {
399
- if (event.toolName !== "subagent") return;
400
- const details = event.details as SubagentDetails | undefined;
401
- if (details?.status === "failed" || details?.status === "aborted") return { isError: true };
402
- });
403
- pi.on("session_start", async (_event, ctx) => {
404
- restoreSessionState(ctx);
405
- await runtime.reset();
406
- await replaceDashboard();
407
- rowState.clear();
408
- fingerprints.clear();
409
- failureNotify = undefined;
410
- });
411
- pi.on("session_tree", (_event, ctx) => {
412
- restoreSessionState(ctx);
413
- });
414
- pi.on("session_shutdown", async () => {
415
- unsubscribeDashboard();
416
- await runtime.shutdown();
417
- const leftover = await dashboard.shutdown();
418
- orphans.push(...leftover);
419
- await retryOrphans();
420
- rowState.clear();
421
- fingerprints.clear();
422
- failureNotify = undefined;
423
- sessionDisabled.clear();
424
- });
425
- }
@@ -1,124 +0,0 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { type Component, getKeybindings, Key, matchesKey, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
3
- import {
4
- rawHint,
5
- SelectableList,
6
- type SelectableListResult,
7
- ToolPanel,
8
- type ToolPanelConfig,
9
- } from "@shanepadgett/tau-tui";
10
-
11
- export interface AgentPanelItem {
12
- id: string;
13
- disabled: boolean;
14
- configured: boolean;
15
- }
16
-
17
- export function createAgentsPanel(
18
- tui: TUI,
19
- theme: Theme,
20
- initial: readonly AgentPanelItem[],
21
- onApply: (disabled: readonly string[]) => void,
22
- done: () => void,
23
- ): Component {
24
- return new AgentsPanel(tui, theme, initial, onApply, done);
25
- }
26
-
27
- class AgentsPanel implements Component {
28
- private readonly tui: TUI;
29
- private readonly onApply: (disabled: readonly string[]) => void;
30
- private readonly done: () => void;
31
- private readonly list: SelectableList<AgentPanelItem>;
32
- private readonly panelConfig: ToolPanelConfig;
33
- private readonly panel: ToolPanel;
34
- private items: readonly AgentPanelItem[];
35
- private closed = false;
36
-
37
- constructor(
38
- tui: TUI,
39
- theme: Theme,
40
- initial: readonly AgentPanelItem[],
41
- onApply: (disabled: readonly string[]) => void,
42
- done: () => void,
43
- ) {
44
- this.tui = tui;
45
- this.items = initial;
46
- this.onApply = onApply;
47
- this.done = done;
48
- this.list = new SelectableList(theme, {
49
- items: initial,
50
- emptyMessage: "No valid subagents found.",
51
- selection: { kind: "single", primaryLabel: "apply" },
52
- actions: [],
53
- cancelLabel: "close",
54
- maxVisible: 10,
55
- renderItem: (item, state, width) => {
56
- const label = state.active ? theme.bold(item.id) : item.id;
57
- const status = item.configured
58
- ? theme.fg("muted", "disabled by Tau settings")
59
- : item.disabled
60
- ? theme.fg("warning", "disabled")
61
- : theme.fg("success", "enabled");
62
- return [truncateToWidth(`${label} ${status}`, width, "…")];
63
- },
64
- onResult: (result) => this.handleResult(result),
65
- });
66
- this.panelConfig = {
67
- title: "Subagents",
68
- secondary: "Changes apply to this session.",
69
- body: this.list,
70
- footer: { kind: "hints", hints: this.hints() },
71
- border: "box",
72
- };
73
- this.panel = new ToolPanel(theme, this.panelConfig);
74
- }
75
-
76
- render(width: number): string[] {
77
- return this.panel.render(width);
78
- }
79
-
80
- invalidate(): void {
81
- this.panel.invalidate();
82
- }
83
-
84
- handleInput(data: string): void {
85
- if (matchesKey(data, Key.space) || data === " ") {
86
- const current = this.list.getCurrentItem();
87
- if (!current || current.configured) return;
88
- this.items = this.items.map((item) => (item.id === current.id ? { ...item, disabled: !item.disabled } : item));
89
- this.list.setItems(this.items, current.id);
90
- this.syncPanel();
91
- return;
92
- }
93
- if (getKeybindings().matches(data, "tui.select.confirm")) {
94
- this.onApply(this.items.filter((item) => item.disabled && !item.configured).map((item) => item.id));
95
- this.closed = true;
96
- this.done();
97
- return;
98
- }
99
- this.list.handleInput(data);
100
- if (!this.closed) this.syncPanel();
101
- }
102
-
103
- private handleResult(result: SelectableListResult<AgentPanelItem>): void {
104
- if (result.kind === "cancel") {
105
- this.closed = true;
106
- this.done();
107
- return;
108
- }
109
- }
110
-
111
- private syncPanel(): void {
112
- this.panelConfig.footer = { kind: "hints", hints: this.hints() };
113
- this.tui.requestRender();
114
- }
115
-
116
- private hints() {
117
- const hints = this.list.getKeyHints();
118
- const current = this.list.getCurrentItem();
119
- if (current && !current.configured) {
120
- hints.splice(1, 0, rawHint("Space", "toggle"));
121
- }
122
- return hints;
123
- }
124
- }
@@ -1,74 +0,0 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { Text, type Component } from "@earendil-works/pi-tui";
3
- import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.js";
4
- import type { SubagentDetails } from "./run.ts";
5
-
6
- const cap = (text: string | undefined, length = 240) =>
7
- !text ? "" : text.length <= length ? text : `${text.slice(0, length - 1)}…`;
8
- const component = (last: unknown) => (last as Text | undefined) ?? new Text("", 0, 0);
9
- const title = (theme: Theme, rowState: ToolRowStateStore, rowId: string, invalidate: () => void) => {
10
- rowState.watch(rowId, invalidate);
11
- return formatToolRowTitle(rowState, rowId, "subagent", theme);
12
- };
13
-
14
- export function renderSubagentCall(
15
- args: { agent?: string; thread?: string; task?: string },
16
- theme: Theme,
17
- context: {
18
- executionStarted: boolean;
19
- isPartial: boolean;
20
- lastComponent?: unknown;
21
- rowState: ToolRowStateStore;
22
- rowId: string;
23
- invalidate: () => void;
24
- },
25
- ): Component {
26
- const text = component(context.lastComponent);
27
- if (context.executionStarted || !context.isPartial) {
28
- text.setText("");
29
- return text;
30
- }
31
- const prefix =
32
- `${title(theme, context.rowState, context.rowId, context.invalidate)} ${theme.fg("accent", args.agent ?? args.thread ?? "")}`.trimEnd();
33
- const task = args.task?.replace(/\s+/g, " ").trim();
34
- text.setText(task ? `${prefix} ${theme.fg("muted", task)}` : prefix);
35
- return text;
36
- }
37
-
38
- export function renderSubagentResult(
39
- result: { details?: SubagentDetails },
40
- expanded: boolean,
41
- theme: Theme,
42
- context: { lastComponent?: unknown; rowState: ToolRowStateStore; rowId: string; invalidate: () => void },
43
- ): Component {
44
- const text = component(context.lastComponent);
45
- const details = result.details;
46
- if (!details) {
47
- text.setText("");
48
- return text;
49
- }
50
- const identity = `${details.displayName} (${details.agent})`;
51
- const ctx = typeof details.contextPercent === "number" ? ` · ${details.contextPercent.toFixed(1)}% ctx` : "";
52
- 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${ctx}`)}`;
53
- if (!expanded) {
54
- text.setText(`${header} ${theme.fg("muted", details.task.replace(/\s+/g, " ").trim())}`);
55
- return text;
56
- }
57
- const lines = [header, theme.fg("muted", `Task: ${cap(details.task, 1000)}`)];
58
- for (const action of details.actions)
59
- lines.push(theme.fg(action.error ? "error" : "dim", `${action.error ? "!" : "·"} ${cap(action.summary)}`));
60
- if (details.response) lines.push(theme.fg("toolOutput", details.response));
61
- if (details.error) lines.push(theme.fg("error", cap(details.error, 1000)));
62
- lines.push(theme.fg("muted", `Model: ${details.model} · thinking: ${details.thinkingLevel}`));
63
- lines.push(
64
- theme.fg(
65
- "muted",
66
- `Tokens: ${details.usage.input} in, ${details.usage.output} out, ${details.usage.cacheRead} cache read, ${details.usage.cacheWrite} cache write · ${details.usage.turns} turns`,
67
- ),
68
- );
69
- if (details.omittedActions)
70
- lines.push(theme.fg("muted", `${details.omittedActions} actions omitted (${details.omittedErrors} errors)`));
71
- if (details.truncation?.path) lines.push(theme.fg("warning", `Full output: ${details.truncation.path}`));
72
- text.setText(lines.join("\n"));
73
- return text;
74
- }
@@ -1,78 +0,0 @@
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
- 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
- }