@oh-my-pi/pi-coding-agent 16.0.5 → 16.0.7

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 (223) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/cli.js +1945 -1386
  3. package/dist/types/advisor/advise-tool.d.ts +22 -19
  4. package/dist/types/autoresearch/tools/init-experiment.d.ts +13 -17
  5. package/dist/types/autoresearch/tools/log-experiment.d.ts +17 -19
  6. package/dist/types/autoresearch/tools/run-experiment.d.ts +3 -4
  7. package/dist/types/autoresearch/tools/update-notes.d.ts +4 -5
  8. package/dist/types/cli/ttsr-cli.d.ts +39 -0
  9. package/dist/types/commands/ttsr.d.ts +57 -0
  10. package/dist/types/commit/agentic/tools/analyze-file.d.ts +4 -5
  11. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +4 -5
  12. package/dist/types/commit/agentic/tools/git-hunk.d.ts +5 -6
  13. package/dist/types/commit/agentic/tools/git-overview.d.ts +4 -5
  14. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +23 -24
  15. package/dist/types/commit/agentic/tools/propose-commit.d.ts +11 -32
  16. package/dist/types/commit/agentic/tools/recent-commits.d.ts +3 -4
  17. package/dist/types/commit/agentic/tools/schemas.d.ts +6 -27
  18. package/dist/types/commit/agentic/tools/split-commit.d.ts +28 -49
  19. package/dist/types/commit/changelog/generate.d.ts +12 -13
  20. package/dist/types/commit/shared-llm.d.ts +10 -37
  21. package/dist/types/config/config-file.d.ts +4 -4
  22. package/dist/types/config/keybindings.d.ts +5 -0
  23. package/dist/types/config/models-config-schema.d.ts +625 -990
  24. package/dist/types/config/models-config.d.ts +229 -217
  25. package/dist/types/config/settings-schema.d.ts +53 -23
  26. package/dist/types/edit/hashline/params.d.ts +7 -11
  27. package/dist/types/edit/index.d.ts +2 -1
  28. package/dist/types/edit/modes/apply-patch.d.ts +4 -5
  29. package/dist/types/edit/modes/patch.d.ts +15 -24
  30. package/dist/types/edit/modes/replace.d.ts +16 -17
  31. package/dist/types/eval/js/index.d.ts +1 -0
  32. package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
  33. package/dist/types/extensibility/custom-tools/types.d.ts +8 -5
  34. package/dist/types/extensibility/extensions/types.d.ts +6 -3
  35. package/dist/types/extensibility/hooks/types.d.ts +7 -4
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +13 -5
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +17 -0
  38. package/dist/types/extensibility/typebox.d.ts +80 -58
  39. package/dist/types/goals/tools/goal-tool.d.ts +11 -24
  40. package/dist/types/index.d.ts +2 -0
  41. package/dist/types/lsp/index.d.ts +11 -26
  42. package/dist/types/lsp/types.d.ts +12 -28
  43. package/dist/types/mcp/client.d.ts +8 -0
  44. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  46. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  47. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  48. package/dist/types/modes/interactive-mode.d.ts +3 -0
  49. package/dist/types/modes/setup-wizard/index.d.ts +1 -0
  50. package/dist/types/modes/setup-wizard/startup-splash.d.ts +7 -0
  51. package/dist/types/modes/theme/theme.d.ts +1 -1
  52. package/dist/types/modes/types.d.ts +3 -0
  53. package/dist/types/sdk.d.ts +5 -0
  54. package/dist/types/session/agent-session.d.ts +4 -0
  55. package/dist/types/startup-splash.d.ts +12 -0
  56. package/dist/types/task/types.d.ts +47 -48
  57. package/dist/types/tools/ask.d.ts +26 -27
  58. package/dist/types/tools/ast-edit.d.ts +17 -17
  59. package/dist/types/tools/ast-grep.d.ts +12 -13
  60. package/dist/types/tools/bash.d.ts +20 -17
  61. package/dist/types/tools/browser.d.ts +46 -71
  62. package/dist/types/tools/checkpoint.d.ts +14 -15
  63. package/dist/types/tools/debug.d.ts +82 -145
  64. package/dist/types/tools/eval.d.ts +30 -40
  65. package/dist/types/tools/find.d.ts +17 -18
  66. package/dist/types/tools/gh.d.ts +49 -78
  67. package/dist/types/tools/image-gen.d.ts +20 -36
  68. package/dist/types/tools/inspect-image.d.ts +10 -11
  69. package/dist/types/tools/irc.d.ts +22 -33
  70. package/dist/types/tools/job.d.ts +11 -12
  71. package/dist/types/tools/learn.d.ts +21 -28
  72. package/dist/types/tools/manage-skill.d.ts +13 -22
  73. package/dist/types/tools/memory-edit.d.ts +15 -24
  74. package/dist/types/tools/memory-recall.d.ts +7 -8
  75. package/dist/types/tools/memory-reflect.d.ts +9 -10
  76. package/dist/types/tools/memory-retain.d.ts +13 -14
  77. package/dist/types/tools/read.d.ts +7 -8
  78. package/dist/types/tools/resolve.d.ts +11 -18
  79. package/dist/types/tools/review.d.ts +9 -15
  80. package/dist/types/tools/search-tool-bm25.d.ts +9 -10
  81. package/dist/types/tools/search.d.ts +16 -17
  82. package/dist/types/tools/ssh.d.ts +14 -15
  83. package/dist/types/tools/todo.d.ts +27 -43
  84. package/dist/types/tools/tts.d.ts +8 -9
  85. package/dist/types/tools/write.d.ts +9 -10
  86. package/dist/types/tui/index.d.ts +1 -0
  87. package/dist/types/tui/width-aware-text.d.ts +23 -0
  88. package/dist/types/utils/markit.d.ts +10 -1
  89. package/dist/types/web/search/index.d.ts +17 -28
  90. package/dist/types/web/search/providers/perplexity.d.ts +0 -2
  91. package/dist/types/web/search/types.d.ts +32 -26
  92. package/package.json +14 -13
  93. package/scripts/omp +1 -1
  94. package/src/advisor/__tests__/advisor.test.ts +44 -1
  95. package/src/advisor/advise-tool.ts +34 -11
  96. package/src/autoresearch/tools/init-experiment.ts +13 -16
  97. package/src/autoresearch/tools/log-experiment.ts +15 -18
  98. package/src/autoresearch/tools/run-experiment.ts +3 -3
  99. package/src/autoresearch/tools/update-notes.ts +4 -4
  100. package/src/cli/ttsr-cli.ts +995 -0
  101. package/src/cli-commands.ts +1 -0
  102. package/src/cli.ts +7 -1
  103. package/src/commands/ttsr.ts +125 -0
  104. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  105. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  106. package/src/commit/agentic/tools/git-hunk.ts +7 -5
  107. package/src/commit/agentic/tools/git-overview.ts +4 -4
  108. package/src/commit/agentic/tools/propose-changelog.ts +18 -15
  109. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  110. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  111. package/src/commit/agentic/tools/schemas.ts +8 -20
  112. package/src/commit/agentic/tools/split-commit.ts +19 -23
  113. package/src/commit/analysis/summary.ts +7 -5
  114. package/src/commit/changelog/generate.ts +15 -11
  115. package/src/commit/shared-llm.ts +17 -24
  116. package/src/config/config-file.ts +13 -15
  117. package/src/config/keybindings.ts +6 -0
  118. package/src/config/models-config-schema.ts +206 -179
  119. package/src/config/settings-schema.ts +34 -0
  120. package/src/discovery/builtin-rules/index.ts +2 -0
  121. package/src/discovery/builtin-rules/ts-import-type.md +2 -2
  122. package/src/discovery/builtin-rules/ts-no-any.md +11 -2
  123. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +55 -0
  124. package/src/edit/hashline/params.ts +12 -11
  125. package/src/edit/index.ts +5 -4
  126. package/src/edit/modes/apply-patch.ts +4 -4
  127. package/src/edit/modes/patch.ts +15 -18
  128. package/src/edit/modes/replace.ts +13 -17
  129. package/src/edit/renderer.ts +0 -1
  130. package/src/eval/agent-bridge.ts +11 -13
  131. package/src/eval/completion-bridge.ts +25 -17
  132. package/src/eval/js/context-manager.ts +17 -2
  133. package/src/eval/js/index.ts +1 -1
  134. package/src/eval/py/executor.ts +2 -2
  135. package/src/extensibility/custom-commands/loader.ts +5 -3
  136. package/src/extensibility/custom-commands/types.ts +6 -3
  137. package/src/extensibility/custom-tools/loader.ts +4 -2
  138. package/src/extensibility/custom-tools/types.ts +8 -5
  139. package/src/extensibility/extensions/loader.ts +4 -2
  140. package/src/extensibility/extensions/types.ts +6 -3
  141. package/src/extensibility/hooks/loader.ts +5 -2
  142. package/src/extensibility/hooks/types.ts +7 -4
  143. package/src/extensibility/legacy-pi-ai-shim.ts +42 -5
  144. package/src/extensibility/legacy-pi-coding-agent-shim.ts +113 -0
  145. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -13
  146. package/src/extensibility/tool-proxy.ts +4 -1
  147. package/src/extensibility/typebox.ts +778 -251
  148. package/src/goals/guided-setup.ts +12 -3
  149. package/src/goals/tools/goal-tool.ts +6 -6
  150. package/src/index.ts +2 -0
  151. package/src/internal-urls/docs-index.generated.ts +11 -9
  152. package/src/lsp/types.ts +13 -27
  153. package/src/main.ts +19 -18
  154. package/src/mcp/client.ts +38 -13
  155. package/src/mcp/render.ts +102 -89
  156. package/src/modes/components/agent-hub.ts +11 -4
  157. package/src/modes/components/btw-panel.ts +5 -1
  158. package/src/modes/components/custom-editor.ts +18 -0
  159. package/src/modes/components/status-line/component.ts +8 -1
  160. package/src/modes/components/tool-execution.ts +17 -10
  161. package/src/modes/controllers/btw-controller.ts +69 -1
  162. package/src/modes/controllers/input-controller.ts +29 -0
  163. package/src/modes/interactive-mode.ts +38 -8
  164. package/src/modes/setup-wizard/index.ts +1 -0
  165. package/src/modes/setup-wizard/scenes/sign-in.ts +77 -5
  166. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  167. package/src/modes/theme/theme.ts +133 -143
  168. package/src/modes/types.ts +3 -0
  169. package/src/modes/utils/context-usage.ts +9 -5
  170. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  171. package/src/prompts/system/system-prompt.md +1 -0
  172. package/src/sdk.ts +21 -4
  173. package/src/session/agent-session.ts +173 -33
  174. package/src/session/session-history-format.ts +11 -2
  175. package/src/session/snapcompact-inline.ts +1 -1
  176. package/src/slash-commands/builtin-registry.ts +3 -10
  177. package/src/startup-splash.ts +19 -0
  178. package/src/task/executor.ts +11 -6
  179. package/src/task/types.ts +44 -41
  180. package/src/tool-discovery/tool-index.ts +17 -4
  181. package/src/tools/ask.ts +14 -14
  182. package/src/tools/ast-edit.ts +17 -14
  183. package/src/tools/ast-grep.ts +10 -9
  184. package/src/tools/bash.ts +15 -10
  185. package/src/tools/browser/launch.ts +13 -0
  186. package/src/tools/browser.ts +26 -32
  187. package/src/tools/checkpoint.ts +7 -7
  188. package/src/tools/debug.ts +72 -69
  189. package/src/tools/eval.ts +18 -19
  190. package/src/tools/find.ts +20 -13
  191. package/src/tools/gh.ts +29 -49
  192. package/src/tools/image-gen.ts +27 -32
  193. package/src/tools/inspect-image.ts +8 -9
  194. package/src/tools/irc.ts +12 -12
  195. package/src/tools/job.ts +6 -6
  196. package/src/tools/learn.ts +11 -14
  197. package/src/tools/manage-skill.ts +19 -23
  198. package/src/tools/memory-edit.ts +8 -8
  199. package/src/tools/memory-recall.ts +4 -4
  200. package/src/tools/memory-reflect.ts +5 -5
  201. package/src/tools/memory-retain.ts +9 -11
  202. package/src/tools/puppeteer/02_stealth_hairline.txt +1 -1
  203. package/src/tools/puppeteer/04_stealth_iframe.txt +4 -4
  204. package/src/tools/puppeteer/05_stealth_webgl.txt +1 -1
  205. package/src/tools/puppeteer/10_stealth_plugins.txt +6 -4
  206. package/src/tools/puppeteer/12_stealth_codecs.txt +2 -2
  207. package/src/tools/puppeteer/13_stealth_worker.txt +1 -1
  208. package/src/tools/read.ts +169 -13
  209. package/src/tools/report-tool-issue.ts +6 -6
  210. package/src/tools/resolve.ts +6 -6
  211. package/src/tools/review.ts +10 -12
  212. package/src/tools/search-tool-bm25.ts +5 -5
  213. package/src/tools/search.ts +20 -29
  214. package/src/tools/ssh.ts +8 -8
  215. package/src/tools/todo.ts +16 -19
  216. package/src/tools/tts.ts +16 -15
  217. package/src/tools/write.ts +5 -5
  218. package/src/tui/index.ts +1 -0
  219. package/src/tui/width-aware-text.ts +58 -0
  220. package/src/utils/markit.ts +17 -2
  221. package/src/web/search/index.ts +9 -9
  222. package/src/web/search/providers/perplexity.ts +373 -126
  223. package/src/web/search/types.ts +28 -48
