@shanepadgett/tau-agent 0.37.0 → 0.39.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.37.0",
3
+ "version": "0.39.0",
4
4
  "description": "Tau is a custom agentic harness built with pi extensions",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -34,13 +34,13 @@
34
34
  "README.md"
35
35
  ],
36
36
  "dependencies": {
37
- "@ast-grep/wasm": "0.45.0",
38
- "@shanepadgett/tau-tui": "0.37.0",
37
+ "@ast-grep/wasm": "0.45.1",
38
+ "@shanepadgett/tau-tui": "0.39.0",
39
39
  "@vscode/tree-sitter-wasm": "0.3.1",
40
40
  "image-size": "2.0.2",
41
- "smol-toml": "1.7.1",
41
+ "smol-toml": "1.8.0",
42
42
  "unbash": "4.0.10",
43
- "web-tree-sitter": "0.26.11"
43
+ "web-tree-sitter": "0.26.12"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@earendil-works/pi-ai": "*",
@@ -12,22 +12,17 @@
12
12
  "extensions": {
13
13
  "type": "object",
14
14
  "properties": {
15
- "checkpoint": {
15
+ "autoCompact": {
16
16
  "type": "object",
17
17
  "required": [
18
- "checkpointTokenLimit"
18
+ "tokenLimit"
19
19
  ],
20
20
  "properties": {
21
- "showToolRows": {
22
- "type": "boolean",
23
- "default": false,
24
- "description": "Show checkpoint and injected-file rows in the TUI for debugging."
25
- },
26
- "checkpointTokenLimit": {
21
+ "tokenLimit": {
27
22
  "type": "integer",
28
23
  "minimum": 1,
29
- "default": 150000,
30
- "description": "Context-token ceiling before a checkpoint is required."
24
+ "default": 175000,
25
+ "description": "Absolute context-token count that triggers compaction before the next model turn."
31
26
  }
32
27
  },
33
28
  "additionalProperties": false
@@ -254,16 +249,31 @@
254
249
  },
255
250
  "soul": {
256
251
  "type": "object",
252
+ "required": [
253
+ "overseer"
254
+ ],
257
255
  "properties": {
258
- "ponytail": {
259
- "type": "boolean",
260
- "default": true,
261
- "description": "Add the lazy-senior-dev build ethos to Tau's system prompt."
262
- },
263
- "simplified": {
264
- "type": "boolean",
265
- "default": true,
266
- "description": "Add Simplified Technical English and small-chunk explanations to Tau's system prompt."
256
+ "overseer": {
257
+ "type": "object",
258
+ "required": [
259
+ "enabled",
260
+ "toolCallInterval"
261
+ ],
262
+ "properties": {
263
+ "enabled": {
264
+ "type": "boolean",
265
+ "default": true,
266
+ "description": "Run hidden primary-directive reviews during long tool-using work."
267
+ },
268
+ "toolCallInterval": {
269
+ "type": "integer",
270
+ "minimum": 1,
271
+ "maximum": 100,
272
+ "default": 20,
273
+ "description": "Unreviewed tool calls required before the next primary-directive review."
274
+ }
275
+ },
276
+ "additionalProperties": false
267
277
  }
268
278
  },
269
279
  "additionalProperties": false
@@ -1,9 +0,0 @@
1
- # Checkpoint
2
-
3
- Keeps long-running agent work focused by letting the agent checkpoint durable working context while disposable conversation history is retired. The agent writes a short resume directive that is appended to the hidden continuation message after each checkpoint.
4
-
5
- Checkpoint rows are hidden by default. Set `extensions.checkpoint.showToolRows` to `true` before `/reload` to watch checkpoint and newly injected-file rows while working. Existing injected-file rows keep their saved display state.
6
-
7
- Checkpoint nudges the agent at 50% and 75% of `extensions.checkpoint.checkpointTokenLimit`, which defaults to 150,000 context tokens. At the limit, it blocks non-checkpoint tools until the agent checkpoints.
8
-
9
- After changing this extension, run `/reload` before testing it.
@@ -1,66 +0,0 @@
1
- export const DEFAULT_CHECKPOINT_TOKEN_LIMIT = 150_000;
2
-
3
- export type CheckpointBudgetLevel = 0 | 50 | 75 | 100;
4
- export type CheckpointBudgetNoticeLevel = Exclude<CheckpointBudgetLevel, 0>;
5
-
6
- export interface CheckpointBudget {
7
- configure(limit: number): void;
8
- beginTurn(tokens: number | null): CheckpointBudgetNoticeLevel | undefined;
9
- finishTurn(tokens: number | null): CheckpointBudgetNoticeLevel | undefined;
10
- shouldBlockTool(toolName: string, checkpointToolName: string): boolean;
11
- reset(): void;
12
- }
13
-
14
- export function createCheckpointBudget(initialLimit = DEFAULT_CHECKPOINT_TOKEN_LIMIT): CheckpointBudget {
15
- let limit = validateLimit(initialLimit);
16
- let highestNoticed: CheckpointBudgetLevel = 0;
17
- let forced = false;
18
-
19
- return {
20
- configure(nextLimit: number): void {
21
- limit = validateLimit(nextLimit);
22
- highestNoticed = 0;
23
- forced = false;
24
- },
25
-
26
- beginTurn(tokens: number | null): CheckpointBudgetNoticeLevel | undefined {
27
- return observe(tokens);
28
- },
29
-
30
- finishTurn(tokens: number | null): CheckpointBudgetNoticeLevel | undefined {
31
- return observe(tokens);
32
- },
33
-
34
- shouldBlockTool(toolName: string, checkpointToolName: string): boolean {
35
- return forced && toolName !== checkpointToolName;
36
- },
37
-
38
- reset(): void {
39
- highestNoticed = 0;
40
- forced = false;
41
- },
42
- };
43
-
44
- function observe(tokens: number | null): CheckpointBudgetNoticeLevel | undefined {
45
- if (tokens === null) return undefined;
46
- const level = levelFor(tokens, limit);
47
- if (level === 100) forced = true;
48
- if (level <= highestNoticed) return undefined;
49
- highestNoticed = level;
50
- return level === 0 ? undefined : level;
51
- }
52
- }
53
-
54
- function levelFor(tokens: number, limit: number): CheckpointBudgetLevel {
55
- if (tokens >= limit) return 100;
56
- if (tokens >= limit * 0.75) return 75;
57
- if (tokens >= limit * 0.5) return 50;
58
- return 0;
59
- }
60
-
61
- function validateLimit(limit: number): number {
62
- if (!Number.isSafeInteger(limit) || limit <= 0) {
63
- throw new Error("Checkpoint token limit must be a positive safe integer");
64
- }
65
- return limit;
66
- }
@@ -1,279 +0,0 @@
1
- import {
2
- defineTool,
3
- keyText,
4
- type ExtensionAPI,
5
- type SessionEntry,
6
- type SessionMessageEntry,
7
- type Theme,
8
- } from "@earendil-works/pi-coding-agent";
9
- import { truncateToWidth, type Component, wrapTextWithAnsi } from "@earendil-works/pi-tui";
10
- import { Type, type Static } from "typebox";
11
- import { prepareFileInjection } from "@shanepadgett/tau-agent";
12
- import { areCheckpointRowsVisible } from "../../shared/checkpoint-visibility.ts";
13
- import type { FileInjectionFile } from "../../src/file-injection/index.ts";
14
- import { extractConversationText } from "./messages.ts";
15
- import { formatCheckpointMessage } from "./prompt.ts";
16
-
17
- export const CHECKPOINT_TOOL = "checkpoint";
18
- const CONTINUATION_TYPE = "tau.checkpoint";
19
- const CONTINUATION_PREAMBLE =
20
- "Continue directly from the checkpoint state and provided files. Trust the provided sources and continue the listed work.";
21
- const CHECKPOINT_PREVIEW_CHARACTERS = 240;
22
- const CHECKPOINT_RENDER_CHARACTERS = 24_000;
23
- const CHECKPOINT_RENDER_LINES = 200;
24
- const CONVERSATION_MESSAGE_ID_ATTRIBUTE = /^(<(?:user|assistant)-message)\s+id="(?:\\.|[^"\\])*">$/gm;
25
-
26
- const lineRange = Type.Object(
27
- {
28
- startLine: Type.Integer({ minimum: 1 }),
29
- endLine: Type.Integer({ minimum: 1 }),
30
- },
31
- { additionalProperties: false },
32
- );
33
-
34
- const checkpointFile = Type.Union([
35
- Type.Object(
36
- {
37
- path: Type.String({ minLength: 1 }),
38
- mode: Type.Literal("read"),
39
- ranges: Type.Optional(Type.Array(lineRange)),
40
- },
41
- { additionalProperties: false },
42
- ),
43
- Type.Object(
44
- {
45
- path: Type.String({ minLength: 1 }),
46
- mode: Type.Literal("outline"),
47
- },
48
- { additionalProperties: false },
49
- ),
50
- Type.Object(
51
- {
52
- path: Type.String({ minLength: 1 }),
53
- mode: Type.Literal("deferred"),
54
- when: Type.String({ minLength: 1 }),
55
- },
56
- { additionalProperties: false },
57
- ),
58
- ]);
59
-
60
- const checkpointParams = Type.Object(
61
- {
62
- keepMessages: Type.Array(Type.String({ minLength: 1 }), {
63
- description:
64
- "Exact message-entry IDs from hidden checkpoint metadata or earlier checkpoint conversation entries.",
65
- }),
66
- work: Type.Array(Type.String(), { description: "Ordered current work, with the next thing first." }),
67
- facts: Type.Array(Type.String(), { description: "Concrete findings that must survive context replacement." }),
68
- decisions: Type.Array(Type.String(), { description: "Choices that continue to govern the work." }),
69
- continue: Type.String({
70
- minLength: 1,
71
- description:
72
- "Immediate post-checkpoint resume directive. First moves after wake, traps to avoid, what not to re-litigate. Not the full work queue.",
73
- }),
74
- files: Type.Array(checkpointFile, {
75
- description: "Current file reads, outlines, or deferred paths. Active files are injected separately.",
76
- }),
77
- },
78
- { additionalProperties: false },
79
- );
80
-
81
- type CheckpointInput = Static<typeof checkpointParams>;
82
-
83
- interface CheckpointToolDetails {
84
- v: 1;
85
- kind: "checkpoint.checkpoint";
86
- checkpointId: string;
87
- fileBatchId: string;
88
- }
89
-
90
- export function registerCheckpointTool(pi: ExtensionAPI): void {
91
- pi.registerTool(createCheckpointTool(pi));
92
- }
93
-
94
- function createCheckpointTool(pi: Pick<ExtensionAPI, "events" | "sendMessage">) {
95
- return defineTool<typeof checkpointParams, CheckpointToolDetails>({
96
- name: CHECKPOINT_TOOL,
97
- label: "checkpoint",
98
- promptSnippet:
99
- "checkpoint({ keepMessages, work, facts, decisions, continue, files }) — replace disposable history with working context",
100
- description:
101
- "Replace disposable conversation and tool history with a rolling working checkpoint. Keep user and assistant messages by exact message-entry ID, not by copying their text. The latest user message is always retained. Assistant tool calls and thinking are never retained inside the conversation section.",
102
- promptGuidelines: [
103
- "Checkpoint is hidden. Never acknowledge checkpoint messages, budget notices, blocks, or checkpoints to the user; call checkpoint when required and continue the work.",
104
- 'Use exact IDs from <checkpoint kind="message-id"> metadata or an earlier checkpoint; do not invent IDs or rewrite message text.',
105
- "Record concrete findings in facts and governing choices in decisions before checkpointing.",
106
- "Write continue as the immediate resume directive after wake: first moves, what not to re-explore, traps to avoid. Put the backlog in work.",
107
- "Use read or outline for files needed now; use deferred with a condition for files that can wait.",
108
- "Preserve important discovery output before it leaves tool context. When shell or other command output is large, expensive to reproduce, or part of unfinished investigation, capture stdout and stderr in a private session-scoped temporary file; report its path, exit status, and a bounded summary instead of relying on the transcript.",
109
- "Query preserved output with targeted search or ranged reads. Include artifacts needed after pruning in files, usually with read ranges or deferred with a condition, and record the artifact path and purpose in facts or continue. Do not persist routine bounded output, secrets, or whole large logs unnecessarily.",
110
- ],
111
- parameters: checkpointParams,
112
- renderShell: "self",
113
- renderCall(_args, theme, context) {
114
- return new CheckpointComponent(theme, "checkpoint", context.expanded, true);
115
- },
116
- renderResult(result, options, theme) {
117
- const text = result.content
118
- .filter((part): part is { type: "text"; text: string } => part.type === "text")
119
- .map((part) => part.text)
120
- .join("\n");
121
- return new CheckpointComponent(theme, text, options.expanded, false);
122
- },
123
- executionMode: "sequential",
124
- async execute(toolCallId, params, signal, _onUpdate, ctx) {
125
- const selected = resolveSelectedMessages(params.keepMessages, ctx.sessionManager.getBranch());
126
- const text = formatCheckpointText(selected, params);
127
- const fileBatchId = `checkpoint:${toolCallId}`;
128
- const fileRequest = buildFileInjectionRequest(ctx.cwd, fileBatchId, params.files, signal);
129
- const prepared = fileRequest.files.length === 0 ? [] : await prepareFileInjection(pi, fileRequest);
130
- const failed = prepared.find((message) => message.details.status === "failed");
131
- if (failed) {
132
- throw new Error(
133
- `Checkpoint file injection failed for ${failed.details.path}: ${failed.details.error ?? "unknown error"}`,
134
- );
135
- }
136
- const display = areCheckpointRowsVisible();
137
- for (const message of prepared) pi.sendMessage({ ...message, display });
138
- pi.sendMessage({
139
- customType: CONTINUATION_TYPE,
140
- content: formatContinuationMessage(params.continue),
141
- display: false,
142
- details: {
143
- v: 1,
144
- kind: "checkpoint.continuation",
145
- source: "checkpoint",
146
- batchId: fileBatchId,
147
- },
148
- });
149
- return {
150
- content: [{ type: "text", text }],
151
- details: { v: 1, kind: "checkpoint.checkpoint", checkpointId: toolCallId, fileBatchId },
152
- };
153
- },
154
- });
155
- }
156
-
157
- class CheckpointComponent implements Component {
158
- private readonly theme: Theme;
159
- private readonly content: string;
160
- private readonly expanded: boolean;
161
- private readonly call: boolean;
162
-
163
- constructor(theme: Theme, content: string, expanded: boolean, call: boolean) {
164
- this.theme = theme;
165
- this.content = content;
166
- this.expanded = expanded;
167
- this.call = call;
168
- }
169
-
170
- render(width: number): string[] {
171
- if (!areCheckpointRowsVisible()) return [];
172
- if (this.call) return [truncateToWidth(this.theme.fg("toolTitle", this.theme.bold(CHECKPOINT_TOOL)), width, "…")];
173
- const displayContent = this.content.replace(CONVERSATION_MESSAGE_ID_ATTRIBUTE, "$1>");
174
- if (!this.expanded) {
175
- const preview = displayContent.replace(/\s+/g, " ").trim().slice(0, CHECKPOINT_PREVIEW_CHARACTERS);
176
- const suffix = displayContent.length > CHECKPOINT_PREVIEW_CHARACTERS ? "…" : "";
177
- return [
178
- truncateToWidth(
179
- `${this.theme.fg("dim", preview + suffix)} ${this.theme.fg("muted", `(${keyText("app.tools.expand")} to expand)`)}`,
180
- width,
181
- "…",
182
- ),
183
- ];
184
- }
185
-
186
- const bounded =
187
- displayContent.length > CHECKPOINT_RENDER_CHARACTERS
188
- ? `${displayContent.slice(0, CHECKPOINT_RENDER_CHARACTERS)}\n…`
189
- : displayContent;
190
- return wrapTextWithAnsi(this.theme.fg("dim", bounded), Math.max(1, width))
191
- .slice(0, CHECKPOINT_RENDER_LINES)
192
- .map((line) => truncateToWidth(line, width, "…"));
193
- }
194
-
195
- invalidate(): void {}
196
- }
197
-
198
- function resolveSelectedMessages(ids: readonly string[], branch: readonly SessionEntry[]): SessionMessageEntry[] {
199
- const entriesById = new Map(branch.map((entry) => [entry.id, entry]));
200
- const selectedIds = new Set(ids);
201
- const latestUser = [...branch].reverse().find(isUserMessageEntry);
202
- if (!latestUser) throw new Error("Checkpoint requires a current user message");
203
- selectedIds.add(latestUser.id);
204
-
205
- for (const id of selectedIds) {
206
- const entry = entriesById.get(id);
207
- if (!entry || !isConversationEntry(entry)) throw new Error(`Unknown user or assistant message ID: ${id}`);
208
- if (entry.message.role === "assistant" && extractConversationText(entry.message) === "") {
209
- throw new Error(`Assistant message ID has no text content: ${id}`);
210
- }
211
- }
212
-
213
- return branch.filter(
214
- (entry): entry is SessionMessageEntry => selectedIds.has(entry.id) && isConversationEntry(entry),
215
- );
216
- }
217
-
218
- function formatCheckpointText(selected: readonly SessionMessageEntry[], params: CheckpointInput): string {
219
- const conversation = selected
220
- .map((entry) => {
221
- const text = extractConversationText(entry.message);
222
- if (text === undefined) throw new Error(`Message ID is not a user or assistant message: ${entry.id}`);
223
- const role = entry.message.role === "user" ? "user" : "assistant";
224
- return `<${role}-message id=${JSON.stringify(entry.id)}>\n${text}\n</${role}-message>`;
225
- })
226
- .join("\n\n");
227
- const deferred = params.files
228
- .filter(
229
- (file): file is Extract<CheckpointInput["files"][number], { mode: "deferred" }> => file.mode === "deferred",
230
- )
231
- .map((file) => `- ${file.path} — ${file.when}`);
232
-
233
- return [
234
- `Conversation:\n${conversation || "(none)"}`,
235
- `Work:\n${formatList(params.work)}`,
236
- `Facts:\n${formatList(params.facts)}`,
237
- `Decisions:\n${formatList(params.decisions)}`,
238
- `Continue:\n${params.continue}`,
239
- `Deferred files:\n${deferred.length ? deferred.join("\n") : "(none)"}`,
240
- ].join("\n\n");
241
- }
242
-
243
- function formatContinuationMessage(continueNudge: string): string {
244
- return formatCheckpointMessage("continuation", `${CONTINUATION_PREAMBLE}\n\n${continueNudge}`);
245
- }
246
-
247
- function buildFileInjectionRequest(
248
- cwd: string,
249
- batchId: string,
250
- files: CheckpointInput["files"],
251
- signal: AbortSignal | undefined,
252
- ) {
253
- const activeFiles: FileInjectionFile[] = [];
254
- for (const file of files) {
255
- if (file.mode === "deferred") continue;
256
- if (file.mode === "outline") {
257
- activeFiles.push({ path: file.path, mode: "outline" });
258
- continue;
259
- }
260
- activeFiles.push({
261
- path: file.path,
262
- mode: "full",
263
- ...(file.ranges === undefined ? {} : { ranges: file.ranges }),
264
- });
265
- }
266
- return { cwd, source: "checkpoint", batchId, files: activeFiles, signal };
267
- }
268
-
269
- function formatList(items: readonly string[]): string {
270
- return items.length ? items.map((item) => `- ${item}`).join("\n") : "(none)";
271
- }
272
-
273
- function isConversationEntry(entry: SessionEntry): entry is SessionMessageEntry {
274
- return entry.type === "message" && (entry.message.role === "user" || entry.message.role === "assistant");
275
- }
276
-
277
- function isUserMessageEntry(entry: SessionEntry): entry is SessionMessageEntry {
278
- return entry.type === "message" && entry.message.role === "user";
279
- }
@@ -1,102 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
3
- import { setCheckpointRowsVisible } from "../../shared/checkpoint-visibility.ts";
4
- import { CHECKPOINT_TOOL, registerCheckpointTool } from "./checkpoint.ts";
5
- import { createCheckpointBudget, type CheckpointBudgetNoticeLevel } from "./checkpoint-budget.ts";
6
- import { projectContextMessages } from "./messages.ts";
7
- import { CHECKPOINT_SYSTEM_GUIDANCE, formatCheckpointMessage } from "./prompt.ts";
8
- import checkpointSettings from "./settings.ts";
9
-
10
- export default function checkpointExtension(pi: ExtensionAPI): void {
11
- const budget = createCheckpointBudget();
12
- let checkpointSucceeded = false;
13
-
14
- const resetBudget = (): void => {
15
- checkpointSucceeded = false;
16
- budget.reset();
17
- };
18
-
19
- registerCheckpointTool(pi);
20
- pi.on("session_start", async (_event, ctx) => {
21
- const settings = await loadTauExtensionSettings(ctx, checkpointSettings);
22
- budget.configure(settings.checkpointTokenLimit);
23
- setCheckpointRowsVisible(settings.showToolRows);
24
- });
25
- pi.on("session_shutdown", () => {
26
- setCheckpointRowsVisible(false);
27
- resetBudget();
28
- });
29
- pi.on("session_compact", resetBudget);
30
- pi.on("session_tree", resetBudget);
31
- pi.on("session_before_fork", resetBudget);
32
- pi.on("session_before_switch", resetBudget);
33
- pi.on("before_agent_start", (event) => ({
34
- systemPrompt: `${event.systemPrompt}\n\n${CHECKPOINT_SYSTEM_GUIDANCE}`,
35
- }));
36
- pi.on("turn_start", (_event, ctx) => {
37
- if (checkpointSucceeded) resetBudget();
38
- const noticeLevel = budget.beginTurn(ctx.getContextUsage()?.tokens ?? null);
39
- if (noticeLevel !== undefined) sendCheckpointBudgetNotice(pi, noticeLevel);
40
- });
41
- pi.on("turn_end", (_event, ctx) => {
42
- if (checkpointSucceeded) {
43
- resetBudget();
44
- return;
45
- }
46
- const noticeLevel = budget.finishTurn(ctx.getContextUsage()?.tokens ?? null);
47
- if (noticeLevel !== undefined) sendCheckpointBudgetNotice(pi, noticeLevel);
48
- });
49
- pi.on("tool_result", (event) => {
50
- if (event.toolName === CHECKPOINT_TOOL && !event.isError) checkpointSucceeded = true;
51
- });
52
- pi.on("tool_call", (event) => {
53
- if (checkpointSucceeded || !budget.shouldBlockTool(event.toolName, CHECKPOINT_TOOL)) return;
54
- return {
55
- block: true,
56
- reason: formatCheckpointMessage(
57
- "block",
58
- "Checkpoint required before using other tools. Call checkpoint, then continue the user's work.",
59
- ),
60
- };
61
- });
62
- pi.on("context", (event, ctx) => ({
63
- messages: projectContextMessages(event.messages, ctx.sessionManager.buildContextEntries()),
64
- }));
65
- }
66
-
67
- function sendCheckpointBudgetNotice(pi: ExtensionAPI, level: CheckpointBudgetNoticeLevel): void {
68
- pi.sendMessage({
69
- customType: "tau.checkpoint",
70
- content: checkpointBudgetMessage(level),
71
- display: false,
72
- details: {
73
- v: 1,
74
- kind: "checkpoint.checkpoint-budget",
75
- source: "checkpoint",
76
- level,
77
- },
78
- });
79
- }
80
-
81
- function checkpointBudgetMessage(level: CheckpointBudgetNoticeLevel): string {
82
- switch (level) {
83
- case 50:
84
- return formatCheckpointMessage(
85
- "budget",
86
- "Context usage reached 50% of the configured checkpoint limit. Continue current work and checkpoint when convenient.",
87
- { level: "50" },
88
- );
89
- case 75:
90
- return formatCheckpointMessage(
91
- "budget",
92
- "Context usage reached 75% of the configured checkpoint limit. Run checkpoint soon. At 100%, non-checkpoint tool calls will be blocked until checkpoint succeeds.",
93
- { level: "75" },
94
- );
95
- case 100:
96
- return formatCheckpointMessage(
97
- "budget",
98
- "Checkpoint budget reached. Call checkpoint now before using any other tool. Do not call other tools.",
99
- { level: "100" },
100
- );
101
- }
102
- }