@shanepadgett/tau-agent 0.33.1 → 0.35.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 (203) 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/bash-approval/README.md +20 -0
  8. package/extensions/bash-approval/index.ts +235 -0
  9. package/extensions/bash-approval/settings.ts +24 -0
  10. package/extensions/branch/index.ts +38 -21
  11. package/extensions/cache-diagnostics/index.ts +70 -58
  12. package/extensions/checkpoint/README.md +9 -0
  13. package/extensions/checkpoint/checkpoint-budget.ts +66 -0
  14. package/extensions/checkpoint/checkpoint.ts +279 -0
  15. package/extensions/checkpoint/index.ts +102 -0
  16. package/extensions/checkpoint/messages.ts +170 -0
  17. package/extensions/checkpoint/prompt.ts +24 -0
  18. package/extensions/checkpoint/settings.ts +27 -0
  19. package/extensions/commit/commit-effort.ts +3 -3
  20. package/extensions/commit/commit-plan.ts +2 -2
  21. package/extensions/context/README.md +12 -8
  22. package/extensions/context/definitions.ts +178 -55
  23. package/extensions/context/index.ts +192 -160
  24. package/extensions/context/panel.ts +57 -37
  25. package/extensions/context/sync.ts +139 -89
  26. package/extensions/context/validation.ts +37 -25
  27. package/extensions/effort/README.md +1 -1
  28. package/extensions/effort/index.ts +3 -3
  29. package/extensions/effort/state.ts +3 -18
  30. package/extensions/explore/guidance.ts +6 -47
  31. package/extensions/explore/index.ts +38 -53
  32. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  33. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  34. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  35. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  36. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  37. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  38. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  39. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  40. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  41. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  42. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  43. package/extensions/explore/tools/target-params.ts +9 -0
  44. package/extensions/footer/index.ts +75 -56
  45. package/extensions/handoff/index.ts +8 -30
  46. package/extensions/image-gen/index.ts +46 -40
  47. package/extensions/patch/executor.ts +190 -135
  48. package/extensions/patch/matcher.ts +82 -48
  49. package/extensions/patch/parser.ts +149 -105
  50. package/extensions/patch/render.ts +32 -39
  51. package/extensions/patch/summary.ts +30 -26
  52. package/extensions/ready/README.md +7 -0
  53. package/extensions/ready/index.ts +58 -0
  54. package/extensions/ready/model.ts +128 -0
  55. package/extensions/ready/packs.ts +272 -0
  56. package/extensions/ready/render.ts +445 -0
  57. package/extensions/ready/scan.ts +760 -0
  58. package/extensions/reference/index.ts +2 -0
  59. package/extensions/reference/panel.ts +94 -31
  60. package/extensions/review/README.md +14 -6
  61. package/extensions/review/index.ts +58 -99
  62. package/extensions/review/model.ts +13 -33
  63. package/extensions/review/session.ts +9 -23
  64. package/extensions/runtime-context/context.ts +30 -26
  65. package/extensions/script-runner/README.md +2 -2
  66. package/extensions/script-runner/index.ts +122 -82
  67. package/extensions/silent-command-runner/index.ts +38 -23
  68. package/extensions/soul/README.md +2 -2
  69. package/extensions/soul/index.ts +4 -4
  70. package/extensions/soul/prompt.ts +6 -10
  71. package/extensions/soul/settings.ts +6 -3
  72. package/extensions/subagent/agents/context-sync.md +185 -33
  73. package/extensions/subagent/agents/scout.md +2 -2
  74. package/extensions/subagent/agents/web-research.md +2 -2
  75. package/extensions/subagent/agents.ts +137 -84
  76. package/extensions/subagent/cmux-dashboard.ts +39 -30
  77. package/extensions/subagent/index.ts +79 -59
  78. package/extensions/subagent/resume.ts +1 -1
  79. package/extensions/subagent/run.ts +308 -193
  80. package/extensions/subagent/runtime.ts +781 -343
  81. package/extensions/tau/index.ts +47 -38
  82. package/extensions/tau-help/help.md +20 -8
  83. package/extensions/tool-loader/README.md +4 -2
  84. package/extensions/tool-loader/index.ts +77 -52
  85. package/extensions/web/index.ts +6 -3
  86. package/extensions/web/webfetch.ts +114 -86
  87. package/package.json +3 -3
  88. package/schemas/tau.schema.json +38 -35
  89. package/shared/bounded-text-result.ts +1 -0
  90. package/shared/checkpoint-visibility.ts +9 -0
  91. package/shared/events.ts +28 -10
  92. package/shared/isolated-session.ts +72 -30
  93. package/shared/model-effort.ts +23 -22
  94. package/shared/model-fallback/index.ts +66 -41
  95. package/shared/settings/load.ts +45 -38
  96. package/shared/temporary-output-store.ts +16 -10
  97. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  98. package/{extensions/explore → src}/ast/engine.ts +54 -38
  99. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  100. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  101. package/src/ast/format/deps.ts +94 -0
  102. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  103. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  104. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  105. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  106. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  107. package/src/ast/format/targeted-result.ts +20 -0
  108. package/src/ast/grammars/c_sharp.wasm +0 -0
  109. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  110. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  111. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  112. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  113. package/{extensions/explore → src}/ast/ir.ts +6 -0
  114. package/src/ast/languages/csharp-file-deps.ts +90 -0
  115. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  116. package/src/ast/languages/dotted-index.ts +131 -0
  117. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  118. package/src/ast/languages/fixtures/sample.cs +53 -0
  119. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  120. package/src/ast/languages/fixtures/sample.go +37 -0
  121. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  122. package/src/ast/languages/fixtures/sample.java +40 -0
  123. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  124. package/src/ast/languages/fixtures/sample.kt +31 -0
  125. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  126. package/src/ast/languages/fixtures/sample.md +23 -0
  127. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  128. package/src/ast/languages/fixtures/sample.rs +54 -0
  129. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  130. package/src/ast/languages/fixtures/sample.swift +45 -0
  131. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  132. package/src/ast/languages/fixtures/sample.ts +45 -0
  133. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  134. package/src/ast/languages/fixtures/sample.tsx +36 -0
  135. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  136. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  137. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  138. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  139. package/src/ast/languages/jvm-file-deps.ts +135 -0
  140. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  141. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  142. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  143. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  144. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  145. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  146. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  147. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  148. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  149. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  150. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  151. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  152. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  153. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  154. package/src/ast/queries/impact.ts +291 -0
  155. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  156. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  157. package/{extensions/explore → src}/ast/query.ts +1 -1
  158. package/{extensions/explore → src}/ast/registry.ts +1 -2
  159. package/{extensions/explore → src}/ast/scan.ts +1 -1
  160. package/src/ast/session.ts +50 -0
  161. package/{extensions/explore → src}/ast/slice.ts +8 -0
  162. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  163. package/src/file-injection/index.ts +547 -0
  164. package/src/image-generation/client.ts +50 -31
  165. package/src/image-generation/index.ts +177 -134
  166. package/src/index.ts +2 -0
  167. package/src/tool-loading/index.ts +58 -0
  168. package/extensions/context/evidence.ts +0 -517
  169. package/extensions/context/projection.ts +0 -141
  170. package/extensions/context/state.ts +0 -30
  171. package/extensions/context/write-scope.ts +0 -109
  172. package/extensions/explore/ast/format/deps.ts +0 -86
  173. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  174. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  175. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  176. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  177. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  178. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  179. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  180. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  181. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  182. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  183. package/extensions/explore/ast/languages/odin.ts +0 -407
  184. package/extensions/explore/ast/queries/impact.ts +0 -250
  185. package/extensions/explore/outline-injection.ts +0 -151
  186. package/extensions/explore/read/autoread.ts +0 -118
  187. package/extensions/review/panel.ts +0 -128
  188. package/extensions/working-memory/README.md +0 -17
  189. package/extensions/working-memory/checkpoint.ts +0 -242
  190. package/extensions/working-memory/index.ts +0 -341
  191. package/extensions/working-memory/memory.ts +0 -235
  192. package/extensions/working-memory/render.ts +0 -185
  193. package/extensions/working-memory/settings.ts +0 -38
  194. package/extensions/working-memory/state.ts +0 -158
  195. package/shared/autoread.ts +0 -234
  196. package/shared/context-messages.ts +0 -19
  197. package/shared/outline-injection.ts +0 -56
  198. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  199. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  200. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  201. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  202. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  203. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.33.1",
