@shanepadgett/tau-agent 0.29.0 → 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.
Files changed (44) hide show
  1. package/docs/context.md +14 -9
  2. package/docs/subagents.md +7 -5
  3. package/extensions/cache-diagnostics/index.ts +3 -4
  4. package/extensions/context/README.md +8 -5
  5. package/extensions/context/definitions.ts +28 -17
  6. package/extensions/context/evidence.ts +4 -3
  7. package/extensions/context/index.ts +57 -35
  8. package/extensions/context/panel.ts +10 -9
  9. package/extensions/context/projection.ts +141 -0
  10. package/extensions/context/state.ts +30 -0
  11. package/extensions/explore/ast/read/hook.ts +3 -1
  12. package/extensions/ideas/browser.ts +27 -16
  13. package/extensions/review/README.md +11 -0
  14. package/extensions/review/index.ts +135 -0
  15. package/extensions/review/model.ts +144 -0
  16. package/extensions/review/panel.ts +128 -0
  17. package/extensions/review/session.ts +106 -0
  18. package/extensions/script-runner/README.md +7 -0
  19. package/extensions/script-runner/index.ts +275 -0
  20. package/extensions/stash/browser.ts +30 -18
  21. package/extensions/subagent/README.md +17 -4
  22. package/extensions/subagent/agents/context-sync.md +2 -2
  23. package/extensions/subagent/agents/scout.md +48 -61
  24. package/extensions/subagent/agents.ts +0 -1
  25. package/extensions/subagent/cmux-dashboard.ts +7 -3
  26. package/extensions/subagent/index.ts +105 -4
  27. package/extensions/subagent/panel.ts +124 -0
  28. package/extensions/subagent/render.ts +2 -1
  29. package/extensions/subagent/run.ts +9 -9
  30. package/extensions/subagent/runtime.ts +5 -5
  31. package/extensions/subagent/session-resource.ts +19 -127
  32. package/extensions/subagent/settings.ts +18 -0
  33. package/extensions/tau-help/help.md +10 -6
  34. package/extensions/working-memory/index.ts +21 -3
  35. package/extensions/working-memory/memory.ts +23 -5
  36. package/package.json +2 -2
  37. package/schemas/tau.schema.json +9 -23
  38. package/shared/context-messages.ts +19 -0
  39. package/shared/injected-context.ts +2 -2
  40. package/shared/isolated-session.ts +151 -0
  41. package/extensions/subagent/agents/review.md +0 -75
  42. package/extensions/turn-budget/README.md +0 -14
  43. package/extensions/turn-budget/index.ts +0 -116
  44. package/extensions/turn-budget/settings.ts +0 -35
@@ -174,14 +174,32 @@ export default function workingMemoryExtension(pi: ExtensionAPI): void {
174
174
  syncBranch(ctx);
175
175
  });
176
176
 
