@shanepadgett/tau-agent 0.16.0 → 0.18.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 (55) hide show
  1. package/docs/subagents.md +25 -6
  2. package/extensions/attention/README.md +1 -0
  3. package/extensions/attention/index.ts +38 -0
  4. package/extensions/context/README.md +13 -3
  5. package/extensions/context/definitions.ts +3 -15
  6. package/extensions/context/evidence.ts +516 -0
  7. package/extensions/context/index.ts +114 -116
  8. package/extensions/context/panel.ts +60 -0
  9. package/extensions/context/settings.ts +30 -1
  10. package/extensions/context/sync.ts +171 -686
  11. package/extensions/context/validation.ts +4 -1
  12. package/extensions/context/write-scope.ts +109 -0
  13. package/extensions/context-pruning/README.md +22 -0
  14. package/extensions/context-pruning/file-evidence.ts +265 -0
  15. package/extensions/context-pruning/index.ts +379 -0
  16. package/extensions/context-pruning/projection.ts +108 -0
  17. package/extensions/context-pruning/prune.ts +346 -0
  18. package/extensions/context-pruning/render.ts +179 -0
  19. package/extensions/context-pruning/settings.ts +41 -0
  20. package/extensions/explore/README.md +3 -1
  21. package/extensions/explore/autoread.ts +85 -22
  22. package/extensions/explore/full-file-knowledge.ts +234 -0
  23. package/extensions/explore/index.ts +2 -3
  24. package/extensions/explore/read-cache.ts +150 -86
  25. package/extensions/explore/read-snapshots.ts +17 -4
  26. package/extensions/explore/read.ts +57 -38
  27. package/extensions/footer/index.ts +62 -60
  28. package/extensions/run-summary/index.ts +5 -5
  29. package/extensions/silent-command-runner/README.md +1 -1
  30. package/extensions/silent-command-runner/index.ts +45 -28
  31. package/extensions/soul/prompt.ts +3 -1
  32. package/extensions/subagent/README.md +21 -5
  33. package/extensions/subagent/agents/context-sync.md +97 -0
  34. package/extensions/subagent/agents/{generalist.md → dormant/generalist.md} +6 -0
  35. package/extensions/subagent/agents/{scout.md → dormant/scout.md} +6 -0
  36. package/extensions/subagent/agents/review.md +48 -0
  37. package/extensions/subagent/agents/web-research.md +6 -0
  38. package/extensions/subagent/agents.ts +15 -2
  39. package/extensions/subagent/cmux-dashboard.ts +454 -0
  40. package/extensions/subagent/index.ts +181 -234
  41. package/extensions/subagent/render.ts +1 -1
  42. package/extensions/subagent/resume.ts +78 -0
  43. package/extensions/subagent/run.ts +213 -118
  44. package/extensions/subagent/runtime.ts +856 -0
  45. package/extensions/subagent/session-resource.ts +169 -0
  46. package/extensions/tau-help/help.md +6 -2
  47. package/extensions/turn-budget/index.ts +8 -36
  48. package/package.json +2 -2
  49. package/schemas/tau.schema.json +48 -1
  50. package/shared/context-pruning-state.ts +364 -0
  51. package/shared/events.ts +18 -0
  52. package/shared/model-fallback/index.ts +21 -10
  53. package/shared/model-fallback/types.ts +5 -3
  54. package/shared/settings/load.ts +78 -1
  55. package/shared/tool-row-state.ts +21 -1