3
+ "version": "0.35.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,10 +35,10 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@ast-grep/wasm": "0.45.0",
38
- "@shanepadgett/tau-tui": "0.33.1",
38
+ "@shanepadgett/tau-tui": "0.35.0",
39
39
  "@vscode/tree-sitter-wasm": "0.3.1",
40
40
  "image-size": "2.0.2",
41
- "smol-toml": "1.7.0",
41
+ "smol-toml": "1.7.1",
42
42
  "web-tree-sitter": "0.26.11"
43
43
  },
44
44
  "peerDependencies": {
@@ -12,6 +12,42 @@
12
12
  "extensions": {
13
13
  "type": "object",
14
14
  "properties": {
15
+ "bashApproval": {
16
+ "type": "object",
17
+ "properties": {
18
+ "enabled": {
19
+ "type": "boolean",
20
+ "default": true,
21
+ "description": "Enable bash command review and approval."
22
+ },
23
+ "autoApprove": {
24
+ "type": "boolean",
25
+ "default": true,
26
+ "description": "Run reviewer-approved commands without human confirmation."
27
+ }
28
+ },
29
+ "additionalProperties": false
30
+ },
31
+ "checkpoint": {
32
+ "type": "object",
33
+ "required": [
34
+ "checkpointTokenLimit"
35
+ ],
36
+ "properties": {
37
+ "showToolRows": {
38
+ "type": "boolean",
39
+ "default": false,
40
+ "description": "Show checkpoint and injected-file rows in the TUI for debugging."
41
+ },
42
+ "checkpointTokenLimit": {
43
+ "type": "integer",
44
+ "minimum": 1,
45
+ "default": 150000,
46
+ "description": "Context-token ceiling before a checkpoint is required."
47
+ }
48
+ },
49
+ "additionalProperties": false
50
+ },
15
51
  "context": {
16
52
  "type": "object",
17
53
  "properties": {
@@ -240,10 +276,10 @@
240
276
  "default": true,
241
277
  "description": "Add the lazy-senior-dev build ethos to Tau's system prompt."
242
278
  },
243
- "caveman": {
279
+ "simplified": {
244
280
  "type": "boolean",
245
281
  "default": true,
246
- "description": "Add the terse communication style to Tau's system prompt."
282
+ "description": "Add Simplified Technical English and small-chunk explanations to Tau's system prompt."
247
283
  }
248
284
  },
249
285
  "additionalProperties": false
@@ -262,39 +298,6 @@
262
298
  }
263
299
  },
264
300
  "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
301
  }
299
302
  },
