@shanepadgett/tau-agent 0.42.3 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +0 -1
  2. package/docs/context.md +1 -1
  3. package/docs/extending-tau-agent.md +1 -2
  4. package/extensions/attention/README.md +1 -1
  5. package/extensions/cache-diagnostics/index.ts +4 -0
  6. package/extensions/context/README.md +1 -24
  7. package/extensions/context/definitions.ts +1 -52
  8. package/extensions/context/index.ts +1 -150
  9. package/extensions/context/panel.ts +0 -72
  10. package/extensions/explore/guidance.ts +9 -1
  11. package/extensions/footer/README.md +1 -1
  12. package/extensions/footer/index.ts +3 -25
  13. package/extensions/qna/choice-question-body.ts +7 -2
  14. package/extensions/run-summary/README.md +1 -1
  15. package/extensions/run-summary/index.ts +18 -25
  16. package/extensions/runtime-context/README.md +1 -1
  17. package/extensions/runtime-context/context.ts +1 -1
  18. package/extensions/runtime-context/index.ts +10 -16
  19. package/extensions/script-runner/index.ts +21 -29
  20. package/extensions/silent-command-runner/README.md +0 -2
  21. package/extensions/silent-command-runner/index.ts +12 -41
  22. package/extensions/soul/README.md +5 -8
  23. package/extensions/soul/context.ts +115 -0
  24. package/extensions/soul/index.ts +141 -7
  25. package/extensions/soul/prompt.ts +47 -102
  26. package/extensions/soul/state.ts +114 -0
  27. package/extensions/soul/tools.ts +31 -0
  28. package/extensions/tau-help/help.md +6 -18
  29. package/extensions/tau-help/index.ts +10 -4
  30. package/extensions/tool-approval/README.md +4 -2
  31. package/extensions/tool-approval/index.ts +141 -53
  32. package/extensions/tool-approval/panel.ts +153 -0
  33. package/extensions/tool-loader/README.md +1 -1
  34. package/extensions/tool-loader/index.ts +91 -22
  35. package/package.json +2 -2
  36. package/schemas/tau.schema.json +0 -104
  37. package/shared/bounded-text-result.ts +0 -1
  38. package/shared/events.ts +19 -15
  39. package/shared/isolated-session.ts +1 -2
  40. package/shared/model-effort.ts +15 -21
  41. package/shared/prompt-contributions.ts +24 -0
  42. package/src/index.ts +1 -1
  43. package/docs/subagents.md +0 -92
  44. package/extensions/auto-compact/README.md +0 -9
  45. package/extensions/auto-compact/index.ts +0 -122
  46. package/extensions/auto-compact/settings.ts +0 -30
  47. package/extensions/context/settings.ts +0 -62
  48. package/extensions/context/sync.ts +0 -276
  49. package/extensions/context/validation.ts +0 -88
  50. package/extensions/effort/README.md +0 -7
  51. package/extensions/effort/index.ts +0 -134
  52. package/extensions/effort/state.ts +0 -18
  53. package/extensions/qna/inline-editor-row.ts +0 -56
  54. package/extensions/soul/overseer.ts +0 -273
  55. package/extensions/soul/settings.ts +0 -34
  56. package/extensions/subagent/README.md +0 -67
  57. package/extensions/subagent/agents/context-sync.md +0 -244
  58. package/extensions/subagent/agents/dormant/generalist.md +0 -33
  59. package/extensions/subagent/agents/scout.md +0 -104
  60. package/extensions/subagent/agents/web-research.md +0 -101
  61. package/extensions/subagent/agents.ts +0 -261
  62. package/extensions/subagent/cmux-dashboard.ts +0 -495
  63. package/extensions/subagent/index.ts +0 -425
  64. package/extensions/subagent/panel.ts +0 -124
  65. package/extensions/subagent/render.ts +0 -74
  66. package/extensions/subagent/resume.ts +0 -78
  67. package/extensions/subagent/run.ts +0 -641
  68. package/extensions/subagent/runtime.ts +0 -1296
  69. package/extensions/subagent/session-resource.ts +0 -61
  70. package/extensions/subagent/settings.ts +0 -18
@@ -9,7 +9,7 @@ interface SnapshotEntry {
9
9
  path: string;
10
10
  }
11
11
 
12
- export interface RuntimeContext {
12
+ interface RuntimeContext {
13
13
  cwd: string;
14
14
  rootSnapshot: readonly string[];
15
15
  }
@@ -1,21 +1,15 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import {
3
- formatLocalDisplayDate,
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
- let runtimeContext: RuntimeContext | undefined;
11
-
12
- pi.on("session_start", (_event, ctx) => {
13
- runtimeContext = freezeRuntimeContext(ctx.cwd);
14
- });
15
-
16
- pi.on("before_agent_start", (event, ctx) => {
17
- runtimeContext ??= freezeRuntimeContext(ctx.cwd);
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 ensureTempDir();
207
- const file = join(dir, language === "python3" ? "_run.py" : "_run.ts");
208
- await writeFile(file, source, "utf8");
209
- const args =
210
- language === "python3"
211
- ? [file]
212
- : language === "node"
213
- ? ["--experimental-strip-types", file]
214
- : ["run", "-A", file];
215
- const result = await pi.exec(command, args, { cwd, signal, timeout: TIMEOUT_MS });
216
- return {
217
- ...result,
218
- stdout: scrubPath(result.stdout, file, dir),
219
- stderr: scrubPath(result.stderr, file, dir),
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, onTauEvent } from "../../shared/events.ts";
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.on("before_agent_start", async (event, ctx) => {
134
- settings = normalizeSettings(await loadTauExtensionSettings(ctx, silentCommandRunnerSettings));
135
- if (!settings.enabled || settings.commands.length === 0) return undefined;
136
- return { systemPrompt: `${event.systemPrompt}\n\n${formatSilentCheckPrompt(settings.commands)}` };
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 is the baseline Tau system prompt. It is always on.
3
+ Soul supplies Tau's system prompt: communication, discussion, planning, execution, and coding guidance. It is always on.
4
4
 
5
- - **communication style** — short Slack-style replies, exact technical names, and a few worked examples.
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
- Pi continues to own tool guidance, project instructions, skills, documentation paths, custom prompts, and working-directory context.
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
- Set `extensions.soul.overseer.enabled` to turn the overseer on or off. Set `extensions.soul.overseer.toolCallInterval` from 1 to 100 to change its review interval.
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
- After changing this extension, run `/reload` before testing the new behavior.
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
+ }
@@ -1,10 +1,144 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { registerPrimaryDirectiveOverseer } from "./overseer.ts";
3
- import { CODE_STYLE, COMMUNICATION_STYLE, OPERATING_MODEL } from "./prompt.ts";
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
- registerPrimaryDirectiveOverseer(pi);
7
- pi.on("before_agent_start", (event) => ({
8
- systemPrompt: [event.systemPrompt, COMMUNICATION_STYLE, OPERATING_MODEL, CODE_STYLE].join("\n\n"),
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
  }