@shanepadgett/tau-agent 0.1.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 (138) hide show
  1. package/README.md +30 -0
  2. package/docs/extending-tau-agent.md +119 -0
  3. package/docs/subagents.md +57 -0
  4. package/docs/tui.md +107 -0
  5. package/extensions/appshot/README.md +11 -0
  6. package/extensions/appshot/capture.swift +191 -0
  7. package/extensions/appshot/index.ts +245 -0
  8. package/extensions/appshot/native-helper.ts +102 -0
  9. package/extensions/attention/README.md +23 -0
  10. package/extensions/attention/index.ts +71 -0
  11. package/extensions/auto-name/README.md +26 -0
  12. package/extensions/auto-name/index.ts +163 -0
  13. package/extensions/branch/README.md +22 -0
  14. package/extensions/branch/index.ts +145 -0
  15. package/extensions/branch/panel.ts +148 -0
  16. package/extensions/clear-screen/README.md +29 -0
  17. package/extensions/clear-screen/index.ts +75 -0
  18. package/extensions/commit/README.md +15 -0
  19. package/extensions/commit/commit-plan.ts +328 -0
  20. package/extensions/commit/git-change-set.ts +268 -0
  21. package/extensions/commit/index.ts +138 -0
  22. package/extensions/commit/review-ui.ts +745 -0
  23. package/extensions/explore/README.md +7 -0
  24. package/extensions/explore/autoread.ts +140 -0
  25. package/extensions/explore/find.ts +147 -0
  26. package/extensions/explore/grep.ts +749 -0
  27. package/extensions/explore/index.ts +17 -0
  28. package/extensions/explore/limits.ts +22 -0
  29. package/extensions/explore/ls.ts +113 -0
  30. package/extensions/explore/path-display.ts +41 -0
  31. package/extensions/explore/path-tree.ts +150 -0
  32. package/extensions/explore/read.ts +160 -0
  33. package/extensions/explore/result.ts +29 -0
  34. package/extensions/explore/traverse.ts +226 -0
  35. package/extensions/footer/README.md +23 -0
  36. package/extensions/footer/index.ts +534 -0
  37. package/extensions/footer/settings.ts +13 -0
  38. package/extensions/ideas/README.md +10 -0
  39. package/extensions/ideas/browser.ts +66 -0
  40. package/extensions/ideas/index.ts +31 -0
  41. package/extensions/ideas/store.ts +32 -0
  42. package/extensions/image-gen/README.md +11 -0
  43. package/extensions/image-gen/client.ts +186 -0
  44. package/extensions/image-gen/index.ts +152 -0
  45. package/extensions/manage-sessions/README.md +12 -0
  46. package/extensions/manage-sessions/index.ts +58 -0
  47. package/extensions/manage-sessions/manager-ui.ts +323 -0
  48. package/extensions/manage-sessions/sessions.ts +126 -0
  49. package/extensions/patch/README.md +48 -0
  50. package/extensions/patch/executor.ts +355 -0
  51. package/extensions/patch/index.ts +170 -0
  52. package/extensions/patch/matcher.ts +236 -0
  53. package/extensions/patch/parser.ts +347 -0
  54. package/extensions/patch/render.ts +247 -0
  55. package/extensions/patch/summary.ts +35 -0
  56. package/extensions/publish/README.md +11 -0
  57. package/extensions/publish/index.ts +225 -0
  58. package/extensions/qna/README.md +45 -0
  59. package/extensions/qna/additional-context-body.ts +51 -0
  60. package/extensions/qna/body-render.ts +6 -0
  61. package/extensions/qna/choice-question-body.ts +322 -0
  62. package/extensions/qna/index.ts +245 -0
  63. package/extensions/qna/inline-editor-row.ts +56 -0
  64. package/extensions/qna/input-question-body.ts +89 -0
  65. package/extensions/qna/model.ts +317 -0
  66. package/extensions/qna/panel.ts +240 -0
  67. package/extensions/qna/ui.ts +16 -0
  68. package/extensions/reference/README.md +47 -0
  69. package/extensions/reference/index.ts +76 -0
  70. package/extensions/reference/panel.ts +874 -0
  71. package/extensions/reference/settings.ts +29 -0
  72. package/extensions/run-summary/README.md +5 -0
  73. package/extensions/run-summary/index.ts +98 -0
  74. package/extensions/silent-command-runner/README.md +25 -0
  75. package/extensions/silent-command-runner/index.ts +468 -0
  76. package/extensions/silent-command-runner/settings.ts +65 -0
  77. package/extensions/soul/README.md +19 -0
  78. package/extensions/soul/index.ts +20 -0
  79. package/extensions/soul/prompt.ts +254 -0
  80. package/extensions/soul/settings.ts +13 -0
  81. package/extensions/stash/README.md +48 -0
  82. package/extensions/stash/browser.ts +51 -0
  83. package/extensions/stash/index.ts +55 -0
  84. package/extensions/stash/store.ts +36 -0
  85. package/extensions/subagent/README.md +34 -0
  86. package/extensions/subagent/agents/scout.md +85 -0
  87. package/extensions/subagent/agents/web-research.md +95 -0
  88. package/extensions/subagent/agents.ts +196 -0
  89. package/extensions/subagent/index.ts +214 -0
  90. package/extensions/subagent/render.ts +89 -0
  91. package/extensions/subagent/run.ts +325 -0
  92. package/extensions/tau/README.md +17 -0
  93. package/extensions/tau/index.ts +122 -0
  94. package/extensions/tau-help/README.md +3 -0
  95. package/extensions/tau-help/help.md +125 -0
  96. package/extensions/tau-help/index.ts +68 -0
  97. package/extensions/turn-budget/README.md +14 -0
  98. package/extensions/turn-budget/index.ts +150 -0
  99. package/extensions/turn-budget/settings.ts +35 -0
  100. package/extensions/web/README.md +13 -0
  101. package/extensions/web/codesearch.ts +79 -0
  102. package/extensions/web/exa.ts +101 -0
  103. package/extensions/web/html.ts +67 -0
  104. package/extensions/web/index.ts +13 -0
  105. package/extensions/web/limits.ts +11 -0
  106. package/extensions/web/tool-output.ts +54 -0
  107. package/extensions/web/webfetch.ts +177 -0
  108. package/extensions/web/websearch.ts +100 -0
  109. package/package.json +57 -0
  110. package/prompts/.gitkeep +1 -0
  111. package/prompts/cavemanify.md +25 -0
  112. package/prompts/implement.md +25 -0
  113. package/prompts/interview.md +48 -0
  114. package/prompts/plan-feature.md +30 -0
  115. package/prompts/plan-implementation.md +50 -0
  116. package/schemas/tau.schema.json +205 -0
  117. package/shared/agent-blocked.ts +12 -0
  118. package/shared/description.ts +28 -0
  119. package/shared/events.ts +135 -0
  120. package/shared/git.ts +41 -0
  121. package/shared/injected-context.ts +69 -0
  122. package/shared/jsonl-store.ts +108 -0
  123. package/shared/model-fallback/index.ts +278 -0
  124. package/shared/model-fallback/settings.ts +18 -0
  125. package/shared/model-fallback/types.ts +8 -0
  126. package/shared/ranges.ts +21 -0
  127. package/shared/settings/define.ts +15 -0
  128. package/shared/settings/files.ts +20 -0
  129. package/shared/settings/json.ts +41 -0
  130. package/shared/settings/load.ts +59 -0
  131. package/shared/settings/merge.ts +21 -0
  132. package/shared/settings/paths.ts +41 -0
  133. package/shared/settings/schema.ts +25 -0
  134. package/shared/settings/specs.ts +23 -0
  135. package/shared/text.ts +11 -0
  136. package/shared/tool-row-state.ts +43 -0
  137. package/skills/writing-preferences/SKILL.md +136 -0
  138. package/themes/.gitkeep +1 -0
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: web-research
3
+ description: Perform multi-step web and code research with source-backed synthesis
4
+ tools:
5
+ - websearch
6
+ - codesearch
7
+ - webfetch
8
+ model: openai-codex/gpt-5.6-sol
9
+ thinking: medium
10
+ ---
11
+
12
+ Stay inside delegated task. Answer exactly what was asked. No broader research, background collection, unrequested recommendations, or implementation work.
13
+
14
+ Delegating prompt is output contract. Requested shape wins. Otherwise use the smallest matching shape below.
15
+
16
+ ## Effort and depth
17
+
18
+ Use the least research and fewest tokens needed for a reliable answer. Match depth to the question, not the agent's available tools.
19
+
20
+ - For a simple lookup, verify the value and return it with a direct source URL in one line.
21
+ - For a focused question, give the direct answer and only the evidence or qualification needed to support it.
22
+ - For a comparison or recommendation, explain the decision criteria, strongest supported reasons, meaningful tradeoffs, and why plausible alternatives fit worse. Include only factors relevant to the requested use case.
23
+ - For a multi-part or explicitly deep task, synthesize across suitable sources using the relevant structured result shape.
24
+
25
+ Do not turn a lookup into a survey. Do not compress a consequential recommendation until its reasoning becomes unusable. Do not produce a research brief unless the delegating prompt requests depth or the question requires synthesis across multiple sources.
26
+
27
+ ## Research discipline
28
+
29
+ 1. Extract the exact question, scope, freshness requirement, and required output before searching.
30
+ 2. Start with the most specific query that could answer the question. Refine queries from evidence instead of searching the whole topic.
31
+ 3. Use `websearch` for discovery, `codesearch` for implementation details and API usage, and `webfetch` to inspect authoritative pages found during discovery.
32
+ 4. Every search or fetch must resolve a pending question. Stop when each requested claim has sufficient evidence.
33
+ 5. Prefer primary sources: official documentation, specifications, source repositories, release notes, and first-party statements. Use secondary sources only when primary sources are unavailable or the task asks for outside analysis.
34
+ 6. Check publication and version context when facts may have changed. Do not combine claims from incompatible versions without saying so.
35
+ 7. Corroborate consequential claims with independent sources when practical. A page repeating another source is not independent evidence.
36
+ 8. Put unresolved or conflicting facts under `Unknowns`. Do not fill gaps with plausible synthesis.
37
+
38
+ ## Result shapes
39
+
40
+ Use only relevant sections. Omit empty sections.
41
+
42
+ ### Answer a factual question
43
+
44
+ - `Answer:` direct answer
45
+ - `Evidence:` claim — source URL
46
+ - `Qualification:` limits, version, or date context when needed
47
+
48
+ ### Explain a topic
49
+
50
+ - `Summary:` concise explanation
51
+ - `Key facts:` source-backed facts
52
+ - `Implications:` requested consequences only
53
+ - `Unknowns:` unresolved facts
54
+
55
+ ### Compare
56
+
57
+ - `Shared:` source-backed similarities
58
+ - `Differences:` differences by requested aspect
59
+ - `Relevant consequence:` requested consequences only
60
+
61
+ ### Find an implementation approach
62
+
63
+ - `Recommended approach:` approach supported by current documentation or examples
64
+ - `API or mechanism:` relevant interfaces, behavior, and constraints
65
+ - `Example sources:` direct documentation or source links
66
+ - `Unknowns:` missing version or environment details
67
+
68
+ ### Verify a claim
69
+
70
+ - `Verdict:` `yes`, `no`, `partially`, or `unknown`
71
+ - `Evidence:` source-backed facts
72
+ - `Qualification:` only when needed
73
+
74
+ ### Survey options
75
+
76
+ `Option` — relevant capability — constraint — source URL
77
+
78
+ State selection criteria. Do not rank options unless the task asks for a recommendation.
79
+
80
+ ### Research brief
81
+
82
+ - `Findings:` ordered by relevance
83
+ - `Evidence:` source URLs attached to each material claim
84
+ - `Conflicts:` disagreements between credible sources
85
+ - `Unknowns:` evidence still needed
86
+
87
+ ## Reporting rules
88
+
89
+ - Cite every material factual claim with a direct source URL.
90
+ - Link to the page containing the evidence, not a search result page.
91
+ - Separate source claims from inference. Label inference.
92
+ - Quote only the smallest fragment needed to preserve exact wording.
93
+ - Describe source quality or limitations when they affect confidence.
94
+ - Use exact dates and versions when freshness matters.
95
+ - No preamble, search log, generic topic summary, repeated evidence, or unrequested next steps.
@@ -0,0 +1,196 @@
1
+ import { access, readdir, readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, extname, join, parse, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
6
+
7
+ export interface AgentDefinition {
8
+ name: string;
9
+ description: string;
10
+ tools: string[];
11
+ model?: string;
12
+ thinking?: ThinkingLevel;
13
+ prompt: string;
14
+ path: string;
15
+ }
16
+
17
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
18
+
19
+ export interface AgentDiagnostic {
20
+ path: string;
21
+ name: string;
22
+ reason: string;
23
+ }
24
+
25
+ export interface AgentDiscovery {
26
+ agents: Map<string, AgentDefinition>;
27
+ invalid: Map<string, AgentDiagnostic[]>;
28
+ diagnostics: AgentDiagnostic[];
29
+ }
30
+
31
+ const BUILTIN_AGENTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "agents");
32
+
33
+ async function directoryExists(path: string): Promise<boolean> {
34
+ try {
35
+ await access(path);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ export async function findProjectAgentsDir(cwd: string): Promise<string | undefined> {
43
+ const home = resolve(homedir());
44
+ let current = resolve(cwd);
45
+ while (current !== home) {
46
+ const candidate = join(current, ".pi", "tau", "agents");
47
+ if (await directoryExists(candidate)) return candidate;
48
+ const parent = dirname(current);
49
+ if (parent === current) break;
50
+ current = parent;
51
+ }
52
+ return undefined;
53
+ }
54
+
55
+ function parseDefinition(
56
+ path: string,
57
+ content: string,
58
+ ): { definition?: AgentDefinition; diagnostic?: AgentDiagnostic } {
59
+ const fallbackName = basename(path, extname(path));
60
+ try {
61
+ const parsed = parseFrontmatter(content);
62
+ const fields = Object.keys(parsed.frontmatter);
63
+ const rawName = parsed.frontmatter.name;
64
+ const name = typeof rawName === "string" && rawName.trim() ? rawName.trim() : fallbackName;
65
+ const reasons: string[] = [];
66
+ for (const field of fields) {
67
+ if (!new Set(["name", "description", "tools", "model", "thinking"]).has(field))
68
+ reasons.push(`unsupported field "${field}"`);
69
+ }
70
+ if (typeof rawName !== "string" || !rawName.trim()) reasons.push("name must be a non-empty string");
71
+ const rawDescription = parsed.frontmatter.description;
72
+ if (typeof rawDescription !== "string" || !rawDescription.trim())
73
+ reasons.push("description must be a non-empty string");
74
+ const rawTools = parsed.frontmatter.tools;
75
+ if (!Array.isArray(rawTools) || rawTools.length === 0) reasons.push("tools must be a non-empty array");
76
+ else {
77
+ const tools = rawTools
78
+ .filter((tool): tool is string => typeof tool === "string" && tool.trim().length > 0)
79
+ .map((tool) => tool.trim());
80
+ if (tools.length !== rawTools.length) reasons.push("tools must contain non-empty strings");
81
+ if (new Set(tools).size !== tools.length) reasons.push("tools must be unique");
82
+ if (tools.includes("subagent")) reasons.push("tool subagent is forbidden");
83
+ }
84
+ if (!parsed.body.trim()) reasons.push("prompt body must be non-empty");
85
+ const rawModel = parsed.frontmatter.model;
86
+ if (rawModel !== undefined && (typeof rawModel !== "string" || !/^[^/\s]+\/[^/\s]+$/.test(rawModel.trim())))
87
+ reasons.push("model must be a provider/model string");
88
+ const rawThinking = parsed.frontmatter.thinking;
89
+ const thinkingLevels: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
90
+ if (rawThinking !== undefined && !thinkingLevels.includes(rawThinking as ThinkingLevel))
91
+ reasons.push(`thinking must be one of ${thinkingLevels.join(", ")}`);
92
+ if (reasons.length > 0) return { diagnostic: { path, name, reason: reasons.join("; ") } };
93
+ return {
94
+ definition: {
95
+ name,
96
+ description: (rawDescription as string).trim(),
97
+ tools: (rawTools as string[]).map((tool) => tool.trim()),
98
+ model: typeof rawModel === "string" ? rawModel.trim() : undefined,
99
+ thinking: rawThinking as ThinkingLevel | undefined,
100
+ prompt: parsed.body.trim(),
101
+ path,
102
+ },
103
+ };
104
+ } catch (error) {
105
+ return {
106
+ diagnostic: {
107
+ path,
108
+ name: fallbackName,
109
+ reason: error instanceof Error ? error.message : "invalid frontmatter",
110
+ },
111
+ };
112
+ }
113
+ }
114
+
115
+ async function loadScope(
116
+ directory: string,
117
+ required: boolean,
118
+ ): Promise<Map<string, AgentDefinition | AgentDiagnostic[]>> {
119
+ let entries;
120
+ try {
121
+ entries = (await readdir(directory, { withFileTypes: true }))
122
+ .filter((entry) => entry.isFile() && extname(entry.name) === ".md")
123
+ .sort((a, b) => a.name.localeCompare(b.name));
124
+ } catch (error) {
125
+ if (!required) return new Map();
126
+ const reason = error instanceof Error ? error.message : "directory unavailable";
127
+ return new Map([
128
+ ["scout", [{ path: directory, name: "scout", reason: `packaged agents unavailable: ${reason}` }]],
129
+ [
130
+ "web-research",
131
+ [{ path: directory, name: "web-research", reason: `packaged agents unavailable: ${reason}` }],
132
+ ],
133
+ ]);
134
+ }
135
+ const grouped = new Map<string, Array<AgentDefinition | AgentDiagnostic>>();
136
+ for (const entry of entries) {
137
+ const path = join(directory, entry.name);
138
+ let parsed;
139
+ try {
140
+ parsed = parseDefinition(path, await readFile(path, "utf8"));
141
+ } catch (error) {
142
+ parsed = {
143
+ diagnostic: {
144
+ path,
145
+ name: parse(entry.name).name,
146
+ reason: error instanceof Error ? error.message : "file unreadable",
147
+ },
148
+ };
149
+ }
150
+ const value = parsed.definition ?? parsed.diagnostic;
151
+ if (!value) continue;
152
+ const values = grouped.get(value.name) ?? [];
153
+ values.push(value);
154
+ grouped.set(value.name, values);
155
+ }
156
+ const scope = new Map<string, AgentDefinition | AgentDiagnostic[]>();
157
+ for (const [name, values] of grouped) {
158
+ if (values.length === 1 && "prompt" in values[0]) scope.set(name, values[0]);
159
+ else {
160
+ const diagnostics = values.map((value) =>
161
+ "reason" in value ? value : { path: value.path, name, reason: `duplicate name "${name}" in this scope` },
162
+ );
163
+ if (values.length > 1) {
164
+ for (const value of values)
165
+ if ("reason" in value) value.reason += `; duplicate name "${name}" in this scope`;
166
+ }
167
+ scope.set(name, diagnostics);
168
+ }
169
+ }
170
+ return scope;
171
+ }
172
+
173
+ export async function discoverAgents(cwd: string, trusted: boolean): Promise<AgentDiscovery> {
174
+ const scopes = [
175
+ await loadScope(BUILTIN_AGENTS_DIR, true),
176
+ await loadScope(join(getAgentDir(), "tau", "agents"), false),
177
+ ];
178
+ if (trusted) {
179
+ const project = await findProjectAgentsDir(cwd);
180
+ if (project) scopes.push(await loadScope(project, false));
181
+ }
182
+ const agents = new Map<string, AgentDefinition>();
183
+ const invalid = new Map<string, AgentDiagnostic[]>();
184
+ const diagnostics = scopes.flatMap((scope) =>
185
+ [...scope.values()].flatMap((value) => (Array.isArray(value) ? value : [])),
186
+ );
187
+ for (const scope of scopes) {
188
+ for (const [name, value] of scope) {
189
+ agents.delete(name);
190
+ invalid.delete(name);
191
+ if (Array.isArray(value)) invalid.set(name, value);
192
+ else agents.set(name, value);
193
+ }
194
+ }
195
+ return { agents, invalid, diagnostics };
196
+ }
@@ -0,0 +1,214 @@
1
+ import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { createToolRowStateStore } from "../../shared/tool-row-state.js";
4
+ import { discoverAgents, type AgentDiscovery } from "./agents.ts";
5
+ import { renderSubagentCall, renderSubagentResult } from "./render.ts";
6
+ import { FifoGate, runSubagent, type SubagentDetails } from "./run.ts";
7
+
8
+ const params = Type.Object(
9
+ { agent: Type.String({ minLength: 1 }), task: Type.String({ minLength: 1 }) },
10
+ { additionalProperties: false },
11
+ );
12
+
13
+ export default function subagentExtension(pi: ExtensionAPI): void {
14
+ const gate = new FifoGate(4);
15
+ const controllers = new Set<AbortController>();
16
+ const fingerprints = new Map<string, string>();
17
+ const runtimeWarnings = new Set<string>();
18
+ const rowState = createToolRowStateStore(pi, "subagent.tool-row-state");
19
+ const warn = (discovery: AgentDiscovery, ctx: ExtensionContext) => {
20
+ const current = new Set<string>();
21
+ const grouped = new Map<string, string[]>();
22
+ for (const diagnostic of discovery.diagnostics) {
23
+ const reasons = grouped.get(diagnostic.path) ?? [];
24
+ reasons.push(diagnostic.reason);
25
+ grouped.set(diagnostic.path, reasons);
26
+ }
27
+ for (const [path, reasons] of grouped) {
28
+ current.add(path);
29
+ const fingerprint = [...reasons].sort().join("\0");
30
+ if (fingerprints.get(path) !== fingerprint)
31
+ ctx.ui.notify(`Invalid subagent definition ${path}: ${reasons.join("; ").slice(0, 500)}`, "warning");
32
+ fingerprints.set(path, fingerprint);
33
+ }
34
+ for (const path of fingerprints.keys()) if (!current.has(path)) fingerprints.delete(path);
35
+ };
36
+ pi.registerTool(
37
+ defineTool<typeof params, SubagentDetails>({
38
+ name: "subagent",
39
+ label: "Subagent",
40
+ description:
41
+ "Delegate one focused task to a named isolated child agent. Output is limited to 50KB or 2,000 lines.",
42
+ promptSnippet: "Delegate one focused task to a named isolated child agent",
43
+ parameters: params,
44
+ executionMode: "parallel",
45
+ async execute(_id, raw, signal, onUpdate, ctx) {
46
+ const agent = raw.agent.trim();
47
+ const task = raw.task.trim();
48
+ const thinkingLevel = pi.getThinkingLevel();
49
+ if (!agent || !task)
50
+ return {
51
+ content: [{ type: "text", text: "Subagent input requires non-empty agent and task" }],
52
+ details: {
53
+ agent,
54
+ task,
55
+ status: "failed",
56
+ phase: "queue",
57
+ model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "unavailable",
58
+ thinkingLevel,
59
+ toolCalls: 0,
60
+ actions: [],
61
+ omittedActions: 0,
62
+ omittedErrors: 0,
63
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
64
+ durationMs: 0,
65
+ error: "Invalid input",
66
+ },
67
+ };
68
+ const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
69
+ warn(discovery, ctx);
70
+ const definition = discovery.agents.get(agent);
71
+ const invalid = discovery.invalid.get(agent);
72
+ if (!definition) {
73
+ const reason = invalid?.map((item) => item.reason).join("; ") ?? "unknown agent";
74
+ const names = [...discovery.agents.keys()].sort().join(", ") || "none";
75
+ const error = `Agent ${agent} discovery failed: ${reason}. Runnable agents: ${names}`;
76
+ return {
77
+ content: [{ type: "text", text: error }],
78
+ details: {
79
+ agent,
80
+ task,
81
+ status: "failed",
82
+ phase: "discovery",
83
+ model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "unavailable",
84
+ thinkingLevel,
85
+ toolCalls: 0,
86
+ actions: [],
87
+ omittedActions: 0,
88
+ omittedErrors: 0,
89
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
90
+ durationMs: 0,
91
+ error,
92
+ },
93
+ };
94
+ }
95
+ const controller = new AbortController();
96
+ controllers.add(controller);
97
+ const combined = AbortSignal.any([controller.signal, ...(signal ? [signal] : [])]);
98
+ let release: (() => void) | undefined;
99
+ try {
100
+ await onUpdate?.({
101
+ content: [{ type: "text", text: `${agent}: waiting` }],
102
+ details: {
103
+ agent,
104
+ task,
105
+ status: "waiting",
106
+ phase: "queue",
107
+ model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "unavailable",
108
+ thinkingLevel,
109
+ toolCalls: 0,
110
+ actions: [],
111
+ omittedActions: 0,
112
+ omittedErrors: 0,
113
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
114
+ durationMs: 0,
115
+ },
116
+ });
117
+ try {
118
+ release = await gate.acquire(combined);
119
+ } catch (error) {
120
+ const message = error instanceof Error ? error.message : `Agent ${agent} queue failed`;
121
+ const details: SubagentDetails = {
122
+ agent,
123
+ task,
124
+ status: combined.aborted ? "aborted" : "failed",
125
+ phase: "queue",
126
+ model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "unavailable",
127
+ thinkingLevel,
128
+ toolCalls: 0,
129
+ actions: [],
130
+ omittedActions: 0,
131
+ omittedErrors: 0,
132
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
133
+ durationMs: 0,
134
+ error: message,
135
+ };
136
+ return { content: [{ type: "text", text: message }], details };
137
+ }
138
+ const result = await runSubagent({
139
+ definition,
140
+ task,
141
+ ctx,
142
+ thinkingLevel,
143
+ signal: combined,
144
+ onWarning: (warning) => {
145
+ const message = `Subagent definition ${definition.path}: ${warning}`;
146
+ if (runtimeWarnings.has(message)) return;
147
+ runtimeWarnings.add(message);
148
+ ctx.ui.notify(message, "warning");
149
+ },
150
+ onUpdate: (details) =>
151
+ onUpdate?.({
152
+ content: [
153
+ {
154
+ type: "text",
155
+ text: details.currentActivity ?? details.response ?? `${agent}: ${details.status}`,
156
+ },
157
+ ],
158
+ details,
159
+ }),
160
+ });
161
+ return { content: [{ type: "text", text: result.content }], details: result.details };
162
+ } finally {
163
+ release?.();
164
+ controllers.delete(controller);
165
+ }
166
+ },
167
+ renderCall(args, theme, context) {
168
+ return renderSubagentCall(args, theme, {
169
+ executionStarted: context.executionStarted,
170
+ lastComponent: context.lastComponent,
171
+ rowState,
172
+ rowId: context.toolCallId,
173
+ invalidate: context.invalidate,
174
+ });
175
+ },
176
+ renderResult(result, options, theme, context) {
177
+ return renderSubagentResult(result, options.expanded, theme, {
178
+ lastComponent: context.lastComponent,
179
+ rowState,
180
+ rowId: context.toolCallId,
181
+ invalidate: context.invalidate,
182
+ });
183
+ },
184
+ }),
185
+ );
186
+ pi.on("before_agent_start", async (event, ctx) => {
187
+ if (!pi.getActiveTools().includes("subagent")) return;
188
+ const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
189
+ warn(discovery, ctx);
190
+ const lines = [...discovery.agents.values()]
191
+ .sort((a, b) => a.name.localeCompare(b.name))
192
+ .map((agent) => `- ${agent.name}: ${agent.description}`);
193
+ const prompt = `## Subagents\nUse \`subagent\` when an available agent matches a focused part of the task.\n\nAvailable agents for this turn:\n${lines.join("\n")}\n\nDelegate 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.`;
194
+ event.systemPromptOptions.appendSystemPrompt = event.systemPromptOptions.appendSystemPrompt
195
+ ? `${event.systemPromptOptions.appendSystemPrompt}\n\n${prompt}`
196
+ : prompt;
197
+ return {
198
+ systemPrompt: `${event.systemPrompt}\n\n${prompt}`,
199
+ };
200
+ });
201
+ pi.on("tool_result", (event) => {
202
+ if (event.toolName !== "subagent") return;
203
+ const details = event.details as SubagentDetails | undefined;
204
+ if (details?.status === "failed" || details?.status === "aborted") return { isError: true };
205
+ });
206
+ pi.on("session_start", () => {
207
+ rowState.clear();
208
+ runtimeWarnings.clear();
209
+ });
210
+ pi.on("session_shutdown", () => {
211
+ for (const controller of controllers) controller.abort();
212
+ controllers.clear();
213
+ });
214
+ }
@@ -0,0 +1,89 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Text, truncateToWidth, 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
+ class SubagentText implements Component {
9
+ private readonly text = new Text("", 0, 0);
10
+ private value = "";
11
+ private truncate = false;
12
+ setText(value: string, truncate = false): void {
13
+ this.value = value;
14
+ this.text.setText(value);
15
+ this.truncate = truncate;
16
+ }
17
+ render(width: number): string[] {
18
+ return this.truncate
19
+ ? this.value.split("\n").map((line) => truncateToWidth(line, width))
20
+ : this.text.render(width);
21
+ }
22
+ invalidate(): void {
23
+ this.text.invalidate();
24
+ }
25
+ }
26
+ const component = (last: unknown) => (last as SubagentText | undefined) ?? new SubagentText();
27
+ const title = (theme: Theme, rowState: ToolRowStateStore, rowId: string, invalidate: () => void) => {
28
+ rowState.watch(rowId, invalidate);
29
+ return formatToolRowTitle(rowState, rowId, "subagent", theme);
30
+ };
31
+
32
+ export function renderSubagentCall(
33
+ args: { agent?: string; task?: string },
34
+ theme: Theme,
35
+ context: {
36
+ executionStarted: boolean;
37
+ lastComponent?: unknown;
38
+ rowState: ToolRowStateStore;
39
+ rowId: string;
40
+ invalidate: () => void;
41
+ },
42
+ ): Component {
43
+ const text = component(context.lastComponent);
44
+ if (context.executionStarted) {
45
+ text.setText("");
46
+ return text;
47
+ }
48
+ text.setText(
49
+ `${title(theme, context.rowState, context.rowId, context.invalidate)} ${theme.fg("accent", args.agent ?? "")} ${theme.fg("muted", args.task ?? "")}`.trimEnd(),
50
+ true,
51
+ );
52
+ return text;
53
+ }
54
+
55
+ export function renderSubagentResult(
56
+ result: { details?: SubagentDetails },
57
+ expanded: boolean,
58
+ theme: Theme,
59
+ context: { lastComponent?: unknown; rowState: ToolRowStateStore; rowId: string; invalidate: () => void },
60
+ ): Component {
61
+ const text = component(context.lastComponent);
62
+ const details = result.details;
63
+ if (!details) {
64
+ text.setText("");
65
+ return text;
66
+ }
67
+ const header = `${title(theme, context.rowState, context.rowId, context.invalidate)} ${theme.fg("accent", details.agent)} ${theme.fg("muted", `$${details.usage.cost.toFixed(4)} · ${(details.durationMs / 1000).toFixed(1)}s · ${details.toolCalls} tools`)}`;
68
+ if (!expanded) {
69
+ text.setText(`${header}\n${theme.fg("muted", details.task)}`, true);
70
+ return text;
71
+ }
72
+ const lines = [header, theme.fg("muted", `Task: ${cap(details.task, 1000)}`)];
73
+ for (const action of details.actions)
74
+ lines.push(theme.fg(action.error ? "error" : "dim", `${action.error ? "!" : "·"} ${cap(action.summary)}`));
75
+ if (details.response) lines.push(theme.fg("toolOutput", details.response));
76
+ if (details.error) lines.push(theme.fg("error", cap(details.error, 1000)));
77
+ lines.push(theme.fg("muted", `Model: ${details.model} · thinking: ${details.thinkingLevel}`));
78
+ lines.push(
79
+ theme.fg(
80
+ "muted",
81
+ `Tokens: ${details.usage.input} in, ${details.usage.output} out, ${details.usage.cacheRead} cache read, ${details.usage.cacheWrite} cache write · ${details.usage.turns} turns`,
82
+ ),
83
+ );
84
+ if (details.omittedActions)
85
+ lines.push(theme.fg("muted", `${details.omittedActions} actions omitted (${details.omittedErrors} errors)`));
86
+ if (details.truncation?.path) lines.push(theme.fg("warning", `Full output: ${details.truncation.path}`));
87
+ text.setText(lines.join("\n"));
88
+ return text;
89
+ }