@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.8

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 (82) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/cli.js +4967 -4948
  3. package/dist/types/config/model-registry.d.ts +1 -1
  4. package/dist/types/config/model-resolver.d.ts +5 -1
  5. package/dist/types/config/settings-schema.d.ts +16 -0
  6. package/dist/types/dap/client.d.ts +19 -0
  7. package/dist/types/extensibility/extensions/runner.d.ts +4 -2
  8. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  9. package/dist/types/extensibility/typebox.d.ts +4 -0
  10. package/dist/types/hindsight/client.d.ts +10 -0
  11. package/dist/types/hindsight/config.d.ts +8 -0
  12. package/dist/types/mcp/tool-bridge.d.ts +9 -0
  13. package/dist/types/mnemopi/state.d.ts +2 -0
  14. package/dist/types/modes/components/user-message.d.ts +25 -1
  15. package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
  16. package/dist/types/modes/interactive-mode.d.ts +7 -1
  17. package/dist/types/modes/types.d.ts +14 -1
  18. package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
  19. package/dist/types/sdk.d.ts +1 -0
  20. package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
  21. package/dist/types/session/agent-session.d.ts +78 -2
  22. package/dist/types/task/executor.d.ts +10 -0
  23. package/dist/types/task/index.d.ts +1 -0
  24. package/dist/types/task/prewalk.d.ts +3 -0
  25. package/dist/types/tools/ask.d.ts +10 -0
  26. package/dist/types/tools/tool-timeouts.d.ts +8 -2
  27. package/dist/types/utils/changelog.d.ts +4 -6
  28. package/package.json +12 -13
  29. package/src/cli/models-cli.ts +37 -17
  30. package/src/cli/update-cli.ts +14 -1
  31. package/src/config/model-registry.ts +47 -24
  32. package/src/config/model-resolver.ts +56 -31
  33. package/src/config/settings-schema.ts +5 -0
  34. package/src/config/settings.ts +68 -5
  35. package/src/dap/client.ts +86 -7
  36. package/src/edit/diff.ts +4 -4
  37. package/src/eval/py/prelude.py +5 -5
  38. package/src/export/html/template.js +23 -9
  39. package/src/extensibility/extensions/runner.ts +9 -4
  40. package/src/extensibility/extensions/types.ts +3 -0
  41. package/src/extensibility/typebox.ts +22 -9
  42. package/src/hindsight/client.test.ts +42 -0
  43. package/src/hindsight/client.ts +40 -3
  44. package/src/hindsight/config.ts +18 -0
  45. package/src/launch/broker.ts +31 -5
  46. package/src/lsp/index.ts +1 -1
  47. package/src/main.ts +15 -5
  48. package/src/mcp/tool-bridge.ts +9 -0
  49. package/src/mnemopi/state.ts +70 -3
  50. package/src/modes/components/agent-dashboard.ts +6 -2
  51. package/src/modes/components/chat-transcript-builder.ts +13 -2
  52. package/src/modes/components/diff.ts +2 -2
  53. package/src/modes/components/plan-review-overlay.ts +20 -4
  54. package/src/modes/components/user-message.ts +100 -1
  55. package/src/modes/controllers/command-controller.ts +22 -5
  56. package/src/modes/controllers/event-controller.ts +4 -1
  57. package/src/modes/controllers/extension-ui-controller.ts +18 -0
  58. package/src/modes/controllers/input-controller.ts +47 -12
  59. package/src/modes/controllers/selector-controller.ts +82 -6
  60. package/src/modes/interactive-mode.ts +36 -4
  61. package/src/modes/types.ts +16 -3
  62. package/src/modes/utils/ui-helpers.ts +40 -20
  63. package/src/prompts/system/workflow-notice.md +1 -1
  64. package/src/sdk.ts +134 -48
  65. package/src/session/agent-session-error-log.test.ts +59 -0
  66. package/src/session/agent-session.ts +300 -26
  67. package/src/system-prompt.ts +1 -2
  68. package/src/task/executor.ts +41 -27
  69. package/src/task/index.ts +11 -0
  70. package/src/task/prewalk.ts +6 -0
  71. package/src/task/worktree.ts +25 -11
  72. package/src/tools/ask.ts +15 -0
  73. package/src/tools/bash.ts +14 -6
  74. package/src/tools/browser.ts +1 -1
  75. package/src/tools/debug.ts +1 -1
  76. package/src/tools/eval.ts +4 -5
  77. package/src/tools/fetch.ts +1 -1
  78. package/src/tools/hub/launch.ts +7 -0
  79. package/src/tools/tool-timeouts.ts +10 -3
  80. package/src/tools/write.ts +31 -0
  81. package/src/utils/changelog.ts +54 -58
  82. package/src/utils/git.ts +57 -49