@@ -0,0 +1,169 @@
1
+ import {
2
+ createAgentSession,
3
+ DefaultResourceLoader,
4
+ getAgentDir,
5
+ ModelRuntime,
6
+ SessionManager,
7
+ type AgentSession,
8
+ type ExtensionContext,
9
+ } from "@earendil-works/pi-coding-agent";
10
+ import type { AgentDefinition, ThinkingLevel } from "./agents.ts";
11
+
12
+ const CHILD_UI_BLOCKED_METHODS = new Set([
13
+ "setEditorComponent",
14
+ "setFooter",
15
+ "setStatus",
16
+ "setTitle",
17
+ "setWidget",
18
+ "setWorkingIndicator",
19
+ ]);
20
+
21
+ type SelectedModel = NonNullable<ExtensionContext["model"]>;
22
+ type SelectedProvider = NonNullable<ReturnType<ExtensionContext["modelRegistry"]["getProvider"]>>;
23
+
24
+ export interface SubagentSessionInputs {
25
+ definition: AgentDefinition;
26
+ extensionPaths: readonly string[];
27
+ cwd: string;
28
+ model: SelectedModel;
29
+ modelName: string;
30
+ provider: SelectedProvider;
31
+ runtimeApiKey: string | undefined;
32
+ thinkingLevel: ThinkingLevel;
33
+ bindTarget: { mode: "print" } | { mode: "tui"; uiContext: ExtensionContext["ui"] };
34
+ }
35
+
36
+ export interface SubagentSessionResource {
37
+ readonly inputs: SubagentSessionInputs;
38
+ readonly session: AgentSession;
39
+ dispose(): Promise<void>;
40
+ }
41
+
42
+ function childUiContext(ui: ExtensionContext["ui"]): ExtensionContext["ui"] {
43
+ return new Proxy(ui, {
44
+ get(target, property, receiver) {
45
+ if (typeof property === "string" && CHILD_UI_BLOCKED_METHODS.has(property)) return () => undefined;
46
+ const value = Reflect.get(target, property, receiver) as unknown;
47
+ return typeof value === "function" ? value.bind(target) : value;
48
+ },
49
+ });
50
+ }
51
+
52
+ export async function resolveSubagentSessionInputs(options: {
53
+ definition: AgentDefinition;
54
+ extensionPaths: readonly string[];
55
+ ctx: ExtensionContext;
56
+ parentThinkingLevel: string;
57
+ signal: AbortSignal;
58
+ onWarning?: (warning: string) => void;
59
+ }): Promise<SubagentSessionInputs> {
60
+ const { definition, extensionPaths, ctx, signal, onWarning } = options;
61
+ let model = ctx.model;
62
+ let thinkingLevel = options.parentThinkingLevel as ThinkingLevel;
63
+ let selectedAuth: Awaited<ReturnType<ExtensionContext["modelRegistry"]["getApiKeyAndHeaders"]>> | undefined;
64
+ if (definition.model) {
65
+ const separator = definition.model.indexOf("/");
66
+ const configured = ctx.modelRegistry.find(
67
+ definition.model.slice(0, separator),
68
+ definition.model.slice(separator + 1),
69
+ );
70
+ if (!configured) onWarning?.(`model ${definition.model} is unavailable; using parent model`);
71
+ else {
72
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(configured);
73
+ if (!auth.ok) onWarning?.(`model ${definition.model} is unavailable: ${auth.error}; using parent model`);
74
+ else {
75
+ model = configured;
76
+ selectedAuth = auth;
77
+ }
78
+ }
79
+ }
80
+ if (definition.thinking) {
81
+ const mapped = model?.thinkingLevelMap?.[definition.thinking];
82
+ const unsupported =
83
+ !model?.reasoning ||
84
+ mapped === null ||
85
+ ((definition.thinking === "xhigh" || definition.thinking === "max") && mapped === undefined);
86
+ if (unsupported)
87
+ onWarning?.(`thinking ${definition.thinking} is unavailable for the selected model; using parent thinking`);
88
+ else thinkingLevel = definition.thinking;
89
+ }
90
+ if (!model) throw new Error(`Agent ${definition.name} startup failed: parent has no model`);
91
+ const auth = selectedAuth ?? (await ctx.modelRegistry.getApiKeyAndHeaders(model));
92
+ if (!auth.ok) throw new Error(`Agent ${definition.name} startup failed: ${auth.error}`);
93
+ const provider = ctx.modelRegistry.getProvider(model.provider);
94
+ if (!provider) throw new Error(`Agent ${definition.name} startup failed: provider ${model.provider} is unavailable`);
95
+ if (signal.aborted) throw new Error(`Agent ${definition.name} startup aborted`);
96
+ return {
97
+ definition,
98
+ extensionPaths: [...extensionPaths],
99
+ cwd: ctx.cwd,
100
+ model,
101
+ modelName: `${model.provider}/${model.id}`,
102
+ provider,
103
+ runtimeApiKey:
104
+ auth.apiKey && provider.auth.apiKey && !ctx.modelRegistry.isUsingOAuth(model) ? auth.apiKey : undefined,
105
+ thinkingLevel,
106
+ bindTarget:
107
+ ctx.mode === "tui" && ctx.hasUI ? { mode: "tui", uiContext: childUiContext(ctx.ui) } : { mode: "print" },
108
+ };
109
+ }
110
+
111
+ export async function createSubagentSessionResource(
112
+ inputs: SubagentSessionInputs,
113
+ signal: AbortSignal,
114
+ ): Promise<SubagentSessionResource> {
115
+ let session: AgentSession | undefined;
116
+ try {
117
+ if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
118
+ const modelRuntime = await ModelRuntime.create();
119
+ modelRuntime.registerNativeProvider(inputs.provider);
120
+ if (inputs.runtimeApiKey !== undefined)
121
+ await modelRuntime.setRuntimeApiKey(inputs.model.provider, inputs.runtimeApiKey);
122
+ if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
123
+ const resourceLoader = new DefaultResourceLoader({
124
+ cwd: inputs.cwd,
125
+ agentDir: getAgentDir(),
126
+ noExtensions: true,
127
+ additionalExtensionPaths: [...inputs.extensionPaths],
128
+ });
129
+ await resourceLoader.reload();
130
+ if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
131
+ const created = await createAgentSession({
132
+ cwd: inputs.cwd,
133
+ model: inputs.model,
134
+ modelRuntime,
135
+ thinkingLevel: inputs.thinkingLevel,
136
+ tools: inputs.definition.tools,
137
+ excludeTools: ["subagent"],
138
+ resourceLoader,
139
+ sessionManager: SessionManager.inMemory(inputs.cwd),
140
+ });
141
+ session = created.session;
142
+ if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
143
+ await session.bindExtensions(inputs.bindTarget);
144
+ if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
145
+ const active = session.getActiveToolNames().sort();
146
+ const expected = [...inputs.definition.tools].sort();
147
+ if (active.join("\0") !== expected.join("\0") || active.includes("subagent")) {
148
+ const missing = expected.filter((tool) => !active.includes(tool));
149
+ throw new Error(
150
+ `Agent ${inputs.definition.name} startup failed: unavailable tools: ${missing.join(", ") || "active tool mismatch"}`,
151
+ );
152
+ }
153
+ let disposed = false;
154
+ return {
155
+ inputs,
156
+ session,
157
+ async dispose() {
158
+ if (disposed) return;
159
+ disposed = true;
160
+ if (session?.isStreaming) await session.abort().catch(() => undefined);
161
+ session?.dispose();
162
+ },
163
+ };
164
+ } catch (error) {
165
+ if (session?.isStreaming) await session.abort().catch(() => undefined);
166
+ session?.dispose();
167
+ throw error;
168
+ }
169
+ }
@@ -28,7 +28,11 @@ Adds `/commit` for semantic commit grouping, review, and committing selected rep
28
28
 