177
- pi.on("before_agent_start", (event, ctx) => {
177
+ pi.on("before_agent_start", (_event, ctx) => {
178
178
  if (!enabled) return undefined;
179
179
  const usage = ctx.getContextUsage();
180
180
  if (!usage || usage.tokens === null || !Number.isFinite(usage.tokens)) return undefined;
181
- const reminder = Math.floor(Math.max(0, usage.tokens) / interval);
181
+ const tokens = Math.max(0, Math.floor(usage.tokens));
182
+ const reminder = Math.floor(tokens / interval);
182
183
  if (reminder < 1) return undefined;
183
184
  const instruction = instructions[Math.min(reminder, instructions.length) - 1] ?? instructions[0];
184
- return { systemPrompt: `${event.systemPrompt}\n\n${automaticInstruction(instruction)}` };
185
+ return {
186
+ message: {
187
+ customType: NUDGE_TYPE,
188
+ content: automaticInstruction(instruction),
189
+ display: false,
190
+ details: {
191
+ v: 1,
192
+ kind: "automatic",
193
+ tokens,
194
+ boundaryTokens: reminder * interval,
195
+ reminder,
196
+ tier: Math.min(reminder, instructions.length),
197
+ tierCount: instructions.length,
198
+ anchorToolCallId:
199
+ replayWorkingMemoryState(ctx.sessionManager.getBranch(), true).latestAnchorToolCallId ?? null,
200
+ },
201
+ },
202
+ };
185
203
  });
186
204
 
187
205
  pi.on("turn_end", (event, ctx) => {
@@ -1,4 +1,5 @@
1
1
  import { sessionEntryToContextMessages, type ContextEvent, type SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import { isContextProjectionMessage, isLegacyContextMessage } from "../../shared/context-messages.ts";
2
3
  import type { ActiveWorkingMemoryState } from "./state.ts";
3
4
 
4
5
  type ContextMessage = ContextEvent["messages"][number];
@@ -67,6 +68,14 @@ export function buildMemoryCatalog(branch: readonly SessionEntry[]): Map<string,
67
68
  }
68
69
  if (message.role === "toolResult") continue;
69
70
  if (message.role === "custom" && EXCLUDED_CUSTOM_TYPES.has(message.customType)) continue;
71
+ if (
72
+ message.role === "custom" &&
73
+ message.customType === "tau.injected-context" &&
74
+ message.details &&
75
+ typeof message.details === "object" &&
76
+ (message.details as Record<string, unknown>).source === "context"
77
+ )
78
+ continue;
70
79
  const ref = `m:${entry.id}`;
71
80
  catalog.set(ref, {
72
81
  ref,
@@ -121,20 +130,29 @@ function referenceCurrentMessages(
121
130
  entries: readonly SessionEntry[],
122
131
  catalog: ReadonlyMap<string, MemoryUnit>,
123
132
  ): MemoryUnit[][] {
124
- const generated = entries.flatMap((entry) =>
125
- sessionEntryToContextMessages(entry).map((message) => ({ entry, message })),
133
+ const generated = entries
134
+ .flatMap((entry) => sessionEntryToContextMessages(entry).map((message) => ({ entry, message })))
135
+ .filter(({ message }) => !isLegacyContextMessage(message));
136
+ const sessionMessages = messages.filter(
137
+ (message) => !isContextProjectionMessage(message) && !isLegacyContextMessage(message),
126
138
  );
127
- if (generated.length !== messages.length) return messages.map(() => []);
139
+ if (generated.length !== sessionMessages.length) return messages.map(() => []);
128
140
  const toolRefs = new Map<string, MemoryUnit[]>();
129
141
  for (const unit of catalog.values()) {
130
142
  const result = unit.messages.find((message) => message.role === "toolResult");
131
143
  if (result?.role !== "toolResult") continue;
132
144
  toolRefs.set(result.toolCallId, [...(toolRefs.get(result.toolCallId) ?? []), unit]);
133
145
  }
134
- return generated.map(({ entry, message }, index) => {
146
+ let generatedIndex = 0;
147
+ return messages.map((currentMessage) => {
148
+ if (isContextProjectionMessage(currentMessage) || isLegacyContextMessage(currentMessage)) return [];
149
+ const current = generated[generatedIndex];
150
+ generatedIndex += 1;
151
+ if (!current) return [];
152
+ const { entry, message } = current;
135
153
  if (message.role === "assistant") {
136
154
  const unit = catalog.get(`m:${entry.id}`);
137
- return unit && messages[index]?.role === "assistant" ? [unit] : [];
155
+ return unit && currentMessage.role === "assistant" ? [unit] : [];
138
156
  }
139
157
  if (message.role === "toolResult") return toolRefs.get(message.toolCallId) ?? [];
140
158
  const unit = catalog.get(`m:${entry.id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Tau is a custom agentic harness built with pi extensions",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@ast-grep/wasm": "0.45.0",
38
- "@shanepadgett/tau-tui": "0.29.0",
38
+ "@shanepadgett/tau-tui": "0.30.0",
39
39
  "@vscode/tree-sitter-wasm": "0.3.1",
40
40
  "image-size": "2.0.2",
41
41
  "smol-toml": "1.7.0",
@@ -248,31 +248,17 @@
248
248
  },
249
249
  "additionalProperties": false
250
250
  },
251
- "turnBudget": {
251
+ "subagent": {
252
252
  "type": "object",
253
253
  "properties": {
254
- "enabled": {
255
- "type": "boolean",
256
- "default": true,
257
- "description": "Enable turn-budget hints."
258
- },
259
- "turnLimit": {
260
- "type": "integer",
261
- "default": 30,
262
- "minimum": 1,
263
- "description": "Initial soft cap for tool-using turns per user prompt."
264
- },
265
- "nudgeEveryTurns": {
266
- "type": "integer",
267
- "default": 5,
268
- "minimum": 1,
269
- "description": "Tool-using turn interval between turn-budget hints."
270
- },
271
- "softCapIncrement": {
272
- "type": "integer",
273
- "default": 10,
274
- "minimum": 1,
275
- "description": "Turns added when the soft cap is reached."
254
+ "disabled": {
255
+ "type": "array",
256
+ "items": {
257
+ "type": "string",
258
+ "minLength": 1
259
+ },
260
+ "default": [],
261
+ "description": "Agent names unavailable for delegation."
276
262
  }
277
263
  },
278
264
  "additionalProperties": false
@@ -0,0 +1,19 @@
1
+ import type { ContextEvent } from "@earendil-works/pi-coding-agent";
2
+
3
+ type ContextMessage = ContextEvent["messages"][number];
4
+
5
+ export const CONTEXT_PROJECTION_TYPE = "tau.context.projection";
6
+
7
+ export function isContextProjectionMessage(message: ContextMessage): boolean {
8
+ return message.role === "custom" && message.customType === CONTEXT_PROJECTION_TYPE;
9
+ }
10
+
11
+ export function isLegacyContextMessage(message: ContextMessage): boolean {
12
+ if (message.role !== "custom") return false;
13
+ if (message.customType !== "tau.injected-context" && message.customType !== "tau.autoread") return false;
14
+ return (
15
+ message.details !== undefined &&
16
+ typeof message.details === "object" &&
17
+ (message.details as Record<string, unknown>).source === "context"
18
+ );
19
+ }
@@ -3,7 +3,7 @@ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
3
3
  export const INJECTED_CONTEXT_TYPE = "tau.injected-context";
4
4
 
5
5
  export interface InjectedContextDetails {
6
- source: "review" | "context";
6
+ source: "review";
7
7
  title?: string;
8
8
  }
9
9
 
@@ -61,7 +61,7 @@ export function getBranchInjectedContexts(entries: readonly SessionEntry[]): Inj
61
61
  function readDetails(value: unknown): InjectedContextDetails | undefined {
62
62
  if (!value || typeof value !== "object") return undefined;
63
63
  const record = value as Record<string, unknown>;
64
- if (record.source !== "review" && record.source !== "context") return undefined;
64
+ if (record.source !== "review") return undefined;
65
65
  return {
66
66
  source: record.source,
67
67
  ...(typeof record.title === "string" ? { title: record.title } : {}),
@@ -0,0 +1,151 @@
1
+ import {
2
+ createAgentSession,
3
+ DefaultResourceLoader,
4
+ getAgentDir,
5
+ ModelRuntime,
6
+ SessionManager,
7
+ type AgentSession,
8
+ type ExtensionContext,
9
+ type ToolDefinition,
10
+ } from "@earendil-works/pi-coding-agent";
11
+
12
+ type IsolatedSessionThinkingLevel = NonNullable<ExtensionContext["thinkingLevel"]>;
13
+
14
+ type SelectedModel = NonNullable<ExtensionContext["model"]>;
15
+ type SelectedProvider = NonNullable<ReturnType<ExtensionContext["modelRegistry"]["getProvider"]>>;
16
+
17
+ export interface IsolatedSessionInputs {
18
+ label: string;
19
+ extensionPaths: readonly string[];
20
+ cwd: string;
21
+ model: SelectedModel;
22
+ provider: SelectedProvider;
23
+ runtimeApiKey: string | undefined;
24
+ thinkingLevel: IsolatedSessionThinkingLevel;
25
+ tools: readonly string[];
26
+ customTools: readonly ToolDefinition[];
27
+ bindTarget: { mode: "print" } | { mode: "tui"; uiContext: ExtensionContext["ui"] };
28
+ }
29
+
30
+ export interface IsolatedSessionResource {
31
+ readonly session: AgentSession;
32
+ dispose(): Promise<void>;
33
+ }
34
+
35
+ export async function resolveIsolatedSessionModel(options: {
36
+ label: string;
37
+ preferredModel: string | undefined;
38
+ preferredThinkingLevel: IsolatedSessionThinkingLevel | undefined;
39
+ usePreferredThinkingAfterModelFallback: boolean;
40
+ ctx: ExtensionContext;
41
+ parentThinkingLevel: IsolatedSessionThinkingLevel;
42
+ signal: AbortSignal;
43
+ onWarning?: (warning: string) => void;
44
+ }): Promise<Pick<IsolatedSessionInputs, "model" | "provider" | "runtimeApiKey" | "thinkingLevel">> {
45
+ const { ctx, signal, onWarning } = options;
46
+ let model = ctx.model;
47
+ let thinkingLevel = options.parentThinkingLevel;
48
+ let preferredModelSelected = false;
49
+ let selectedAuth: Awaited<ReturnType<ExtensionContext["modelRegistry"]["getApiKeyAndHeaders"]>> | undefined;
50
+ if (options.preferredModel) {
51
+ const separator = options.preferredModel.indexOf("/");
52
+ const configured = ctx.modelRegistry.find(
53
+ options.preferredModel.slice(0, separator),
54
+ options.preferredModel.slice(separator + 1),
55
+ );
56
+ if (!configured) onWarning?.(`model ${options.preferredModel} is unavailable; using parent model`);
57
+ else {
58
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(configured);
59
+ if (!auth.ok) onWarning?.(`model ${options.preferredModel} is unavailable: ${auth.error}; using parent model`);
60
+ else {
61
+ model = configured;
62
+ selectedAuth = auth;
63
+ preferredModelSelected = true;
64
+ }
65
+ }
66
+ }
67
+ if (options.preferredThinkingLevel && (preferredModelSelected || options.usePreferredThinkingAfterModelFallback)) {
68
+ const preferred = options.preferredThinkingLevel;
69
+ const mapped = model?.thinkingLevelMap?.[preferred];
70
+ const unsupported =
71
+ !model?.reasoning ||
72
+ mapped === null ||
73
+ ((preferred === "xhigh" || preferred === "max") && mapped === undefined);
74
+ if (unsupported)
75
+ onWarning?.(`thinking ${preferred} is unavailable for the selected model; using parent thinking`);
76
+ else thinkingLevel = preferred;
77
+ }
78
+ if (!model) throw new Error(`${options.label} startup failed: parent has no model`);
79
+ const auth = selectedAuth ?? (await ctx.modelRegistry.getApiKeyAndHeaders(model));
80
+ if (!auth.ok) throw new Error(`${options.label} startup failed: ${auth.error}`);
81
+ const provider = ctx.modelRegistry.getProvider(model.provider);
82
+ if (!provider) throw new Error(`${options.label} startup failed: provider ${model.provider} is unavailable`);
83
+ if (signal.aborted) throw new Error(`${options.label} startup aborted`);
84
+ return {
85
+ model,
86
+ provider,
87
+ runtimeApiKey:
88
+ auth.apiKey && provider.auth.apiKey && !ctx.modelRegistry.isUsingOAuth(model) ? auth.apiKey : undefined,
89
+ thinkingLevel,
90
+ };
91
+ }
92
+
93
+ export async function createIsolatedSessionResource(
94
+ inputs: IsolatedSessionInputs,
95
+ signal: AbortSignal,
96
+ ): Promise<IsolatedSessionResource> {
97
+ let session: AgentSession | undefined;
98
+ try {
99
+ if (signal.aborted) throw new Error(`${inputs.label} startup aborted`);
100
+ const modelRuntime = await ModelRuntime.create();
101
+ modelRuntime.registerNativeProvider(inputs.provider);
102
+ if (inputs.runtimeApiKey !== undefined)
103
+ await modelRuntime.setRuntimeApiKey(inputs.model.provider, inputs.runtimeApiKey, { allowNetwork: false });
104
+ if (signal.aborted) throw new Error(`${inputs.label} startup aborted`);
105
+ const resourceLoader = new DefaultResourceLoader({
106
+ cwd: inputs.cwd,
107
+ agentDir: getAgentDir(),
108
+ noExtensions: true,
109
+ additionalExtensionPaths: [...inputs.extensionPaths],
110
+ });
111
+ await resourceLoader.reload();
112
+ if (signal.aborted) throw new Error(`${inputs.label} startup aborted`);
113
+ const created = await createAgentSession({
114
+ cwd: inputs.cwd,
115
+ model: inputs.model,
116
+ modelRuntime,
117
+ thinkingLevel: inputs.thinkingLevel,
118
+ tools: [...inputs.tools],
119
+ excludeTools: ["subagent"],
120
+ resourceLoader,
121
+ customTools: [...inputs.customTools],
122
+ sessionManager: SessionManager.inMemory(inputs.cwd),
123
+ });
124
+ session = created.session;
125
+ if (signal.aborted) throw new Error(`${inputs.label} startup aborted`);
126
+ await session.bindExtensions(inputs.bindTarget);
127
+ if (signal.aborted) throw new Error(`${inputs.label} startup aborted`);
128
+ const active = session.getActiveToolNames().sort();
129
+ const expected = [...inputs.tools].sort();
130
+ if (active.join("\0") !== expected.join("\0") || active.includes("subagent")) {
131
+ const missing = expected.filter((tool) => !active.includes(tool));
132
+ throw new Error(
133
+ `${inputs.label} startup failed: unavailable tools: ${missing.join(", ") || "active tool mismatch"}`,
134
+ );
135
+ }
136
+ let disposed = false;
137
+ return {
138
+ session,
139
+ async dispose() {
140
+ if (disposed) return;
141
+ disposed = true;
142
+ if (session?.isStreaming) await session.abort().catch(() => undefined);
143
+ session?.dispose();
144
+ },
145
+ };
146
+ } catch (error) {
147
+ if (session?.isStreaming) await session.abort().catch(() => undefined);
148
+ session?.dispose();
149
+ throw error;
150
+ }
151
+ }
@@ -1,75 +0,0 @@
1
- ---
2
- name: review
3
- description: Perform a nuclear, architecture-first review for necessity, reuse, ownership, duplication, and simplification; runtime correctness is secondary
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 centered on delegated change, but inspect enough surrounding code to find correct ownership and existing reuse. Every review is a nuclear review of codebase health. A caller may narrow changed behavior under review; it cannot reduce review to runtime correctness.
30
-
31
- Answer in this order:
32
-
33
- 1. Should this code exist? Is every added behavior requested and necessary?
34
- 2. Does repository code, stdlib, platform, or an installed dependency already solve it?
35
- 3. Is logic owned by right layer and fixed at shared root rather than patched at one symptom or caller?
36
- 4. Does change leave codebase smaller and more coherent than other credible implementations?
37
- 5. Is runtime behavior correct?
38
-
39
- Find concrete architectural damage, missed simplifications, and failures. Report. Stop. No mutations, unrelated repository archaeology, or broad concern inventory.
40
-
41
- ## Evidence ladder
42
-
43
- Use cheapest evidence that settles each tier. Escalate only when answer could change verdict.
44
-
45
- 1. **Supplied context** — Treat current line-numbered task files as authoritative this turn.
46
- 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.
47
- 3. **Structure, ownership, and reuse** — Default to `outline`. Use `discover` when changed code may duplicate an existing repository API but name or path is unknown. Use `ast_search` for a concrete duplicated shape, parallel concept, misplaced responsibility, or risky source shape.
48
- 4. **Exact declarations** — Use `show` with path + name (+ line when needed). Retrieve only contract, body, imports, or nearby lines needed for verdict.
49
- 5. **Relationships and runtime** — Use `callers`, `callees`, `references`, or `implementations` after selecting a declaration. Use `deps`/`reverse_deps` for file ownership and imports. Use `impact` for full blast radius and `context` for one bounded declaration pack.
50
-
51
- Keep roots and result limits narrow. Structural evidence proves bounded syntax, not dynamic dispatch. Preserve inferred and ambiguous labels.
52
-
53
- ## Review procedure
54
-
55
- Run every tier in order, even when caller asks only for runtime review:
56
-
57
- 1. **Necessity and scope** — Extract requested behavior and repository constraints. Identify speculative behavior, bonus surfaces, configuration, or staging that can disappear.
58
- 2. **Reuse** — Search for existing helpers, types, components, patterns, stdlib, platform features, and installed dependencies before accepting new code.
59
- 3. **Ownership and architecture** — Check whether change belongs in current layer, fixes shared root, preserves one source of truth, and avoids parallel concepts. Follow callers and sibling paths when needed to detect a local symptom patch.
60
- 4. **Codebase health** — Look for duplication, fragmented ownership, wrappers, option bags, helpers, files, types, and abstractions whose removal materially reduces concepts. Consider a focused refactor when local patch deepens bad structure.
61
- 5. **Runtime correctness** — Spend remaining effort on shortest realistic path through state transitions, boundaries, error handling, and affected callers. Avoid theoretical branch inventory.
62
- 6. Stop when every tier has enough evidence for verdict.
63
-
64
- Treat every added concept as guilty until evidence justifies it. Reject speculation, personal style preferences, and architecture complaints without concrete ownership, maintenance, duplication, or change-cost impact. Do not modify files.
65
-
66
- ## Output
67
-
68
- List architectural findings first, then runtime findings. Order each group by severity. Each finding needs:
69
-
70
- - severity and direct title;
71
- - exact file, line range, and declaration when one exists;
72
- - failure mechanism or concrete architectural cost;
73
- - smallest credible fix direction.
74
-
75
- Then list only unresolved questions that materially affect verdict. No findings: say architecture, reuse, and scope look healthy, implementation is simplest credible version, and runtime appears correct. 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
- }
@@ -1,35 +0,0 @@
1
- import { Type } from "typebox";
2
- import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
3
-
4
- export default defineTauExtensionSettings({
5
- key: "turnBudget",
6
- defaults: {
7
- enabled: true as boolean,
8
- turnLimit: 30 as number,
9
- nudgeEveryTurns: 5 as number,
10
- softCapIncrement: 10 as number,
11
- },
12
- schema: Type.Object(
13
- {
14
- enabled: Type.Optional(Type.Boolean({ default: true, description: "Enable turn-budget hints." })),
15
- turnLimit: Type.Optional(
16
- Type.Integer({
17
- default: 30,
18
- minimum: 1,
19
- description: "Initial soft cap for tool-using turns per user prompt.",
20
- }),
21
- ),
22
- nudgeEveryTurns: Type.Optional(
23
- Type.Integer({
24
- default: 5,
25
- minimum: 1,
26
- description: "Tool-using turn interval between turn-budget hints.",
27
- }),
28
- ),
29
- softCapIncrement: Type.Optional(
30
- Type.Integer({ default: 10, minimum: 1, description: "Turns added when the soft cap is reached." }),
31
- ),
32
- },
33
- { additionalProperties: false },
34
- ),
35
- });