@shanepadgett/tau-agent 0.33.0 → 0.34.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 (190) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/branch/index.ts +38 -21
  8. package/extensions/cache-diagnostics/index.ts +70 -58
  9. package/extensions/checkpoint/README.md +9 -0
  10. package/extensions/checkpoint/checkpoint-budget.ts +79 -0
  11. package/extensions/checkpoint/checkpoint.ts +277 -0
  12. package/extensions/checkpoint/index.ts +102 -0
  13. package/extensions/checkpoint/messages.ts +169 -0
  14. package/extensions/checkpoint/prompt.ts +24 -0
  15. package/extensions/checkpoint/settings.ts +27 -0
  16. package/extensions/commit/commit-effort.ts +3 -3
  17. package/extensions/context/README.md +12 -8
  18. package/extensions/context/definitions.ts +178 -55
  19. package/extensions/context/index.ts +192 -160
  20. package/extensions/context/panel.ts +57 -37
  21. package/extensions/context/sync.ts +139 -89
  22. package/extensions/context/validation.ts +37 -25
  23. package/extensions/effort/README.md +1 -1
  24. package/extensions/effort/index.ts +3 -3
  25. package/extensions/effort/state.ts +3 -18
  26. package/extensions/explore/guidance.ts +6 -47
  27. package/extensions/explore/index.ts +38 -53
  28. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  29. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  30. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  31. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  32. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  33. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  34. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  35. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  36. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  37. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  38. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  39. package/extensions/explore/tools/target-params.ts +9 -0
  40. package/extensions/footer/index.ts +75 -56
  41. package/extensions/handoff/index.ts +8 -30
  42. package/extensions/image-gen/index.ts +46 -40
  43. package/extensions/patch/executor.ts +190 -135
  44. package/extensions/patch/matcher.ts +82 -48
  45. package/extensions/patch/parser.ts +149 -105
  46. package/extensions/patch/render.ts +32 -39
  47. package/extensions/patch/summary.ts +30 -26
  48. package/extensions/ready/README.md +7 -0
  49. package/extensions/ready/index.ts +58 -0
  50. package/extensions/ready/model.ts +128 -0
  51. package/extensions/ready/packs.ts +272 -0
  52. package/extensions/ready/render.ts +445 -0
  53. package/extensions/ready/scan.ts +760 -0
  54. package/extensions/reference/panel.ts +28 -16
  55. package/extensions/review/index.ts +63 -51
  56. package/extensions/review/panel.ts +5 -34
  57. package/extensions/runtime-context/context.ts +30 -26
  58. package/extensions/script-runner/README.md +2 -2
  59. package/extensions/script-runner/index.ts +130 -93
  60. package/extensions/silent-command-runner/index.ts +38 -23
  61. package/extensions/subagent/agents/context-sync.md +185 -33
  62. package/extensions/subagent/agents/scout.md +2 -2
  63. package/extensions/subagent/agents.ts +137 -84
  64. package/extensions/subagent/cmux-dashboard.ts +39 -30
  65. package/extensions/subagent/index.ts +79 -59
  66. package/extensions/subagent/resume.ts +1 -1
  67. package/extensions/subagent/run.ts +304 -193
  68. package/extensions/subagent/runtime.ts +781 -343
  69. package/extensions/tau/index.ts +47 -38
  70. package/extensions/tau-help/help.md +14 -6
  71. package/extensions/tool-loader/README.md +4 -2
  72. package/extensions/tool-loader/index.ts +77 -52
  73. package/extensions/web/index.ts +6 -3
  74. package/extensions/web/webfetch.ts +114 -86
  75. package/package.json +3 -3
  76. package/schemas/tau.schema.json +20 -33
  77. package/shared/bounded-text-result.ts +1 -0
  78. package/shared/checkpoint-visibility.ts +9 -0
  79. package/shared/events.ts +28 -10
  80. package/shared/isolated-session.ts +71 -29
  81. package/shared/model-effort.ts +9 -19
  82. package/shared/model-fallback/index.ts +66 -41
  83. package/shared/settings/load.ts +45 -38
  84. package/shared/temporary-output-store.ts +16 -10
  85. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  86. package/{extensions/explore → src}/ast/engine.ts +54 -38
  87. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  88. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  89. package/src/ast/format/deps.ts +94 -0
  90. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  91. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  92. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  93. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  94. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  95. package/src/ast/format/targeted-result.ts +20 -0
  96. package/src/ast/grammars/c_sharp.wasm +0 -0
  97. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  98. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  99. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  100. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  101. package/{extensions/explore → src}/ast/ir.ts +6 -0
  102. package/src/ast/languages/csharp-file-deps.ts +90 -0
  103. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  104. package/src/ast/languages/dotted-index.ts +131 -0
  105. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  106. package/src/ast/languages/fixtures/sample.cs +53 -0
  107. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  108. package/src/ast/languages/fixtures/sample.go +37 -0
  109. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  110. package/src/ast/languages/fixtures/sample.java +40 -0
  111. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  112. package/src/ast/languages/fixtures/sample.kt +31 -0
  113. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  114. package/src/ast/languages/fixtures/sample.md +23 -0
  115. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  116. package/src/ast/languages/fixtures/sample.rs +54 -0
  117. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  118. package/src/ast/languages/fixtures/sample.swift +45 -0
  119. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  120. package/src/ast/languages/fixtures/sample.ts +45 -0
  121. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  122. package/src/ast/languages/fixtures/sample.tsx +36 -0
  123. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  124. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  125. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  126. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  127. package/src/ast/languages/jvm-file-deps.ts +135 -0
  128. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  129. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  130. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  131. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  132. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  133. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  134. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  135. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  136. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  137. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  138. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  139. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  140. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  141. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  142. package/src/ast/queries/impact.ts +291 -0
  143. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  144. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  145. package/{extensions/explore → src}/ast/query.ts +1 -1
  146. package/{extensions/explore → src}/ast/registry.ts +1 -2
  147. package/{extensions/explore → src}/ast/scan.ts +1 -1
  148. package/src/ast/session.ts +50 -0
  149. package/{extensions/explore → src}/ast/slice.ts +8 -0
  150. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  151. package/src/file-injection/index.ts +547 -0
  152. package/src/image-generation/client.ts +50 -31
  153. package/src/image-generation/index.ts +177 -134
  154. package/src/index.ts +2 -0
  155. package/src/tool-loading/index.ts +58 -0
  156. package/extensions/context/evidence.ts +0 -517
  157. package/extensions/context/projection.ts +0 -141
  158. package/extensions/context/state.ts +0 -30
  159. package/extensions/context/write-scope.ts +0 -109
  160. package/extensions/explore/ast/format/deps.ts +0 -86
  161. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  162. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  163. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  164. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  165. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  166. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  167. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  168. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  169. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  170. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  171. package/extensions/explore/ast/languages/odin.ts +0 -407
  172. package/extensions/explore/ast/queries/impact.ts +0 -250
  173. package/extensions/explore/outline-injection.ts +0 -151
  174. package/extensions/explore/read/autoread.ts +0 -118
  175. package/extensions/working-memory/README.md +0 -17
  176. package/extensions/working-memory/checkpoint.ts +0 -242
  177. package/extensions/working-memory/index.ts +0 -341
  178. package/extensions/working-memory/memory.ts +0 -235
  179. package/extensions/working-memory/render.ts +0 -185
  180. package/extensions/working-memory/settings.ts +0 -38
  181. package/extensions/working-memory/state.ts +0 -158
  182. package/shared/autoread.ts +0 -234
  183. package/shared/context-messages.ts +0 -19
  184. package/shared/outline-injection.ts +0 -56
  185. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  186. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  187. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  188. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  189. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  190. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -12,6 +12,26 @@