29
29
  ## context
30
30
 
31
- Adds `/context` to select reusable repository work scopes from `.pi/contexts`, and `/context-sync` to reconcile affected scopes from current Git changes. Entry `files` are autoread; entry `anchors` are unloaded navigation paths the agent can inspect when needed. Context validation is disabled by default; when enabled, Tau checks both classes for changed-file membership and stale references after agent turns. Folder names are tabs, TOML files are concepts, and TOML sections are selectable entries.
31
+ Adds `/context` to select reusable repository work scopes from `.pi/contexts`, and `/context-sync` or `/context-sync <nudge>` for human-driven catalog sync (optional nudge). Escape cancels a running manual sync. When `sync.automation` is on, the coding agent can also run the `context-sync` subagent after meaningful uncommitted work. Sync catalogs durable code and long-lived documentation; recurring scratch, planning, interview, and rough-idea paths belong in `validation.ignoreGlobs`. `sync.enabled` is the master switch for command, automation, and validation auto-run. Entry `files` are autoread; entry `anchors` are unloaded navigation paths. Context validation is off by default; when on (and sync enabled), Tau auto-runs context-sync on failure. Folder names are tabs, TOML files are concepts, and TOML sections are selectable entries.
32
+
33
+ ## context-pruning
34
+
35
+ Gives the agent `context_prune` for removing substantial stale tool evidence after broad exploration converges. The agent records durable conclusions before creating an anchor, can retain exact exchanges or current file knowledge, and continues the task automatically. Context markers occasionally steer the agent after tool-heavy growth and become more direct under pressure. Run `/prune` with no arguments to request an anchor manually. Session history remains unchanged, and anchors, markers, and pruned-row state follow the active branch.
32
36
 
33
37
  ## explore
34
38
 
@@ -88,7 +92,7 @@ Adds `Alt+S` to stash the current prompt draft and `/pop` to browse stashed draf
88
92
 
