@shanepadgett/tau-agent 0.44.0 → 0.45.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/docs/extending-tau-agent.md +3 -3
- package/extensions/appshot/index.ts +3 -0
- package/extensions/aside/index.ts +3 -11
- package/extensions/attention/README.md +2 -4
- package/extensions/attention/index.ts +2 -40
- package/extensions/auto-name/index.ts +5 -8
- package/extensions/cache-diagnostics/index.ts +1 -1
- package/extensions/codex-priority/README.md +7 -0
- package/extensions/codex-priority/index.ts +95 -0
- package/extensions/compaction/README.md +5 -0
- package/extensions/compaction/index.ts +47 -0
- package/extensions/cost-report/README.md +1 -1
- package/extensions/cost-report/analyze.ts +11 -60
- package/extensions/cost-report/html.ts +1 -37
- package/extensions/cost-report/types.ts +0 -9
- package/extensions/handoff/index.ts +1 -1
- package/extensions/image-gen/index.ts +1 -0
- package/extensions/review/README.md +1 -1
- package/extensions/run-summary/README.md +1 -1
- package/extensions/run-summary/index.ts +6 -24
- package/extensions/runtime-context/README.md +1 -1
- package/extensions/script-runner/README.md +3 -1
- package/extensions/script-runner/index.ts +41 -96
- package/extensions/silent-command-runner/index.ts +38 -69
- package/extensions/soul/README.md +3 -5
- package/extensions/soul/index.ts +59 -135
- package/extensions/soul/prompt.ts +2 -0
- package/extensions/tau-help/help.md +10 -2
- package/extensions/tool-approval/README.md +3 -3
- package/extensions/tool-approval/index.ts +86 -47
- package/extensions/tool-approval/panel.ts +16 -2
- package/extensions/tool-loader/README.md +5 -5
- package/extensions/tool-loader/index.ts +10 -222
- package/extensions/web/codesearch.ts +1 -0
- package/extensions/web/webfetch.ts +1 -0
- package/extensions/web/websearch.ts +1 -0
- package/package.json +2 -2
- package/shared/events.ts +7 -20
- package/shared/model-effort.ts +12 -8
- package/shared/model-fallback/index.ts +12 -29
- package/shared/model-fallback/types.ts +1 -5
- package/shared/prompt-contributions.ts +0 -2
- package/shared/script-source.ts +94 -0
- package/src/tool-loading/index.ts +5 -42
- package/extensions/soul/context.ts +0 -115
- package/extensions/soul/state.ts +0 -114
- package/extensions/soul/tools.ts +0 -31
|
@@ -14,6 +14,6 @@ Choose one focused review type:
|
|
|
14
14
|
- `Architecture` reconsiders ownership, boundaries, reuse, and overall structure.
|
|
15
15
|
- `Correctness` checks concrete runtime bugs and failure paths after accepting the architecture.
|
|
16
16
|
|
|
17
|
-
Then choose which logged-in provider runs the review. OpenAI Codex uses `gpt-
|
|
17
|
+
Then choose which logged-in provider runs the review. Models come from Tau's shared `deep` effort tier: OpenAI Codex uses `gpt-6-astra` and Anthropic uses `claude-opus-5-5`, both at medium thinking. Only providers you are logged in to appear. With no logged-in provider, the review uses the current model.
|
|
18
18
|
|
|
19
19
|
Tau writes each result as Markdown under `.pi/tau/reviews/`. Review results do not enter the parent agent context. Reference the Markdown file later when you want an agent to use it.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Run Summary
|
|
2
2
|
|
|
3
|
-
Run Summary adds a compact marker after the agent settles with no automatic continuation pending. It shows wall time and model cost across the full continuation chain.
|
|
3
|
+
Run Summary adds a compact marker after the agent settles with no automatic continuation pending. It shows wall time and model cost across the full continuation chain.
|
|
4
4
|
|
|
5
5
|
The marker is stored as a display-only session entry. It does not enter model context or trigger another agent turn.
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import { Marker } from "@shanepadgett/tau-tui";
|
|
4
3
|
|
|
@@ -9,11 +8,6 @@ interface RunSummary {
|
|
|
9
8
|
runCost: number;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
interface PreviousRunSummary extends RunSummary {
|
|
13
|
-
subagentCost: number;
|
|
14
|
-
totalCost: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
11
|
export default function runSummaryExtension(pi: ExtensionAPI): void {
|
|
18
12
|
let startedAt: number | undefined;
|
|
19
13
|
let runCost = 0;
|
|
@@ -25,13 +19,7 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
|
|
|
25
19
|
theme,
|
|
26
20
|
state: "muted",
|
|
27
21
|
label: "Run complete:",
|
|
28
|
-
parts: [
|
|
29
|
-
`Wall ${formatDuration(summary.wallMs)}`,
|
|
30
|
-
`Run ${formatCost(summary.runCost)}`,
|
|
31
|
-
...("subagentCost" in summary
|
|
32
|
-
? [`Subagents ${formatCost(summary.subagentCost)}`, `Total ${formatCost(summary.totalCost)}`]
|
|
33
|
-
: []),
|
|
34
|
-
],
|
|
22
|
+
parts: [`Wall ${formatDuration(summary.wallMs)}`, `Run ${formatCost(summary.runCost)}`],
|
|
35
23
|
});
|
|
36
24
|
});
|
|
37
25
|
|
|
@@ -46,7 +34,10 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
|
|
|
46
34
|
|
|
47
35
|
pi.on("agent_end", (event) => {
|
|
48
36
|
for (const message of event.messages) {
|
|
49
|
-
|
|
37
|
+
// Tool results carry the usage of model calls a tool made.
|
|
38
|
+
if (message.role === "assistant") runCost += finiteNonNegative(message.usage.cost.total);
|
|
39
|
+
else if (message.role === "toolResult" && message.usage)
|
|
40
|
+
runCost += finiteNonNegative(message.usage.cost.total);
|
|
50
41
|
}
|
|
51
42
|
});
|
|
52
43
|
|
|
@@ -62,19 +53,10 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
|
|
|
62
53
|
});
|
|
63
54
|
}
|
|
64
55
|
|
|
65
|
-
function readRunSummary(value: unknown): RunSummary |
|
|
56
|
+
function readRunSummary(value: unknown): RunSummary | undefined {
|
|
66
57
|
if (!value || typeof value !== "object") return undefined;
|
|
67
58
|
const record = value as Record<string, unknown>;
|
|
68
59
|
if (![record.wallMs, record.runCost].every(isFiniteNonNegative)) return undefined;
|
|
69
|
-
if ("subagentCost" in record || "totalCost" in record) {
|
|
70
|
-
if (![record.subagentCost, record.totalCost].every(isFiniteNonNegative)) return undefined;
|
|
71
|
-
return {
|
|
72
|
-
wallMs: record.wallMs as number,
|
|
73
|
-
runCost: record.runCost as number,
|
|
74
|
-
subagentCost: record.subagentCost as number,
|
|
75
|
-
totalCost: record.totalCost as number,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
60
|
return {
|
|
79
61
|
wallMs: record.wallMs as number,
|
|
80
62
|
runCost: record.runCost as number,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Runtime Context
|
|
2
2
|
|
|
3
|
-
Supplies Soul with the local date and root directory snapshot. Both are captured
|
|
3
|
+
Supplies Soul with the local date and root directory snapshot. Both are captured on the first prompt of a session and refreshed after successful compaction. Reload, resume, and midnight do not change them.
|
|
4
4
|
|
|
5
5
|
After changing this extension, run `/reload` before testing the new behavior.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Gives the agent a first-class `script_runner` tool for running Python 3, Node.js, and Deno scripts instead of falling back to bash. The agent picks whichever runtime is more efficient for the task.
|
|
4
4
|
|
|
5
|
-
When a run fails, the tool keeps the script and returns a `scriptId`. The agent retries with targeted `{oldText, newText}` edits against what it just wrote instead of resending the whole script, saving output tokens and keeping duplicate scripts out of context. Only the source the agent already sent is referenced; no file path is exposed.
|
|
5
|
+
When a run fails, the tool keeps the script and returns a `scriptId`. The agent retries with targeted `{oldText, newText}` edits against what it just wrote instead of resending the whole script, saving output tokens and keeping duplicate scripts out of context. Only the source the agent already sent is referenced; no script source file path is exposed.
|
|
6
|
+
|
|
7
|
+
Long output shows the tail and a path to the complete output in a temporary file for the active session.
|
|
6
8
|
|
|
7
9
|
Runtimes are detected from the environment: Python 3 via `python3`, Node via the current process when Node is 22.6 or newer (`node --experimental-strip-types`), Deno via `deno` (`deno run -A`). The `node` language is the local Node.js runtime with full Node APIs; scripts may be TypeScript with erasable syntax or plain JavaScript. The `deno` language is the local Deno runtime with full permissions and native TypeScript/JavaScript. The tool registers only the runtimes actually available and is hidden from the prompt entirely when none are present.
|
|
@@ -4,17 +4,13 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
7
|
-
import {
|
|
8
|
-
DEFAULT_MAX_BYTES,
|
|
9
|
-
DEFAULT_MAX_LINES,
|
|
10
|
-
defineTool,
|
|
11
|
-
type ExecResult,
|
|
12
|
-
type ExtensionAPI,
|
|
13
|
-
type Theme,
|
|
14
|
-
truncateTail,
|
|
15
|
-
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { defineTool, type ExecResult, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
|
|
16
8
|
import { Text } from "@earendil-works/pi-tui";
|
|
17
9
|
import { Type } from "typebox";
|
|
10
|
+
import { BoundedTextResultBuilder } from "../../shared/bounded-text-result.ts";
|
|
11
|
+
import { onTauEventImmediately } from "../../shared/events.ts";
|
|
12
|
+
import { createScriptSourceStore } from "../../shared/script-source.ts";
|
|
13
|
+
import { createTemporaryOutputStore } from "../../shared/temporary-output-store.ts";
|
|
18
14
|
import { renderToolOutputPreview } from "../../shared/text.ts";
|
|
19
15
|
|
|
20
16
|
type Language = "python3" | "node" | "deno";
|
|
@@ -25,13 +21,7 @@ interface Runtimes {
|
|
|
25
21
|
deno: string | undefined;
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
interface StoredScript {
|
|
29
|
-
language: Language;
|
|
30
|
-
source: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
24
|
const TIMEOUT_MS = 120_000;
|
|
34
|
-
const MAX_STORED = 8;
|
|
35
25
|
|
|
36
26
|
function detectRuntimes(): Runtimes {
|
|
37
27
|
let python3: string | undefined;
|
|
@@ -81,18 +71,6 @@ function scrubPath(text: string, file: string, dir: string): string {
|
|
|
81
71
|
return text.replaceAll(file, "<script>").replaceAll(dir, "<tmpdir>");
|
|
82
72
|
}
|
|
83
73
|
|
|
84
|
-
function applyEdits(source: string, edits: ReadonlyArray<{ oldText: string; newText: string }>): string {
|
|
85
|
-
let next = source;
|
|
86
|
-
for (const edit of edits) {
|
|
87
|
-
const idx = next.indexOf(edit.oldText);
|
|
88
|
-
if (idx === -1) {
|
|
89
|
-
throw new Error("edits oldText not found. Copy exact text from the script you wrote.");
|
|
90
|
-
}
|
|
91
|
-
next = next.slice(0, idx) + edit.newText + next.slice(idx + edit.oldText.length);
|
|
92
|
-
}
|
|
93
|
-
return next;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
74
|
function renderEditsPreview(edits: ReadonlyArray<{ oldText: string; newText: string }>, theme: Theme): string {
|
|
97
75
|
return edits
|
|
98
76
|
.map((edit) => {
|
|
@@ -109,56 +87,6 @@ function renderEditsPreview(edits: ReadonlyArray<{ oldText: string; newText: str
|
|
|
109
87
|
.join("\n");
|
|
110
88
|
}
|
|
111
89
|
|
|
112
|
-
function resolveScriptSource(
|
|
113
|
-
scripts: Map<string, StoredScript>,
|
|
114
|
-
language: Language,
|
|
115
|
-
params: {
|
|
116
|
-
script?: string;
|
|
117
|
-
scriptId?: string;
|
|
118
|
-
edits?: ReadonlyArray<{ oldText: string; newText: string }>;
|
|
119
|
-
},
|
|
120
|
-
): { scriptId: string; source: string } {
|
|
121
|
-
const edits = params.edits;
|
|
122
|
-
if (edits && edits.length > 0) {
|
|
123
|
-
const scriptId = params.scriptId;
|
|
124
|
-
if (!scriptId) throw new Error("edits require scriptId from the failed run.");
|
|
125
|
-
const stored = scripts.get(scriptId);
|
|
126
|
-
if (!stored) throw new Error(`No stored script for scriptId ${scriptId}. Evicted; resend full script.`);
|
|
127
|
-
if (stored.language !== language) {
|
|
128
|
-
throw new Error(`Language mismatch: scriptId ${scriptId} is ${stored.language}, not ${language}.`);
|
|
129
|
-
}
|
|
130
|
-
return { scriptId, source: applyEdits(stored.source, edits) };
|
|
131
|
-
}
|
|
132
|
-
if (typeof params.script !== "string" || params.script.length === 0) {
|
|
133
|
-
throw new Error("Provide script, or edits + scriptId.");
|
|
134
|
-
}
|
|
135
|
-
return { scriptId: params.scriptId ?? newScriptId(), source: params.script };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function formatTruncatedTail(text: string): { body: string; note: string } {
|
|
139
|
-
const trunc = truncateTail(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
|
|
140
|
-
const note = trunc.truncated
|
|
141
|
-
? `\n\n[output truncated: kept tail ${trunc.outputLines} / ${trunc.totalLines} lines]`
|
|
142
|
-
: "";
|
|
143
|
-
return { body: trunc.content, note };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function successScriptResult(stdout: string): { content: [{ type: "text"; text: string }]; details: undefined } {
|
|
147
|
-
const { body, note } = formatTruncatedTail(stdout.trim());
|
|
148
|
-
const out = body.trim();
|
|
149
|
-
return {
|
|
150
|
-
content: [{ type: "text", text: out ? `${out}${note}` : `(no output)${note}` }],
|
|
151
|
-
details: undefined,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function throwScriptFailure(scriptId: string, result: { stdout: string; stderr: string }): never {
|
|
156
|
-
const diag = result.stderr.trim() || result.stdout.trim();
|
|
157
|
-
const { body, note } = formatTruncatedTail(diag);
|
|
158
|
-
const detail = body ? `${body}${note}\n\n` : "";
|
|
159
|
-
throw new Error(`${detail}scriptId: ${scriptId}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
90
|
export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
163
91
|
const runtimes = detectRuntimes();
|
|
164
92
|
const detected = (["python3", "node", "deno"] as const).filter(
|
|
@@ -168,16 +96,15 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
168
96
|
|
|
169
97
|
const langPhrase = formatLangList(detected);
|
|
170
98
|
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
99
|
+
const scriptStore = createScriptSourceStore();
|
|
100
|
+
const temporaryOutput = createTemporaryOutputStore();
|
|
101
|
+
onTauEventImmediately(pi, "script-runner.source-store", "tau:script-runner.source-store", ({ accept }) =>
|
|
102
|
+
accept(scriptStore),
|
|
103
|
+
);
|
|
104
|
+
pi.on("session_start", async () => {
|
|
105
|
+
await temporaryOutput.shutdown();
|
|
106
|
+
await temporaryOutput.start();
|
|
107
|
+
});
|
|
181
108
|
|
|
182
109
|
function resolveCommand(language: Language): string {
|
|
183
110
|
const cmd = runtimes[language];
|
|
@@ -265,24 +192,41 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
265
192
|
"script_runner never exposes the script path; you already have the source. Never try to read it back.",
|
|
266
193
|
],
|
|
267
194
|
parameters: paramsSchema,
|
|
268
|
-
async execute(
|
|
195
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
269
196
|
const language = params.language;
|
|
270
197
|
if (signal?.aborted) {
|
|
271
198
|
return { content: [{ type: "text", text: "Cancelled." }], details: undefined };
|
|
272
199
|
}
|
|
200
|
+
scriptStore.verifyAndConsume(toolCallId, params);
|
|
273
201
|
const command = resolveCommand(language);
|
|
274
|
-
const
|
|
275
|
-
|
|
202
|
+
const resolved = scriptStore.resolve(params);
|
|
203
|
+
const scriptId = resolved.scriptId ?? newScriptId();
|
|
204
|
+
const source = resolved.source;
|
|
205
|
+
scriptStore.remember(scriptId, language, source);
|
|
276
206
|
await onUpdate?.({
|
|
277
207
|
content: [{ type: "text", text: `Running ${languageLabel(language)}...` }],
|
|
278
208
|
details: undefined,
|
|
279
209
|
});
|
|
280
210
|
const result = await runScript(language, command, source, ctx.cwd, signal);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
211
|
+
const succeeded = result.code === 0 && !result.killed;
|
|
212
|
+
const output = new BoundedTextResultBuilder(temporaryOutput, "tail");
|
|
213
|
+
let content: string;
|
|
214
|
+
try {
|
|
215
|
+
await output.append(succeeded ? result.stdout.trim() : result.stderr.trim() || result.stdout.trim());
|
|
216
|
+
if (signal?.aborted) {
|
|
217
|
+
await output.abort();
|
|
218
|
+
return { content: [{ type: "text", text: "Cancelled." }], details: undefined };
|
|
219
|
+
}
|
|
220
|
+
content = (await output.finish()).content;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
await output.abort();
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
if (succeeded) {
|
|
226
|
+
scriptStore.forget(scriptId);
|
|
227
|
+
return { content: [{ type: "text", text: content.trim() || "(no output)" }], details: undefined };
|
|
284
228
|
}
|
|
285
|
-
|
|
229
|
+
throw new Error(`${content ? `${content}\n\n` : ""}scriptId: ${scriptId}`);
|
|
286
230
|
},
|
|
287
231
|
renderCall(args, theme, context) {
|
|
288
232
|
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
@@ -312,7 +256,8 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
312
256
|
|
|
313
257
|
pi.registerTool(tool);
|
|
314
258
|
|
|
315
|
-
pi.on("session_shutdown", () => {
|
|
316
|
-
|
|
259
|
+
pi.on("session_shutdown", async () => {
|
|
260
|
+
scriptStore.clear();
|
|
261
|
+
await temporaryOutput.shutdown();
|
|
317
262
|
});
|
|
318
263
|
}
|
|
@@ -2,7 +2,6 @@ import { readdir, stat } from "node:fs/promises";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { type ExecResult, type ExtensionAPI, keyText, type Theme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
5
|
-
import { emitTauEvent } from "../../shared/events.ts";
|
|
6
5
|
import { registerPromptSource } from "../../shared/prompt-contributions.ts";
|
|
7
6
|
import { matchGlob, posixPath } from "../../shared/glob.ts";
|
|
8
7
|
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
@@ -82,16 +81,6 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
82
81
|
let turnPaths = new Set<string>();
|
|
83
82
|
let abortController: AbortController | undefined;
|
|
84
83
|
let sessionActive = false;
|
|
85
|
-
let chainActive = false;
|
|
86
|
-
let attentionHoldSequence = 0;
|
|
87
|
-
let attentionHoldId: string | undefined;
|
|
88
|
-
|
|
89
|
-
function finalizeChain(): void {
|
|
90
|
-
chainActive = false;
|
|
91
|
-
const holdId = attentionHoldId;
|
|
92
|
-
attentionHoldId = undefined;
|
|
93
|
-
if (holdId) emitTauEvent(pi, "tau:attention.hold.release", { id: holdId, disposition: "notify" });
|
|
94
|
-
}
|
|
95
84
|
|
|
96
85
|
pi.registerMessageRenderer<FailureDetails>(MESSAGE_TYPE, (message, { expanded }, theme) =>
|
|
97
86
|
renderFailure(asFailureDetails(message.details), expanded, theme),
|
|
@@ -102,9 +91,6 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
102
91
|
sessionActive = true;
|
|
103
92
|
turnStart = Date.now();
|
|
104
93
|
turnPaths = new Set();
|
|
105
|
-
chainActive = false;
|
|
106
|
-
attentionHoldSequence = 0;
|
|
107
|
-
attentionHoldId = undefined;
|
|
108
94
|
});
|
|
109
95
|
|
|
110
96
|
registerPromptSource(pi, {
|
|
@@ -119,41 +105,43 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
119
105
|
});
|
|
120
106
|
|
|
121
107
|
pi.on("agent_start", async (_event, ctx) => {
|
|
122
|
-
const startingChain = !chainActive;
|
|
123
|
-
chainActive = true;
|
|
124
|
-
if (!settings.enabled || settings.commands.length === 0) {
|
|
125
|
-
if (startingChain) {
|
|
126
|
-
turnStart = Date.now();
|
|
127
|
-
turnPaths = new Set();
|
|
128
|
-
}
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (!startingChain) return;
|
|
132
|
-
attentionHoldId = `silent-command-runner:${++attentionHoldSequence}`;
|
|
133
|
-
emitTauEvent(pi, "tau:attention.hold.acquire", { id: attentionHoldId });
|
|
134
108
|
turnStart = Date.now();
|
|
135
|
-
|
|
136
|
-
|
|
109
|
+
turnPaths =
|
|
110
|
+
settings.enabled && settings.commands.length > 0
|
|
111
|
+
? new Set(await walkFiles(await resolveProjectRoot(ctx.cwd)))
|
|
112
|
+
: new Set();
|
|
137
113
|
});
|
|
138
114
|
|
|
139
|
-
|
|
140
|
-
|
|
115
|
+
// The last boundary before the run settles: failures continue the run instead of starting a new one.
|
|
116
|
+
pi.on("agent_before_settle", async (event, ctx) => {
|
|
117
|
+
if (event.outcome !== "completed") return undefined;
|
|
141
118
|
try {
|
|
142
|
-
await runChangedCommands(ctx.cwd,
|
|
119
|
+
const failures = await runChangedCommands(ctx.cwd, ctx.ui.notify);
|
|
120
|
+
if (!failures || failures.length === 0) return undefined;
|
|
121
|
+
return {
|
|
122
|
+
entries: [
|
|
123
|
+
...event.entries,
|
|
124
|
+
{
|
|
125
|
+
type: "custom_message" as const,
|
|
126
|
+
customType: MESSAGE_TYPE,
|
|
127
|
+
content: formatAgentMessage(failures),
|
|
128
|
+
display: true,
|
|
129
|
+
details: { failed: [...failures] } satisfies FailureDetails,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
continue: true,
|
|
133
|
+
};
|
|
143
134
|
} catch (error: unknown) {
|
|
144
135
|
ctx.ui.notify(`silent-command-runner: ${errorMessage(error)}`, "error");
|
|
136
|
+
return undefined;
|
|
145
137
|
}
|
|
146
138
|
});
|
|
147
139
|
|
|
148
|
-
pi.on("agent_settled", finalizeChain);
|
|
149
|
-
|
|
150
140
|
pi.on("session_shutdown", () => {
|
|
151
141
|
sessionActive = false;
|
|
152
142
|
abortController?.abort();
|
|
153
143
|
abortController = undefined;
|
|
154
144
|
turnPaths = new Set();
|
|
155
|
-
chainActive = false;
|
|
156
|
-
attentionHoldId = undefined;
|
|
157
145
|
});
|
|
158
146
|
|
|
159
147
|
async function collectCommandFailures(
|
|
@@ -183,37 +171,16 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
183
171
|
return failures;
|
|
184
172
|
}
|
|
185
173
|
|
|
186
|
-
function reportCommandResults(
|
|
187
|
-
changed: readonly CommandConfig[],
|
|
188
|
-
failures: readonly FailedCommandDetails[],
|
|
189
|
-
notify: (message: string, type?: "info" | "warning" | "error") => void,
|
|
190
|
-
): void {
|
|
191
|
-
const failedNames = new Set(failures.map((failure) => failure.name));
|
|
192
|
-
const passed = changed.filter((command) => !failedNames.has(command.name));
|
|
193
|
-
if (passed.length > 0) notify(`silent-command-runner: passed ${formatCommandNames(passed)}`, "info");
|
|
194
|
-
if (failures.length === 0) return;
|
|
195
|
-
pi.sendMessage<FailureDetails>(
|
|
196
|
-
{
|
|
197
|
-
customType: MESSAGE_TYPE,
|
|
198
|
-
content: formatAgentMessage(failures),
|
|
199
|
-
display: true,
|
|
200
|
-
details: { failed: [...failures] },
|
|
201
|
-
},
|
|
202
|
-
{ deliverAs: "followUp" },
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
174
|
async function runChangedCommands(
|
|
207
175
|
cwd: string,
|
|
208
|
-
turnStart: number,
|
|
209
176
|
notify: (message: string, type?: "info" | "warning" | "error") => void,
|
|
210
|
-
): Promise<
|
|
211
|
-
if (!settings.enabled || settings.commands.length === 0) return;
|
|
177
|
+
): Promise<FailedCommandDetails[] | undefined> {
|
|
178
|
+
if (!settings.enabled || settings.commands.length === 0) return undefined;
|
|
212
179
|
|
|
213
180
|
const projectRoot = await resolveProjectRoot(cwd);
|
|
214
181
|
const paths = await walkFiles(projectRoot);
|
|
215
182
|
const changed = await scanChangedCommands(projectRoot, settings.commands, paths, turnPaths, turnStart);
|
|
216
|
-
if (!sessionActive || changed.length === 0) return;
|
|
183
|
+
if (!sessionActive || changed.length === 0) return undefined;
|
|
217
184
|
|
|
218
185
|
notify(
|
|
219
186
|
changed.length === 1
|
|
@@ -223,8 +190,16 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
223
190
|
);
|
|
224
191
|
|
|
225
192
|
const failures = await collectCommandFailures(projectRoot, changed, notify);
|
|
226
|
-
if (failures === undefined || !sessionActive) return;
|
|
227
|
-
|
|
193
|
+
if (failures === undefined || !sessionActive) return undefined;
|
|
194
|
+
const failedNames = new Set(failures.map((failure) => failure.name));
|
|
195
|
+
const passed = changed.filter((command) => !failedNames.has(command.name));
|
|
196
|
+
if (passed.length > 0) notify(`silent-command-runner: passed ${formatCommandNames(passed)}`, "info");
|
|
197
|
+
if (failures.length > 0) {
|
|
198
|
+
// Only files the agent changes after this report can trigger another check and continuation.
|
|
199
|
+
turnStart = Date.now();
|
|
200
|
+
turnPaths = new Set(paths);
|
|
201
|
+
}
|
|
202
|
+
return failures;
|
|
228
203
|
}
|
|
229
204
|
}
|
|
230
205
|
|
|
@@ -255,7 +230,7 @@ function normalizeSettings(value: typeof silentCommandRunnerSettings.defaults):
|
|
|
255
230
|
|
|
256
231
|
function formatSilentCheckPrompt(commands: readonly CommandConfig[]): string {
|
|
257
232
|
return [
|
|
258
|
-
"Do not manually run the commands listed below. They run automatically after matching changes.
|
|
233
|
+
"Do not manually run the commands listed below. They run automatically after matching changes. Complete the work and finish normally; treat it as correct unless a failure is reported. Do not announce pending validation, hedge completion because of these commands, or tell the user that checks will run or rerun. If a failure is reported, fix it and describe the correction, then finish normally. Do not claim commands passed without evidence. Targeted checks outside this list remain allowed when needed.",
|
|
259
234
|
"Automatic commands:",
|
|
260
235
|
...commands.map(formatSilentCheckCommand),
|
|
261
236
|
].join("\n");
|
|
@@ -271,12 +246,6 @@ function formatSilentCheckCommand(command: CommandConfig): string {
|
|
|
271
246
|
].join("\n");
|
|
272
247
|
}
|
|
273
248
|
|
|
274
|
-
function hasAbortedAssistantMessage(messages: readonly unknown[]): boolean {
|
|
275
|
-
return messages.some(
|
|
276
|
-
(message) => isRecord(message) && message.role === "assistant" && message.stopReason === "aborted",
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
249
|
async function scanChangedCommands(
|
|
281
250
|
projectRoot: string,
|
|
282
251
|
commands: readonly CommandConfig[],
|
|
@@ -403,7 +372,7 @@ function formatAgentMessage(failures: readonly FailedCommandDetails[]): string {
|
|
|
403
372
|
return [
|
|
404
373
|
`silent-command-runner failed: ${failures.length} command${failures.length === 1 ? "" : "s"}`,
|
|
405
374
|
...failures.map(formatAgentFailure),
|
|
406
|
-
"
|
|
375
|
+
"Fix the reported failures without manually rerunning these commands. Describe the correction and finish normally; do not announce validation or reruns.",
|
|
407
376
|
].join("\n\n");
|
|
408
377
|
}
|
|
409
378
|
|
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Soul supplies Tau's system prompt: communication, discussion, planning, execution, and coding guidance. It is always on.
|
|
4
4
|
|
|
5
|
-
Soul
|
|
5
|
+
Soul adds Pi documentation pointers, tool guidance, and the context that other Tau extensions supply, such as the local date, directory snapshot, and automatic-check instructions. The date and directory snapshot are captured on the first prompt and again after successful compaction, so they stay fixed across turns, reload, resume, and tree navigation.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Everything else is read each turn. When an instruction changes, such as an edited `AGENTS.md` after `/reload`, a changed automatic-check configuration, or a different set of active tools, Pi appends the updated section without rewriting earlier instructions. Models that cannot take later system messages receive the change in the leading instructions, which costs one cache miss on the next request.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Run `/reload` after changing this extension. The first request after installation captures the new Soul baseline; later instruction edits take effect after compaction.
|
|
9
|
+
Run `/reload` after changing this extension.
|