@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.
- package/README.md +30 -0
- package/docs/extending-tau-agent.md +119 -0
- package/docs/subagents.md +57 -0
- package/docs/tui.md +107 -0
- package/extensions/appshot/README.md +11 -0
- package/extensions/appshot/capture.swift +191 -0
- package/extensions/appshot/index.ts +245 -0
- package/extensions/appshot/native-helper.ts +102 -0
- package/extensions/attention/README.md +23 -0
- package/extensions/attention/index.ts +71 -0
- package/extensions/auto-name/README.md +26 -0
- package/extensions/auto-name/index.ts +163 -0
- package/extensions/branch/README.md +22 -0
- package/extensions/branch/index.ts +145 -0
- package/extensions/branch/panel.ts +148 -0
- package/extensions/clear-screen/README.md +29 -0
- package/extensions/clear-screen/index.ts +75 -0
- package/extensions/commit/README.md +15 -0
- package/extensions/commit/commit-plan.ts +328 -0
- package/extensions/commit/git-change-set.ts +268 -0
- package/extensions/commit/index.ts +138 -0
- package/extensions/commit/review-ui.ts +745 -0
- package/extensions/explore/README.md +7 -0
- package/extensions/explore/autoread.ts +140 -0
- package/extensions/explore/find.ts +147 -0
- package/extensions/explore/grep.ts +749 -0
- package/extensions/explore/index.ts +17 -0
- package/extensions/explore/limits.ts +22 -0
- package/extensions/explore/ls.ts +113 -0
- package/extensions/explore/path-display.ts +41 -0
- package/extensions/explore/path-tree.ts +150 -0
- package/extensions/explore/read.ts +160 -0
- package/extensions/explore/result.ts +29 -0
- package/extensions/explore/traverse.ts +226 -0
- package/extensions/footer/README.md +23 -0
- package/extensions/footer/index.ts +534 -0
- package/extensions/footer/settings.ts +13 -0
- package/extensions/ideas/README.md +10 -0
- package/extensions/ideas/browser.ts +66 -0
- package/extensions/ideas/index.ts +31 -0
- package/extensions/ideas/store.ts +32 -0
- package/extensions/image-gen/README.md +11 -0
- package/extensions/image-gen/client.ts +186 -0
- package/extensions/image-gen/index.ts +152 -0
- package/extensions/manage-sessions/README.md +12 -0
- package/extensions/manage-sessions/index.ts +58 -0
- package/extensions/manage-sessions/manager-ui.ts +323 -0
- package/extensions/manage-sessions/sessions.ts +126 -0
- package/extensions/patch/README.md +48 -0
- package/extensions/patch/executor.ts +355 -0
- package/extensions/patch/index.ts +170 -0
- package/extensions/patch/matcher.ts +236 -0
- package/extensions/patch/parser.ts +347 -0
- package/extensions/patch/render.ts +247 -0
- package/extensions/patch/summary.ts +35 -0
- package/extensions/publish/README.md +11 -0
- package/extensions/publish/index.ts +225 -0
- package/extensions/qna/README.md +45 -0
- package/extensions/qna/additional-context-body.ts +51 -0
- package/extensions/qna/body-render.ts +6 -0
- package/extensions/qna/choice-question-body.ts +322 -0
- package/extensions/qna/index.ts +245 -0
- package/extensions/qna/inline-editor-row.ts +56 -0
- package/extensions/qna/input-question-body.ts +89 -0
- package/extensions/qna/model.ts +317 -0
- package/extensions/qna/panel.ts +240 -0
- package/extensions/qna/ui.ts +16 -0
- package/extensions/reference/README.md +47 -0
- package/extensions/reference/index.ts +76 -0
- package/extensions/reference/panel.ts +874 -0
- package/extensions/reference/settings.ts +29 -0
- package/extensions/run-summary/README.md +5 -0
- package/extensions/run-summary/index.ts +98 -0
- package/extensions/silent-command-runner/README.md +25 -0
- package/extensions/silent-command-runner/index.ts +468 -0
- package/extensions/silent-command-runner/settings.ts +65 -0
- package/extensions/soul/README.md +19 -0
- package/extensions/soul/index.ts +20 -0
- package/extensions/soul/prompt.ts +254 -0
- package/extensions/soul/settings.ts +13 -0
- package/extensions/stash/README.md +48 -0
- package/extensions/stash/browser.ts +51 -0
- package/extensions/stash/index.ts +55 -0
- package/extensions/stash/store.ts +36 -0
- package/extensions/subagent/README.md +34 -0
- package/extensions/subagent/agents/scout.md +85 -0
- package/extensions/subagent/agents/web-research.md +95 -0
- package/extensions/subagent/agents.ts +196 -0
- package/extensions/subagent/index.ts +214 -0
- package/extensions/subagent/render.ts +89 -0
- package/extensions/subagent/run.ts +325 -0
- package/extensions/tau/README.md +17 -0
- package/extensions/tau/index.ts +122 -0
- package/extensions/tau-help/README.md +3 -0
- package/extensions/tau-help/help.md +125 -0
- package/extensions/tau-help/index.ts +68 -0
- package/extensions/turn-budget/README.md +14 -0
- package/extensions/turn-budget/index.ts +150 -0
- package/extensions/turn-budget/settings.ts +35 -0
- package/extensions/web/README.md +13 -0
- package/extensions/web/codesearch.ts +79 -0
- package/extensions/web/exa.ts +101 -0
- package/extensions/web/html.ts +67 -0
- package/extensions/web/index.ts +13 -0
- package/extensions/web/limits.ts +11 -0
- package/extensions/web/tool-output.ts +54 -0
- package/extensions/web/webfetch.ts +177 -0
- package/extensions/web/websearch.ts +100 -0
- package/package.json +57 -0
- package/prompts/.gitkeep +1 -0
- package/prompts/cavemanify.md +25 -0
- package/prompts/implement.md +25 -0
- package/prompts/interview.md +48 -0
- package/prompts/plan-feature.md +30 -0
- package/prompts/plan-implementation.md +50 -0
- package/schemas/tau.schema.json +205 -0
- package/shared/agent-blocked.ts +12 -0
- package/shared/description.ts +28 -0
- package/shared/events.ts +135 -0
- package/shared/git.ts +41 -0
- package/shared/injected-context.ts +69 -0
- package/shared/jsonl-store.ts +108 -0
- package/shared/model-fallback/index.ts +278 -0
- package/shared/model-fallback/settings.ts +18 -0
- package/shared/model-fallback/types.ts +8 -0
- package/shared/ranges.ts +21 -0
- package/shared/settings/define.ts +15 -0
- package/shared/settings/files.ts +20 -0
- package/shared/settings/json.ts +41 -0
- package/shared/settings/load.ts +59 -0
- package/shared/settings/merge.ts +21 -0
- package/shared/settings/paths.ts +41 -0
- package/shared/settings/schema.ts +25 -0
- package/shared/settings/specs.ts +23 -0
- package/shared/text.ts +11 -0
- package/shared/tool-row-state.ts +43 -0
- package/skills/writing-preferences/SKILL.md +136 -0
- package/themes/.gitkeep +1 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
5
|
+
import {
|
|
6
|
+
createAgentSession,
|
|
7
|
+
DEFAULT_MAX_BYTES,
|
|
8
|
+
DEFAULT_MAX_LINES,
|
|
9
|
+
formatSize,
|
|
10
|
+
SessionManager,
|
|
11
|
+
truncateHead,
|
|
12
|
+
type AgentSession,
|
|
13
|
+
type AgentSessionEvent,
|
|
14
|
+
type ExtensionContext,
|
|
15
|
+
} from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { AgentDefinition } from "./agents.ts";
|
|
17
|
+
|
|
18
|
+
const PREVIEW_LIMIT = 600;
|
|
19
|
+
const VALUE_LIMIT = 180;
|
|
20
|
+
|
|
21
|
+
export interface SubagentAction {
|
|
22
|
+
tool: string;
|
|
23
|
+
summary: string;
|
|
24
|
+
error: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface SubagentUsage {
|
|
27
|
+
input: number;
|
|
28
|
+
output: number;
|
|
29
|
+
cacheRead: number;
|
|
30
|
+
cacheWrite: number;
|
|
31
|
+
cost: number;
|
|
32
|
+
turns: number;
|
|
33
|
+
}
|
|
34
|
+
export interface SubagentDetails {
|
|
35
|
+
agent: string;
|
|
36
|
+
status: "waiting" | "running" | "completed" | "failed" | "aborted";
|
|
37
|
+
phase: "discovery" | "queue" | "startup" | "run" | "output";
|
|
38
|
+
completionState?: string;
|
|
39
|
+
task: string;
|
|
40
|
+
model: string;
|
|
41
|
+
thinkingLevel: string;
|
|
42
|
+
response?: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
currentActivity?: string;
|
|
45
|
+
toolCalls: number;
|
|
46
|
+
actions: SubagentAction[];
|
|
47
|
+
omittedActions: number;
|
|
48
|
+
omittedErrors: number;
|
|
49
|
+
usage: SubagentUsage;
|
|
50
|
+
durationMs: number;
|
|
51
|
+
truncation?: {
|
|
52
|
+
truncated: boolean;
|
|
53
|
+
path?: string;
|
|
54
|
+
outputLines: number;
|
|
55
|
+
totalLines: number;
|
|
56
|
+
outputBytes: number;
|
|
57
|
+
totalBytes: number;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class FifoGate {
|
|
62
|
+
private active = 0;
|
|
63
|
+
private readonly waiters: Array<{
|
|
64
|
+
signal: AbortSignal;
|
|
65
|
+
resolve: (release: () => void) => void;
|
|
66
|
+
reject: (error: Error) => void;
|
|
67
|
+
}> = [];
|
|
68
|
+
private readonly limit: number;
|
|
69
|
+
constructor(limit = 4) {
|
|
70
|
+
this.limit = limit;
|
|
71
|
+
}
|
|
72
|
+
acquire(signal: AbortSignal): Promise<() => void> {
|
|
73
|
+
if (signal.aborted) return Promise.reject(new Error("Subagent call aborted while waiting"));
|
|
74
|
+
if (this.active < this.limit) {
|
|
75
|
+
this.active += 1;
|
|
76
|
+
return Promise.resolve(this.releaseOnce());
|
|
77
|
+
}
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const waiter = { signal, resolve: (_release: () => void) => {}, reject };
|
|
80
|
+
const abort = () => {
|
|
81
|
+
const index = this.waiters.indexOf(waiter);
|
|
82
|
+
if (index >= 0) this.waiters.splice(index, 1);
|
|
83
|
+
reject(new Error("Subagent call aborted while waiting"));
|
|
84
|
+
};
|
|
85
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
86
|
+
waiter.resolve = (release) => {
|
|
87
|
+
signal.removeEventListener("abort", abort);
|
|
88
|
+
resolve(release);
|
|
89
|
+
};
|
|
90
|
+
this.waiters.push(waiter);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
private releaseOnce(): () => void {
|
|
94
|
+
let released = false;
|
|
95
|
+
return () => {
|
|
96
|
+
if (released) return;
|
|
97
|
+
released = true;
|
|
98
|
+
const next = this.waiters.shift();
|
|
99
|
+
if (next) next.resolve(this.releaseOnce());
|
|
100
|
+
else this.active -= 1;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function capped(value: unknown, limit = VALUE_LIMIT): string {
|
|
106
|
+
let text: string;
|
|
107
|
+
try {
|
|
108
|
+
text = typeof value === "string" ? value : (JSON.stringify(value) ?? String(value));
|
|
109
|
+
} catch {
|
|
110
|
+
text = "[unavailable]";
|
|
111
|
+
}
|
|
112
|
+
return text.length <= limit ? text : `${text.slice(0, limit - 1)}…`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function textOf(message: AssistantMessage): string {
|
|
116
|
+
return message.content
|
|
117
|
+
.filter((part) => part.type === "text")
|
|
118
|
+
.map((part) => part.text)
|
|
119
|
+
.join("");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function assistants(session: AgentSession): AssistantMessage[] {
|
|
123
|
+
return session.messages.filter((message): message is AssistantMessage => message.role === "assistant");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function runSubagent(options: {
|
|
127
|
+
definition: AgentDefinition;
|
|
128
|
+
task: string;
|
|
129
|
+
ctx: ExtensionContext;
|
|
130
|
+
thinkingLevel: string;
|
|
131
|
+
signal: AbortSignal;
|
|
132
|
+
onUpdate?: (details: SubagentDetails) => void | Promise<void>;
|
|
133
|
+
onWarning?: (warning: string) => void;
|
|
134
|
+
}): Promise<{ content: string; details: SubagentDetails }> {
|
|
135
|
+
const { definition, task, ctx, thinkingLevel: parentThinkingLevel, signal, onUpdate, onWarning } = options;
|
|
136
|
+
const started = Date.now();
|
|
137
|
+
let model = ctx.model;
|
|
138
|
+
let thinkingLevel = parentThinkingLevel;
|
|
139
|
+
if (definition.model) {
|
|
140
|
+
const separator = definition.model.indexOf("/");
|
|
141
|
+
const configured = ctx.modelRegistry.find(
|
|
142
|
+
definition.model.slice(0, separator),
|
|
143
|
+
definition.model.slice(separator + 1),
|
|
144
|
+
);
|
|
145
|
+
if (!configured) onWarning?.(`model ${definition.model} is unavailable; using parent model`);
|
|
146
|
+
else {
|
|
147
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(configured);
|
|
148
|
+
if (!auth.ok) onWarning?.(`model ${definition.model} is unavailable: ${auth.error}; using parent model`);
|
|
149
|
+
else model = configured;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (definition.thinking) {
|
|
153
|
+
const mapped = model?.thinkingLevelMap?.[definition.thinking];
|
|
154
|
+
const unsupported =
|
|
155
|
+
!model?.reasoning ||
|
|
156
|
+
mapped === null ||
|
|
157
|
+
((definition.thinking === "xhigh" || definition.thinking === "max") && mapped === undefined);
|
|
158
|
+
if (unsupported)
|
|
159
|
+
onWarning?.(`thinking ${definition.thinking} is unavailable for the selected model; using parent thinking`);
|
|
160
|
+
else thinkingLevel = definition.thinking;
|
|
161
|
+
}
|
|
162
|
+
const modelName = model ? `${model.provider}/${model.id}` : "unavailable";
|
|
163
|
+
const usage: SubagentUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
164
|
+
const details: SubagentDetails = {
|
|
165
|
+
agent: definition.name,
|
|
166
|
+
status: "running",
|
|
167
|
+
phase: "startup",
|
|
168
|
+
task,
|
|
169
|
+
model: modelName,
|
|
170
|
+
thinkingLevel,
|
|
171
|
+
toolCalls: 0,
|
|
172
|
+
actions: [],
|
|
173
|
+
omittedActions: 0,
|
|
174
|
+
omittedErrors: 0,
|
|
175
|
+
usage,
|
|
176
|
+
durationMs: 0,
|
|
177
|
+
};
|
|
178
|
+
let session: AgentSession | undefined;
|
|
179
|
+
let unsubscribe: (() => void) | undefined;
|
|
180
|
+
let updateChain = Promise.resolve();
|
|
181
|
+
let lastTextUpdate = 0;
|
|
182
|
+
const publish = (force = false) => {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
if (!force && now - lastTextUpdate < 100) return;
|
|
185
|
+
lastTextUpdate = now;
|
|
186
|
+
details.durationMs = now - started;
|
|
187
|
+
const snapshot = structuredClone(details);
|
|
188
|
+
snapshot.actions = snapshot.actions.slice(-5);
|
|
189
|
+
updateChain = updateChain.then(() => onUpdate?.(snapshot)).catch(() => undefined);
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
if (!model) throw new Error(`Agent ${definition.name} startup failed: parent has no model`);
|
|
193
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
194
|
+
if (!auth.ok) throw new Error(`Agent ${definition.name} startup failed: ${auth.error}`);
|
|
195
|
+
if (signal.aborted) throw new Error(`Agent ${definition.name} startup aborted`);
|
|
196
|
+
const created = await createAgentSession({
|
|
197
|
+
cwd: ctx.cwd,
|
|
198
|
+
model,
|
|
199
|
+
thinkingLevel: thinkingLevel as "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max",
|
|
200
|
+
modelRegistry: ctx.modelRegistry,
|
|
201
|
+
tools: definition.tools,
|
|
202
|
+
excludeTools: ["subagent"],
|
|
203
|
+
sessionManager: SessionManager.inMemory(ctx.cwd),
|
|
204
|
+
});
|
|
205
|
+
session = created.session;
|
|
206
|
+
await session.bindExtensions({ mode: "print" });
|
|
207
|
+
const active = session.getActiveToolNames().sort();
|
|
208
|
+
const expected = [...definition.tools].sort();
|
|
209
|
+
if (active.join("\0") !== expected.join("\0") || active.includes("subagent")) {
|
|
210
|
+
const missing = expected.filter((tool) => !active.includes(tool));
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Agent ${definition.name} startup failed: unavailable tools: ${missing.join(", ") || "active tool mismatch"}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
details.phase = "run";
|
|
216
|
+
const actionById = new Map<string, string>();
|
|
217
|
+
unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
218
|
+
if (event.type === "message_update" && event.message.role === "assistant") {
|
|
219
|
+
details.response = capped(textOf(event.message), PREVIEW_LIMIT);
|
|
220
|
+
publish();
|
|
221
|
+
} else if (event.type === "tool_execution_start") {
|
|
222
|
+
details.toolCalls += 1;
|
|
223
|
+
const summary = `${event.toolName} ${capped(event.args)}`.trim();
|
|
224
|
+
actionById.set(event.toolCallId, summary);
|
|
225
|
+
details.currentActivity = summary;
|
|
226
|
+
publish(true);
|
|
227
|
+
} else if (event.type === "tool_execution_end") {
|
|
228
|
+
const action = {
|
|
229
|
+
tool: event.toolName,
|
|
230
|
+
summary: actionById.get(event.toolCallId) ?? event.toolName,
|
|
231
|
+
error: event.isError,
|
|
232
|
+
};
|
|
233
|
+
details.actions.push(action);
|
|
234
|
+
if (details.actions.length > 20) {
|
|
235
|
+
const removable = details.actions.findIndex((item) => !item.error);
|
|
236
|
+
const [removed] = details.actions.splice(removable >= 0 ? removable : 0, 1);
|
|
237
|
+
details.omittedActions += 1;
|
|
238
|
+
if (removed?.error) details.omittedErrors += 1;
|
|
239
|
+
}
|
|
240
|
+
details.currentActivity = undefined;
|
|
241
|
+
publish(true);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
const abort = () => {
|
|
245
|
+
void session?.abort();
|
|
246
|
+
};
|
|
247
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
248
|
+
try {
|
|
249
|
+
await session.prompt(
|
|
250
|
+
`You are an isolated delegated child agent. Stay within the delegated task and return only the requested result.\n\n## Agent instructions\n${definition.prompt}\n\n## Delegated task\n${task}`,
|
|
251
|
+
{ expandPromptTemplates: false },
|
|
252
|
+
);
|
|
253
|
+
} finally {
|
|
254
|
+
signal.removeEventListener("abort", abort);
|
|
255
|
+
}
|
|
256
|
+
const messages = assistants(session);
|
|
257
|
+
for (const message of messages) {
|
|
258
|
+
usage.input += message.usage.input;
|
|
259
|
+
usage.output += message.usage.output;
|
|
260
|
+
usage.cacheRead += message.usage.cacheRead;
|
|
261
|
+
usage.cacheWrite += message.usage.cacheWrite;
|
|
262
|
+
usage.cost += message.usage.cost.total;
|
|
263
|
+
usage.turns += 1;
|
|
264
|
+
}
|
|
265
|
+
const terminal = messages.at(-1);
|
|
266
|
+
if (!terminal) throw new Error(`Agent ${definition.name} run failed: no terminal assistant response`);
|
|
267
|
+
const response = textOf(terminal);
|
|
268
|
+
if (terminal.stopReason === "aborted") {
|
|
269
|
+
details.status = "aborted";
|
|
270
|
+
details.completionState = "aborted";
|
|
271
|
+
details.error = capped(terminal.errorMessage ?? "Child aborted");
|
|
272
|
+
} else if (terminal.stopReason === "error") {
|
|
273
|
+
details.status = "failed";
|
|
274
|
+
details.completionState = "error";
|
|
275
|
+
details.error = capped(terminal.errorMessage ?? "Child terminal error");
|
|
276
|
+
} else if ((terminal.stopReason === "stop" || terminal.stopReason === "length") && response.trim()) {
|
|
277
|
+
details.status = "completed";
|
|
278
|
+
details.completionState = terminal.stopReason;
|
|
279
|
+
details.phase = "output";
|
|
280
|
+
const truncation = truncateHead(response, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES });
|
|
281
|
+
let returned = truncation.content;
|
|
282
|
+
let path: string | undefined;
|
|
283
|
+
if (truncation.truncated) {
|
|
284
|
+
const directory = await mkdtemp(join(tmpdir(), "tau-subagent-"));
|
|
285
|
+
path = join(directory, "output.md");
|
|
286
|
+
await writeFile(path, response, { encoding: "utf8", mode: 0o600 });
|
|
287
|
+
returned += `\n\n[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full output saved to: ${path}]`;
|
|
288
|
+
}
|
|
289
|
+
details.response = returned;
|
|
290
|
+
details.truncation = {
|
|
291
|
+
truncated: truncation.truncated,
|
|
292
|
+
path,
|
|
293
|
+
outputLines: truncation.outputLines,
|
|
294
|
+
totalLines: truncation.totalLines,
|
|
295
|
+
outputBytes: truncation.outputBytes,
|
|
296
|
+
totalBytes: truncation.totalBytes,
|
|
297
|
+
};
|
|
298
|
+
} else {
|
|
299
|
+
details.status = "failed";
|
|
300
|
+
details.completionState = terminal.stopReason;
|
|
301
|
+
details.error = `Agent ${definition.name} run failed: empty or non-terminal response`;
|
|
302
|
+
}
|
|
303
|
+
details.durationMs = Date.now() - started;
|
|
304
|
+
publish(true);
|
|
305
|
+
await updateChain;
|
|
306
|
+
return {
|
|
307
|
+
content:
|
|
308
|
+
details.status === "completed"
|
|
309
|
+
? (details.response ?? "")
|
|
310
|
+
: (details.error ?? `Agent ${definition.name} failed`),
|
|
311
|
+
details,
|
|
312
|
+
};
|
|
313
|
+
} catch (error) {
|
|
314
|
+
details.status = signal.aborted ? "aborted" : "failed";
|
|
315
|
+
details.error = capped(error instanceof Error ? error.message : "Subagent failed");
|
|
316
|
+
details.durationMs = Date.now() - started;
|
|
317
|
+
publish(true);
|
|
318
|
+
await updateChain;
|
|
319
|
+
return { content: details.error, details };
|
|
320
|
+
} finally {
|
|
321
|
+
unsubscribe?.();
|
|
322
|
+
if (session?.isStreaming) await session.abort().catch(() => undefined);
|
|
323
|
+
session?.dispose();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Tau
|
|
2
|
+
|
|
3
|
+
Tau utility commands for configuration setup and diagnostics.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
/tau
|
|
9
|
+
/tau init [--global|--project]
|
|
10
|
+
/tau doctor
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Behavior
|
|
14
|
+
|
|
15
|
+
- `/tau` opens a TUI picker when available.
|
|
16
|
+
- `/tau init` writes a Tau settings file if one does not already exist.
|
|
17
|
+
- `/tau doctor` checks Tau settings JSON and extension settings sections.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Value } from "typebox/value";
|
|
3
|
+
import { readJsonStatus, writeJsonObject } from "../../shared/settings/json.ts";
|
|
4
|
+
import { exists, globalTauSettingsPath, projectTauSettingsPath, TAU_SCHEMA_URL } from "../../shared/settings/paths.ts";
|
|
5
|
+
import { discoverTauSettingsSpecs } from "../../shared/settings/specs.ts";
|
|
6
|
+
|
|
7
|
+
type Finding = { level: "global" | "project"; message: string; startup: boolean };
|
|
8
|
+
|
|
9
|
+
export default function tauExtension(pi: ExtensionAPI): void {
|
|
10
|
+
pi.registerCommand("tau", {
|
|
11
|
+
description: "Tau utilities. Usage: /tau [init [--global|--project]|doctor]",
|
|
12
|
+
handler: async (args, ctx) => run(ctx, args),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
pi.on("session_start", async (event, ctx) => {
|
|
16
|
+
if (event.reason !== "startup" && event.reason !== "reload") return;
|
|
17
|
+
const findings = (await inspect(ctx)).filter((finding) => finding.startup);
|
|
18
|
+
if (findings.length > 0 && ctx.hasUI) ctx.ui.notify(renderDoctor(findings), "warning");
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(ctx: ExtensionCommandContext, args: string): Promise<void> {
|
|
23
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
24
|
+
if (parts.length === 0) return runPicker(ctx);
|
|
25
|
+
|
|
26
|
+
if (parts[0] === "doctor" && parts.length === 1) {
|
|
27
|
+
const findings = await inspect(ctx);
|
|
28
|
+
ctx.ui.notify(renderDoctor(findings), findings.length > 0 ? "warning" : "info");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (parts[0] === "init") {
|
|
33
|
+
const scope = parts.includes("--global") ? "global" : "project";
|
|
34
|
+
await init(ctx, scope);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ctx.ui.notify("Usage: /tau [init [--global|--project]|doctor]", "warning");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function runPicker(ctx: ExtensionCommandContext): Promise<void> {
|
|
42
|
+
if (ctx.mode !== "tui") {
|
|
43
|
+
ctx.ui.notify("Usage: /tau [init [--global|--project]|doctor]", "info");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const choice = await ctx.ui.select("Tau", ["init project config", "init global config", "doctor"]);
|
|
48
|
+
if (choice === "init project config") await init(ctx, "project");
|
|
49
|
+
else if (choice === "init global config") await init(ctx, "global");
|
|
50
|
+
else if (choice === "doctor") {
|
|
51
|
+
const findings = await inspect(ctx);
|
|
52
|
+
ctx.ui.notify(renderDoctor(findings), findings.length > 0 ? "warning" : "info");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function init(ctx: ExtensionCommandContext, scope: "global" | "project"): Promise<void> {
|
|
57
|
+
const path = scope === "global" ? globalTauSettingsPath() : await projectTauSettingsPath(ctx.cwd);
|
|
58
|
+
if (await exists(path)) {
|
|
59
|
+
ctx.ui.notify(`tau init skipped: ${path} already exists`, "warning");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await writeJsonObject(path, { $schema: TAU_SCHEMA_URL, extensions: {} });
|
|
64
|
+
ctx.ui.notify(`tau init wrote ${path}`, "info");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function inspect(ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">): Promise<Finding[]> {
|
|
68
|
+
const findings: Finding[] = [];
|
|
69
|
+
const specs = await discoverTauSettingsSpecs(ctx.cwd);
|
|
70
|
+
const paths = [
|
|
71
|
+
{ level: "global" as const, path: globalTauSettingsPath(), trusted: true },
|
|
72
|
+
{ level: "project" as const, path: await projectTauSettingsPath(ctx.cwd), trusted: ctx.isProjectTrusted() },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
for (const item of paths) {
|
|
76
|
+
const status = await readJsonStatus(item.path);
|
|
77
|
+
if (!status.exists) continue;
|
|
78
|
+
if (!item.trusted) {
|
|
79
|
+
findings.push({
|
|
80
|
+
level: item.level,
|
|
81
|
+
message: `project Tau settings ignored because project is not trusted: ${item.path}. Run /trust, then /reload.`,
|
|
82
|
+
startup: true,
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!status.ok) {
|
|
87
|
+
findings.push({ level: item.level, message: `malformed JSON: ${item.path}: ${status.error}`, startup: true });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (typeof status.value.$schema !== "string") {
|
|
91
|
+
findings.push({ level: item.level, message: `missing $schema: ${item.path}`, startup: false });
|
|
92
|
+
}
|
|
93
|
+
const extensions = status.value.extensions;
|
|
94
|
+
if (extensions !== undefined && (!extensions || typeof extensions !== "object" || Array.isArray(extensions))) {
|
|
95
|
+
findings.push({ level: item.level, message: `extensions must be an object: ${item.path}`, startup: true });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const extensionRecord = (extensions ?? {}) as Record<string, unknown>;
|
|
99
|
+
for (const spec of specs) {
|
|
100
|
+
const section = extensionRecord[spec.key];
|
|
101
|
+
if (section === undefined) continue;
|
|
102
|
+
if (!Value.Check(spec.schema, section)) {
|
|
103
|
+
findings.push({
|
|
104
|
+
level: item.level,
|
|
105
|
+
message: `invalid extensions.${spec.key}: ${item.path}`,
|
|
106
|
+
startup: true,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return findings;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderDoctor(findings: readonly Finding[]): string {
|
|
116
|
+
if (findings.length === 0) return "tau doctor\nNo issues found.";
|
|
117
|
+
return [
|
|
118
|
+
"tau doctor",
|
|
119
|
+
`Found ${findings.length} issue(s):`,
|
|
120
|
+
...findings.map((finding) => `- ${finding.message}`),
|
|
121
|
+
].join("\n");
|
|
122
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Tau
|
|
2
|
+
|
|
3
|
+
Tau is a coding agent built to be a reliable partner during software work. It cares about quality code, explicit standards, efficient token use, and decisions that hold up after the chat ends. Extensions add focused capabilities; prompts expand into instructions for the next request.
|
|
4
|
+
|
|
5
|
+
## appshot
|
|
6
|
+
|
|
7
|
+
Gives the agent macOS window discovery, screenshots, and app activation tools for visual validation. Requires macOS 14 and Screen & System Audio Recording permission.
|
|
8
|
+
|
|
9
|
+
## attention
|
|
10
|
+
|
|
11
|
+
Shows attention state when Tau needs the user to look at the chat.
|
|
12
|
+
|
|
13
|
+
## auto-name
|
|
14
|
+
|
|
15
|
+
Names sessions from their first request so saved sessions remain findable.
|
|
16
|
+
|
|
17
|
+
## branch
|
|
18
|
+
|
|
19
|
+
Adds `/branch` to create and switch Git branches from the TUI.
|
|
20
|
+
|
|
21
|
+
## clear-screen
|
|
22
|
+
|
|
23
|
+
Adds `/clear-screen` to clear terminal output without changing the session.
|
|
24
|
+
|
|
25
|
+
## commit
|
|
26
|
+
|
|
27
|
+
Adds `/commit` for semantic commit grouping, review, and committing selected repository changes.
|
|
28
|
+
|
|
29
|
+
## explore
|
|
30
|
+
|
|
31
|
+
Replaces Pi’s filesystem inspection tools with compact Tau versions: `ls`, `find`, `grep`, and `read`. They produce smaller model payloads and readable tool rows. `read` includes focused line ranges; `grep`, `find`, and `ls` keep discovery output compact so the agent spends fewer tokens rereading directory and search results.
|
|
32
|
+
|
|
33
|
+
## footer
|
|
34
|
+
|
|
35
|
+
Adds `/footer` to toggle and refresh Tau’s status footer.
|
|
36
|
+
|
|
37
|
+
## ideas
|
|
38
|
+
|
|
39
|
+
Adds `/ideas` to log rough ideas or open the ideas browser.
|
|
40
|
+
|
|
41
|
+
## image-gen
|
|
42
|
+
|
|
43
|
+
Gives the agent an image-generation tool using the configured image service. Generated images are saved for inspection.
|
|
44
|
+
|
|
45
|
+
## manage-sessions
|
|
46
|
+
|
|
47
|
+
Adds `/manage-sessions` to browse saved sessions and `/sweep` to archive or delete the current session after starting a new one.
|
|
48
|
+
|
|
49
|
+
## patch
|
|
50
|
+
|
|
51
|
+
Replaces separate edit/write operations with one multi-file `patch` tool. It can create, rewrite, edit, move, and delete files in one structured call. Fewer tool calls means fewer turns, and each avoided turn prevents the full chat context from being sent again.
|
|
52
|
+
|
|
53
|
+
## publish
|
|
54
|
+
|
|
55
|
+
Adds `/publish` to create a tagged release, trigger trusted npm publishing in GitHub Actions, and show its status. It recommends a semantic version bump from commits since the prior release tag, but you must confirm the release type and publish action.
|
|
56
|
+
|
|
57
|
+
## qna
|
|
58
|
+
|
|
59
|
+
Adds `/qna` for when the agent has asked you several questions in chat and you want a friendly UI for answering them on your own terms. It is only active when you manually run the command.
|
|
60
|
+
|
|
61
|
+
## reference
|
|
62
|
+
|
|
63
|
+
Adds `/reference` to manage separate repositories kept outside the current project for inspiration or comparison. Add one with `/reference new <git-url>`, update it, switch its referenced branch, or open it in an editor. Select references and explain why they matter; Tau then puts their paths and that reason into the editor for the agent. References stay outside the project so the agent does not wander into unrelated code unless you explicitly point it there.
|
|
64
|
+
|
|
65
|
+
## run-summary
|
|
66
|
+
|
|
67
|
+
Shows a compact display-only marker after each run with wall time and model cost. It does not enter agent context.
|
|
68
|
+
|
|
69
|
+
## silent-command-runner
|
|
70
|
+
|
|
71
|
+
Runs configured commands while keeping their output out of agent context when that is useful.
|
|
72
|
+
|
|
73
|
+
## soul
|
|
74
|
+
|
|
75
|
+
Manages the session’s soul prompt and its persistent working guidance.
|
|
76
|
+
|
|
77
|
+
## stash
|
|
78
|
+
|
|
79
|
+
Adds `Alt+S` to stash the current prompt draft and `/pop` to browse stashed drafts and put one back in the editor.
|
|
80
|
+
|
|
81
|
+
## subagent
|
|
82
|
+
|
|
83
|
+
Gives Tau a subagent delegation tool for isolated, focused work. You can also create your own subagents in the supported subagent directories. Ask Tau how to do it and have it consult the extension’s own documentation; the built-in `scout` and `web-research` subagents show the pattern. Each subagent can register its own model and the tools it is allowed to use.
|
|
84
|
+
|
|
85
|
+
## tau-help
|
|
86
|
+
|
|
87
|
+
Adds `/tau-help` to show this guide as rendered Markdown in the chat.
|
|
88
|
+
|
|
89
|
+
## tau
|
|
90
|
+
|
|
91
|
+
Adds `/tau`, `/tau init [--global|--project]`, and `/tau doctor` for Tau setup and diagnostics.
|
|
92
|
+
|
|
93
|
+
## turn-budget
|
|
94
|
+
|
|
95
|
+
Tracks and limits agent turns to keep work bounded.
|
|
96
|
+
|
|
97
|
+
## web
|
|
98
|
+
|
|
99
|
+
Gives the agent compact `web_search`, `web_fetch`, and `code_search` tools for web and implementation research.
|
|
100
|
+
|
|
101
|
+
## Prompts
|
|
102
|
+
|
|
103
|
+
Prompt commands expand into instructions before the request reaches the agent.
|
|
104
|
+
|
|
105
|
+
### cavemanify
|
|
106
|
+
|
|
107
|
+
`/cavemanify` makes prose short, blunt, and direct.
|
|
108
|
+
|
|
109
|
+
### implement
|
|
110
|
+
|
|
111
|
+
`/implement` provides the implementation workflow for an approved change.
|
|
112
|
+
|
|
113
|
+
### interview
|
|
114
|
+
|
|
115
|
+
`/interview` drives practical questions when a request is underspecified.
|
|
116
|
+
|
|
117
|
+
### plan-feature
|
|
118
|
+
|
|
119
|
+
`/plan-feature` explores a feature and produces a scoped plan before editing.
|
|
120
|
+
|
|
121
|
+
### plan-implementation
|
|
122
|
+
|
|
123
|
+
`/plan-implementation` turns an approved plan into concrete implementation steps.
|
|
124
|
+
|
|
125
|
+
Keep this file updated when an extension or prompt is added, removed, renamed, or its basic usage changes.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getMarkdownTheme, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { Markdown, type Component, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
|
+
|
|
7
|
+
const TAU_SYMBOL = [
|
|
8
|
+
" %%%%%%%%#######*******+++++",
|
|
9
|
+
" @@%%%%%%%########*******+++++++",
|
|
10
|
+
" @@%%%%%%%########*******+++++++=",
|
|
11
|
+
" @%%%%%%%#######********+++++++==",
|
|
12
|
+
" %%%%%%%#######********+++++++===",
|
|
13
|
+
" %% *******+",
|
|
14
|
+
" ******+++",
|
|
15
|
+
" *****++++",
|
|
16
|
+
" ***+++++",
|
|
17
|
+
" *+++++++",
|
|
18
|
+
" ++++++++=",
|
|
19
|
+
" ++++++===",
|
|
20
|
+
" ++++=====",
|
|
21
|
+
" +++======= -:",
|
|
22
|
+
" +=======--------::",
|
|
23
|
+
" ======-------::::",
|
|
24
|
+
" ==-------::::",
|
|
25
|
+
" ----:::",
|
|
26
|
+
].join("\n");
|
|
27
|
+
|
|
28
|
+
class TauHelpMessage implements Component {
|
|
29
|
+
private readonly markdown: Markdown;
|
|
30
|
+
|
|
31
|
+
constructor(content: string, theme: ReturnType<typeof getMarkdownTheme>) {
|
|
32
|
+
this.markdown = new Markdown(content, 0, 0, theme);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
render(width: number): string[] {
|
|
36
|
+
const symbolLines = TAU_SYMBOL.split("\n");
|
|
37
|
+
const symbolWidth = Math.max(...symbolLines.map((line) => visibleWidth(line)));
|
|
38
|
+
const padding = Math.max(0, Math.floor((width - symbolWidth) / 2));
|
|
39
|
+
const symbol = symbolLines.map((line) => " ".repeat(padding) + line);
|
|
40
|
+
return [...symbol, "", ...this.markdown.render(width)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
invalidate(): void {
|
|
44
|
+
this.markdown.invalidate();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function tauHelpExtension(pi: ExtensionAPI): void {
|
|
49
|
+
pi.registerMessageRenderer("tau-help", (message, _options, _theme) => {
|
|
50
|
+
if (typeof message.content !== "string") return undefined;
|
|
51
|
+
return new TauHelpMessage(message.content, getMarkdownTheme());
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
pi.registerCommand("tau-help", {
|
|
55
|
+
description: "Show the Tau extension and prompt guide",
|
|
56
|
+
handler: async (_args, _ctx) => {
|
|
57
|
+
const content = await readFile(join(dirname(fileURLToPath(import.meta.url)), "help.md"), "utf8");
|
|
58
|
+
await pi.sendMessage(
|
|
59
|
+
{
|
|
60
|
+
customType: "tau-help",
|
|
61
|
+
content,
|
|
62
|
+
display: true,
|
|
63
|
+
},
|
|
64
|
+
{ triggerTurn: false },
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Turn Budget
|
|
2
|
+
|
|
3
|
+
Adds soft turn budget hints for Tau sessions.
|
|
4
|
+
|
|
5
|
+
Turn Budget helps agents spend fewer provider cycles by nudging them to batch related tool work. It counts
|
|
6
|
+
tool-using turns per user prompt, sends visible steering messages at configured intervals, and extends the soft cap
|
|
7
|
+
instead of blocking work.
|
|
8
|
+
|
|
9
|
+
## Settings
|
|
10
|
+
|
|
11
|
+
- `enabled`: defaults to `true`
|
|
12
|
+
- `turnLimit`: defaults to `30`
|
|
13
|
+
- `nudgeEveryTurns`: defaults to `5`
|
|
14
|
+
- `softCapIncrement`: defaults to `10`
|