89
93
  ## subagent
90
94
 
91
- Gives Tau a subagent delegation tool for isolated, focused work. Tau can continue a retained child thread when follow-up work depends on its prior reads and reasoning. You can also create your own subagents in the supported subagent directories. Ask Tau how to do it and have it consult the extension’s own documentation; the built-in agents show the pattern. Each subagent can register its own model and the tools it is allowed to use.
95
+ Gives Tau a subagent delegation tool for isolated, focused work. The built-in `review` agent performs adversarial, read-only code reviews; `web-research` handles external research. Known files can be autoread as line-numbered snapshots into a fresh or retained child turn. Tau can continue a retained child thread when follow-up work depends on its prior reads and reasoning. You can also create your own subagents in the supported subagent directories. Ask Tau how to do it and have it consult the extension’s own documentation; the built-in agents show the pattern. Each subagent can register its own model, tools, and pool of display names. Reused pool names get numeric suffixes. In interactive cmux sessions, Tau opens one temporary Markdown dashboard for live subagent progress; it does not change how children run and closes shortly after the active cohort finishes.
92
96
 
93
97
  ## tau-help
94
98
 
@@ -20,15 +20,12 @@ interface MarkerDetails {
20
20
  used: number;
21
21
  cap: number;
22
22
  extended: boolean;
23
- sequence: number;
24
23
  }
25
24
 
