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

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 +53 -0
  2. package/dist/cli.js +1927 -1376
  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 +160 -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 +4 -11
  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;
@@ -4422,7 +4443,7 @@ export class AgentSession {
4422
4443
  }
4423
4444
  return new Proxy(tool, {
4424
4445
  get: (target, prop) => {
4425
- if (prop !== "execute") return Reflect.get(target, prop, target);
4446
+ if (prop !== "execute") return target[prop as keyof T];
4426
4447
  return async (
4427
4448
  toolCallId: string,
4428
4449
  args: unknown,
@@ -4964,20 +4985,15 @@ export class AgentSession {
4964
4985
  const antigravityEndpointMode =
4965
4986
  provider === "google-antigravity" ? this.settings.get("providers.antigravityEndpoint") : undefined;
4966
4987
 
4967
- if (
4968
- !sessionOnPayload &&
4969
- !sessionOnResponse &&
4970
- !sessionMetadata &&
4971
- !sessionOnSseEvent &&
4972
- !openrouterVariant &&
4973
- !antigravityEndpointMode
4974
- )
4975
- return options;
4976
-
4977
4988
  const preparedOptions: SimpleStreamOptions = {
4978
4989
  ...options,
4979
4990
  ...(openrouterVariant !== undefined && { openrouterVariant }),
4980
4991
  ...(antigravityEndpointMode !== undefined && { antigravityEndpointMode }),
4992
+ loopGuard: {
4993
+ enabled: this.settings.get("model.loopGuard.enabled"),
4994
+ checkAssistantContent: this.settings.get("model.loopGuard.checkAssistantContent"),
4995
+ ...options.loopGuard,
4996
+ },
4981
4997
  };
4982
4998
 
4983
4999
  // Stamp session metadata (e.g. user_id={session_id}) onto direct-call requests so
@@ -9225,7 +9241,9 @@ export class AgentSession {
9225
9241
  let compactResult: CompactionResult | undefined;
9226
9242
  let lastError: unknown;
9227
9243
 
9228
- for (const candidate of candidates) {
9244
+ for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) {
9245
+ const candidate = candidates[candidateIndex];
9246
+ const hasMoreCandidates = candidateIndex < candidates.length - 1;
9229
9247
  const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
9230
9248
  if (!apiKey) continue;
9231
9249
 
@@ -9264,6 +9282,20 @@ export class AgentSession {
9264
9282
  lastError = this.#buildCompactionAuthError();
9265
9283
  break;
9266
9284
  }
9285
+ if (this.#isCompactionSummarizationTimeoutMessage(message)) {
9286
+ logger.warn(
9287
+ hasMoreCandidates
9288
+ ? "Auto-compaction summarization timed out, trying next model"
9289
+ : "Auto-compaction summarization timed out, not retrying same model",
9290
+ {
9291
+ error: message,
9292
+ model: `${candidate.provider}/${candidate.id}`,
9293
+ },
9294
+ );
9295
+ lastError = error;
9296
+ break;
9297
+ }
9298
+
9267
9299
  const retryAfterMs = this.#parseRetryAfterMsFromError(message);
9268
9300
  const shouldRetry =
9269
9301
  retrySettings.enabled &&
@@ -9281,19 +9313,15 @@ export class AgentSession {
9281
9313
 
9282
9314
  // If retry delay is too long (>30s), try next candidate instead of waiting
9283
9315
  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
9316
+ if (delayMs > maxAcceptableDelayMs && hasMoreCandidates) {
9317
+ logger.warn("Auto-compaction retry delay too long, trying next model", {
9318
+ delayMs,
9319
+ retryAfterMs,
9320
+ error: message,
9321
+ model: `${candidate.provider}/${candidate.id}`,
9322
+ });
9323
+ lastError = error;
9324
+ break; // Exit retry loop, continue to next candidate
9297
9325
  }
9298
9326
 
9299
9327
  attempt++;
@@ -9654,6 +9682,7 @@ export class AgentSession {
9654
9682
  if (isContextOverflow(message, contextWindow)) return false;
9655
9683
 
9656
9684
  if (this.#isClassifierRefusal(message)) return true;
9685
+ if (this.#isProviderErrorFinishReasonBeforeToolUse(message)) return true;
9657
9686
  if (this.#streamInterruptedAfterObservableOutput(message)) return false;
9658
9687
  if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
9659
9688
 
@@ -9698,6 +9727,12 @@ export class AgentSession {
9698
9727
  return stopType === "refusal" || stopType === "sensitive";
9699
9728
  }
9700
9729
 
9730
+ #isProviderErrorFinishReasonBeforeToolUse(message: AssistantMessage): boolean {
9731
+ if (!message.errorMessage) return false;
9732
+ if (message.content.some(block => block.type === "toolCall")) return false;
9733
+ return /\bProvider (?:returned error finish_reason|finish_reason:\s*error)\b/i.test(message.errorMessage);
9734
+ }
9735
+
9701
9736
  #isTransientErrorMessage(errorMessage: string): boolean {
9702
9737
  return (
9703
9738
  this.#isTransientEnvelopeErrorMessage(errorMessage) || this.#isTransientTransportErrorMessage(errorMessage)
@@ -9709,6 +9744,10 @@ export class AgentSession {
9709
9744
  return /anthropic stream envelope error:/i.test(errorMessage) && /before message_start/i.test(errorMessage);
9710
9745
  }
9711
9746
 
9747
+ #isCompactionSummarizationTimeoutMessage(errorMessage: string): boolean {
9748
+ return /\b(?:operation\s+)?timed?\s*out\b|\btimeout\b|\bstream stall\b/i.test(errorMessage);
9749
+ }
9750
+
9712
9751
  #isTransientTransportErrorMessage(errorMessage: string): boolean {
9713
9752
  // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
9714
9753
  // service unavailable, provider-suggested retry, network/connection/socket errors, fetch failed,
@@ -11157,6 +11196,94 @@ export class AgentSession {
11157
11196
  return { selectedText, cancelled: false };
11158
11197
  }
11159
11198
 
11199
+ async branchFromBtw(
11200
+ question: string,
11201
+ assistantMessage: AssistantMessage,
11202
+ ): Promise<{ cancelled: boolean; sessionFile: string | undefined }> {
11203
+ const previousSessionFile = this.sessionFile;
11204
+ if (!this.sessionManager.getSessionFile()) {
11205
+ throw new Error("Cannot branch /btw: session is not persisted");
11206
+ }
11207
+
11208
+ const leafId = this.sessionManager.getLeafId();
11209
+ if (!leafId) {
11210
+ throw new Error("Cannot branch /btw: current session has no leaf");
11211
+ }
11212
+
11213
+ if (
11214
+ this.isBashRunning ||
11215
+ this.isEvalRunning ||
11216
+ this.isCompacting ||
11217
+ this.isGeneratingHandoff ||
11218
+ this.isRetrying
11219
+ ) {
11220
+ throw new Error("Cannot branch /btw while session maintenance or user work is still running");
11221
+ }
11222
+
11223
+ if (this.#extensionRunner?.hasHandlers("session_before_branch")) {
11224
+ const result = (await this.#extensionRunner.emit({
11225
+ type: "session_before_branch",
11226
+ entryId: leafId,
11227
+ })) as SessionBeforeBranchResult | undefined;
11228
+
11229
+ if (result?.cancel) {
11230
+ return { cancelled: true, sessionFile: previousSessionFile };
11231
+ }
11232
+ }
11233
+
11234
+ await this.#cancelPostPromptTasks();
11235
+ if (
11236
+ this.isBashRunning ||
11237
+ this.isEvalRunning ||
11238
+ this.isCompacting ||
11239
+ this.isGeneratingHandoff ||
11240
+ this.isRetrying
11241
+ ) {
11242
+ throw new Error("Cannot branch /btw while session maintenance or user work is still running");
11243
+ }
11244
+
11245
+ this.#pendingNextTurnMessages = [];
11246
+ this.#scheduledHiddenNextTurnGeneration = undefined;
11247
+ this.agent.replaceQueues([], []);
11248
+ if (this.isStreaming) {
11249
+ await this.abort({ goalReason: "internal", reason: "branching /btw" });
11250
+ this.agent.replaceQueues([], []);
11251
+ }
11252
+ await this.sessionManager.flush();
11253
+ this.#cancelOwnAsyncJobs();
11254
+
11255
+ this.sessionManager.createBranchedSession(leafId);
11256
+ this.sessionManager.appendMessage({
11257
+ role: "user",
11258
+ content: [{ type: "text", text: question }],
11259
+ timestamp: Date.now(),
11260
+ });
11261
+ this.sessionManager.appendMessage(assistantMessage);
11262
+ this.#syncTodoPhasesFromBranch();
11263
+ this.#freshProviderSessionId = undefined;
11264
+ this.#syncAgentSessionId();
11265
+ this.#rekeyHindsightMemoryForCurrentSessionId();
11266
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
11267
+ this.#resetHindsightConversationTrackingIfHindsight();
11268
+ this.#resetMnemopiConversationTrackingIfMnemopi();
11269
+
11270
+ const sessionContext = this.buildDisplaySessionContext();
11271
+ await this.#restoreMCPSelectionsForSessionContext(sessionContext);
11272
+
11273
+ if (this.#extensionRunner) {
11274
+ await this.#extensionRunner.emit({
11275
+ type: "session_branch",
11276
+ previousSessionFile,
11277
+ });
11278
+ }
11279
+
11280
+ this.agent.replaceMessages(sessionContext.messages);
11281
+ this.#advisorRuntime?.reset();
11282
+ this.#closeCodexProviderSessionsForHistoryRewrite();
11283
+
11284
+ return { cancelled: false, sessionFile: this.sessionFile };
11285
+ }
11286
+
11160
11287
  // =========================================================================
11161
11288
  // Tree Navigation
11162
11289
  // =========================================================================
@@ -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) {
@@ -341,7 +334,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
341
334
  return commandConsumed();
342
335
  },
343
336
  handleTui: (_command, runtime) => {
344
- runtime.ctx.showModelSelector();
337
+ runtime.ctx.showModelSelector({ temporaryOnly: true });
345
338
  runtime.ctx.editor.setText("");
346
339
  },
347
340
  },
@@ -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: {