package/src/sdk.ts CHANGED
@@ -119,7 +119,7 @@ import {
119
119
  obfuscateProviderContext,
120
120
  SecretObfuscator,
121
121
  } from "./secrets";
122
- import { AgentSession, type PlanYolo, type Prewalk } from "./session/agent-session";
122
+ import { AgentSession, type InitialRetryFallbackState, type PlanYolo, type Prewalk } from "./session/agent-session";
123
123
  import { discoverAuthStorage as discoverAuthStorageFromConfig } from "./session/auth-broker-config";
124
124
  import type { AuthStorage } from "./session/auth-storage";
125
125
  import { createInterruptedTurnAbortMessage } from "./session/exit-diagnostics";
@@ -907,7 +907,7 @@ function registerEvalCleanup(): void {
907
907
  postmortem.register("julia-cleanup", disposeAllJuliaKernelSessions);
908
908
  }
909
909
 
910
- function customToolToDefinition(tool: CustomTool): ToolDefinition {
910
+ export function customToolToDefinition(tool: CustomTool): ToolDefinition {
911
911
  const definition: ToolDefinition & { [TOOL_DEFINITION_MARKER]: true } = {
912
912
  name: tool.name,
913
913
  label: tool.label,
@@ -917,6 +917,9 @@ function customToolToDefinition(tool: CustomTool): ToolDefinition {
917
917
  loadMode: defaultLoadModeForToolName(tool.name, tool.loadMode),
918
918
  deferrable: tool.deferrable,
919
919
  approval: typeof tool.approval === "function" ? tool.approval.bind(tool) : tool.approval,
920
+ // Preserved through RegisteredToolAdapter so MCP-backed tools' explicit
921
+ // `strict: false` (#4336/#4340) survives the custom-tool → definition bridge.
922
+ strict: tool.strict,
920
923
  mcpServerName: tool.mcpServerName,
921
924
  mcpToolName: tool.mcpToolName,
922
925
  execute: (toolCallId, params, signal, onUpdate, ctx) =>
@@ -1387,6 +1390,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1387
1390
  );
1388
1391
  let model = options.model;
1389
1392
  let modelFallbackMessage: string | undefined;
1393
+ let initialRetryFallback: InitialRetryFallbackState | undefined;
1390
1394
  // Identify session model strings to restore in fallback order. We do an
1391
1395
  // initial pass here so model-dependent setup (thinking-level resolution,
1392
1396
  // host preconnect) can use the restored model; extension-registered
@@ -2085,26 +2089,70 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2085
2089
  preferences: matchPreferences,
2086
2090
  });
2087
2091
  if (resolved.configuredPatterns && resolved.configuredPatterns.length > 0) {
2088
- return resolved.configuredPatterns;
2092
+ const primaryPatterns: Array<{
2093
+ pattern: string;
2094
+ retryFallback: InitialRetryFallbackState | undefined;
2095
+ }> = resolved.configuredPatterns.map(pattern => ({
2096
+ pattern,
2097
+ retryFallback: undefined,
2098
+ }));
2099
+ if (!resolved.configuredRole || !settings.get("retry.modelFallback")) {
2100
+ return primaryPatterns;
2101
+ }
2102
+ const fallbackChains = settings.get("retry.fallbackChains");
2103
+ const roleFallbacks =
2104
+ fallbackChains[resolved.configuredRole] ??
2105
+ (resolved.configuredRole === "default" ? undefined : fallbackChains.default);
2106
+ if (!Array.isArray(roleFallbacks)) return primaryPatterns;
2107
+ const originalSelector = resolved.configuredPatterns[0];
2108
+ const parsedOriginal = parseModelString(originalSelector, {
2109
+ allowMaxSuffix: true,
2110
+ allowAutoAlias: true,
2111
+ isLiteralModelId: (provider, id) => modelRegistry.find(provider, id) !== undefined,
2112
+ });
2113
+ const retryFallback: InitialRetryFallbackState = {
2114
+ role: resolved.configuredRole,
2115
+ originalSelector,
2116
+ originalThinkingLevel: parsedOriginal?.thinkingLevel,
2117
+ };
2118
+ return [
2119
+ ...primaryPatterns,
2120
+ ...roleFallbacks
2121
+ .filter((pattern): pattern is string => typeof pattern === "string")
2122
+ .map(pattern => ({ pattern, retryFallback })),
2123
+ ];
2089
2124
  }
2090
2125
  if (resolved.model) {
2091
2126
  return [
2092
- formatModelSelectorValue(
2093
- resolved.selector ?? formatModelStringWithRouting(resolved.model),
2094
- resolved.thinkingLevel,
2095
- ),
2127
+ {
2128
+ pattern: formatModelSelectorValue(
2129
+ resolved.selector ?? formatModelStringWithRouting(resolved.model),
2130
+ resolved.thinkingLevel,
2131
+ ),
2132
+ retryFallback: undefined,
2133
+ },
2096
2134
  ];
2097
2135
  }
2098
- return resolveConfiguredModelPatterns([trimmedSelector], settings);
2136
+ return resolveConfiguredModelPatterns([trimmedSelector], settings).map(pattern => ({
2137
+ pattern,
2138
+ retryFallback: undefined,
2139
+ }));
2099
2140
  }),