26
25
  export default function turnBudgetExtension(pi: ExtensionAPI): void {
27
26
  let settings = normalizeSettings(turnBudgetSettings.defaults);
28
27
  let turnCount = 0;
29
28
  let softCap = settings.turnLimit;
30
- let nextMarkerSequence = 1;
31
- let activeMarkerSequence: number | undefined;
32
29
 
33
30
  pi.registerMessageRenderer<MarkerDetails>(MARKER_TYPE, (message, _options, theme) => {
34
31
  const details = readMarkerDetails(message.details);
@@ -48,7 +45,6 @@ export default function turnBudgetExtension(pi: ExtensionAPI): void {
48
45
  pi.on("agent_start", () => {
49
46
  turnCount = 0;
50
47
  softCap = settings.turnLimit;
51
- activeMarkerSequence = undefined;
52
48
  });
53
49
 
54
50
  pi.on("turn_end", (event) => {
@@ -62,46 +58,24 @@ export default function turnBudgetExtension(pi: ExtensionAPI): void {
62
58
  previousCap: softCap,
63
59
  newCap: turnCount + settings.softCapIncrement,
64
60
  };
65
- activeMarkerSequence = nextMarkerSequence;
66
- sendTurnBudgetMessage(pi, hint, nextMarkerSequence);
67
- nextMarkerSequence += 1;
61
+ sendTurnBudgetMessage(pi, hint);
68
62
  softCap = hint.newCap;
69
63
  return undefined;
70
64
  }
71
65
  if (turnCount % settings.nudgeEveryTurns === 0) {
72
66
  const hint: Hint = { kind: "normal", used: turnCount, cap: softCap };
73
- activeMarkerSequence = nextMarkerSequence;
74
- sendTurnBudgetMessage(pi, hint, nextMarkerSequence);
75
- nextMarkerSequence += 1;
67
+ sendTurnBudgetMessage(pi, hint);
76
68
  }
77
69
  return undefined;
78
70
  });
79
-
80
- pi.on("context", (event) => {
81
- const messages = event.messages.filter(
82
- (message) =>
83
- message.role !== "custom" || message.customType !== MARKER_TYPE || markerIsActive(message.details),
84
- );
85
- if (messages.length === event.messages.length) return undefined;
86
- return { messages };
87
- });
88
-
89
- pi.on("after_provider_response", () => {
90
- activeMarkerSequence = undefined;
91
- });
92
- function markerIsActive(value: unknown): boolean {
93
- if (activeMarkerSequence === undefined) return false;
94
- const details = readMarkerDetails(value);
95
- return details?.sequence === activeMarkerSequence;
96
- }
97
71
  }
98
72
 
99
- function sendTurnBudgetMessage(pi: ExtensionAPI, hint: Hint, sequence: number): void {
73
+ function sendTurnBudgetMessage(pi: ExtensionAPI, hint: Hint): void {
100
74
  pi.sendMessage<MarkerDetails>({
101
75
  customType: MARKER_TYPE,
102
76
  content: formatSteeringMessage(hint),
103
77
  display: true,
104
- details: markerDetails(hint, sequence),
78
+ details: markerDetails(hint),
105
79
  });
106
80
  }
107
81
 
@@ -124,10 +98,10 @@ function normalizeSettings(value: typeof turnBudgetSettings.defaults): Settings
124
98
  };
125
99
  }
126
100
 
127
- function markerDetails(hint: Hint, sequence: number): MarkerDetails {
101
+ function markerDetails(hint: Hint): MarkerDetails {
128
102
  return hint.kind === "extended"
129
- ? { used: hint.used, cap: hint.newCap, extended: true, sequence }
130
- : { used: hint.used, cap: hint.cap, extended: false, sequence };
103
+ ? { used: hint.used, cap: hint.newCap, extended: true }
104
+ : { used: hint.used, cap: hint.cap, extended: false };
131
105
  }
132
106
 
133
107
  function readMarkerDetails(value: unknown): MarkerDetails | undefined {
@@ -135,10 +109,8 @@ function readMarkerDetails(value: unknown): MarkerDetails | undefined {
135
109
  const record = value as Record<string, unknown>;
136
110
  const used = record.used;
137
111
  const cap = record.cap;
138
- const sequence = record.sequence;
139
112
  if (typeof used !== "number" || !Number.isInteger(used) || used < 0) return undefined;
140
113
  if (typeof cap !== "number" || !Number.isInteger(cap) || cap < 1) return undefined;
141
- if (typeof sequence !== "number" || !Number.isInteger(sequence) || sequence < 1) return undefined;
142
114
  if (typeof record.extended !== "boolean") return undefined;
143
- return { used, cap, extended: record.extended, sequence };
115
+ return { used, cap, extended: record.extended };
144
116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Tau is a custom agentic harness built with pi extensions",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  "README.md"
29
29
  ],
30
30
  "dependencies": {
31
- "@shanepadgett/tau-tui": "0.16.0",
31
+ "@shanepadgett/tau-tui": "0.18.0",
32
32
  "@toon-format/toon": "2.3.0",
33
33
  "smol-toml": "1.7.0"
34
34
  },
@@ -15,13 +15,29 @@
15
15
  "context": {
16
16
  "type": "object",
17
17
  "properties": {
18
+ "sync": {
19
+ "type": "object",
20
+ "properties": {
21
+ "enabled": {
22
+ "type": "boolean",
23
+ "default": true,
24
+ "description": "Master switch for context-sync. When false: no /context-sync, parent cannot call the agent, validation does not auto-run sync."
25
+ },
26
+ "automation": {
27
+ "type": "boolean",
28
+ "default": true,
29
+ "description": "When true with sync.enabled, the coding agent may call context-sync. When false, only manual /context-sync (validation auto-run still allowed if validation.enabled)."
30
+ }
31
+ },
32
+ "additionalProperties": false
33
+ },
18
34
  "validation": {
19
35
  "type": "object",
20
36
  "properties": {
21
37
  "enabled": {
22
38
  "type": "boolean",
23
39
  "default": false,
24
- "description": "Validate context membership after agent turns."
40
+ "description": "Validate context membership after agent turns and auto-run context-sync on failure (requires sync.enabled)."
25
41
  },
26
42
  "ignoreGlobs": {
27
43
  "type": "array",
@@ -37,6 +53,37 @@
37
53
  },
38
54
  "additionalProperties": false
39
55
  },
56
+ "contextPruning": {
57
+ "type": "object",
58
+ "properties": {
59
+ "enabled": {
60
+ "type": "boolean",
61
+ "default": true,
62
+ "description": "Enable context pruning."
63
+ },
64
+ "nudgeEveryPercent": {
65
+ "type": "integer",
66
+ "default": 20,
67
+ "minimum": 1,
68
+ "maximum": 100,
69
+ "description": "Context growth interval between automatic pruning hints."
70
+ },
71
+ "pressurePercent": {
72
+ "type": "integer",
73
+ "default": 50,
74
+ "minimum": 1,
75
+ "maximum": 99,
76
+ "description": "Context usage above which pruning hints become urgent."
77
+ },
78
+ "minimumReclaimTokens": {
79
+ "type": "integer",
80
+ "default": 8000,
81
+ "minimum": 1,
82
+ "description": "Minimum estimated tokens a prune must reclaim."
83
+ }
84
+ },
85
+ "additionalProperties": false
86
+ },
40
87
  "footer": {
41
88
  "type": "object",
42
89
  "properties": {