12
12
  "extensions": {
13
13
  "type": "object",
14
14
  "properties": {
15
+ "checkpoint": {
16
+ "type": "object",
17
+ "required": [
18
+ "checkpointTokenLimit"
19
+ ],
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": {
27
+ "type": "integer",
28
+ "minimum": 1,
29
+ "default": 150000,
30
+ "description": "Context-token ceiling before a checkpoint is required."
31
+ }
32
+ },
33
+ "additionalProperties": false
34
+ },
15
35
  "context": {
16
36
  "type": "object",
17
37
  "properties": {
@@ -262,39 +282,6 @@
262
282
  }
263
283
  },
264
284
  "additionalProperties": false
265
- },
266
- "workingMemory": {
267
- "type": "object",
268
- "properties": {
269
- "enabled": {
270
- "type": "boolean",
271
- "default": true,
272
- "description": "Enable working-memory checkpoints."
273
- },
274
- "nudgeEveryTokens": {
275
- "type": "integer",
276
- "default": 40000,
277
- "minimum": 1,
278
- "description": "Active-context token interval between advisory working-memory reminders."
279
- },
280
- "nudgeInstructions": {
281
- "type": "array",
282
- "items": {
283
- "type": "string",
284
- "minLength": 1,
285
- "maxLength": 2000
286
- },
287
- "default": [
288
- "Reassess working memory. Continue coherent exploration when its evidence remains useful; otherwise prune known dead ends, obsolete outputs, and other context with no expected value.",
289
- "Context is materially larger. Prune stale or bulky irrelevant evidence when safe, but keep active working evidence that would otherwise need to be reread.",
290
- "Strongly reassess before more broad work. Remove accumulated waste and carry useful information at cheapest sufficient fidelity without scrubbing the active working set."
291
- ],
292
- "minItems": 1,
293
- "maxItems": 5,
294
- "description": "Ordered advisory working-memory instructions. Later reminders repeat final instruction."
295
- }
296
- },
297
- "additionalProperties": false
298
285
  }