300
303
  "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
  }
@@ -100,7 +142,7 @@ export async function createIsolatedSessionResource(
100
142
  const modelRuntime = await ModelRuntime.create();
101
143
  modelRuntime.registerNativeProvider(inputs.provider);
102
144
  if (inputs.runtimeApiKey !== undefined)
103
- await modelRuntime.setRuntimeApiKey(inputs.model.provider, inputs.runtimeApiKey, { allowNetwork: false });
145
+ await modelRuntime.setRuntimeApiKey(inputs.model.provider, inputs.runtimeApiKey, { signal });
104
146
  if (signal.aborted) throw new Error(`${inputs.label} startup aborted`);
105
147
  const resourceLoader = new DefaultResourceLoader({
106
148
  cwd: inputs.cwd,
@@ -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;
@@ -24,43 +24,38 @@ export interface EffortProviderCandidates {
24
24
  }>;
25
25
  }
26
26
 
27
+ export interface EffortCandidateOptions {
28
+ includeParentModel: boolean;
29
+ preferredProvider?: string;
30
+ }
31
+
27
32
  const MODEL_PREFERENCES: Record<ModelEffort, readonly ProviderPreference[]> = {
28
- low: [
33
+ quick: [
29
34
  {
30
35
  provider: "openai-codex",
31
- models: [
32
- { model: "gpt-5.6-luna", reasoning: "high" },
33
- { model: "gpt-5.5", reasoning: "low" },
34
- ],
36
+ models: [{ model: "gpt-5.6-luna", reasoning: "medium" }],
35
37
  },
36
38
  { provider: "xai", models: [{ model: "grok-4.5", reasoning: "medium" }] },
37
39
  { provider: "anthropic", models: [{ model: "claude-haiku-4-5", reasoning: "high" }] },
38
40
  ],
39
- medium: [
41
+ standard: [
40
42
  {
41
43
  provider: "openai-codex",
42
- models: [
43
- { model: "gpt-5.6-terra", reasoning: "high" },
44
- { model: "gpt-5.5", reasoning: "medium" },
45
- ],
44
+ models: [{ model: "gpt-5.6-luna", reasoning: "max" }],
46
45
  },
47
46
  { provider: "xai", models: [{ model: "grok-4.5", reasoning: "high" }] },
48
47
  { provider: "anthropic", models: [{ model: "claude-sonnet-5", reasoning: "high" }] },
49
48
  ],
50
- high: [
49
+ deep: [
51
50
  {
52
51
  provider: "openai-codex",
53
- models: [
54
- { model: "gpt-5.6-sol", reasoning: "high" },
55
- { model: "gpt-5.5", reasoning: "high" },
56
- ],
52
+ models: [{ model: "gpt-5.6-sol", reasoning: "high" }],
57
53
  },
58
- { provider: "xai", models: [{ model: "grok-4.5", reasoning: "high" }] },
59
54
  {
60
55
  provider: "anthropic",
61
56
  models: [
62
57
  { model: "claude-opus-5", reasoning: "high" },
63
- { model: "claude-opus-4-8", reasoning: "high" },
58
+ { model: "claude-fable-5", reasoning: "low" },
64
59
  ],
65
60
  },
66
61
  ],
@@ -72,7 +67,7 @@ export function effortForSelection(
72
67
  reasoning: string | undefined,
73
68
  ): ModelEffort | undefined {
74
69
  if (!provider || !model || !reasoning) return undefined;
75
- for (const effort of ["high", "medium", "low"] as const) {
70
+ for (const effort of ["deep", "standard", "quick"] as const) {
76
71
  const preference = MODEL_PREFERENCES[effort].find((item) => item.provider === provider);
77
72
  if (preference?.models.some((item) => item.model === model && item.reasoning === reasoning)) return effort;
78
73
  }
@@ -104,13 +99,19 @@ export function resolveEffortProviders(
104
99
  export function resolveEffortCandidates(
105
100
  ctx: Pick<ExtensionContext, "modelRegistry" | "model" | "cwd" | "isProjectTrusted">,
106
101
  effort: ModelEffort,
107
- includeParentModel: boolean,
102
+ options: EffortCandidateOptions,
108
103
  ): Promise<ModelCandidate[]> {
104
+ const preferences = options.preferredProvider
105
+ ? [
106
+ ...MODEL_PREFERENCES[effort].filter(({ provider }) => provider === options.preferredProvider),
107
+ ...MODEL_PREFERENCES[effort].filter(({ provider }) => provider !== options.preferredProvider),
108
+ ]
109
+ : MODEL_PREFERENCES[effort];
109
110
  return resolveCandidates(
110
111
  ctx,
111
- MODEL_PREFERENCES[effort].flatMap((preference) =>
112
+ preferences.flatMap((preference) =>
112
113
  preference.models.map((model) => ({ provider: preference.provider, ...model })),
113
114
  ),
114
- includeParentModel,
115
+ options.includeParentModel,
115
116
  );
116
117
  }
@@ -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