@shanepadgett/tau-agent 0.42.3 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/docs/context.md +1 -1
- package/docs/extending-tau-agent.md +1 -2
- package/extensions/attention/README.md +1 -1
- package/extensions/cache-diagnostics/index.ts +4 -0
- package/extensions/context/README.md +1 -24
- package/extensions/context/definitions.ts +1 -52
- package/extensions/context/index.ts +1 -150
- package/extensions/context/panel.ts +0 -72
- package/extensions/explore/guidance.ts +9 -1
- package/extensions/footer/README.md +1 -1
- package/extensions/footer/index.ts +3 -25
- package/extensions/qna/choice-question-body.ts +7 -2
- package/extensions/run-summary/README.md +1 -1
- package/extensions/run-summary/index.ts +18 -25
- package/extensions/runtime-context/README.md +1 -1
- package/extensions/runtime-context/context.ts +1 -1
- package/extensions/runtime-context/index.ts +10 -16
- package/extensions/script-runner/index.ts +21 -29
- package/extensions/silent-command-runner/README.md +0 -2
- package/extensions/silent-command-runner/index.ts +12 -41
- package/extensions/soul/README.md +5 -8
- package/extensions/soul/context.ts +115 -0
- package/extensions/soul/index.ts +141 -7
- package/extensions/soul/prompt.ts +47 -102
- package/extensions/soul/state.ts +114 -0
- package/extensions/soul/tools.ts +31 -0
- package/extensions/tau-help/help.md +6 -18
- package/extensions/tau-help/index.ts +10 -4
- package/extensions/tool-approval/README.md +4 -2
- package/extensions/tool-approval/index.ts +141 -53
- package/extensions/tool-approval/panel.ts +153 -0
- package/extensions/tool-loader/README.md +1 -1
- package/extensions/tool-loader/index.ts +91 -22
- package/package.json +2 -2
- package/schemas/tau.schema.json +0 -104
- package/shared/bounded-text-result.ts +0 -1
- package/shared/events.ts +19 -15
- package/shared/isolated-session.ts +1 -2
- package/shared/model-effort.ts +15 -21
- package/shared/prompt-contributions.ts +24 -0
- package/src/index.ts +1 -1
- package/docs/subagents.md +0 -92
- package/extensions/auto-compact/README.md +0 -9
- package/extensions/auto-compact/index.ts +0 -122
- package/extensions/auto-compact/settings.ts +0 -30
- package/extensions/context/settings.ts +0 -62
- package/extensions/context/sync.ts +0 -276
- package/extensions/context/validation.ts +0 -88
- package/extensions/effort/README.md +0 -7
- package/extensions/effort/index.ts +0 -134
- package/extensions/effort/state.ts +0 -18
- package/extensions/qna/inline-editor-row.ts +0 -56
- package/extensions/soul/overseer.ts +0 -273
- package/extensions/soul/settings.ts +0 -34
- package/extensions/subagent/README.md +0 -67
- package/extensions/subagent/agents/context-sync.md +0 -244
- package/extensions/subagent/agents/dormant/generalist.md +0 -33
- package/extensions/subagent/agents/scout.md +0 -104
- package/extensions/subagent/agents/web-research.md +0 -101
- package/extensions/subagent/agents.ts +0 -261
- package/extensions/subagent/cmux-dashboard.ts +0 -495
- package/extensions/subagent/index.ts +0 -425
- package/extensions/subagent/panel.ts +0 -124
- package/extensions/subagent/render.ts +0 -74
- package/extensions/subagent/resume.ts +0 -78
- package/extensions/subagent/run.ts +0 -641
- package/extensions/subagent/runtime.ts +0 -1296
- package/extensions/subagent/session-resource.ts +0 -61
- package/extensions/subagent/settings.ts +0 -18
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
formatRuntimeContextMessage,
|
|
5
|
-
freezeRuntimeContext,
|
|
6
|
-
type RuntimeContext,
|
|
7
|
-
} from "./context.ts";
|
|
2
|
+
import { registerPromptSource } from "../../shared/prompt-contributions.ts";
|
|
3
|
+
import { formatLocalDisplayDate, formatRuntimeContextMessage, freezeRuntimeContext } from "./context.ts";
|
|
8
4
|
|
|
9
5
|
export default function runtimeContextExtension(pi: ExtensionAPI): void {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const content = formatRuntimeContextMessage(formatLocalDisplayDate(new Date()), runtimeContext.rootSnapshot);
|
|
19
|
-
return { systemPrompt: `${event.systemPrompt}\n\n${content}` };
|
|
6
|
+
registerPromptSource(pi, {
|
|
7
|
+
key: "runtime/environment",
|
|
8
|
+
section: "environment",
|
|
9
|
+
refresh: "compaction",
|
|
10
|
+
async read(ctx) {
|
|
11
|
+
const snapshot = freezeRuntimeContext(ctx.cwd);
|
|
12
|
+
return formatRuntimeContextMessage(formatLocalDisplayDate(new Date()), snapshot.rootSnapshot);
|
|
13
|
+
},
|
|
20
14
|
});
|
|
21
15
|
}
|
|
@@ -169,7 +169,6 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
169
169
|
const langPhrase = formatLangList(detected);
|
|
170
170
|
|
|
171
171
|
const scripts = new Map<string, StoredScript>();
|
|
172
|
-
let tempDir: string | undefined;
|
|
173
172
|
|
|
174
173
|
function remember(scriptId: string, script: StoredScript): void {
|
|
175
174
|
scripts.set(scriptId, script);
|
|
@@ -190,12 +189,6 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
190
189
|
throw new Error("Deno is not available on this machine.");
|
|
191
190
|
}
|
|
192
191
|
|
|
193
|
-
async function ensureTempDir(): Promise<string> {
|
|
194
|
-
if (tempDir) return tempDir;
|
|
195
|
-
tempDir = await mkdtemp(join(tmpdir(), "tau-script-runner-"));
|
|
196
|
-
return tempDir;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
192
|
async function runScript(
|
|
200
193
|
language: Language,
|
|
201
194
|
command: string,
|
|
@@ -203,21 +196,27 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
203
196
|
cwd: string,
|
|
204
197
|
signal: AbortSignal | undefined,
|
|
205
198
|
): Promise<ExecResult> {
|
|
206
|
-
const dir = await
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
199
|
+
const dir = await mkdtemp(join(tmpdir(), "tau-script-runner-"));
|
|
200
|
+
try {
|
|
201
|
+
const file = join(dir, language === "python3" ? "_run.py" : "_run.ts");
|
|
202
|
+
await writeFile(file, source, "utf8");
|
|
203
|
+
const args =
|
|
204
|
+
language === "python3"
|
|
205
|
+
? [file]
|
|
206
|
+
: language === "node"
|
|
207
|
+
? ["--experimental-strip-types", file]
|
|
208
|
+
: ["run", "-A", file];
|
|
209
|
+
const result = await pi.exec(command, args, { cwd, signal, timeout: TIMEOUT_MS });
|
|
210
|
+
return {
|
|
211
|
+
...result,
|
|
212
|
+
stdout: scrubPath(result.stdout, file, dir),
|
|
213
|
+
stderr: scrubPath(result.stderr, file, dir),
|
|
214
|
+
};
|
|
215
|
+
} finally {
|
|
216
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
217
|
+
// best-effort cleanup
|
|
218
|
+
});
|
|
219
|
+
}
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
const paramsSchema = Type.Object(
|
|
@@ -315,12 +314,5 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
315
314
|
|
|
316
315
|
pi.on("session_shutdown", () => {
|
|
317
316
|
scripts.clear();
|
|
318
|
-
const dir = tempDir;
|
|
319
|
-
tempDir = undefined;
|
|
320
|
-
if (dir) {
|
|
321
|
-
void rm(dir, { recursive: true, force: true }).catch(() => {
|
|
322
|
-
// best-effort cleanup
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
317
|
});
|
|
326
318
|
}
|
|
@@ -23,5 +23,3 @@ Configure it in Tau settings under `extensions.silentCommandRunner`:
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Passes are notifications only. Failures are shown in chat and sent to the agent. Tau's ready-for-input notification waits for these checks and stays quiet when a failure starts another agent turn.
|
|
26
|
-
|
|
27
|
-
When auto-compact pauses a long run to compact context, checks wait until that work chain finishes. They do not run on the compact abort, and file changes from before the compact still count.
|
|
@@ -2,7 +2,8 @@ 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
|
|
5
|
+
import { emitTauEvent } from "../../shared/events.ts";
|
|
6
|
+
import { registerPromptSource } from "../../shared/prompt-contributions.ts";
|
|
6
7
|
import { matchGlob, posixPath } from "../../shared/glob.ts";
|
|
7
8
|
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
8
9
|
import { resolveProjectRoot } from "../../shared/settings/paths.ts";
|
|
@@ -84,15 +85,8 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
84
85
|
let chainActive = false;
|
|
85
86
|
let attentionHoldSequence = 0;
|
|
86
87
|
let attentionHoldId: string | undefined;
|
|
87
|
-
const checkPauses = new Set<string>();
|
|
88
|
-
let settleDeferred = false;
|
|
89
|
-
|
|
90
|
-
function checksPaused(): boolean {
|
|
91
|
-
return checkPauses.size > 0;
|
|
92
|
-
}
|
|
93
88
|
|
|
94
89
|
function finalizeChain(): void {
|
|
95
|
-
settleDeferred = false;
|
|
96
90
|
chainActive = false;
|
|
97
91
|
const holdId = attentionHoldId;
|
|
98
92
|
attentionHoldId = undefined;
|
|
@@ -103,21 +97,6 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
103
97
|
renderFailure(asFailureDetails(message.details), expanded, theme),
|
|
104
98
|
);
|
|
105
99
|
|
|
106
|
-
onTauEvent(pi, "silent-command-runner.pause", "tau:silent-command-runner.pause", ({ id }) => {
|
|
107
|
-
checkPauses.add(id);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
onTauEvent(pi, "silent-command-runner.resume", "tau:silent-command-runner.resume", ({ id, disposition }) => {
|
|
111
|
-
checkPauses.delete(id);
|
|
112
|
-
if (checksPaused()) return;
|
|
113
|
-
if (disposition === "continue") {
|
|
114
|
-
// Compact will start a new agent run on the same work chain; keep baseline + hold.
|
|
115
|
-
settleDeferred = false;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (settleDeferred || chainActive) finalizeChain();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
100
|
pi.on("session_start", async (_event, ctx) => {
|
|
122
101
|
settings = normalizeSettings(await loadTauExtensionSettings(ctx, silentCommandRunnerSettings));
|
|
123
102
|
sessionActive = true;
|
|
@@ -126,20 +105,22 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
126
105
|
chainActive = false;
|
|
127
106
|
attentionHoldSequence = 0;
|
|
128
107
|
attentionHoldId = undefined;
|
|
129
|
-
checkPauses.clear();
|
|
130
|
-
settleDeferred = false;
|
|
131
108
|
});
|
|
132
109
|
|
|
133
|
-
pi
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
110
|
+
registerPromptSource(pi, {
|
|
111
|
+
key: "checks/instructions",
|
|
112
|
+
section: "automatic-checks",
|
|
113
|
+
refresh: "append",
|
|
114
|
+
async read(ctx) {
|
|
115
|
+
settings = normalizeSettings(await loadTauExtensionSettings(ctx, silentCommandRunnerSettings));
|
|
116
|
+
if (!settings.enabled || settings.commands.length === 0) return "";
|
|
117
|
+
return formatSilentCheckPrompt(settings.commands);
|
|
118
|
+
},
|
|
137
119
|
});
|
|
138
120
|
|
|
139
121
|
pi.on("agent_start", async (_event, ctx) => {
|
|
140
122
|
const startingChain = !chainActive;
|
|
141
123
|
chainActive = true;
|
|
142
|
-
settleDeferred = false;
|
|
143
124
|
if (!settings.enabled || settings.commands.length === 0) {
|
|
144
125
|
if (startingChain) {
|
|
145
126
|
turnStart = Date.now();
|
|
@@ -156,7 +137,6 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
156
137
|
});
|
|
157
138
|
|
|
158
139
|
pi.on("agent_end", async (event, ctx) => {
|
|
159
|
-
if (checksPaused()) return;
|
|
160
140
|
if (hasAbortedAssistantMessage(event.messages)) return;
|
|
161
141
|
try {
|
|
162
142
|
await runChangedCommands(ctx.cwd, turnStart, ctx.ui.notify);
|
|
@@ -165,14 +145,7 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
165
145
|
}
|
|
166
146
|
});
|
|
167
147
|
|
|
168
|
-
pi.on("agent_settled",
|
|
169
|
-
if (checksPaused()) {
|
|
170
|
-
// Compact aborted this run; keep chain + file baseline until resume.
|
|
171
|
-
settleDeferred = true;
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
finalizeChain();
|
|
175
|
-
});
|
|
148
|
+
pi.on("agent_settled", finalizeChain);
|
|
176
149
|
|
|
177
150
|
pi.on("session_shutdown", () => {
|
|
178
151
|
sessionActive = false;
|
|
@@ -181,8 +154,6 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
|
|
|
181
154
|
turnPaths = new Set();
|
|
182
155
|
chainActive = false;
|
|
183
156
|
attentionHoldId = undefined;
|
|
184
|
-
checkPauses.clear();
|
|
185
|
-
settleDeferred = false;
|
|
186
157
|
});
|
|
187
158
|
|
|
188
159
|
async function collectCommandFailures(
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
# Soul
|
|
2
2
|
|
|
3
|
-
Soul
|
|
3
|
+
Soul supplies Tau's system prompt: communication, discussion, planning, execution, and coding guidance. It is always on.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- **operating model** — answer questions before acting, keep research tight and documentation-first, plan in small steps, and stop when a fix needs unusual force.
|
|
7
|
-
- **code style** — fast when the user is proving an idea, small and clean when the user wants real product code.
|
|
8
|
-
- **primary-directive overseer** — after 20 tool calls, privately checks whether long-running work still follows the user's request and the normal supported path. Any guidance is applied silently on the next model turn.
|
|
5
|
+
Soul captures tools, skills, project instructions, documentation, and environment context in a saved baseline. Reload and resume reuse it. Successful compaction captures a fresh baseline, including a new date and directory listing.
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
Changes to available agents, automatic checks, approval guidance, and loaded tools are supplied as saved context updates. Earlier instructions and updates keep their positions instead of being rewritten each turn.
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
Tools that cannot be loaded without changing the cached prefix wait for compaction. Tool execution permissions still take effect immediately. Soul reports incompatible prompt replacements or changes to previously sent history rather than silently replacing its baseline.
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
Run `/reload` after changing this extension. The first request after installation captures the new Soul baseline; later instruction edits take effect after compaction.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatSkillsForPrompt,
|
|
3
|
+
getDocsPath,
|
|
4
|
+
getExamplesPath,
|
|
5
|
+
getReadmePath,
|
|
6
|
+
type BuildSystemPromptOptions,
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ExtensionContext,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { collectPromptSources, type PromptValue } from "../../shared/prompt-contributions.ts";
|
|
11
|
+
import { FIXED_INSTRUCTIONS } from "./prompt.ts";
|
|
12
|
+
|
|
13
|
+
export async function readPromptValues(
|
|
14
|
+
pi: ExtensionAPI,
|
|
15
|
+
ctx: ExtensionContext,
|
|
16
|
+
inputs: BuildSystemPromptOptions,
|
|
17
|
+
capture: boolean,
|
|
18
|
+
): Promise<PromptValue[]> {
|
|
19
|
+
const sources = collectPromptSources(pi).filter((source) => capture || source.refresh === "append");
|
|
20
|
+
const values = await Promise.all(
|
|
21
|
+
sources.map(async (source) => ({
|
|
22
|
+
key: source.key,
|
|
23
|
+
section: source.section,
|
|
24
|
+
refresh: source.refresh,
|
|
25
|
+
text: await source.read(ctx),
|
|
26
|
+
})),
|
|
27
|
+
);
|
|
28
|
+
const active = new Set(pi.getActiveTools());
|
|
29
|
+
const tools = pi.getAllTools().filter((tool) => active.has(tool.name));
|
|
30
|
+
values.push(
|
|
31
|
+
{
|
|
32
|
+
key: "pi/tools",
|
|
33
|
+
section: "tools",
|
|
34
|
+
refresh: "append",
|
|
35
|
+
text: tools
|
|
36
|
+
.map((tool) => `- ${tool.name}: ${inputs.toolSnippets?.[tool.name] ?? tool.description}`)
|
|
37
|
+
.join("\n"),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: "pi/tool-guidance",
|
|
41
|
+
section: "tool-guidance",
|
|
42
|
+
refresh: "append",
|
|
43
|
+
text: [
|
|
44
|
+
...new Set([
|
|
45
|
+
...(active.has("bash") ? ["Use bash for file operations like ls, rg, find."] : []),
|
|
46
|
+
...tools.flatMap((tool) => tool.promptGuidelines ?? []),
|
|
47
|
+
...(inputs.promptGuidelines ?? []),
|
|
48
|
+
]),
|
|
49
|
+
]
|
|
50
|
+
.map((rule) => `- ${rule}`)
|
|
51
|
+
.join("\n"),
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
return values.sort((a, b) => a.key.localeCompare(b.key, "en"));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renderBaseline(inputs: BuildSystemPromptOptions, values: readonly PromptValue[]): string {
|
|
58
|
+
const sections = new Map<string, string[]>([
|
|
59
|
+
["tools", []],
|
|
60
|
+
["tool-guidance", []],
|
|
61
|
+
["skills", []],
|
|
62
|
+
["documentation", []],
|
|
63
|
+
["project-context", []],
|
|
64
|
+
["environment", []],
|
|
65
|
+
["additional-instructions", []],
|
|
66
|
+
]);
|
|
67
|
+
const skillTool = (["read", "bash"] as const).find((name) => inputs.selectedTools?.includes(name));
|
|
68
|
+
if (skillTool) sections.get("skills")?.push(formatSkillsForPrompt(inputs.skills ?? [], skillTool).trim());
|
|
69
|
+
sections.get("documentation")
|
|
70
|
+
?.push(`Consult Pi or Tau documentation when the request concerns their usage or extension APIs.
|
|
71
|
+
Pi documentation:
|
|
72
|
+
- Main documentation: ${getReadmePath()}
|
|
73
|
+
- Additional docs: ${getDocsPath()}
|
|
74
|
+
- Examples: ${getExamplesPath()}
|
|
75
|
+
- Resolve docs/... and examples/... under those installed paths, not the working directory.
|
|
76
|
+
- Extensions: docs/extensions.md and examples/extensions/; themes: docs/themes.md; skills: docs/skills.md; prompt templates: docs/prompt-templates.md; TUI: docs/tui.md; keybindings: docs/keybindings.md; SDK: docs/sdk.md; providers: docs/custom-provider.md; models: docs/models.md; packages: docs/packages.md; environment: docs/environment-variables.md.
|
|
77
|
+
- Read the relevant documentation and follow related Markdown references before implementing Pi integrations.`);
|
|
78
|
+
sections
|
|
79
|
+
.get("project-context")
|
|
80
|
+
?.push(
|
|
81
|
+
...(inputs.contextFiles ?? []).map(
|
|
82
|
+
(file) =>
|
|
83
|
+
`<project_instructions path=${JSON.stringify(file.path)}>\n${file.content}\n</project_instructions>`,
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
sections.get("environment")?.push(`Working directory: ${inputs.cwd}`);
|
|
87
|
+
sections.get("additional-instructions")?.push(
|
|
88
|
+
inputs.customPrompt ?? "",
|
|
89
|
+
inputs.appendSystemPrompt ?? "",
|
|
90
|
+
...Object.entries(inputs.sections ?? {})
|
|
91
|
+
.sort(([a], [b]) => a.localeCompare(b, "en"))
|
|
92
|
+
.map(([key, text]) => `<${key}>\n${text}\n</${key}>`),
|
|
93
|
+
);
|
|
94
|
+
for (const value of values) {
|
|
95
|
+
const content = sections.get(value.section) ?? [];
|
|
96
|
+
content.push(value.text);
|
|
97
|
+
sections.set(value.section, content);
|
|
98
|
+
}
|
|
99
|
+
return [
|
|
100
|
+
FIXED_INSTRUCTIONS,
|
|
101
|
+
...[...sections].flatMap(([name, pieces]) => {
|
|
102
|
+
const text = pieces.filter((piece) => piece.trim()).join("\n\n");
|
|
103
|
+
return text ? [`<${name}>\n${text}\n</${name}>`] : [];
|
|
104
|
+
}),
|
|
105
|
+
].join("\n\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function renderUpdate(values: readonly PromptValue[]): string {
|
|
109
|
+
return values
|
|
110
|
+
.map(
|
|
111
|
+
(value) =>
|
|
112
|
+
`<context-update source=${JSON.stringify(value.key)}>\nThis replaces the previous information for this source.\n${value.text || "This source no longer supplies instructions."}\n</context-update>`,
|
|
113
|
+
)
|
|
114
|
+
.join("\n\n");
|
|
115
|
+
}
|
package/extensions/soul/index.ts
CHANGED
|
@@ -1,10 +1,144 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { getCurrentTools, toToolDeclaration } from "@earendil-works/pi-ai";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import type { BuildSystemPromptOptions, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { emitTauEvent, onTauEventImmediately } from "../../shared/events.ts";
|
|
5
|
+
import { readPromptValues, renderBaseline, renderUpdate } from "./context.ts";
|
|
6
|
+
import {
|
|
7
|
+
admittedTools,
|
|
8
|
+
BASELINE_TYPE,
|
|
9
|
+
projectPrompt,
|
|
10
|
+
restorePrompt,
|
|
11
|
+
UPDATE_TYPE,
|
|
12
|
+
type SavedBaseline,
|
|
13
|
+
type SavedUpdate,
|
|
14
|
+
} from "./state.ts";
|
|
15
|
+
import { toolChangeReason } from "./tools.ts";
|
|
16
|
+
|
|
17
|
+
const CHECKPOINT_TYPE = "tau.soul.prefix";
|
|
18
|
+
interface PrefixCheckpoint {
|
|
19
|
+
baselineEntryId: string;
|
|
20
|
+
count: number;
|
|
21
|
+
hash: string;
|
|
22
|
+
}
|
|
4
23
|
|
|
5
24
|
export default function soulExtension(pi: ExtensionAPI): void {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
25
|
+
let inputs: BuildSystemPromptOptions | null = null;
|
|
26
|
+
|
|
27
|
+
pi.on("session_start", () => {
|
|
28
|
+
inputs = null;
|
|
29
|
+
});
|
|
30
|
+
pi.on("session_shutdown", () => {
|
|
31
|
+
inputs = null;
|
|
32
|
+
});
|
|
33
|
+
pi.on("before_agent_start", (event) => {
|
|
34
|
+
// Keep the shared options reference until all contributors have finished.
|
|
35
|
+
inputs = event.systemPromptOptions;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
onTauEventImmediately(pi, "soul.tools", "tau:prompt.tools.check", ({ ctx, tools, reject }) => {
|
|
39
|
+
try {
|
|
40
|
+
const saved = restorePrompt(ctx.sessionManager.getBranch());
|
|
41
|
+
if (!saved || !ctx.model) return;
|
|
42
|
+
const previous = admittedTools(saved);
|
|
43
|
+
// getAllTools exposes schemas but not constrainedSampling. Preserve that
|
|
44
|
+
// metadata here; the request boundary checks the complete Pi declarations.
|
|
45
|
+
const reason = toolChangeReason(
|
|
46
|
+
ctx.model,
|
|
47
|
+
previous,
|
|
48
|
+
tools.map((tool) => ({
|
|
49
|
+
...previous.find((candidate) => candidate.name === tool.name),
|
|
50
|
+
...tool,
|
|
51
|
+
})),
|
|
52
|
+
);
|
|
53
|
+
if (reason) reject(reason);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
reject(String(error));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
pi.on("context_with_system", async (event, ctx) => {
|
|
60
|
+
try {
|
|
61
|
+
if (!inputs) throw new Error("Soul has no loaded prompt inputs.");
|
|
62
|
+
if (inputs.forceSystemPrompt !== undefined)
|
|
63
|
+
throw new Error(
|
|
64
|
+
"A forced system prompt conflicts with Soul. Remove the extension's systemPrompt replacement.",
|
|
65
|
+
);
|
|
66
|
+
const branch = ctx.sessionManager.getBranch();
|
|
67
|
+
const newestFirst = [...branch].reverse();
|
|
68
|
+
const projection = ctx.sessionManager.buildSessionProjection();
|
|
69
|
+
const anchor = [...projection.entries].reverse().find((entry) => entry.messages.length > 0);
|
|
70
|
+
if (!anchor) throw new Error("Soul has no conversation anchor.");
|
|
71
|
+
const saved = restorePrompt(branch);
|
|
72
|
+
const active = new Set(pi.getActiveTools());
|
|
73
|
+
const requested = getCurrentTools(event.messages)
|
|
74
|
+
.filter((tool) => active.has(tool.name))
|
|
75
|
+
.map(toToolDeclaration);
|
|
76
|
+
if (saved && ctx.model) {
|
|
77
|
+
const reason = toolChangeReason(ctx.model, admittedTools(saved), requested);
|
|
78
|
+
if (reason) throw new Error(reason);
|
|
79
|
+
}
|
|
80
|
+
const values = await readPromptValues(pi, ctx, inputs, saved === null);
|
|
81
|
+
if (!saved) {
|
|
82
|
+
pi.appendEntry<SavedBaseline>(BASELINE_TYPE, {
|
|
83
|
+
version: 1,
|
|
84
|
+
compactionId: newestFirst.find((entry) => entry.type === "compaction")?.id ?? null,
|
|
85
|
+
afterEntryId: anchor.sourceEntry.id,
|
|
86
|
+
text: renderBaseline(inputs, values),
|
|
87
|
+
values,
|
|
88
|
+
initialTools: getCurrentTools(event.messages),
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
const previous = new Map(saved.baseline.values.map((value) => [value.key, value]));
|
|
92
|
+
for (const update of saved.updates) for (const value of update.data.values) previous.set(value.key, value);
|
|
93
|
+
const currentKeys = new Set(values.map((value) => value.key));
|
|
94
|
+
for (const value of previous.values()) {
|
|
95
|
+
if (value.refresh === "append" && !currentKeys.has(value.key)) values.push({ ...value, text: "" });
|
|
96
|
+
}
|
|
97
|
+
const changed = values.filter((value) => previous.get(value.key)?.text !== value.text);
|
|
98
|
+
const tools = new Map(admittedTools(saved).map((tool) => [tool.name, tool]));
|
|
99
|
+
const added = requested.some((tool) => !tools.has(tool.name));
|
|
100
|
+
for (const tool of requested) tools.set(tool.name, tool);
|
|
101
|
+
if (changed.length > 0 || added)
|
|
102
|
+
pi.appendEntry<SavedUpdate>(UPDATE_TYPE, {
|
|
103
|
+
version: 1,
|
|
104
|
+
baselineEntryId: saved.entryId,
|
|
105
|
+
afterEntryId: anchor.sourceEntry.id,
|
|
106
|
+
text: renderUpdate(changed),
|
|
107
|
+
values: changed,
|
|
108
|
+
tools: [...tools.values()],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const admitted = restorePrompt(ctx.sessionManager.getBranch());
|
|
112
|
+
if (!admitted) throw new Error("Soul failed to save its baseline.");
|
|
113
|
+
const messages = projectPrompt(event.messages, admitted, ctx);
|
|
114
|
+
const checkpointEntry = newestFirst.find(
|
|
115
|
+
(entry) => entry.type === "custom" && entry.customType === CHECKPOINT_TYPE,
|
|
116
|
+
);
|
|
117
|
+
const checkpoint = checkpointEntry?.type === "custom" ? (checkpointEntry.data as PrefixCheckpoint) : null;
|
|
118
|
+
if (checkpoint?.baselineEntryId === admitted.entryId) {
|
|
119
|
+
const prefix = createHash("sha256")
|
|
120
|
+
.update(JSON.stringify(messages.slice(0, checkpoint.count)))
|
|
121
|
+
.digest("hex");
|
|
122
|
+
if (prefix !== checkpoint.hash)
|
|
123
|
+
throw new Error("Previously sent conversation content changed. Compact before continuing.");
|
|
124
|
+
}
|
|
125
|
+
const hash = createHash("sha256").update(JSON.stringify(messages)).digest("hex");
|
|
126
|
+
if (hash !== checkpoint?.hash)
|
|
127
|
+
pi.appendEntry<PrefixCheckpoint>(CHECKPOINT_TYPE, {
|
|
128
|
+
baselineEntryId: admitted.entryId,
|
|
129
|
+
count: messages.length,
|
|
130
|
+
hash,
|
|
131
|
+
});
|
|
132
|
+
emitTauEvent(pi, "tau:prompt.snapshot", {
|
|
133
|
+
text: [admitted.baseline.text, ...admitted.updates.map((update) => update.data.text)].join("\n\n"),
|
|
134
|
+
});
|
|
135
|
+
return { messages };
|
|
136
|
+
} catch (error) {
|
|
137
|
+
ctx.ui.notify(`Soul stopped this request: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
138
|
+
ctx.abort();
|
|
139
|
+
// Pi currently enters the provider with an aborted signal; do not weaken that
|
|
140
|
+
// cancellation into a fallback prompt. See soul-request-check-findings.md.
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
10
144
|
}
|