299
286
  },
300
287
  "additionalProperties": true,
@@ -63,6 +63,7 @@ export class BoundedTextResultBuilder {
63
63
  this.strategy = strategy;
64
64
  }
65
65
 
66
+ // fallow-ignore-next-line unused-class-member -- head/tail strategy API; callers use completeBlocks today
66
67
  async append(text: string): Promise<void> {
67
68
  if (this.strategy === "completeBlocks") throw new Error("Complete-block results require appendBlock");
68
69
  this.requireOpen();
@@ -0,0 +1,9 @@
1
+ let showCheckpointRows = false;
2
+
3
+ export function setCheckpointRowsVisible(visible: boolean): void {
4
+ showCheckpointRows = visible;
5
+ }
6
+
7
+ export function areCheckpointRowsVisible(): boolean {
8
+ return showCheckpointRows;
9
+ }
package/shared/events.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { ModelEffort } from "./model-effort.ts";
3
- import type { OutlineInjectionRequest, OutlineInjectionResponse } from "./outline-injection.js";
4
3
  import type { ToolRowVisualState } from "./tool-row-state.js";
4
+ import type { FileInjectionRequest, PreparedFileInjection } from "../src/file-injection/index.ts";
5
5
 
6
6
  export type TauAgentEvents = {
7
7
  "tau:agent.blocked": {
@@ -31,16 +31,14 @@ export type TauAgentEvents = {
31
31
  snapshotRanges?: Array<{ startLine: number; endLine: number }>;
32
32
  }>;
33
33
  };
34
- "tau:autoread.requested": {
35
- source: string;
36
- title?: string;
37
- cwd: string;
38
- batchId: string;
39
- files: Array<{ path: string }>;
34
+ /** @internal Runtime-scoped discovery for deferred tool groups. */
35
+ "tau:deferred-tool-group.request": {
36
+ accept(group: { id: string; description: string; toolNames: readonly string[] }): void;
40
37
  };
41
- "tau:outline-injection.requested": {
42
- request: OutlineInjectionRequest;
43
- accept(response: Promise<OutlineInjectionResponse>): void;
38
+ /** @internal Runtime-scoped request for Explore-owned file preparation. */
39
+ "tau:file-injection.prepare": {
40
+ request: FileInjectionRequest;
41
+ accept(preparation: Promise<PreparedFileInjection[]>): void;
44
42
  };
45
43
  "tau:footer-item": {
46
44
  id: string;
@@ -105,6 +103,25 @@ export function onTauEvent<Name extends keyof TauAgentEvents>(
105
103
  owner: string,
106
104
  name: Name,
107
105
  handler: TauEventHandler<Name>,
106
+ ): () => void {
107
+ return subscribeToTauEvent(pi, owner, name, handler, false);
108
+ }
109
+
110
+ export function onTauEventImmediately<Name extends keyof TauAgentEvents>(
111
+ pi: TauEventAPI,
112
+ owner: string,
113
+ name: Name,
114
+ handler: TauEventHandler<Name>,
115
+ ): () => void {
116
+ return subscribeToTauEvent(pi, owner, name, handler, true);
117
+ }
118
+
119
+ function subscribeToTauEvent<Name extends keyof TauAgentEvents>(
120
+ pi: TauEventAPI,
121
+ owner: string,
122
+ name: Name,
123
+ handler: TauEventHandler<Name>,
124
+ attachImmediately: boolean,
108
125
  ): () => void {
109
126
  if (owner.length === 0) throw new Error("Tau event owner is required.");
110
127
 
@@ -135,6 +152,7 @@ export function onTauEvent<Name extends keyof TauAgentEvents>(
135
152
  }
136
153
 
137
154
  subscriptions.set(name, subscription);
155
+ if (attachImmediately) attach();
138
156
  pi.on("session_start", attach);
139
157
  pi.on("session_shutdown", detach);
140
158
  return subscription.stop;
@@ -32,6 +32,61 @@ export interface IsolatedSessionResource {
32
32
  dispose(): Promise<void>;
33
33
  }
34
34
 
35
+ type ModelAuth = Awaited<ReturnType<ExtensionContext["modelRegistry"]["getApiKeyAndHeaders"]>>;
36
+
37
+ async function tryPreferredModel(
38
+ ctx: ExtensionContext,
39
+ preferredModel: string,
40
+ onWarning?: (warning: string) => void,
41
+ ): Promise<{ model: SelectedModel; auth: ModelAuth } | undefined> {
42
+ const separator = preferredModel.indexOf("/");
43
+ const configured = ctx.modelRegistry.find(preferredModel.slice(0, separator), preferredModel.slice(separator + 1));
44
+ if (!configured) {
45
+ onWarning?.(`model ${preferredModel} is unavailable; using parent model`);
46
+ return undefined;
47
+ }
48
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(configured);
49
+ if (!auth.ok) {
50
+ onWarning?.(`model ${preferredModel} is unavailable: ${auth.error}; using parent model`);
51
+ return undefined;
52
+ }
53
+ return { model: configured, auth };
54
+ }
55
+
56
+ function thinkingSupported(
57
+ model: SelectedModel | undefined,
58
+ preferred: IsolatedSessionThinkingLevel,
59
+ ): preferred is IsolatedSessionThinkingLevel {
60
+ const mapped = model?.thinkingLevelMap?.[preferred];
61
+ if (!model?.reasoning || mapped === null) return false;
62
+ if ((preferred === "xhigh" || preferred === "max") && mapped === undefined) return false;
63
+ return true;
64
+ }
65
+
66
+ function resolveThinkingLevel(
67
+ model: SelectedModel | undefined,
68
+ parentThinkingLevel: IsolatedSessionThinkingLevel,
69
+ preferredThinkingLevel: IsolatedSessionThinkingLevel | undefined,
70
+ allowPreferred: boolean,
71
+ onWarning?: (warning: string) => void,
72
+ ): IsolatedSessionThinkingLevel {
73
+ if (!preferredThinkingLevel || !allowPreferred) return parentThinkingLevel;
74
+ if (thinkingSupported(model, preferredThinkingLevel)) return preferredThinkingLevel;
75
+ onWarning?.(`thinking ${preferredThinkingLevel} is unavailable for the selected model; using parent thinking`);
76
+ return parentThinkingLevel;
77
+ }
78
+
79
+ function runtimeApiKeyFor(
80
+ ctx: ExtensionContext,
81
+ model: SelectedModel,
82
+ provider: SelectedProvider,
83
+ auth: ModelAuth,
84
+ ): string | undefined {
85
+ if (!auth.ok) return undefined;
86
+ if (!(auth.apiKey && provider.auth.apiKey && !ctx.modelRegistry.isUsingOAuth(model))) return undefined;
87
+ return auth.apiKey;
88
+ }
89
+
35
90
  export async function resolveIsolatedSessionModel(options: {
36
91
  label: string;
37
92
  preferredModel: string | undefined;
@@ -44,37 +99,25 @@ export async function resolveIsolatedSessionModel(options: {
44
99
  }): Promise<Pick<IsolatedSessionInputs, "model" | "provider" | "runtimeApiKey" | "thinkingLevel">> {
45
100
  const { ctx, signal, onWarning } = options;
46
101
  let model = ctx.model;
47
- let thinkingLevel = options.parentThinkingLevel;
102
+ let selectedAuth: ModelAuth | undefined;
48
103
  let preferredModelSelected = false;
49
- let selectedAuth: Awaited<ReturnType<ExtensionContext["modelRegistry"]["getApiKeyAndHeaders"]>> | undefined;
104
+
50
105
  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
- }
106
+ const preferred = await tryPreferredModel(ctx, options.preferredModel, onWarning);
107
+ if (preferred) {
108
+ model = preferred.model;
109
+ selectedAuth = preferred.auth;
110
+ preferredModelSelected = true;
65
111
  }
66
112
  }
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
- }
113
+
114
+ const thinkingLevel = resolveThinkingLevel(
115
+ model,
116
+ options.parentThinkingLevel,
117
+ options.preferredThinkingLevel,
118
+ preferredModelSelected || options.usePreferredThinkingAfterModelFallback,
119
+ onWarning,
120
+ );
78
121
  if (!model) throw new Error(`${options.label} startup failed: parent has no model`);
79
122
  const auth = selectedAuth ?? (await ctx.modelRegistry.getApiKeyAndHeaders(model));
80
123
  if (!auth.ok) throw new Error(`${options.label} startup failed: ${auth.error}`);
@@ -84,8 +127,7 @@ export async function resolveIsolatedSessionModel(options: {
84
127
  return {
85
128
  model,
86
129
  provider,
87
- runtimeApiKey:
88
- auth.apiKey && provider.auth.apiKey && !ctx.modelRegistry.isUsingOAuth(model) ? auth.apiKey : undefined,
130
+ runtimeApiKey: runtimeApiKeyFor(ctx, model, provider, auth),
89
131
  thinkingLevel,
90
132
  };
91
133
  }
@@ -3,7 +3,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { resolveCandidates } from "./model-fallback/index.ts";
4
4
  import type { ModelCandidate } from "./model-fallback/types.ts";
5
5
 
6
- export type ModelEffort = "low" | "medium" | "high";
6
+ export type ModelEffort = "quick" | "standard" | "deep";
7
7
 
8
8
  interface ModelPreference {
9
9
  model: string;
@@ -25,42 +25,32 @@ export interface EffortProviderCandidates {
25
25
  }
26
26
 
27
27
  const MODEL_PREFERENCES: Record<ModelEffort, readonly ProviderPreference[]> = {
28
- low: [
28
+ quick: [
29
29
  {
30
30
  provider: "openai-codex",
31
- models: [
32
- { model: "gpt-5.6-luna", reasoning: "high" },
33
- { model: "gpt-5.5", reasoning: "low" },
34
- ],
31
+ models: [{ model: "gpt-5.6-luna", reasoning: "medium" }],
35
32
  },
36
33
  { provider: "xai", models: [{ model: "grok-4.5", reasoning: "medium" }] },
37
34
  { provider: "anthropic", models: [{ model: "claude-haiku-4-5", reasoning: "high" }] },
38
35
  ],
39
- medium: [
36
+ standard: [
40
37
  {
41
38
  provider: "openai-codex",
42
- models: [
43
- { model: "gpt-5.6-terra", reasoning: "high" },
44
- { model: "gpt-5.5", reasoning: "medium" },
45
- ],
39
+ models: [{ model: "gpt-5.6-luna", reasoning: "max" }],
46
40
  },
47
41
  { provider: "xai", models: [{ model: "grok-4.5", reasoning: "high" }] },
48
42
  { provider: "anthropic", models: [{ model: "claude-sonnet-5", reasoning: "high" }] },
49
43
  ],
50
- high: [
44
+ deep: [
51
45
  {
52
46
  provider: "openai-codex",
53
- models: [
54
- { model: "gpt-5.6-sol", reasoning: "high" },
55
- { model: "gpt-5.5", reasoning: "high" },
56
- ],
47
+ models: [{ model: "gpt-5.6-sol", reasoning: "high" }],
57
48
  },
58
- { provider: "xai", models: [{ model: "grok-4.5", reasoning: "high" }] },
59
49
  {
60
50
  provider: "anthropic",
61
51
  models: [
62
52
  { model: "claude-opus-5", reasoning: "high" },
63
- { model: "claude-opus-4-8", reasoning: "high" },
53
+ { model: "claude-fable-5", reasoning: "low" },
64
54
  ],
65
55
  },
66
56
  ],
@@ -72,7 +62,7 @@ export function effortForSelection(
72
62
  reasoning: string | undefined,
73
63
  ): ModelEffort | undefined {
74
64
  if (!provider || !model || !reasoning) return undefined;
75
- for (const effort of ["high", "medium", "low"] as const) {
65
+ for (const effort of ["deep", "standard", "quick"] as const) {
76
66
  const preference = MODEL_PREFERENCES[effort].find((item) => item.provider === provider);
77
67
  if (preference?.models.some((item) => item.model === model && item.reasoning === reasoning)) return effort;
78
68
  }
@@ -98,6 +98,20 @@ export async function generateToolValidated<T>(
98
98
  );
99
99
  }
100
100
 
101
+ async function noteCandidateFailure(
102
+ ctx: GenerationContext,
103
+ options: ModelFallbackOptions | undefined,
104
+ label: string,
105
+ message: string,
106
+ hasMore: boolean,
107
+ ): Promise<void> {
108
+ if (!hasMore) return;
109
+ await options?.onStatus?.(`Model failed (${label}); trying next model`);
110
+ if (options?.notifyOnFallback ?? false) {
111
+ ctx.ui.notify(`Model failed (${label}): ${message}\nTrying next model.`, "info");
112
+ }
113
+ }
114
+
101
115
  async function withModelFallback<T>(
102
116
  ctx: GenerationContext,
103
117
  candidates: readonly ModelCandidate[],
@@ -106,13 +120,11 @@ async function withModelFallback<T>(
106
120
  ): Promise<T> {
107
121
  const failures: string[] = [];
108
122
  const statusKey = options?.statusKey;
109
- const notifyOnFallback = options?.notifyOnFallback ?? false;
110
123
 
111
124
  for (const [index, candidate] of candidates.entries()) {
112
125
  const label = `${candidate.model.provider}/${candidate.model.id}`;
113
126
  if (statusKey) ctx.ui.setStatus(statusKey, `generating (${label})`);
114
127
  await options?.onStatus?.(`Generating with ${label}`);
115
-
116
128
  try {
117
129
  return await request(candidate);
118
130
  } catch (error) {
@@ -120,16 +132,42 @@ async function withModelFallback<T>(
120
132
  if (shouldCooldownProvider(error)) await markProviderUnavailable(candidate.model.provider);
121
133
  const message = errorText(error);
122
134
  failures.push(`- ${label}: ${message}`);
123
- if (index < candidates.length - 1) await options?.onStatus?.(`Model failed (${label}); trying next model`);
124
- if (index < candidates.length - 1 && notifyOnFallback) {
125
- ctx.ui.notify(`Model failed (${label}): ${message}\nTrying next model.`, "info");
126
- }
135
+ await noteCandidateFailure(ctx, options, label, message, index < candidates.length - 1);
127
136
  }
128
137
  }
129
138
 
130
139
  throw new Error(["Model generation failed for all candidates:", ...failures].join("\n"));
131
140
  }
132
141
 
142
+ function throwUnlessRetryableStopError(response: AssistantMessage, attempt: number, maxAttempts: number): void {
143
+ if (response.stopReason !== "error") return;
144
+ const error = new Error(response.errorMessage || "model returned an error");
145
+ if (attempt < maxAttempts && !shouldCooldownProvider(error)) return;
146
+ throw error;
147
+ }
148
+
149
+ function pushCorrection(
150
+ messages: Message[],
151
+ error: Error,
152
+ output: string,
153
+ correctionPrompt: (error: Error, output: string) => string,
154
+ ): void {
155
+ messages.push({
156
+ role: "user",
157
+ content: [{ type: "text", text: correctionPrompt(error, truncAt(output, 4_000)) }],
158
+ timestamp: Date.now(),
159
+ });
160
+ }
161
+
162
+ function shouldRetryValidation(
163
+ error: unknown,
164
+ attempt: number,
165
+ maxAttempts: number,
166
+ correctionPrompt: ((error: Error, output: string) => string) | undefined,
167
+ ): error is Error {
168
+ return error instanceof Error && attempt < maxAttempts && correctionPrompt !== undefined;
169
+ }
170
+
133
171
  async function requestValidated<T>(
134
172
  ctx: GenerationContext,
135
173
  candidate: ModelCandidate,
@@ -137,37 +175,36 @@ async function requestValidated<T>(
137
175
  validate: (text: string) => T,
138
176
  correctionPrompt?: (error: Error, text: string) => string,
139
177
  ): Promise<T> {
140
- const userMessage: Message = { role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() };
141
- const messages: Message[] = [userMessage];
178
+ const messages: Message[] = [{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() }];
142
179
  const sessionId = randomUUID();
143
180
 
144
181
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
145
182
  const response = await completeCandidate(ctx, candidate, messages, sessionId);
146
183
  const text = responseText(response);
147
- if (response.stopReason === "error") {
148
- const error = new Error(response.errorMessage || "model returned an error");
149
- if (attempt < MAX_ATTEMPTS && !shouldCooldownProvider(error)) continue;
150
- throw error;
151
- }
152
-
153
184
  try {
185
+ throwUnlessRetryableStopError(response, attempt, MAX_ATTEMPTS);
186
+ if (response.stopReason === "error") continue;
154
187
  return validate(text);
155
188
  } catch (error) {
156
- if (!(error instanceof Error) || attempt >= MAX_ATTEMPTS) throw error;
157
- if (!correctionPrompt) throw error;
158
-
189
+ if (!shouldRetryValidation(error, attempt, MAX_ATTEMPTS, correctionPrompt) || !correctionPrompt) {
190
+ throw error;
191
+ }
159
192
  if (text) messages.push({ ...response, content: [{ type: "text", text }] });
160
- messages.push({
161
- role: "user",
162
- content: [{ type: "text", text: correctionPrompt(error, truncAt(text, 4_000)) }],
163
- timestamp: Date.now(),
164
- });
193
+ pushCorrection(messages, error, text, correctionPrompt);
165
194
  }
166
195
  }
167
196
 
168
197
  throw new Error("Model generation failed.");
169
198
  }
170
199
 
200
+ function validateSingleToolCall(tool: Tool, toolCalls: readonly { name: string; arguments: unknown }[]): unknown {
201
+ if (toolCalls.length !== 1) throw new Error(`Model must call ${tool.name} exactly once.`);
202
+ const toolCall = toolCalls[0];
203
+ if (!toolCall) throw new Error(`Model must call ${tool.name}.`);
204
+ if (toolCall.name !== tool.name) throw new Error(`Model called ${toolCall.name}; expected ${tool.name}.`);
205
+ return toolCall.arguments;
206
+ }
207
+
171
208
  async function requestToolValidated<T>(
172
209
  ctx: GenerationContext,
173
210
  candidate: ModelCandidate,
@@ -185,27 +222,15 @@ async function requestToolValidated<T>(
185
222
  const text = responseText(response);
186
223
  const toolCalls = response.content.flatMap((part) => (part.type === "toolCall" ? [part] : []));
187
224
  const output = text || formatToolCalls(toolCalls);
188
- if (response.stopReason === "error") {
189
- const error = new Error(response.errorMessage || "model returned an error");
190
- if (attempt < maxAttempts && !shouldCooldownProvider(error)) continue;
191
- throw error;
192
- }
193
-
194
225
  try {
195
- if (toolCalls.length !== 1) throw new Error(`Model must call ${tool.name} exactly once.`);
196
- const [toolCall] = toolCalls;
197
- if (!toolCall) throw new Error(`Model must call ${tool.name}.`);
198
- if (toolCall.name !== tool.name) throw new Error(`Model called ${toolCall.name}; expected ${tool.name}.`);
199
- return validate(toolCall.arguments);
226
+ throwUnlessRetryableStopError(response, attempt, maxAttempts);
227
+ if (response.stopReason === "error") continue;
228
+ return validate(validateSingleToolCall(tool, toolCalls));
200
229
  } catch (error) {
201
- if (!(error instanceof Error) || attempt >= maxAttempts) throw error;
202
- if (!correctionPrompt) throw error;
203
-
204
- messages.push({
205
- role: "user",
206
- content: [{ type: "text", text: correctionPrompt(error, truncAt(output, 4_000)) }],
207
- timestamp: Date.now(),
208
- });
230
+ if (!shouldRetryValidation(error, attempt, maxAttempts, correctionPrompt) || !correctionPrompt) {
231
+ throw error;
232
+ }
233
+ pushCorrection(messages, error, output, correctionPrompt);
209
234
  }
210
235
  }
211
236
 
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { IsObject, type TSchema } from "typebox";
2
+ import { IsObject, type TObject, type TSchema } from "typebox";
3
3
  import { Value } from "typebox/value";
4
4
  import type { JsonObject, TauExtensionSettingsSpec } from "./define.ts";
5
5
  import { asObject, readJsonStatus, writeJsonObject } from "./json.ts";
@@ -63,6 +63,47 @@ function extensionSection(root: JsonObject, key: string): JsonObject | undefined
63
63
 
64
64
  const NO_DEFAULT = Symbol("no-default");
65
65
 
66
+ function repairKnownProperties(
67
+ schema: TObject,
68
+ valueObject: JsonObject,
69
+ defaultObject: JsonObject | undefined,
70
+ ): JsonObject | typeof NO_DEFAULT {
71
+ const repaired: JsonObject = {};
72
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
73
+ for (const [key, propertySchema] of Object.entries(schema.properties)) {
74
+ if (!Object.hasOwn(valueObject, key)) {
75
+ if (!required.has(key)) continue;
76
+ const requiredDefault = propertyDefault(propertySchema, defaultObject, key);
77
+ if (requiredDefault === NO_DEFAULT) return NO_DEFAULT;
78
+ repaired[key] = Value.Clone(requiredDefault);
79
+ continue;
80
+ }
81
+
82
+ const propertyValue = replaceInvalidProperties(
83
+ propertySchema,
84
+ valueObject[key],
85
+ propertyDefault(propertySchema, defaultObject, key),
86
+ );
87
+ if (propertyValue !== NO_DEFAULT) repaired[key] = propertyValue;
88
+ else if (required.has(key)) return NO_DEFAULT;
89
+ }
90
+ return repaired;
91
+ }
92
+
93
+ function repairAdditionalProperties(schema: TObject, valueObject: JsonObject, repaired: JsonObject): void {
94
+ const additionalProperties = asObject(schema)?.additionalProperties;
95
+ if (additionalProperties === false) return;
96
+ for (const [key, additionalValue] of Object.entries(valueObject)) {
97
+ if (Object.hasOwn(schema.properties, key)) continue;
98
+ if (additionalProperties === undefined || additionalProperties === true) {
99
+ repaired[key] = additionalValue;
100
+ continue;
101
+ }
102
+ const repairedAdditional = replaceInvalidProperties(additionalProperties as TSchema, additionalValue, NO_DEFAULT);
103
+ if (repairedAdditional !== NO_DEFAULT) repaired[key] = repairedAdditional;
104
+ }
105
+ }
106
+
66
107
  function replaceInvalidProperties(
67
108
  schema: TSchema,
68
109
  value: unknown,
@@ -73,43 +114,9 @@ function replaceInvalidProperties(
73
114
  const valueObject = asObject(value);
74
115
  if (IsObject(schema) && valueObject) {
75
116
  const defaultObject = documentedDefault === NO_DEFAULT ? undefined : asObject(documentedDefault);
76
- const repaired: JsonObject = {};
77
- const required = new Set(Array.isArray(schema.required) ? schema.required : []);
78
- for (const [key, propertySchema] of Object.entries(schema.properties)) {
79
- if (!Object.hasOwn(valueObject, key)) {
80
- if (!required.has(key)) continue;
81
- const requiredDefault = propertyDefault(propertySchema, defaultObject, key);
82
- if (requiredDefault === NO_DEFAULT) return validatedDefault(schema, documentedDefault);
83
- repaired[key] = Value.Clone(requiredDefault);
84
- continue;
85
- }
86
-
87
- const propertyValue = replaceInvalidProperties(
88
- propertySchema,
89
- valueObject[key],
90
- propertyDefault(propertySchema, defaultObject, key),
91
- );
92
- if (propertyValue !== NO_DEFAULT) repaired[key] = propertyValue;
93
- else if (required.has(key)) return validatedDefault(schema, documentedDefault);
94
- }
95
-
96
- const schemaObject = asObject(schema);
97
- const additionalProperties = schemaObject?.additionalProperties;
98
- if (additionalProperties !== false) {
99
- for (const [key, additionalValue] of Object.entries(valueObject)) {
100
- if (Object.hasOwn(schema.properties, key)) continue;
101
- if (additionalProperties === undefined || additionalProperties === true) repaired[key] = additionalValue;
102
- else {
103
- const repairedAdditional = replaceInvalidProperties(
104
- additionalProperties as TSchema,
105
- additionalValue,
106
- NO_DEFAULT,
107
- );
108
- if (repairedAdditional !== NO_DEFAULT) repaired[key] = repairedAdditional;
109
- }
110
- }
111
- }
112
-
117
+ const repaired = repairKnownProperties(schema, valueObject, defaultObject);
118
+ if (repaired === NO_DEFAULT) return validatedDefault(schema, documentedDefault);
119
+ repairAdditionalProperties(schema, valueObject, repaired);
113
120
  if (Value.Check(schema, repaired)) return repaired;
114
121
  }
115
122
 
@@ -126,6 +126,7 @@ export class TemporaryOutputStore {
126
126
  return writer;
127
127
  }
128
128
 
129
+ // fallow-ignore-next-line unused-class-member -- called via createTemporaryOutputStore() from explore session lifecycle
129
130
  async shutdown(): Promise<void> {
130
131
  if (this.shutdownPromise) return this.shutdownPromise;
131
132
  this.shutdownPromise = this.performShutdown();
@@ -177,6 +178,20 @@ export class TemporaryOutputStore {
177
178
  this.active.delete(writer);
178
179
  }
179
180
 
181
+ private async isRemovableOrphan(path: string): Promise<boolean> {
182
+ const metadata = await stat(path);
183
+ if (Date.now() - metadata.mtimeMs < this.orphanLifetimeMs) return false;
184
+ const marker = JSON.parse(await readFile(join(path, OWNER_FILE), "utf8")) as Partial<OwnerMarker>;
185
+ if (marker.kind !== "tau-tool-output" || marker.version !== 1 || typeof marker.pid !== "number") return false;
186
+ try {
187
+ process.kill(marker.pid, 0);
188
+ return false;
189
+ } catch (error) {
190
+ if (!isErrorCode(error, "ESRCH")) return false;
191
+ }
192
+ return true;
193
+ }
194
+
180
195
  private async removeStaleOrphans(): Promise<void> {
181
196
  let directory;
182
197
  try {
@@ -188,16 +203,7 @@ export class TemporaryOutputStore {
188
203
  if (!entry.isDirectory() || !entry.name.startsWith(DIRECTORY_PREFIX)) continue;
189
204
  const path = join(this.baseDirectory, entry.name);
190
205
  try {
191
- const metadata = await stat(path);
192
- if (Date.now() - metadata.mtimeMs < this.orphanLifetimeMs) continue;
193
- const marker = JSON.parse(await readFile(join(path, OWNER_FILE), "utf8")) as Partial<OwnerMarker>;
194
- if (marker.kind !== "tau-tool-output" || marker.version !== 1 || typeof marker.pid !== "number") continue;
195
- try {
196
- process.kill(marker.pid, 0);
197
- continue;
198
- } catch (error) {
199
- if (!isErrorCode(error, "ESRCH")) continue;
200
- }
206
+ if (!(await this.isRemovableOrphan(path))) continue;
201
207
  await rm(path, { recursive: true, force: true });
202
208
  } catch {
203
209
  // An untrusted or concurrently changing directory is not ours to remove.