@shanepadgett/tau-agent 0.28.1 → 0.30.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/context.md +14 -9
- package/docs/subagents.md +7 -5
- package/extensions/cache-diagnostics/index.ts +4 -5
- package/extensions/context/README.md +8 -5
- package/extensions/context/definitions.ts +28 -17
- package/extensions/context/evidence.ts +4 -3
- package/extensions/context/index.ts +57 -35
- package/extensions/context/panel.ts +10 -9
- package/extensions/context/projection.ts +141 -0
- package/extensions/context/state.ts +30 -0
- package/extensions/explore/README.md +1 -1
- package/extensions/explore/ast/read/hook.ts +5 -35
- package/extensions/explore/ast/read/policy.ts +0 -32
- package/extensions/explore/index.ts +2 -0
- package/extensions/explore/outline-injection.ts +151 -0
- package/extensions/explore/settings.ts +0 -8
- package/extensions/ideas/browser.ts +27 -16
- package/extensions/review/README.md +11 -0
- package/extensions/review/index.ts +135 -0
- package/extensions/review/model.ts +144 -0
- package/extensions/review/panel.ts +128 -0
- package/extensions/review/session.ts +106 -0
- package/extensions/runtime-context/README.md +1 -1
- package/extensions/runtime-context/index.ts +3 -66
- package/extensions/script-runner/README.md +7 -0
- package/extensions/script-runner/index.ts +275 -0
- package/extensions/stash/browser.ts +30 -18
- package/extensions/subagent/README.md +17 -4
- package/extensions/subagent/agents/context-sync.md +2 -2
- package/extensions/subagent/agents/scout.md +49 -52
- package/extensions/subagent/agents.ts +0 -1
- package/extensions/subagent/cmux-dashboard.ts +7 -3
- package/extensions/subagent/index.ts +105 -4
- package/extensions/subagent/panel.ts +124 -0
- package/extensions/subagent/render.ts +2 -1
- package/extensions/subagent/run.ts +9 -9
- package/extensions/subagent/runtime.ts +5 -5
- package/extensions/subagent/session-resource.ts +19 -127
- package/extensions/subagent/settings.ts +18 -0
- package/extensions/tau-help/help.md +12 -8
- package/extensions/working-memory/README.md +9 -0
- package/extensions/working-memory/checkpoint.ts +175 -0
- package/extensions/working-memory/index.ts +338 -0
- package/extensions/working-memory/memory.ts +266 -0
- package/extensions/working-memory/render.ts +178 -0
- package/extensions/working-memory/settings.ts +38 -0
- package/extensions/working-memory/state.ts +152 -0
- package/package.json +2 -2
- package/schemas/tau.schema.json +35 -55
- package/shared/context-messages.ts +19 -0
- package/shared/events.ts +5 -0
- package/shared/full-file-knowledge.ts +0 -1
- package/shared/injected-context.ts +2 -2
- package/shared/isolated-session.ts +151 -0
- package/shared/outline-injection.ts +56 -0
- package/extensions/context-pruning/README.md +0 -39
- package/extensions/context-pruning/index.ts +0 -382
- package/extensions/context-pruning/projection.ts +0 -60
- package/extensions/context-pruning/prune.ts +0 -199
- package/extensions/context-pruning/render.ts +0 -251
- package/extensions/context-pruning/settings.ts +0 -39
- package/extensions/subagent/agents/review.md +0 -68
- package/extensions/turn-budget/README.md +0 -14
- package/extensions/turn-budget/index.ts +0 -116
- package/extensions/turn-budget/settings.ts +0 -35
- package/shared/context-pruning-state.ts +0 -152
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { type Static, Type } from "typebox";
|
|
4
|
-
import type { ContextPruneDeferredFileV2, ContextPruneDetailsV2 } from "../../shared/context-pruning-state.ts";
|
|
5
|
-
import { prepareAutoreadMessage, type PreparedAutoreadMessage } from "../../shared/autoread.ts";
|
|
6
|
-
import { MAX_COMPLETE_FILE_SNAPSHOT_BYTES } from "../../shared/full-file-knowledge.ts";
|
|
7
|
-
|
|
8
|
-
export const contextPruneParameters = Type.Object(
|
|
9
|
-
{
|
|
10
|
-
keepFiles: Type.Array(
|
|
11
|
-
Type.Object({ path: Type.String(), relevance: Type.String() }, { additionalProperties: false }),
|
|
12
|
-
),
|
|
13
|
-
keepToolCalls: Type.Array(
|
|
14
|
-
Type.Object({ toolCallId: Type.String(), relevance: Type.String() }, { additionalProperties: false }),
|
|
15
|
-
),
|
|
16
|
-
deferFiles: Type.Array(
|
|
17
|
-
Type.Object(
|
|
18
|
-
{ path: Type.String(), reason: Type.String(), relevantWhen: Type.String() },
|
|
19
|
-
{ additionalProperties: false },
|
|
20
|
-
),
|
|
21
|
-
),
|
|
22
|
-
},
|
|
23
|
-
{ additionalProperties: false },
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
export type ContextPruneInput = Static<typeof contextPruneParameters>;
|
|
27
|
-
|
|
28
|
-
interface ContextPruneExecutionOptions {
|
|
29
|
-
toolCallId: string;
|
|
30
|
-
params: ContextPruneInput;
|
|
31
|
-
signal: AbortSignal | undefined;
|
|
32
|
-
ctx: ExtensionContext;
|
|
33
|
-
generation: number;
|
|
34
|
-
currentGeneration: () => number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface ContextPruneToolResult {
|
|
38
|
-
content: Array<{ type: "text"; text: string }>;
|
|
39
|
-
details: ContextPruneDetailsV2;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface ContextPruneExecutionResult {
|
|
43
|
-
result: ContextPruneToolResult;
|
|
44
|
-
autoreads: PreparedAutoreadMessage[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function executeContextPrune(options: ContextPruneExecutionOptions): Promise<ContextPruneExecutionResult> {
|
|
48
|
-
assertCurrent(options);
|
|
49
|
-
const retainedToolCallIds = [...new Set(options.params.keepToolCalls.map((selection) => selection.toolCallId))];
|
|
50
|
-
const retainedTools = new Set(retainedToolCallIds);
|
|
51
|
-
const { prunedToolCallIds, prunedAutoreadRowIds } = collectPrunedRows(
|
|
52
|
-
options.ctx.sessionManager.getBranch(),
|
|
53
|
-
options.toolCallId,
|
|
54
|
-
retainedTools,
|
|
55
|
-
);
|
|
56
|
-
const warnings: string[] = [];
|
|
57
|
-
const preparedSnapshots: PreparedAutoreadMessage[] = [];
|
|
58
|
-
const keptPaths = new Set<string>();
|
|
59
|
-
for (const selection of options.params.keepFiles) {
|
|
60
|
-
assertCurrent(options);
|
|
61
|
-
const path = normalizePath(selection.path);
|
|
62
|
-
const pathKey = resolve(options.ctx.cwd, path);
|
|
63
|
-
if (keptPaths.has(pathKey)) continue;
|
|
64
|
-
keptPaths.add(pathKey);
|
|
65
|
-
try {
|
|
66
|
-
preparedSnapshots.push(
|
|
67
|
-
await prepareAutoreadMessage({
|
|
68
|
-
rowId: `${options.toolCallId}:${preparedSnapshots.length}`,
|
|
69
|
-
path,
|
|
70
|
-
cwd: options.ctx.cwd,
|
|
71
|
-
source: "context-pruning",
|
|
72
|
-
batchId: options.toolCallId,
|
|
73
|
-
signal: options.signal,
|
|
74
|
-
isLifecycleCurrent: () => options.generation === options.currentGeneration(),
|
|
75
|
-
maximumBytes: MAX_COMPLETE_FILE_SNAPSHOT_BYTES,
|
|
76
|
-
}),
|
|
77
|
-
);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
if (options.signal?.aborted || options.generation !== options.currentGeneration()) throw error;
|
|
80
|
-
warnings.push(`${path}: ${errorMessage(error)}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const deferredFiles: ContextPruneDeferredFileV2[] = [];
|
|
85
|
-
const deferredPaths = new Set<string>();
|
|
86
|
-
for (const selection of options.params.deferFiles) {
|
|
87
|
-
const path = normalizePath(selection.path);
|
|
88
|
-
const pathKey = resolve(options.ctx.cwd, path);
|
|
89
|
-
if (keptPaths.has(pathKey) || deferredPaths.has(pathKey)) continue;
|
|
90
|
-
deferredPaths.add(pathKey);
|
|
91
|
-
deferredFiles.push({ path, reason: selection.reason, relevantWhen: selection.relevantWhen });
|
|
92
|
-
}
|
|
93
|
-
assertCurrent(options);
|
|
94
|
-
|
|
95
|
-
const retainedAutoreadRowIds = preparedSnapshots.map((snapshot) => snapshot.details.rowId);
|
|
96
|
-
const refreshedFiles = preparedSnapshots.map((snapshot) => ({
|
|
97
|
-
path: snapshot.details.path,
|
|
98
|
-
rowId: snapshot.details.rowId,
|
|
99
|
-
}));
|
|
100
|
-
const details: ContextPruneDetailsV2 = {
|
|
101
|
-
v: 2,
|
|
102
|
-
anchorToolCallId: options.toolCallId,
|
|
103
|
-
prunedToolCallIds,
|
|
104
|
-
prunedAutoreadRowIds,
|
|
105
|
-
retainedToolCallIds,
|
|
106
|
-
retainedAutoreadRowIds,
|
|
107
|
-
refreshedFiles,
|
|
108
|
-
deferredFiles,
|
|
109
|
-
warnings,
|
|
110
|
-
};
|
|
111
|
-
const status =
|
|
112
|
-
warnings.length === 0
|
|
113
|
-
? "Context checkpoint applied. Continue with the next action stated before this call."
|
|
114
|
-
: `Context checkpoint applied with ${warnings.length} warning${warnings.length === 1 ? "" : "s"}:\n${warnings.map((warning) => `- ${warning}`).join("\n")}\nContinue with the next action stated before this call.`;
|
|
115
|
-
return {
|
|
116
|
-
result: {
|
|
117
|
-
content: [
|
|
118
|
-
{ type: "text", text: status },
|
|
119
|
-
...(deferredFiles.length === 0 ? [] : [{ type: "text" as const, text: deferredFileText(deferredFiles) }]),
|
|
120
|
-
],
|
|
121
|
-
details,
|
|
122
|
-
},
|
|
123
|
-
autoreads: preparedSnapshots,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function collectPrunedRows(
|
|
128
|
-
branch: readonly SessionEntry[],
|
|
129
|
-
anchorToolCallId: string,
|
|
130
|
-
retainedToolCallIds: ReadonlySet<string>,
|
|
131
|
-
): { prunedToolCallIds: string[]; prunedAutoreadRowIds: string[] } {
|
|
132
|
-
let anchorIndex = -1;
|
|
133
|
-
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
134
|
-
const entry = branch[index];
|
|
135
|
-
if (
|
|
136
|
-
entry?.type === "message" &&
|
|
137
|
-
entry.message.role === "assistant" &&
|
|
138
|
-
entry.message.content.some(
|
|
139
|
-
(block) => block.type === "toolCall" && block.id === anchorToolCallId && block.name === "context_prune",
|
|
140
|
-
)
|
|
141
|
-
) {
|
|
142
|
-
anchorIndex = index;
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (anchorIndex < 0) return { prunedToolCallIds: [], prunedAutoreadRowIds: [] };
|
|
147
|
-
const prunedToolCallIds = new Set<string>();
|
|
148
|
-
const prunedAutoreadRowIds = new Set<string>();
|
|
149
|
-
for (let index = 0; index < anchorIndex; index += 1) {
|
|
150
|
-
const entry = branch[index];
|
|
151
|
-
if (entry?.type === "message" && entry.message.role === "assistant") {
|
|
152
|
-
for (const block of entry.message.content) {
|
|
153
|
-
if (block.type === "toolCall" && !retainedToolCallIds.has(block.id)) prunedToolCallIds.add(block.id);
|
|
154
|
-
}
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (entry?.type !== "custom_message" || entry.customType !== "tau.autoread") continue;
|
|
158
|
-
if (isRecord(entry.details) && typeof entry.details.rowId === "string") {
|
|
159
|
-
prunedAutoreadRowIds.add(entry.details.rowId);
|
|
160
|
-
}
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
for (let index = 0; index < anchorIndex; index += 1) {
|
|
164
|
-
const entry = branch[index];
|
|
165
|
-
if (entry?.type !== "message" || entry.message.role !== "toolResult") continue;
|
|
166
|
-
if (entry.message.toolName !== "context_prune" || !isRecord(entry.message.details)) continue;
|
|
167
|
-
const files = entry.message.details.refreshedFiles;
|
|
168
|
-
if (!Array.isArray(files)) continue;
|
|
169
|
-
for (const file of files)
|
|
170
|
-
if (isRecord(file) && typeof file.rowId === "string") prunedAutoreadRowIds.add(file.rowId);
|
|
171
|
-
}
|
|
172
|
-
return { prunedToolCallIds: [...prunedToolCallIds], prunedAutoreadRowIds: [...prunedAutoreadRowIds] };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function deferredFileText(files: readonly ContextPruneDeferredFileV2[]): string {
|
|
176
|
-
return [
|
|
177
|
-
"Deferred files are advisory. Reconsider them only when their condition applies:",
|
|
178
|
-
...files.map((file) => `- ${file.path}: ${file.reason} Relevant when: ${file.relevantWhen}`),
|
|
179
|
-
].join("\n");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function normalizePath(path: string): string {
|
|
183
|
-
return path.replace(/^@/, "");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function assertCurrent(options: ContextPruneExecutionOptions): void {
|
|
187
|
-
options.signal?.throwIfAborted();
|
|
188
|
-
if (options.generation !== options.currentGeneration()) {
|
|
189
|
-
throw new Error("Context checkpoint crossed a session lifecycle boundary");
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function errorMessage(error: unknown): string {
|
|
194
|
-
return error instanceof Error ? error.message : String(error);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
198
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
199
|
-
}
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import type { AgentToolResult, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import { Marker } from "@shanepadgett/tau-tui";
|
|
4
|
-
import { parseContextPruneDetailsV2 } from "../../shared/context-pruning-state.ts";
|
|
5
|
-
import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.ts";
|
|
6
|
-
import type { ContextPruneInput } from "./prune.ts";
|
|
7
|
-
|
|
8
|
-
const MAX_EXPANDED_ITEMS = 20;
|
|
9
|
-
const MAX_EXPANDED_LINE_CHARACTERS = 240;
|
|
10
|
-
const MAX_EXPANDED_TEXT_CHARACTERS = 3_000;
|
|
11
|
-
const MAX_WARNING_CHARACTERS = 1_000;
|
|
12
|
-
|
|
13
|
-
export type ContextPruningNudgeDetailsV3 =
|
|
14
|
-
| {
|
|
15
|
-
v: 3;
|
|
16
|
-
kind: "automatic";
|
|
17
|
-
tokens: number;
|
|
18
|
-
boundaryTokens: number;
|
|
19
|
-
reminder: number;
|
|
20
|
-
tier: number;
|
|
21
|
-
tierCount: number;
|
|
22
|
-
tierFloor: number;
|
|
23
|
-
anchorToolCallId: string | null;
|
|
24
|
-
suppressedThroughTokens: number;
|
|
25
|
-
}
|
|
26
|
-
| {
|
|
27
|
-
v: 3;
|
|
28
|
-
kind: "manual";
|
|
29
|
-
tokens: null;
|
|
30
|
-
boundaryTokens: null;
|
|
31
|
-
reminder: null;
|
|
32
|
-
tier: null;
|
|
33
|
-
tierCount: null;
|
|
34
|
-
tierFloor: null;
|
|
35
|
-
anchorToolCallId: string | null;
|
|
36
|
-
suppressedThroughTokens: null;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export function parseContextPruningNudgeDetailsV3(value: unknown): ContextPruningNudgeDetailsV3 | undefined {
|
|
40
|
-
if (!isRecord(value)) return undefined;
|
|
41
|
-
const keys = [
|
|
42
|
-
"v",
|
|
43
|
-
"kind",
|
|
44
|
-
"tokens",
|
|
45
|
-
"boundaryTokens",
|
|
46
|
-
"reminder",
|
|
47
|
-
"tier",
|
|
48
|
-
"tierCount",
|
|
49
|
-
"tierFloor",
|
|
50
|
-
"anchorToolCallId",
|
|
51
|
-
"suppressedThroughTokens",
|
|
52
|
-
];
|
|
53
|
-
if (Object.keys(value).length !== keys.length || !keys.every((key) => Object.hasOwn(value, key))) return undefined;
|
|
54
|
-
if (value.v !== 3 || (value.kind !== "automatic" && value.kind !== "manual")) return undefined;
|
|
55
|
-
if (
|
|
56
|
-
value.anchorToolCallId !== null &&
|
|
57
|
-
(typeof value.anchorToolCallId !== "string" || value.anchorToolCallId.length === 0)
|
|
58
|
-
) {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
if (value.kind === "manual") {
|
|
62
|
-
if (
|
|
63
|
-
value.tokens !== null ||
|
|
64
|
-
value.boundaryTokens !== null ||
|
|
65
|
-
value.reminder !== null ||
|
|
66
|
-
value.tier !== null ||
|
|
67
|
-
value.tierCount !== null ||
|
|
68
|
-
value.tierFloor !== null ||
|
|
69
|
-
value.suppressedThroughTokens !== null
|
|
70
|
-
)
|
|
71
|
-
return undefined;
|
|
72
|
-
return {
|
|
73
|
-
v: 3,
|
|
74
|
-
kind: "manual",
|
|
75
|
-
tokens: null,
|
|
76
|
-
boundaryTokens: null,
|
|
77
|
-
reminder: null,
|
|
78
|
-
tier: null,
|
|
79
|
-
tierCount: null,
|
|
80
|
-
tierFloor: null,
|
|
81
|
-
anchorToolCallId: value.anchorToolCallId,
|
|
82
|
-
suppressedThroughTokens: null,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
if (
|
|
86
|
-
!isTokenCount(value.tokens) ||
|
|
87
|
-
!isBoundary(value.boundaryTokens) ||
|
|
88
|
-
!isReminder(value.reminder) ||
|
|
89
|
-
!isTier(value.tier) ||
|
|
90
|
-
!isTier(value.tierCount) ||
|
|
91
|
-
!isTierFloor(value.tierFloor) ||
|
|
92
|
-
!isTokenCount(value.suppressedThroughTokens) ||
|
|
93
|
-
value.boundaryTokens > value.tokens ||
|
|
94
|
-
value.boundaryTokens <= value.suppressedThroughTokens ||
|
|
95
|
-
value.tier > value.tierCount ||
|
|
96
|
-
value.tierFloor > value.tierCount ||
|
|
97
|
-
value.tier !== Math.max(Math.min(value.reminder, value.tierCount), value.tierFloor) ||
|
|
98
|
-
(value.anchorToolCallId === null && value.suppressedThroughTokens !== 0)
|
|
99
|
-
) {
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
const interval = value.boundaryTokens / value.reminder;
|
|
103
|
-
if (
|
|
104
|
-
!Number.isSafeInteger(interval) ||
|
|
105
|
-
interval < 1 ||
|
|
106
|
-
value.tokens - value.boundaryTokens >= interval ||
|
|
107
|
-
value.suppressedThroughTokens % interval !== 0
|
|
108
|
-
)
|
|
109
|
-
return undefined;
|
|
110
|
-
return {
|
|
111
|
-
v: 3,
|
|
112
|
-
kind: "automatic",
|
|
113
|
-
tokens: value.tokens,
|
|
114
|
-
boundaryTokens: value.boundaryTokens,
|
|
115
|
-
reminder: value.reminder,
|
|
116
|
-
tier: value.tier,
|
|
117
|
-
tierCount: value.tierCount,
|
|
118
|
-
tierFloor: value.tierFloor,
|
|
119
|
-
anchorToolCallId: value.anchorToolCallId,
|
|
120
|
-
suppressedThroughTokens: value.suppressedThroughTokens,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function renderContextPruningNudge(details: unknown, theme: Theme): Marker | undefined {
|
|
125
|
-
const parsed = parseContextPruningNudgeDetailsV3(details);
|
|
126
|
-
if (!parsed) return undefined;
|
|
127
|
-
return new Marker({
|
|
128
|
-
theme,
|
|
129
|
-
state: "muted",
|
|
130
|
-
label: "Context:",
|
|
131
|
-
parts:
|
|
132
|
-
parsed.kind === "manual"
|
|
133
|
-
? ["Prune requested."]
|
|
134
|
-
: [
|
|
135
|
-
formatTokens(parsed.tokens),
|
|
136
|
-
...(parsed.tier === parsed.tierCount ? ["Prune now."] : parsed.tier > 1 ? ["Prune soon."] : []),
|
|
137
|
-
],
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function renderContextPruneCall(
|
|
142
|
-
args: ContextPruneInput,
|
|
143
|
-
theme: Theme,
|
|
144
|
-
context: {
|
|
145
|
-
rowState: ToolRowStateStore;
|
|
146
|
-
rowId: string;
|
|
147
|
-
invalidate: () => void;
|
|
148
|
-
lastComponent: unknown;
|
|
149
|
-
},
|
|
150
|
-
): Text {
|
|
151
|
-
context.rowState.watch(context.rowId, context.invalidate);
|
|
152
|
-
const component = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
153
|
-
const count =
|
|
154
|
-
(Array.isArray(args.keepFiles) ? args.keepFiles.length : 0) +
|
|
155
|
-
(Array.isArray(args.keepToolCalls) ? args.keepToolCalls.length : 0) +
|
|
156
|
-
(Array.isArray(args.deferFiles) ? args.deferFiles.length : 0);
|
|
157
|
-
component.setText(
|
|
158
|
-
`${formatToolRowTitle(context.rowState, context.rowId, "context_prune", theme)} ${theme.fg("muted", `${count} selection${count === 1 ? "" : "s"}`)}`,
|
|
159
|
-
);
|
|
160
|
-
return component;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function renderContextPruneResult(
|
|
164
|
-
result: AgentToolResult<unknown>,
|
|
165
|
-
expanded: boolean,
|
|
166
|
-
theme: Theme,
|
|
167
|
-
lastComponent: unknown,
|
|
168
|
-
): Text {
|
|
169
|
-
const component = lastComponent instanceof Text ? lastComponent : new Text("", 0, 0);
|
|
170
|
-
const details = parseContextPruneDetailsV2(result.details);
|
|
171
|
-
if (!details) {
|
|
172
|
-
component.setText(
|
|
173
|
-
theme.fg(
|
|
174
|
-
"warning",
|
|
175
|
-
boundedText(firstResultText(result) || "context_prune returned invalid details", MAX_WARNING_CHARACTERS),
|
|
176
|
-
),
|
|
177
|
-
);
|
|
178
|
-
return component;
|
|
179
|
-
}
|
|
180
|
-
const pruned = details.prunedToolCallIds.length + details.prunedAutoreadRowIds.length;
|
|
181
|
-
const retained = details.retainedToolCallIds.length + details.retainedAutoreadRowIds.length;
|
|
182
|
-
let text = theme.fg(
|
|
183
|
-
details.warnings.length === 0 ? "success" : "warning",
|
|
184
|
-
`Checkpoint · pruned ${pruned} · retained ${retained} · refreshed ${details.refreshedFiles.length} · deferred ${details.deferredFiles.length}${details.warnings.length === 0 ? "" : ` · warnings ${details.warnings.length}`}`,
|
|
185
|
-
);
|
|
186
|
-
if (expanded) {
|
|
187
|
-
const lines = [
|
|
188
|
-
...details.prunedToolCallIds.map((id) => `pruned tool: ${id}`),
|
|
189
|
-
...details.prunedAutoreadRowIds.map((id) => `pruned autoread: ${id}`),
|
|
190
|
-
...details.retainedToolCallIds.map((id) => `retained tool: ${id}`),
|
|
191
|
-
...details.retainedAutoreadRowIds.map((id) => `retained autoread: ${id}`),
|
|
192
|
-
...details.refreshedFiles.map((file) => `refreshed: ${file.path} (${file.rowId})`),
|
|
193
|
-
...details.deferredFiles.map((file) => `deferred: ${file.path} — ${file.reason}; when ${file.relevantWhen}`),
|
|
194
|
-
...details.warnings.map((warning) => `warning: ${warning}`),
|
|
195
|
-
];
|
|
196
|
-
const shown = lines.slice(0, MAX_EXPANDED_ITEMS).map((line) => boundedText(line, MAX_EXPANDED_LINE_CHARACTERS));
|
|
197
|
-
if (lines.length > shown.length) shown.push(`… ${lines.length - shown.length} more`);
|
|
198
|
-
let remainingWidth = Math.max(0, MAX_EXPANDED_TEXT_CHARACTERS - visibleWidth(text));
|
|
199
|
-
for (const line of shown) {
|
|
200
|
-
if (remainingWidth <= 1) break;
|
|
201
|
-
const styled = theme.fg("dim", line);
|
|
202
|
-
const availableWidth = remainingWidth - 1;
|
|
203
|
-
text += `\n${truncateToWidth(styled, availableWidth, "…")}`;
|
|
204
|
-
remainingWidth -= 1 + Math.min(visibleWidth(styled), availableWidth);
|
|
205
|
-
if (visibleWidth(styled) > availableWidth) break;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
component.setText(text);
|
|
209
|
-
return component;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function boundedText(text: string, maximumCharacters: number): string {
|
|
213
|
-
if (text.length <= maximumCharacters) return text;
|
|
214
|
-
return `${text.slice(0, Math.max(0, maximumCharacters - 1))}…`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function firstResultText(result: AgentToolResult<unknown>): string {
|
|
218
|
-
for (const part of result.content) if (part.type === "text") return part.text;
|
|
219
|
-
return "";
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function formatTokens(count: number): string {
|
|
223
|
-
if (count < 1_000) return `${count}`;
|
|
224
|
-
if (count < 10_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
225
|
-
if (count < 1_000_000) return `${Math.round(count / 1_000)}k`;
|
|
226
|
-
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function isTokenCount(value: unknown): value is number {
|
|
230
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function isBoundary(value: unknown): value is number {
|
|
234
|
-
return isTokenCount(value) && value > 0;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function isReminder(value: unknown): value is number {
|
|
238
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function isTier(value: unknown): value is number {
|
|
242
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 5;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function isTierFloor(value: unknown): value is number {
|
|
246
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 5;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
250
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
251
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Type } from "typebox";
|
|
2
|
-
import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
|
|
3
|
-
|
|
4
|
-
const DEFAULT_NUDGE_INSTRUCTIONS: [string, ...string[]] = [
|
|
5
|
-
"No prune is required yet unless broad exploration has converged or substantial evidence is already irrelevant. Continue coherent work.",
|
|
6
|
-
"Move toward a pruning point now. Finish the current coherent step, then prune before starting another broad exploration. Managed context is materially increasing model cost.",
|
|
7
|
-
"Prune now before further tool work. Continuing with stale managed context is wasting money.",
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
export default defineTauExtensionSettings({
|
|
11
|
-
key: "contextPruning",
|
|
12
|
-
defaults: {
|
|
13
|
-
enabled: true as boolean,
|
|
14
|
-
nudgeEveryTokens: 30_000 as number,
|
|
15
|
-
nudgeInstructions: DEFAULT_NUDGE_INSTRUCTIONS,
|
|
16
|
-
},
|
|
17
|
-
schema: Type.Object(
|
|
18
|
-
{
|
|
19
|
-
enabled: Type.Optional(Type.Boolean({ default: true, description: "Enable context pruning." })),
|
|
20
|
-
nudgeEveryTokens: Type.Optional(
|
|
21
|
-
Type.Integer({
|
|
22
|
-
default: 30_000,
|
|
23
|
-
minimum: 1,
|
|
24
|
-
description: "Active-context token interval between automatic pruning hints.",
|
|
25
|
-
}),
|
|
26
|
-
),
|
|
27
|
-
nudgeInstructions: Type.Optional(
|
|
28
|
-
Type.Array(Type.String({ minLength: 1, maxLength: 2_000 }), {
|
|
29
|
-
default: DEFAULT_NUDGE_INSTRUCTIONS,
|
|
30
|
-
minItems: 1,
|
|
31
|
-
maxItems: 5,
|
|
32
|
-
description:
|
|
33
|
-
"Ordered automatic pruning instructions. Later reminders repeat the final instruction, and the final tier requires an anchor before further tool work.",
|
|
34
|
-
}),
|
|
35
|
-
),
|
|
36
|
-
},
|
|
37
|
-
{ additionalProperties: false },
|
|
38
|
-
),
|
|
39
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: review
|
|
3
|
-
description: Perform an adversarial, read-only review for correctness, runtime risks, duplication, and over- or under-engineering
|
|
4
|
-
tools:
|
|
5
|
-
- read
|
|
6
|
-
- bash
|
|
7
|
-
- outline
|
|
8
|
-
- show
|
|
9
|
-
- discover
|
|
10
|
-
- ast_search
|
|
11
|
-
- deps
|
|
12
|
-
- reverse_deps
|
|
13
|
-
- callers
|
|
14
|
-
- callees
|
|
15
|
-
- references
|
|
16
|
-
- implementations
|
|
17
|
-
- impact
|
|
18
|
-
- context
|
|
19
|
-
names:
|
|
20
|
-
- Auditor
|
|
21
|
-
- Inspector
|
|
22
|
-
- Skeptic
|
|
23
|
-
- Examiner
|
|
24
|
-
- Sentinel
|
|
25
|
-
model: openai-codex/gpt-5.6-sol
|
|
26
|
-
thinking: high
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
Stay inside delegated change. Answer two questions:
|
|
30
|
-
|
|
31
|
-
1. Is runtime behavior correct?
|
|
32
|
-
2. Is this the simplest implementation of requested behavior?
|
|
33
|
-
|
|
34
|
-
Find concrete failures or needless complexity. Report. Stop. No mutations, unrelated review, or broad concern inventory.
|
|
35
|
-
|
|
36
|
-
## Evidence ladder
|
|
37
|
-
|
|
38
|
-
Use cheapest evidence that settles each question. Escalate only when answer could change verdict.
|
|
39
|
-
|
|
40
|
-
1. **Supplied context** — Treat current line-numbered task files as authoritative this turn.
|
|
41
|
-
2. **Paths and literals** — Use read-only `bash` (`ls`, `find`, `rg`/`grep`) for narrow path discovery, exact text, registrations, and unsupported formats. Use ranged `read` when formatting or source context matters.
|
|
42
|
-
3. **Structure and reuse** — Default to `outline`. Use `discover` only when changed code may duplicate an existing repository API but name or path is unknown. Use `ast_search` for a concrete risky or duplicated source shape.
|
|
43
|
-
4. **Exact declarations** — Use `show` with path + name (+ line when needed). Retrieve only contract, body, imports, or nearby lines needed for verdict.
|
|
44
|
-
5. **Runtime relationships** — Use `callers`, `callees`, `references`, or `implementations` after selecting a declaration. Use `deps`/`reverse_deps` for file imports. Use `impact` for full blast radius and `context` for one bounded declaration pack.
|
|
45
|
-
|
|
46
|
-
Keep roots and result limits narrow. Structural evidence proves bounded syntax, not dynamic dispatch. Preserve inferred and ambiguous labels.
|
|
47
|
-
|
|
48
|
-
## Review procedure
|
|
49
|
-
|
|
50
|
-
1. Extract requested behavior, changed scope, and repository constraints from task and supplied files.
|
|
51
|
-
2. Inspect changed declarations. Form only actionable runtime and simplicity questions.
|
|
52
|
-
3. Runtime: follow shortest relevant path through callers, state transitions, boundaries, and error handling. Check realistic sequences, not theoretical branch inventory.
|
|
53
|
-
4. Simplicity: identify wrappers, helpers, option bags, duplicated logic, or staged abstractions that can disappear without changing requested behavior. Report only material reductions in concepts or ownership.
|
|
54
|
-
5. Inspect surrounding code only to confirm suspected failure, contract mismatch, missed caller, or existing simpler API.
|
|
55
|
-
6. Stop when both questions have evidence-backed answers.
|
|
56
|
-
|
|
57
|
-
Treat implementation as untrusted. Reject speculation, personal style preferences, and complexity complaints without concrete cost. Do not modify files.
|
|
58
|
-
|
|
59
|
-
## Output
|
|
60
|
-
|
|
61
|
-
List findings first, ordered by severity. Each finding needs:
|
|
62
|
-
|
|
63
|
-
- severity and direct title;
|
|
64
|
-
- exact file, line range, and declaration when one exists;
|
|
65
|
-
- runtime failure mechanism or concrete complexity cost;
|
|
66
|
-
- smallest credible fix direction.
|
|
67
|
-
|
|
68
|
-
Then list only unresolved questions that materially affect runtime correctness. No findings: say runtime appears correct and implementation is already simplest credible version. Briefly name inspected scope. No preamble, search log, broad summary, or repeated evidence.
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Turn Budget
|
|
2
|
-
|
|
3
|
-
Adds soft turn budget hints for Tau sessions.
|
|
4
|
-
|
|
5
|
-
Turn Budget helps agents spend fewer provider cycles by nudging them to batch related tool work. It counts
|
|
6
|
-
tool-using turns per user prompt, sends visible steering messages at configured intervals, and extends the soft cap
|
|
7
|
-
instead of blocking work.
|
|
8
|
-
|
|
9
|
-
## Settings
|
|
10
|
-
|
|
11
|
-
- `enabled`: defaults to `true`
|
|
12
|
-
- `turnLimit`: defaults to `30`
|
|
13
|
-
- `nudgeEveryTurns`: defaults to `5`
|
|
14
|
-
- `softCapIncrement`: defaults to `10`
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
3
|
-
import { Marker } from "@shanepadgett/tau-tui";
|
|
4
|
-
import turnBudgetSettings from "./settings.ts";
|
|
5
|
-
|
|
6
|
-
const MARKER_TYPE = "tau.turn-budget.marker";
|
|
7
|
-
|
|
8
|
-
interface Settings {
|
|
9
|
-
enabled: boolean;
|
|
10
|
-
turnLimit: number;
|
|
11
|
-
nudgeEveryTurns: number;
|
|
12
|
-
softCapIncrement: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type Hint =
|
|
16
|
-
| { kind: "normal"; used: number; cap: number }
|
|
17
|
-
| { kind: "extended"; used: number; previousCap: number; newCap: number };
|
|
18
|
-
|
|
19
|
-
interface MarkerDetails {
|
|
20
|
-
used: number;
|
|
21
|
-
cap: number;
|
|
22
|
-
extended: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export default function turnBudgetExtension(pi: ExtensionAPI): void {
|
|
26
|
-
let settings = normalizeSettings(turnBudgetSettings.defaults);
|
|
27
|
-
let turnCount = 0;
|
|
28
|
-
let softCap = settings.turnLimit;
|
|
29
|
-
|
|
30
|
-
pi.registerMessageRenderer<MarkerDetails>(MARKER_TYPE, (message, _options, theme) => {
|
|
31
|
-
const details = readMarkerDetails(message.details);
|
|
32
|
-
if (!details) return undefined;
|
|
33
|
-
return new Marker({
|
|
34
|
-
theme,
|
|
35
|
-
state: "muted",
|
|
36
|
-
label: "Turn Budget:",
|
|
37
|
-
parts: [`${details.used}/${details.cap}`, ...(details.extended ? ["Soft cap extended."] : [])],
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
42
|
-
settings = normalizeSettings(await loadTauExtensionSettings(ctx, turnBudgetSettings));
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
pi.on("agent_start", () => {
|
|
46
|
-
turnCount = 0;
|
|
47
|
-
softCap = settings.turnLimit;
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
pi.on("turn_end", (event) => {
|
|
51
|
-
if (!settings.enabled) return undefined;
|
|
52
|
-
if (event.toolResults.length === 0) return undefined;
|
|
53
|
-
turnCount += 1;
|
|
54
|
-
if (turnCount >= softCap) {
|
|
55
|
-
const hint: Hint = {
|
|
56
|
-
kind: "extended",
|
|
57
|
-
used: turnCount,
|
|
58
|
-
previousCap: softCap,
|
|
59
|
-
newCap: turnCount + settings.softCapIncrement,
|
|
60
|
-
};
|
|
61
|
-
sendTurnBudgetMessage(pi, hint);
|
|
62
|
-
softCap = hint.newCap;
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
if (turnCount % settings.nudgeEveryTurns === 0) {
|
|
66
|
-
const hint: Hint = { kind: "normal", used: turnCount, cap: softCap };
|
|
67
|
-
sendTurnBudgetMessage(pi, hint);
|
|
68
|
-
}
|
|
69
|
-
return undefined;
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function sendTurnBudgetMessage(pi: ExtensionAPI, hint: Hint): void {
|
|
74
|
-
pi.sendMessage<MarkerDetails>({
|
|
75
|
-
customType: MARKER_TYPE,
|
|
76
|
-
content: formatSteeringMessage(hint),
|
|
77
|
-
display: true,
|
|
78
|
-
details: markerDetails(hint),
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function formatSteeringMessage(hint: Hint): string {
|
|
83
|
-
const instruction =
|
|
84
|
-
"Internal steering instruction. Work within it silently. Do not mention or acknowledge turn counts, budget messages, or budget summaries.";
|
|
85
|
-
if (hint.kind === "extended") {
|
|
86
|
-
return `${instruction} Turn budget: ${hint.used}/${hint.previousCap} turns used for this user prompt. Soft cap extended to ${hint.newCap}. Batch tools when more tool work remains.`;
|
|
87
|
-
}
|
|
88
|
-
return `${instruction} Turn budget: ${hint.used}/${hint.cap} turns used for this user prompt. Batch tools when more tool work remains.`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function normalizeSettings(value: typeof turnBudgetSettings.defaults): Settings {
|
|
92
|
-
return {
|
|
93
|
-
enabled: value.enabled ?? true,
|
|
94
|
-
turnLimit: Number.isInteger(value.turnLimit) && value.turnLimit > 0 ? value.turnLimit : 30,
|
|
95
|
-
nudgeEveryTurns: Number.isInteger(value.nudgeEveryTurns) && value.nudgeEveryTurns > 0 ? value.nudgeEveryTurns : 5,
|
|
96
|
-
softCapIncrement:
|
|
97
|
-
Number.isInteger(value.softCapIncrement) && value.softCapIncrement > 0 ? value.softCapIncrement : 10,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function markerDetails(hint: Hint): MarkerDetails {
|
|
102
|
-
return hint.kind === "extended"
|
|
103
|
-
? { used: hint.used, cap: hint.newCap, extended: true }
|
|
104
|
-
: { used: hint.used, cap: hint.cap, extended: false };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function readMarkerDetails(value: unknown): MarkerDetails | undefined {
|
|
108
|
-
if (!value || typeof value !== "object") return undefined;
|
|
109
|
-
const record = value as Record<string, unknown>;
|
|
110
|
-
const used = record.used;
|
|
111
|
-
const cap = record.cap;
|
|
112
|
-
if (typeof used !== "number" || !Number.isInteger(used) || used < 0) return undefined;
|
|
113
|
-
if (typeof cap !== "number" || !Number.isInteger(cap) || cap < 1) return undefined;
|
|
114
|
-
if (typeof record.extended !== "boolean") return undefined;
|
|
115
|
-
return { used, cap, extended: record.extended };
|
|
116
|
-
}
|