2100
2141
  );
2101
2142
  for (let patternIndex = 0; patternIndex < expandedModelPatterns.length; patternIndex += 1) {
2102
- const pattern = expandedModelPatterns[patternIndex];
2143
+ const { pattern, retryFallback } = expandedModelPatterns[patternIndex];
2103
2144
  const primary = parseModelPattern(pattern, availableModels, matchPreferences);
2104
- if (!primary.model) continue;
2145
+ if (!primary.model || (retryFallback && !hasModelAuth(primary.model))) continue;
2105
2146
  let selectedModel = primary.model;
2106
2147
  let selectedThinkingLevel = primary.thinkingLevel;
2107
2148
  let selectedExplicitThinkingLevel = primary.explicitThinkingLevel;
2149
+ // A chain entry without its own `:level` suffix inherits the
2150
+ // unavailable primary's configured thinking level, matching
2151
+ // runtime fallback-chain semantics.
2152
+ if (retryFallback && !selectedExplicitThinkingLevel && retryFallback.originalThinkingLevel !== undefined) {
2153
+ selectedThinkingLevel = retryFallback.originalThinkingLevel;
2154
+ selectedExplicitThinkingLevel = true;
2155
+ }
2108
2156
  let authFallbackUsed = false;
2109
2157
  if (options.modelPatternAuthFallback) {
2110
2158
  const primaryKey = await modelRegistry.getApiKey(primary.model);
@@ -2132,8 +2180,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2132
2180
  );
2133
2181
  const seenSelectors = new Set<string>([primarySelector]);
2134
2182
  const fallbackSelectors: string[] = [];
2135
- for (const fallbackPattern of expandedModelPatterns.slice(patternIndex + 1)) {
2136
- const fallback = parseModelPattern(fallbackPattern, availableModels, matchPreferences);
2183
+ for (const fallbackEntry of expandedModelPatterns.slice(patternIndex + 1)) {
2184
+ const fallback = parseModelPattern(fallbackEntry.pattern, availableModels, matchPreferences);
2137
2185
  if (!fallback.model) continue;
2138
2186
  const fallbackSelector = formatModelSelectorValue(
2139
2187
  formatModelStringWithRouting(fallback.model),
@@ -2174,6 +2222,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2174
2222
  }
2175
2223
  }
2176
2224
  model = selectedModel;
2225
+ initialRetryFallback = retryFallback;
2177
2226
  modelFallbackMessage = undefined;
2178
2227
  if (selectedExplicitThinkingLevel) {
2179
2228
  restoredSessionThinkingLevel = selectedThinkingLevel;
@@ -2202,48 +2251,84 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2202
2251
  // path-scoped `enabledModels` allow-list when configured. Skip when the
2203
2252
  // user explicitly requested a model via --model that wasn't found.
2204
2253
  if (!model && deferredModelPatterns.length === 0) {
2205
- // Re-resolve the allowed set: extension factories above may have
2206
- // registered providers/models that weren't visible at startup.
2207
- const fallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
2208
-
2209
- // Retry the default-role lookup against the post-extension allowed
2210
- // set. Extension factories register providers AFTER the early
2211
- // `defaultRoleSpec` resolution, so a role pointing at an extension
2212
- // model (e.g. an openai-compat plugin's `posthog/claude-opus-4-8`)
2213
- // returned `undefined` there. Without this retry the next step's
2214
- // `pickDefaultAvailableModel` happily replaces the user's configured
2215
- // default with a bundled provider's default whenever a stray
2216
- // `OPENAI_API_KEY`/`ANTHROPIC_API_KEY` is in the environment.
2217
- // (issue #3569)
2218
- if (!hasExplicitModel && !defaultRoleSpec.model) {
2254
+ // Retry the configured default role against the current catalog,
2255
+ // setting `model` (+ thinking level) when it resolves. Extension
2256
+ // factories register providers AFTER the early `defaultRoleSpec`
2257
+ // resolution, and configured discovery providers may still be
2258
+ // mid-discovery, so a role pointing at such a model (an openai-compat
2259
+ // plugin's `posthog/claude-opus-4-8`, a models.yml `openai-models-list`
2260
+ // endpoint) returned `undefined` there. Without this retry the
2261
+ // `pickDefaultAvailableModel` fallback below happily replaces the
2262
+ // user's configured default with a bundled provider's default whenever
2263
+ // a stray `OPENAI_API_KEY`/`ANTHROPIC_API_KEY` is in the environment.
2264
+ // (issues #3569, #6162)
2265
+ const tryResolveDefaultRole = async (): Promise<boolean> => {
2266
+ if (hasExplicitModel) return false;
2267
+ // Re-resolve the allowed set: extension factories and discovery
2268
+ // refreshes above may have registered models not visible earlier.
2269
+ const fallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
2219
2270
  const reResolvedRoleSpec = resolveModelRoleValue(settings.getModelRole("default"), fallbackCandidates, {
2220
2271
  settings,
2221
2272
  matchPreferences: modelMatchPreferences,
2222
2273
  });
2223
- if (reResolvedRoleSpec.model) {
2224
- defaultRoleSpec = reResolvedRoleSpec;
2225
- const resolvedDefaultModel = reResolvedRoleSpec.model;
2226
- model = resolvedDefaultModel;
2227
- modelFallbackMessage = undefined;
2228
- // Recompute the thinking level against the now-real model.
2229
- // `pickInitialThinkingLevel` closes over `defaultRoleSpec`,
2230
- // so the role's explicit selector (e.g. `:max`) now applies.
2231
- thinkingLevel = pickInitialThinkingLevel(resolvedDefaultModel);
2232
- autoThinking = thinkingLevel === AUTO_THINKING;
2233
- effectiveThinkingLevel = concreteThinkingLevel(thinkingLevel);
2234
- effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
2235
- autoThinking
2236
- ? resolveProvisionalAutoLevel(resolvedDefaultModel)
2237
- : resolveThinkingLevelForModel(resolvedDefaultModel, effectiveThinkingLevel),
2238
- );
2239
- preconnectModelHost(resolvedDefaultModel.baseUrl);
2240
- }
2241
- }
2274
+ if (!reResolvedRoleSpec.model) return false;
2275
+ defaultRoleSpec = reResolvedRoleSpec;
2276
+ const resolvedDefaultModel = reResolvedRoleSpec.model;
2277
+ model = resolvedDefaultModel;
2278
+ modelFallbackMessage = undefined;
2279
+ // Recompute the thinking level against the now-real model.
2280
+ // `pickInitialThinkingLevel` closes over `defaultRoleSpec`,
2281
+ // so the role's explicit selector (e.g. `:max`) now applies.
2282
+ thinkingLevel = pickInitialThinkingLevel(resolvedDefaultModel);
2283
+ autoThinking = thinkingLevel === AUTO_THINKING;
2284
+ effectiveThinkingLevel = concreteThinkingLevel(thinkingLevel);
2285
+ effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
2286
+ autoThinking
2287
+ ? resolveProvisionalAutoLevel(resolvedDefaultModel)
2288
+ : resolveThinkingLevelForModel(resolvedDefaultModel, effectiveThinkingLevel),
2289
+ );
2290
+ preconnectModelHost(resolvedDefaultModel.baseUrl);
2291
+ return true;
2292
+ };
2293
+
2294
+ await tryResolveDefaultRole();
2242
2295
 
2243
2296
  if (!model) {
2244
- const defaultModel = pickDefaultAvailableModel(fallbackCandidates.filter(hasModelAuth));
2245
- if (defaultModel) {
2246
- model = defaultModel;
2297
+ const fallbackCandidates = await resolveAllowedModels(modelRegistry, settings, modelMatchPreferences);
2298
+ let pick = pickDefaultAvailableModel(fallbackCandidates.filter(hasModelAuth));
2299
+
2300
+ // Cold-cache discovery race (issues #6114, #6162): a discovery
2301
+ // provider (models.yml `openai-models-list`, LM Studio/Ollama/
2302
+ // llama.cpp, or an openai-compat proxy) ships no static models, so
2303
+ // the static+cached catalog resolved nothing above. Background
2304
+ // discovery in main.ts fires only AFTER createAgentSession returns,
2305
+ // so on a cache-cold boot the configured default stays unresolved
2306
+ // and `pick` silently degrades to an unrelated authed provider's
2307
+ // default (#6162) or "No models available" (#6114) — even though
2308
+ // `omp models` (which awaits discovery) lists the model. Await one
2309
+ // cache-aware discovery pass and retry when a default role is
2310
+ // configured (must win over `pick`) or nothing resolved at all.
2311
+ // The common path — role already resolved, or a `pick` with no
2312
+ // configured default — never pays for it.
2313
+ const defaultRoleConfigured = Boolean(settings.getModelRole("default"));
2314
+ if (
2315
+ !hasExplicitModel &&
2316
+ (defaultRoleConfigured || !pick) &&
2317
+ modelRegistry.getDiscoverableProviders().length > 0
2318
+ ) {
2319
+ await logger.time("resolveModelDiscoveryFallback", () => modelRegistry.refresh("online-if-uncached"));
2320
+ if (!(await tryResolveDefaultRole()) && !model) {
2321
+ const refreshedCandidates = await resolveAllowedModels(
2322
+ modelRegistry,
2323
+ settings,
2324
+ modelMatchPreferences,
2325
+ );
2326
+ pick = pickDefaultAvailableModel(refreshedCandidates.filter(hasModelAuth));
2327
+ }
2328
+ }
2329
+
2330
+ if (!model && pick) {
2331
+ model = pick;
2247
2332
  }
2248
2333
  }
2249
2334
  if (model) {
@@ -2918,6 +3003,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2918
3003
  agent,
2919
3004
  pruneToolDescriptions: inlineToolDescriptors,
2920
3005
  thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
3006
+ initialRetryFallback,
2921
3007
  prewalk: options.prewalk,
2922
3008
  planYolo: options.planYolo,
2923
3009
  serviceTierByFamily: initialServiceTierByFamily,
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Contract: a turn ending in a provider error surfaces one warn-level log
3
+ * carrying `provider`/`model`/`errorMessage`/`errorStatus`/`errorId`, so
4
+ * recurring provider stream failures are diagnosable from the main log alone
5
+ * (issue #6177). Non-error stops must not emit it.
6
+ */
7
+ import { afterAll, afterEach, describe, expect, it } from "bun:test";
8
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
9
+ import { logger } from "@oh-my-pi/pi-utils";
10
+ import { logProviderTurnError } from "./agent-session";
11
+
12
+ function makeMessage(overrides: Partial<AssistantMessage>): AssistantMessage {
13
+ return {
14
+ role: "assistant",
15
+ content: [],
16
+ api: "cursor",
17
+ provider: "cursor",
18
+ model: "composer-2.5",
19
+ usage: {} as AssistantMessage["usage"],
20
+ stopReason: "error",
21
+ timestamp: 1,
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ describe("logProviderTurnError", () => {
27
+ const events: logger.LogEvent[] = [];
28
+ const dispose = logger.registerLogSink(event => events.push(event));
29
+
30
+ afterEach(() => {
31
+ events.length = 0;
32
+ });
33
+ afterAll(() => dispose());
34
+
35
+ it("emits a warn log with the provider error fields for stopReason:error", () => {
36
+ logProviderTurnError(
37
+ makeMessage({
38
+ errorMessage: "stream stall: idle watchdog fired",
39
+ errorStatus: 500,
40
+ errorId: 42,
41
+ }),
42
+ );
43
+
44
+ const warn = events.find(e => e.level === "warn" && e.message === "agent turn ended with provider error");
45
+ expect(warn).toBeDefined();
46
+ expect(warn?.context).toMatchObject({
47
+ provider: "cursor",
48
+ model: "composer-2.5",
49
+ errorMessage: "stream stall: idle watchdog fired",
50
+ errorStatus: 500,
51
+ errorId: 42,
52
+ });
53
+ });
54
+
55
+ it("does not emit for a successful stop", () => {
56
+ logProviderTurnError(makeMessage({ stopReason: "stop", errorMessage: undefined }));
57
+ expect(events.some(e => e.message === "agent turn ended with provider error")).toBe(false);
58
+ });
59
+ });