@@ -32,11 +32,11 @@ import {
32
32
  AppendOnlyContextManager,
33
33
  type AsideMessage,
34
34
  type CompactionSummaryMessage,
35
+ countTokens,
35
36
  resolveTelemetry,
36
37
  STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL,
37
38
  ThinkingLevel,
38
39
  } from "@oh-my-pi/pi-agent-core";
39
-
40
40
  import {
41
41
  AGGRESSIVE_SHAKE_CONFIG,
42
42
  AUTO_HANDOFF_THRESHOLD_FOCUS,
@@ -104,7 +104,7 @@ import {
104
104
  } from "@oh-my-pi/pi-ai";
105
105
  import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
106
106
  import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
107
- import { countTokens, MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
107
+ import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
108
108
  import {
109
109
  extractRetryHint,
110
110
  formatDuration,
@@ -150,7 +150,7 @@ import {
150
150
  resolveModelRoleValue,
151
151
  resolveRoleSelection,
152
152
  } from "../config/model-resolver";
153
- import { MODEL_ROLE_IDS } from "../config/model-roles";
153
+ import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
154
154
  import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
155
155
  import type { Settings, SkillsSettings } from "../config/settings";
156
156
  import { onAppendOnlyModeChanged } from "../config/settings";
@@ -1755,6 +1755,24 @@ export class AgentSession {
1755
1755
  systemPrompt.push(this.#advisorWatchdogPrompt);
1756
1756
  }
1757
1757
  const advisorSessionId = this.sessionId ? `${this.sessionId}-advisor` : undefined;
1758
+
1759
+ // Thread the primary's telemetry into the advisor loop so the advisor
1760
+ // model's GenAI spans + usage/cost hooks fire like every other model call,
1761
+ // stamped with the advisor's own identity. `conversationId` is cleared so
1762
+ // the advisor loop falls back to its own `-advisor` session id for
1763
+ // `gen_ai.conversation.id` instead of inheriting the primary's
1764
+ // conversation; undefined telemetry stays undefined (zero-overhead no-op).
1765
+ const advisorTelemetry = this.agent.telemetry
1766
+ ? {
1767
+ ...this.agent.telemetry,
1768
+ agent: {
1769
+ id: advisorSessionId,
1770
+ name: MODEL_ROLES.advisor.name,
1771
+ description: formatModelString(advisorSel.model),
1772
+ },
1773
+ conversationId: undefined,
1774
+ }
1775
+ : undefined;
1758
1776
  const advisorAgent = new Agent({
1759
1777
  initialState: {
1760
1778
  systemPrompt,
@@ -1766,6 +1784,7 @@ export class AgentSession {
1766
1784
  sessionId: advisorSessionId,
1767
1785
  getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorSessionId),
1768
1786
  intentTracing: false,
1787
+ telemetry: advisorTelemetry,
1769
1788
  });
1770
1789
  advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
1771
1790
 
@@ -1941,24 +1960,26 @@ export class AgentSession {
1941
1960
 
1942
1961
  let compactResult: CompactionResult | undefined;
1943
1962
  let lastError: unknown;
1963
+ const advisorSessionId = this.sessionId ? `${this.sessionId}-advisor` : undefined;
1964
+ // Instrument the advisor's overflow-compaction one-shot like the primary
1965
+ // compaction path so the advisor model's maintenance call also emits spans.
1966
+ const telemetry = resolveTelemetry(advisor.telemetry, advisorSessionId);
1944
1967
 
1945
1968
  for (const candidate of candidates) {
1946
- const apiKey = await this.#modelRegistry.getApiKey(
1947
- candidate,
1948
- this.sessionId ? `${this.sessionId}-advisor` : undefined,
1949
- );
1969
+ const apiKey = await this.#modelRegistry.getApiKey(candidate, advisorSessionId);
1950
1970
  if (!apiKey) continue;
1951
1971
 
1952
1972
  try {
1953
1973
  compactResult = await compact(
1954
1974
  preparation,
1955
1975
  candidate,
1956
- this.#modelRegistry.resolver(candidate, this.sessionId ? `${this.sessionId}-advisor` : undefined),
1976
+ this.#modelRegistry.resolver(candidate, advisorSessionId),
1957
1977
  undefined,
1958
1978
  undefined,
1959
1979
  {
1960
1980
  thinkingLevel: advisorCompactionThinkingLevel,
1961
1981
  convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
1982
+ telemetry,
1962
1983
  },
1963
1984
  );
1964
1985
  break;
@@ -2478,6 +2499,13 @@ export class AgentSession {
2478
2499
  });
2479
2500
  this.#retryAttempt = 0;
2480
2501
  }
2502
+ if (assistantMsg.provider === "opencode-go") {
2503
+ this.#modelRegistry.authStorage.recordUsageCost(assistantMsg.provider, assistantMsg.usage.cost.total, {
2504
+ sessionId: this.#activeProviderSessionId(),
2505
+ recordedAt: assistantMsg.timestamp,
2506
+ baseUrl: this.#modelRegistry.getProviderBaseUrl?.(assistantMsg.provider),
2507
+ });
2508
+ }
2481
2509
  }
2482
2510
  if (event.message.role === "toolResult") {
2483
2511
  const { toolName, details, isError, content } = event.message as {
@@ -4422,7 +4450,7 @@ export class AgentSession {
4422
4450
  }
4423
4451
  return new Proxy(tool, {
4424
4452
  get: (target, prop) => {
4425
- if (prop !== "execute") return Reflect.get(target, prop, target);
4453
+ if (prop !== "execute") return target[prop as keyof T];
4426
4454
  return async (
4427
4455
  toolCallId: string,
4428
4456
  args: unknown,
@@ -4964,20 +4992,15 @@ export class AgentSession {
4964
4992
  const antigravityEndpointMode =
4965
4993
  provider === "google-antigravity" ? this.settings.get("providers.antigravityEndpoint") : undefined;
4966
4994
 
4967
- if (
4968
- !sessionOnPayload &&
4969
- !sessionOnResponse &&
4970
- !sessionMetadata &&
4971
- !sessionOnSseEvent &&
4972
- !openrouterVariant &&
4973
- !antigravityEndpointMode
4974
- )
4975
- return options;
4976
-
4977
4995
  const preparedOptions: SimpleStreamOptions = {
4978
4996
  ...options,
4979
4997
  ...(openrouterVariant !== undefined && { openrouterVariant }),
4980
4998
  ...(antigravityEndpointMode !== undefined && { antigravityEndpointMode }),
4999
+ loopGuard: {
5000
+ enabled: this.settings.get("model.loopGuard.enabled"),
5001
+ checkAssistantContent: this.settings.get("model.loopGuard.checkAssistantContent"),
5002
+ ...options.loopGuard,
5003
+ },
4981
5004
  };
4982
5005
 
4983
5006
  // Stamp session metadata (e.g. user_id={session_id}) onto direct-call requests so
@@ -9225,7 +9248,9 @@ export class AgentSession {
9225
9248
  let compactResult: CompactionResult | undefined;
9226
9249
  let lastError: unknown;
9227
9250
 
9228
- for (const candidate of candidates) {
9251
+ for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) {
9252
+ const candidate = candidates[candidateIndex];
9253
+ const hasMoreCandidates = candidateIndex < candidates.length - 1;
9229
9254
  const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
9230
9255
  if (!apiKey) continue;
9231
9256
 
@@ -9264,6 +9289,20 @@ export class AgentSession {
9264
9289
  lastError = this.#buildCompactionAuthError();
9265
9290
  break;
9266
9291
  }
9292
+ if (this.#isCompactionSummarizationTimeoutMessage(message)) {
9293
+ logger.warn(
9294
+ hasMoreCandidates
9295
+ ? "Auto-compaction summarization timed out, trying next model"
9296
+ : "Auto-compaction summarization timed out, not retrying same model",
9297
+ {
9298
+ error: message,
9299
+ model: `${candidate.provider}/${candidate.id}`,
9300
+ },
9301
+ );
9302
+ lastError = error;
9303
+ break;
9304
+ }
9305
+
9267
9306
  const retryAfterMs = this.#parseRetryAfterMsFromError(message);
9268
9307
  const shouldRetry =
9269
9308
  retrySettings.enabled &&
@@ -9281,19 +9320,15 @@ export class AgentSession {
9281
9320
 
9282
9321
  // If retry delay is too long (>30s), try next candidate instead of waiting
9283
9322
  const maxAcceptableDelayMs = 30_000;
9284
- if (delayMs > maxAcceptableDelayMs) {
9285
- const hasMoreCandidates = candidates.indexOf(candidate) < candidates.length - 1;
9286
- if (hasMoreCandidates) {
9287
- logger.warn("Auto-compaction retry delay too long, trying next model", {
9288
- delayMs,
9289
- retryAfterMs,
9290
- error: message,
9291
- model: `${candidate.provider}/${candidate.id}`,
9292
- });
9293
- lastError = error;
9294
- break; // Exit retry loop, continue to next candidate
9295
- }
9296
- // No more candidates - we have to wait
9323
+ if (delayMs > maxAcceptableDelayMs && hasMoreCandidates) {
9324
+ logger.warn("Auto-compaction retry delay too long, trying next model", {
9325
+ delayMs,
9326
+ retryAfterMs,
9327
+ error: message,
9328
+ model: `${candidate.provider}/${candidate.id}`,
9329
+ });
9330
+ lastError = error;
9331
+ break; // Exit retry loop, continue to next candidate
9297
9332
  }
9298
9333
 
9299
9334
  attempt++;
@@ -9654,6 +9689,8 @@ export class AgentSession {
9654
9689
  if (isContextOverflow(message, contextWindow)) return false;
9655
9690
 
9656
9691
  if (this.#isClassifierRefusal(message)) return true;
9692
+ if (this.#isProviderErrorFinishReasonBeforeToolUse(message)) return true;
9693
+ if (this.#isMalformedFunctionCallError(message)) return true;
9657
9694
  if (this.#streamInterruptedAfterObservableOutput(message)) return false;
9658
9695
  if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
9659
9696
 
@@ -9698,6 +9735,17 @@ export class AgentSession {
9698
9735
  return stopType === "refusal" || stopType === "sensitive";
9699
9736
  }
9700
9737
 
9738
+ #isProviderErrorFinishReasonBeforeToolUse(message: AssistantMessage): boolean {
9739
+ if (!message.errorMessage) return false;
9740
+ if (message.content.some(block => block.type === "toolCall")) return false;
9741
+ return /\bProvider (?:returned error finish_reason|finish_reason:\s*error)\b/i.test(message.errorMessage);
9742
+ }
9743
+
9744
+ #isMalformedFunctionCallError(message: AssistantMessage): boolean {
9745
+ if (!message.errorMessage) return false;
9746
+ return /\bmalformed.?function.?call\b/i.test(message.errorMessage);
9747
+ }
9748
+
9701
9749
  #isTransientErrorMessage(errorMessage: string): boolean {
9702
9750
  return (
9703
9751
  this.#isTransientEnvelopeErrorMessage(errorMessage) || this.#isTransientTransportErrorMessage(errorMessage)
@@ -9709,6 +9757,10 @@ export class AgentSession {
9709
9757
  return /anthropic stream envelope error:/i.test(errorMessage) && /before message_start/i.test(errorMessage);
9710
9758
  }
9711
9759
 
9760
+ #isCompactionSummarizationTimeoutMessage(errorMessage: string): boolean {
9761
+ return /\b(?:operation\s+)?timed?\s*out\b|\btimeout\b|\bstream stall\b/i.test(errorMessage);
9762
+ }
9763
+
9712
9764
  #isTransientTransportErrorMessage(errorMessage: string): boolean {
9713
9765
  // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
9714
9766
  // service unavailable, provider-suggested retry, network/connection/socket errors, fetch failed,
@@ -11157,6 +11209,94 @@ export class AgentSession {
11157
11209
  return { selectedText, cancelled: false };
11158
11210
  }
11159
11211
 
11212
+ async branchFromBtw(
11213
+ question: string,
11214
+ assistantMessage: AssistantMessage,
11215
+ ): Promise<{ cancelled: boolean; sessionFile: string | undefined }> {
11216
+ const previousSessionFile = this.sessionFile;
11217
+ if (!this.sessionManager.getSessionFile()) {
11218
+ throw new Error("Cannot branch /btw: session is not persisted");
11219
+ }
11220
+
11221
+ const leafId = this.sessionManager.getLeafId();
11222
+ if (!leafId) {
11223
+ throw new Error("Cannot branch /btw: current session has no leaf");
11224
+ }
11225
+
11226
+ if (
11227
+ this.isBashRunning ||
11228
+ this.isEvalRunning ||
11229
+ this.isCompacting ||
11230
+ this.isGeneratingHandoff ||
11231
+ this.isRetrying
11232
+ ) {
11233
+ throw new Error("Cannot branch /btw while session maintenance or user work is still running");
11234
+ }
11235
+
11236
+ if (this.#extensionRunner?.hasHandlers("session_before_branch")) {
11237
+ const result = (await this.#extensionRunner.emit({
11238
+ type: "session_before_branch",
11239
+ entryId: leafId,
11240
+ })) as SessionBeforeBranchResult | undefined;
11241
+
11242
+ if (result?.cancel) {
11243
+ return { cancelled: true, sessionFile: previousSessionFile };
11244
+ }
11245
+ }
11246
+
11247
+ await this.#cancelPostPromptTasks();
11248
+ if (
11249
+ this.isBashRunning ||
11250
+ this.isEvalRunning ||
11251
+ this.isCompacting ||
11252
+ this.isGeneratingHandoff ||
11253
+ this.isRetrying
11254
+ ) {
11255
+ throw new Error("Cannot branch /btw while session maintenance or user work is still running");
11256
+ }
11257
+
11258
+ this.#pendingNextTurnMessages = [];
11259
+ this.#scheduledHiddenNextTurnGeneration = undefined;
11260
+ this.agent.replaceQueues([], []);
11261
+ if (this.isStreaming) {
11262
+ await this.abort({ goalReason: "internal", reason: "branching /btw" });
11263
+ this.agent.replaceQueues([], []);
11264
+ }
11265
+ await this.sessionManager.flush();
11266
+ this.#cancelOwnAsyncJobs();
11267
+
11268
+ this.sessionManager.createBranchedSession(leafId);
11269
+ this.sessionManager.appendMessage({
11270
+ role: "user",
11271
+ content: [{ type: "text", text: question }],
11272
+ timestamp: Date.now(),
11273
+ });
11274
+ this.sessionManager.appendMessage(assistantMessage);
11275
+ this.#syncTodoPhasesFromBranch();
11276
+ this.#freshProviderSessionId = undefined;
11277
+ this.#syncAgentSessionId();
11278
+ this.#rekeyHindsightMemoryForCurrentSessionId();
11279
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
11280
+ this.#resetHindsightConversationTrackingIfHindsight();
11281
+ this.#resetMnemopiConversationTrackingIfMnemopi();
11282
+
11283
+ const sessionContext = this.buildDisplaySessionContext();
11284
+ await this.#restoreMCPSelectionsForSessionContext(sessionContext);
11285
+
11286
+ if (this.#extensionRunner) {
11287
+ await this.#extensionRunner.emit({
11288
+ type: "session_branch",
11289
+ previousSessionFile,
11290
+ });
11291
+ }
11292
+
11293
+ this.agent.replaceMessages(sessionContext.messages);
11294
+ this.#advisorRuntime?.reset();
11295
+ this.#closeCodexProviderSessionsForHistoryRewrite();
11296
+
11297
+ return { cancelled: false, sessionFile: this.sessionFile };
11298
+ }
11299
+
11160
11300
  // =========================================================================
11161
11301
  // Tree Navigation
11162
11302
  // =========================================================================
@@ -45,6 +45,7 @@ const PRIMARY_ARG_KEYS = [
45
45
  "query",
46
46
  "prompt",
47
47
  "assignment",
48
+ "note",
48
49
  "message",
49
50
  "op",
50
51
  "name",
@@ -74,8 +75,16 @@ function lineCount(text: string): number {
74
75
  }
75
76
 
76
77
  /** Pick the most informative scalar argument of a tool call. */
77
- function primaryArg(args: Record<string, unknown> | undefined): string {
78
+ function primaryArg(name: string, args: Record<string, unknown> | undefined): string {
78
79
  if (!args || typeof args !== "object") return "";
80
+ // Advisor note is the most informative summary; preserve severity too.
81
+ if (name === "advise") {
82
+ const note = typeof args.note === "string" ? args.note : "";
83
+ const severity = typeof args.severity === "string" ? args.severity : "";
84
+ if (note && severity) return oneLine(`${severity}: ${note}`);
85
+ if (note) return oneLine(note);
86
+ if (severity) return oneLine(severity);
87
+ }
79
88
  for (const key of PRIMARY_ARG_KEYS) {
80
89
  const value = args[key];
81
90
  if (typeof value === "string" && value.length > 0) return oneLine(value);
@@ -108,7 +117,7 @@ function toolCallLine(
108
117
  result: ToolResultMessage | undefined,
109
118
  includeToolIntent?: boolean,
110
119
  ): string {
111
- const head = `→ ${name}(${primaryArg(args)})`;
120
+ const head = `→ ${name}(${primaryArg(name, args)})`;
112
121
  let base: string;
113
122
  if (!result) {
114
123
  base = `${head} ⇒ pending`;
@@ -14,8 +14,8 @@
14
14
  * estimate (`estimateInlineSavings`) so the two can never disagree.
15
15
  */
16
16
 
17
+ import { countTokens } from "@oh-my-pi/pi-agent-core";
17
18
  import type { Context, ImageContent, Model, TextContent, ToolResultMessage, UserMessage } from "@oh-my-pi/pi-ai";
18
- import { countTokens } from "@oh-my-pi/pi-natives";
19
19
  import * as snapcompact from "@oh-my-pi/snapcompact";
20
20
  import contextFramesNote from "../prompts/system/snapcompact-context-frames-note.md" with { type: "text" };
21
21
  import contextStub from "../prompts/system/snapcompact-context-stub.md" with { type: "text" };
@@ -240,13 +240,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
240
240
  allowArgs: true,
241
241
  handleTui: async (command, runtime) => {
242
242
  const hadArgs = !!command.args;
243
- // Capture state BEFORE the call: when plan mode is already active,
244
- // handlePlanModeCommand may exit it (on confirmed exit) or leave it on (on cancel
245
- // or warning). In every "already active" case the typed args are NOT consumed,
246
- // so preserve them in history regardless of the user's confirm/cancel choice.
247
- const wasPlanModeEnabled = runtime.ctx.planModeEnabled;
248
243
  await runtime.ctx.handlePlanModeCommand(command.args || undefined);
249
- if (hadArgs && wasPlanModeEnabled) {
244
+ if (hadArgs) {
250
245
  runtime.ctx.editor.addToHistory(command.text);
251
246
  }
252
247
  runtime.ctx.editor.setText("");
@@ -275,10 +270,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
275
270
  allowArgs: true,
276
271
  handleTui: async (command, runtime) => {
277
272
  const hadArgs = !!command.args;
278
- // Capture state BEFORE the call (see /plan above for rationale).
279
- const wasGoalModeEnabled = runtime.ctx.goalModeEnabled;
280
273
  await runtime.ctx.handleGoalModeCommand(command.args || undefined);
281
- if (hadArgs && wasGoalModeEnabled) {
274
+ if (hadArgs) {
282
275
  runtime.ctx.editor.addToHistory(command.text);
283
276
  }
284
277
  runtime.ctx.editor.setText("");
@@ -308,7 +301,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
308
301
  {
309
302
  name: "model",
310
303
  aliases: ["models"],
311
- description: "Select model (opens selector UI)",
304
+ description: "Switch model for this session",
312
305
  acpDescription: "Show current model selection",
313
306
  handle: async (command, runtime) => {
314
307
  if (command.args) {
@@ -0,0 +1,19 @@
1
+ /** Inputs used to decide whether the optional startup splash may run for this process. */
2
+ export interface StartupSplashDecisionOptions {
3
+ readonly configured: boolean;
4
+ readonly isInteractive: boolean;
5
+ readonly resuming: boolean;
6
+ readonly quiet: boolean;
7
+ readonly timing: boolean;
8
+ readonly stdinIsTTY: boolean | undefined;
9
+ readonly stdoutIsTTY: boolean | undefined;
10
+ }
11
+
12
+ /** Returns true only for explicitly enabled, normal interactive TTY startup. */
13
+ export function shouldShowStartupSplash(options: StartupSplashDecisionOptions): boolean {
14
+ if (!options.configured) return false;
15
+ if (!options.isInteractive) return false;
16
+ if (options.resuming || options.quiet) return false;
17
+ if (options.timing) return false;
18
+ return options.stdinIsTTY === true && options.stdoutIsTTY === true;
19
+ }
@@ -516,6 +516,7 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
516
516
  const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
517
517
  let abortedViaYield = false;
518
518
  const hasYield = Array.isArray(yieldItems) && yieldItems.length > 0;
519
+ const hadFailureBeforeYield = exitCode !== 0 && stderr.trim().length > 0;
519
520
 
520
521
  if (hasYield) {
521
522
  const lastYield = yieldItems[yieldItems.length - 1];
@@ -553,12 +554,16 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
553
554
  const errorMessage = err instanceof Error ? err.message : String(err);
554
555
  rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
555
556
  }
556
- exitCode = 0;
557
- stderr = overridden
558
- ? SUBAGENT_WARNING_SCHEMA_OVERRIDDEN
559
- : schemaError
560
- ? `invalid output schema: ${schemaError}`
561
- : "";
557
+ if (!hadFailureBeforeYield) {
558
+ exitCode = 0;
559
+ stderr = overridden
560
+ ? SUBAGENT_WARNING_SCHEMA_OVERRIDDEN
561
+ : schemaError
562
+ ? `invalid output schema: ${schemaError}`
563
+ : "";
564
+ } else if (!stderr) {
565
+ stderr = "Subagent failed after yielding a result.";
566
+ }
562
567
  }
563
568
  }
564
569
  }
package/src/task/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Usage } from "@oh-my-pi/pi-ai";
3
3
  import { $env } from "@oh-my-pi/pi-utils";
4
- import { z } from "zod/v4";
4
+ import { type } from "arktype";
5
5
  import type { AgentSessionEvent } from "../session/agent-session";
6
6
  import type { NestedRepoPatch } from "./worktree";
7
7
 
@@ -78,37 +78,23 @@ export interface SubagentLifecyclePayload {
78
78
  export const ROLE_LABEL_MAX = 80;
79
79
  /** Schema bound on the raw `role` input, before it is label-normalized at every use site. */
80
80
  export const ROLE_INPUT_MAX = 256;
81
+ const ROLE_INPUT_SCHEMA = `string <= ${ROLE_INPUT_MAX}` as const;
81
82
 
82
- /**
83
- * One unit of work. The single-spawn schema is `{ agent, ...taskItemSchema }`;
84
- * the batch schema (`task.batch`) is `{ agent, context, tasks: taskItemSchema[] }`.
85
- * When task isolation is enabled, `isolated` joins the item shape (per-item in
86
- * batch form, top-level in the flat form via the spread).
87
- */
88
- const taskItemShape = {
89
- id: z.string().max(48).optional().describe("stable agent id; default generated"),
90
- description: z.string().optional().describe("ui label, not seen by subagent"),
91
- role: z
92
- .string()
93
- .max(ROLE_INPUT_MAX)
94
- .optional()
95
- .describe(
96
- "specialist role/expertise this subagent embodies (e.g. 'Rust async-runtime specialist'); shapes its identity and display name",
97
- ),
98
- assignment: z.string().describe("the work; self-contained instructions"),
99
- };
100
- const isolatedShape = {
101
- isolated: z.boolean().optional().describe("run in isolated env; returns patches"),
102
- };
103
- const agentShape = {
104
- agent: z.string().describe("agent type to spawn"),
105
- };
106
- const contextShape = {
107
- context: z.string().describe("shared background prepended to each assignment"),
108
- };
109
-
110
- export const taskItemSchema = z.object(taskItemShape);
111
- const taskItemSchemaIsolated = z.object({ ...taskItemShape, ...isolatedShape });
83
+ export const taskItemSchema = type({
84
+ "id?": "string",
85
+ "description?": "string",
86
+ "role?": ROLE_INPUT_SCHEMA,
87
+ assignment: "string",
88
+ "+": "delete",
89
+ });
90
+ const taskItemSchemaIsolated = type({
91
+ "id?": "string",
92
+ "description?": "string",
93
+ "role?": ROLE_INPUT_SCHEMA,
94
+ assignment: "string",
95
+ "isolated?": "boolean",
96
+ "+": "delete",
97
+ });
112
98
 
113
99
  /** Single task item. Fields are optional defensively: args stream in token by token. */
114
100
  export interface TaskItem {
@@ -124,17 +110,34 @@ export interface TaskItem {
124
110
  isolated?: boolean;
125
111
  }
126
112
 
127
- export const taskSchema = z.object({ ...agentShape, ...taskItemShape, ...isolatedShape });
128
- const taskSchemaNoIsolation = z.object({ ...agentShape, ...taskItemShape });
129
- const taskSchemaBatch = z.object({
130
- ...agentShape,
131
- ...contextShape,
132
- tasks: z.array(taskItemSchemaIsolated).describe("tasks to spawn; one subagent per item"),
113
+ export const taskSchema = type({
114
+ agent: "string",
115
+ "id?": "string",
116
+ "description?": "string",
117
+ "role?": ROLE_INPUT_SCHEMA,
118
+ assignment: "string",
119
+ "isolated?": "boolean",
120
+ "+": "delete",
121
+ });
122
+ const taskSchemaNoIsolation = type({
123
+ agent: "string",
124
+ "id?": "string",
125
+ "description?": "string",
126
+ "role?": ROLE_INPUT_SCHEMA,
127
+ assignment: "string",
128
+ "+": "delete",
129
+ });
130
+ const taskSchemaBatch = type({
131
+ agent: "string",
132
+ context: "string",
133
+ tasks: taskItemSchemaIsolated.array(),
134
+ "+": "delete",
133
135
  });
134
- const taskSchemaBatchNoIsolation = z.object({
135
- ...agentShape,
136
- ...contextShape,
137
- tasks: z.array(taskItemSchema).describe("tasks to spawn; one subagent per item"),
136
+ const taskSchemaBatchNoIsolation = type({
137
+ agent: "string",
138
+ context: "string",
139
+ tasks: taskItemSchema.array(),
140
+ "+": "delete",
138
141
  });
139
142
  const ALL_TASK_SCHEMAS = [taskSchema, taskSchemaNoIsolation, taskSchemaBatch, taskSchemaBatchNoIsolation] as const;
140
143
 
@@ -1,5 +1,6 @@
1
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
- import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
2
+ import type { Tool as AiTool } from "@oh-my-pi/pi-ai";
3
+ import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
3
4
 
4
5
  // ─── Generic Tool Discovery Types ────────────────────────────────────────────
5
6
 
@@ -65,8 +66,13 @@ export function isMCPToolName(name: string): boolean {
65
66
  return name.startsWith("mcp__");
66
67
  }
67
68
 
68
- function getSchemaPropertyKeys(parameters: unknown): string[] {
69
- if (isZodSchema(parameters)) parameters = zodToWireSchema(parameters);
69
+ function getSchemaPropertyKeys(tool: Pick<AiTool, "name" | "description" | "parameters">): string[] {
70
+ let parameters: unknown = tool.parameters;
71
+ try {
72
+ parameters = toolWireSchema(tool as AiTool);
73
+ } catch {
74
+ // Schema may contain functions or cycles; fall back to the raw shape.
75
+ }
70
76
  if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) return [];
71
77
  const properties = (parameters as { properties?: unknown }).properties;
72
78
  if (!properties || typeof properties !== "object" || Array.isArray(properties)) return [];
@@ -149,7 +155,14 @@ export function getDiscoverableTool(
149
155
  source,
150
156
  serverName: typeof toolRecord.mcpServerName === "string" ? toolRecord.mcpServerName : undefined,
151
157
  mcpToolName: typeof toolRecord.mcpToolName === "string" ? toolRecord.mcpToolName : undefined,
152
- schemaKeys: getSchemaPropertyKeys(toolRecord.parameters),
158
+ schemaKeys:
159
+ toolRecord.parameters === undefined
160
+ ? []
161
+ : getSchemaPropertyKeys({
162
+ name: tool.name,
163
+ description: rawDescription,
164
+ parameters: toolRecord.parameters as AiTool["parameters"],
165
+ }),
153
166
  };
154
167
  }
155
168
 
package/src/tools/ask.ts CHANGED
@@ -19,7 +19,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
19
19
  import type { ToolExample } from "@oh-my-pi/pi-ai";
20
20
  import { type Component, Markdown, type MarkdownTheme, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
21
21
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
22
- import { z } from "zod/v4";
22
+ import { type as arkType } from "arktype";
23
23
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
24
24
  import type { ExtensionUISelectItem } from "../extensibility/extensions";
25
25
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
@@ -34,24 +34,24 @@ import { ToolAbortError } from "./tool-errors";
34
34
  // Types
35
35
  // =============================================================================
36
36
 
37
- const OptionItem = z.object({
38
- label: z.string().describe("display label"),
39
- description: z.string().describe("optional explanatory text displayed below the label").optional(),
37
+ const OptionItem = arkType({
38
+ label: arkType("string").describe("display label"),
39
+ "description?": arkType("string").describe("optional explanatory text displayed below the label"),
40
40
  });
41
41
 
42
- const QuestionItem = z.object({
43
- id: z.string().describe("question id"),
44
- question: z.string().describe("question text"),
45
- options: z.array(OptionItem).describe("available options"),
46
- multi: z.boolean().describe("allow multiple selections").optional(),
47
- recommended: z.number().describe("recommended option index").optional(),
42
+ const QuestionItem = arkType({
43
+ id: arkType("string").describe("question id"),
44
+ question: arkType("string").describe("question text"),
45
+ options: OptionItem.array().describe("available options"),
46
+ "multi?": arkType("boolean").describe("allow multiple selections"),
47
+ "recommended?": arkType("number").describe("recommended option index"),
48
48
  });
49
49
 
50
- const askSchema = z.object({
51
- questions: z.array(QuestionItem).min(1).describe("questions to ask"),
50
+ const askSchema = arkType({
51
+ questions: QuestionItem.array().atLeastLength(1).describe("questions to ask"),
52
52
  });
53
53
 
54
- export type AskToolInput = z.infer<typeof askSchema>;
54
+ export type AskToolInput = typeof askSchema.infer;
55
55
 
56
56
  /** Result for a single question */
57
57
  export interface QuestionResult {
@@ -424,7 +424,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
424
424
  readonly parameters = askSchema;
425
425
  readonly strict = true;
426
426
 
427
- readonly examples: readonly ToolExample<z.input<typeof askSchema>>[] = [
427
+ readonly examples: readonly ToolExample<typeof askSchema.infer>[] = [
428
428
  {
429
429
  caption: "Single question",
430
430
  call: {