@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.1

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 (176) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  4. package/dist/types/commit/analysis/summary.d.ts +2 -2
  5. package/dist/types/commit/changelog/generate.d.ts +2 -2
  6. package/dist/types/commit/changelog/index.d.ts +2 -2
  7. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  8. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  10. package/dist/types/commit/model-selection.d.ts +10 -4
  11. package/dist/types/config/api-key-resolver.d.ts +34 -0
  12. package/dist/types/config/model-registry.d.ts +17 -1
  13. package/dist/types/config/settings-schema.d.ts +9 -0
  14. package/dist/types/dap/config.d.ts +14 -1
  15. package/dist/types/dap/types.d.ts +10 -0
  16. package/dist/types/lsp/utils.d.ts +3 -2
  17. package/dist/types/modes/components/chat-block.d.ts +64 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  19. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  20. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  21. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  22. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  23. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  24. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  25. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  26. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  27. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  28. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  29. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  30. package/dist/types/modes/interactive-mode.d.ts +15 -5
  31. package/dist/types/modes/theme/theme.d.ts +1 -1
  32. package/dist/types/modes/types.d.ts +18 -5
  33. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  34. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  35. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  36. package/dist/types/sdk.d.ts +2 -0
  37. package/dist/types/session/agent-session.d.ts +21 -0
  38. package/dist/types/session/messages.d.ts +12 -0
  39. package/dist/types/session/session-manager.d.ts +3 -1
  40. package/dist/types/slash-commands/types.d.ts +4 -6
  41. package/dist/types/task/executor.d.ts +7 -0
  42. package/dist/types/task/index.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +3 -2
  44. package/dist/types/tools/archive-reader.d.ts +5 -0
  45. package/dist/types/tools/ast-edit.d.ts +3 -0
  46. package/dist/types/tools/ast-grep.d.ts +3 -0
  47. package/dist/types/tools/bash.d.ts +1 -0
  48. package/dist/types/tools/find.d.ts +8 -4
  49. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  50. package/dist/types/tools/memory-render.d.ts +4 -1
  51. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  52. package/dist/types/tools/render-utils.d.ts +5 -9
  53. package/dist/types/tools/search.d.ts +4 -0
  54. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  55. package/dist/types/tools/todo.d.ts +3 -2
  56. package/dist/types/tools/write.d.ts +3 -0
  57. package/dist/types/tui/output-block.d.ts +16 -4
  58. package/dist/types/tui/status-line.d.ts +3 -0
  59. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  60. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  61. package/package.json +9 -9
  62. package/src/auto-thinking/classifier.ts +5 -1
  63. package/src/cli/dry-balance-cli.ts +52 -17
  64. package/src/cli/gallery-cli.ts +4 -1
  65. package/src/cli/gallery-fixtures/misc.ts +29 -0
  66. package/src/commit/analysis/conventional.ts +2 -2
  67. package/src/commit/analysis/summary.ts +2 -2
  68. package/src/commit/changelog/generate.ts +2 -2
  69. package/src/commit/changelog/index.ts +2 -2
  70. package/src/commit/map-reduce/index.ts +3 -3
  71. package/src/commit/map-reduce/map-phase.ts +2 -2
  72. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  73. package/src/commit/model-selection.ts +33 -9
  74. package/src/commit/pipeline.ts +4 -4
  75. package/src/config/api-key-resolver.ts +58 -0
  76. package/src/config/model-registry.ts +25 -2
  77. package/src/config/settings-schema.ts +10 -0
  78. package/src/config/settings.ts +20 -2
  79. package/src/dap/config.ts +41 -2
  80. package/src/dap/defaults.json +1 -0
  81. package/src/dap/session.ts +1 -0
  82. package/src/dap/types.ts +10 -0
  83. package/src/debug/index.ts +40 -54
  84. package/src/edit/renderer.ts +82 -78
  85. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  86. package/src/eval/llm-bridge.ts +8 -3
  87. package/src/goals/tools/goal-tool.ts +36 -26
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/lsp/utils.ts +3 -2
  90. package/src/main.ts +9 -7
  91. package/src/memories/index.ts +12 -5
  92. package/src/mnemopi/backend.ts +5 -1
  93. package/src/modes/acp/acp-agent.ts +33 -26
  94. package/src/modes/components/assistant-message.ts +2 -9
  95. package/src/modes/components/chat-block.ts +111 -0
  96. package/src/modes/components/copy-selector.ts +1 -44
  97. package/src/modes/components/custom-editor.ts +23 -0
  98. package/src/modes/components/custom-message.ts +1 -3
  99. package/src/modes/components/execution-shared.ts +1 -2
  100. package/src/modes/components/hook-message.ts +1 -3
  101. package/src/modes/components/overlay-box.ts +108 -0
  102. package/src/modes/components/plan-review-overlay.ts +799 -0
  103. package/src/modes/components/plan-toc.ts +138 -0
  104. package/src/modes/components/read-tool-group.ts +20 -4
  105. package/src/modes/components/skill-message.ts +0 -1
  106. package/src/modes/components/tips.txt +1 -0
  107. package/src/modes/components/todo-reminder.ts +0 -2
  108. package/src/modes/components/tool-execution.ts +68 -88
  109. package/src/modes/components/transcript-container.ts +84 -24
  110. package/src/modes/components/user-message.ts +1 -2
  111. package/src/modes/controllers/command-controller-shared.ts +7 -6
  112. package/src/modes/controllers/command-controller.ts +57 -55
  113. package/src/modes/controllers/event-controller.ts +41 -40
  114. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  115. package/src/modes/controllers/input-controller.ts +124 -119
  116. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  117. package/src/modes/controllers/selector-controller.ts +23 -25
  118. package/src/modes/controllers/streaming-reveal.ts +212 -0
  119. package/src/modes/controllers/tan-command-controller.ts +173 -0
  120. package/src/modes/interactive-mode.ts +169 -94
  121. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  122. package/src/modes/theme/theme-schema.json +1 -1
  123. package/src/modes/theme/theme.ts +8 -4
  124. package/src/modes/types.ts +18 -7
  125. package/src/modes/utils/copy-targets.ts +133 -27
  126. package/src/modes/utils/ui-helpers.ts +44 -46
  127. package/src/plan-mode/approved-plan.ts +66 -43
  128. package/src/plan-mode/plan-protection.ts +4 -4
  129. package/src/prompts/system/background-tan-dispatch.md +8 -0
  130. package/src/prompts/system/plan-mode-active.md +67 -58
  131. package/src/prompts/system/plan-mode-approved.md +1 -1
  132. package/src/sdk.ts +11 -37
  133. package/src/session/agent-session.ts +82 -6
  134. package/src/session/messages.ts +26 -0
  135. package/src/session/session-manager.ts +13 -5
  136. package/src/slash-commands/builtin-registry.ts +36 -9
  137. package/src/slash-commands/types.ts +4 -6
  138. package/src/task/executor.ts +5 -2
  139. package/src/task/index.ts +4 -0
  140. package/src/task/render.ts +212 -147
  141. package/src/tools/archive-reader.ts +64 -0
  142. package/src/tools/ask.ts +119 -164
  143. package/src/tools/ast-edit.ts +98 -71
  144. package/src/tools/ast-grep.ts +37 -43
  145. package/src/tools/bash.ts +50 -6
  146. package/src/tools/debug.ts +20 -8
  147. package/src/tools/fetch.ts +297 -7
  148. package/src/tools/find.ts +44 -30
  149. package/src/tools/gh-renderer.ts +81 -42
  150. package/src/tools/grouped-file-output.ts +272 -48
  151. package/src/tools/image-gen.ts +150 -103
  152. package/src/tools/inspect-image-renderer.ts +63 -41
  153. package/src/tools/inspect-image.ts +8 -1
  154. package/src/tools/job.ts +3 -4
  155. package/src/tools/memory-render.ts +4 -1
  156. package/src/tools/plan-mode-guard.ts +21 -39
  157. package/src/tools/read.ts +23 -16
  158. package/src/tools/render-utils.ts +21 -37
  159. package/src/tools/resolve.ts +14 -0
  160. package/src/tools/search-tool-bm25.ts +36 -23
  161. package/src/tools/search.ts +80 -78
  162. package/src/tools/sqlite-reader.ts +9 -12
  163. package/src/tools/todo.ts +118 -52
  164. package/src/tools/write.ts +81 -62
  165. package/src/tui/output-block.ts +60 -13
  166. package/src/tui/status-line.ts +5 -1
  167. package/src/utils/commit-message-generator.ts +9 -1
  168. package/src/utils/enhanced-paste.ts +202 -0
  169. package/src/utils/title-generator.ts +2 -1
  170. package/src/web/search/providers/anthropic.ts +25 -19
  171. package/src/web/search/providers/exa.ts +11 -3
  172. package/src/web/search/providers/kimi.ts +28 -17
  173. package/src/web/search/providers/parallel.ts +35 -24
  174. package/src/web/search/providers/synthetic.ts +8 -6
  175. package/src/web/search/providers/tavily.ts +9 -8
  176. package/src/web/search/providers/zai.ts +8 -6
@@ -6,111 +6,120 @@ You NEVER:
6
6
  - Run state-changing commands (git commit, npm install, etc.)
7
7
  - Make any system changes
8
8
 
9
- To implement: call `resolve` with `action: "apply"`, a `reason`, and `extra: { title: "<PLAN_TITLE>" }` → user approves an execution option → full write access is restored. `<PLAN_TITLE>` may only contain letters, numbers, underscores, and hyphens; the approved plan is renamed to `local://<PLAN_TITLE>.md`.
9
+ To implement: call `resolve` with `action: "apply"`, a `reason`, and `extra: { title: "<slug>" }` where `<slug>` matches your `local://<slug>-plan.md` file → user approves an execution option → full write access is restored. `<slug>` may only contain letters, numbers, underscores, and hyphens. The plan file is never renamed, so its name is yours to choose.
10
10
 
11
11
  You NEVER ask the user to exit plan mode for you; you MUST call `resolve` yourself.
12
12
  </critical>
13
13
 
14
+ ## Objective
15
+
16
+ A plan is **decision-complete**: another engineer or agent can execute it end-to-end without making a single design decision. Optimize every choice for that. Detail exists to remove the implementer's decisions — not to look thorough. A document that reads like a design doc (Non-Goals, Alternatives, risk matrices) yet leaves real decisions open is a FAILED plan.
17
+
14
18
  ## Plan File
15
19
 
16
20
  {{#if planExists}}
17
- Plan file exists at `{{planFilePath}}`; you MUST read and update it incrementally.
21
+ Plan file exists at `{{planFilePath}}`; you MUST read and update it incrementally. If this request is a different task, write a fresh `local://<slug>-plan.md` instead and leave the old plan in place.
18
22
  {{else}}
19
- You MUST create a plan at `{{planFilePath}}`.
23
+ Choose a short kebab-case `<slug>` that names this task (letters, numbers, hyphens) and write the plan to `local://<slug>-plan.md` — e.g. `local://auth-token-refresh-plan.md`. You MUST pass that same `<slug>` as `title` when you call `resolve`.
20
24
  {{/if}}
21
25
 
22
- You MUST use `{{editToolName}}` for incremental updates; use `{{writeToolName}}` only for create/full replace.
26
+ You MUST use `{{editToolName}}` for incremental updates; use `{{writeToolName}}` only for create/full replace. You MUST update the plan as you learn — you NEVER batch all writing to the end.
23
27
 
24
- <caution>
25
- The approval selector includes:
26
- - **Approve and execute**: starts execution in fresh context (session cleared).
27
- - **Approve and compact context**: distills the plan-mode discussion into a summary, then starts execution in this session.
28
- - **Approve and keep context**: starts execution in this session, preserving exploration history.
28
+ ## Resolving Unknowns
29
29
 
30
- You MUST still make the plan file self-contained: include requirements, decisions, key findings, and remaining todos.
31
- </caution>
30
+ You MUST eliminate unknowns by discovering facts, not by asking. Before asking the user anything, perform at least one targeted exploration pass.
31
+
32
+ Two kinds of unknowns, treated differently:
33
+ - **Discoverable facts** — repo/system truth: file locations, current behavior, existing patterns, types, configs. You MUST explore first (`find`, `search`, `read`, parallel explore subagents). You NEVER ask what the codebase can answer (e.g. "where is this defined?"). Ask only when several plausible candidates remain or a required identifier is genuinely absent — and then present the candidates with a recommendation.
34
+ - **Preferences and tradeoffs** — intent, UX, scope boundaries, performance-vs-simplicity: not derivable from code. You MUST surface these early via `{{askToolName}}` with 2–4 mutually exclusive options and a recommended default. If left unanswered, proceed with the default and record it under Assumptions.
35
+
36
+ Every question MUST materially change the plan, confirm a load-bearing assumption, or choose between real tradeoffs. You MUST batch questions. You NEVER ask filler questions or offer obviously-wrong options.
32
37
 
33
38
  {{#if reentry}}
34
39
  ## Re-entry
35
40
 
36
41
  <procedure>
37
- 1. Read existing plan
38
- 2. Evaluate request against it
42
+ 1. Read the existing plan.
43
+ 2. Evaluate the new request against it.
39
44
  3. Decide:
40
- - **Different task** → Overwrite plan
41
- - **Same task, continuing** → Update and clean outdated sections
42
- 4. Call `resolve` with `action: "apply"` and `extra: { title }` when complete
45
+ - **Different task** → overwrite the plan.
46
+ - **Same task, continuing** → update and delete outdated sections.
47
+ 4. Call `resolve` with `action: "apply"` and `extra: { title }` when complete.
43
48
  </procedure>
44
49
  {{/if}}
45
50
 
46
51
  {{#if iterative}}
47
- ## Iterative Planning
52
+ ## Workflow — Iterative
48
53
 
49
54
  <procedure>
50
55
  ### 1. Explore
51
- You MUST use `find`, `search`, `read` to understand the codebase.
56
+ You MUST use `find`, `search`, `read` to ground yourself in the actual code. Hunt for existing functions, utilities, and conventions to reuse before proposing anything new.
52
57
 
53
58
  ### 2. Interview
54
- You MUST use `{{askToolName}}` to clarify:
55
- - Ambiguous requirements
56
- - Technical decisions and tradeoffs
57
- - Preferences: UI/UX, performance, edge cases
59
+ You MUST use `{{askToolName}}` to resolve preferences and tradeoffs (see Resolving Unknowns). Batch questions; never ask what exploration answers.
58
60
 
59
- You MUST batch questions. You NEVER ask what you can answer by exploring.
60
-
61
- ### 3. Update Incrementally
62
- You MUST use `{{editToolName}}` to update plan file as you learn; NEVER wait until end.
61
+ ### 3. Update incrementally
62
+ You MUST use `{{editToolName}}` to revise the plan file as you learn.
63
63
 
64
64
  ### 4. Calibrate
65
- - Large unspecified task → multiple interview rounds
66
- - Smaller task → fewer or no questions
65
+ - Large, unspecified task → multiple interview rounds.
66
+ - Small, well-specified task → few or no questions.
67
67
  </procedure>
68
-
69
- <caution>
70
- ### Plan Structure
71
-
72
- You MUST use clear markdown headers; include:
73
- - Recommended approach (not alternatives)
74
- - Paths of critical files to modify
75
- - Verification: how to test end-to-end
76
-
77
- The plan MUST be scannable yet detailed enough to execute.
78
- </caution>
79
-
80
68
  {{else}}
81
- ## Planning Workflow
69
+ ## Workflow — Parallel
82
70
 
83
71
  <procedure>
84
- ### Phase 1: Understand
85
- You MUST focus on the request and associated code. You SHOULD launch parallel explore agents when scope spans multiple areas.
72
+ ### Phase 1 Understand
73
+ You MUST focus on the request and the code behind it. You SHOULD launch parallel `explore` subagents (via `task`) when scope spans multiple areas — give each a distinct focus (existing implementations, related components, test patterns). Actively hunt for reusable functions, utilities, and conventions; avoid proposing new code when a suitable implementation already exists.
86
74
 
87
- ### Phase 2: Design
88
- You MUST draft an approach based on exploration. You MUST consider trade-offs briefly, then choose.
75
+ ### Phase 2 Design
76
+ You MUST draft an approach from your exploration, weigh trade-offs briefly, then commit to one. For large or cross-cutting changes you MAY spawn a planning/critique subagent to pressure-test the approach before you commit.
89
77
 
90
- ### Phase 3: Review
91
- You MUST read critical files. You MUST verify plan matches original request. You SHOULD use `{{askToolName}}` to clarify remaining questions.
78
+ ### Phase 3 Review
79
+ You MUST read the critical files you intend to touch to confirm the approach holds against the real code. You MUST verify the plan still matches the original request. You SHOULD use `{{askToolName}}` to close remaining preference questions.
92
80
 
93
- ### Phase 4: Update Plan
94
- You MUST update `{{planFilePath}}` (`{{editToolName}}` for changes, `{{writeToolName}}` only if creating from scratch):
95
- - Recommended approach only
96
- - Paths of critical files to modify
97
- - Verification section
81
+ ### Phase 4 Write the plan
82
+ You MUST write the plan file (see **Plan File** above) per **The Plan** below.
98
83
  </procedure>
84
+ {{/if}}
85
+
86
+ ## The Plan
87
+
88
+ The plan MUST be self-contained: approval may clear or compact this conversation, so the file alone must carry everything needed to execute.
99
89
 
100
90
  <caution>
101
- You MUST ask questions throughout. You NEVER make large assumptions about user intent.
91
+ Write 3–5 short, scannable markdown sections. The usual shape:
92
+ - **Context** — why this change: the problem or need, what prompted it, the intended outcome. 2–4 sentences.
93
+ - **Approach** — the recommended approach only. Group bullets by subsystem or behavior, NOT file-by-file. Name existing functions/utilities to reuse, with their paths. Describe a repeated pattern once with a few representative paths — you NEVER enumerate every file or line.
94
+ - **Critical files** — the ≤5 files that disambiguate non-obvious changes, each with a one-line reason. Skip files whose change is already obvious from the Approach.
95
+ - **Verification** — how to test end-to-end: exact commands, tests to run or add, manual steps.
96
+ - **Assumptions** — only the decisions you made that the user might want to override.
97
+
98
+ Prefer the minimum detail needed for safe implementation, not exhaustive coverage. Compress related changes into high-signal bullets; omit branch-by-branch logic, restated invariants, and lists of unaffected behavior. Behavior-level descriptions beat symbol-by-symbol removal lists.
102
99
  </caution>
103
- {{/if}}
104
100
 
105
101
  <directives>
106
- - You MUST use `{{askToolName}}` only for clarifying requirements or choosing approaches
102
+ - You NEVER include sections that decide nothing: Non-Goals, Out of Scope, Alternatives Considered, Risks/Mitigations boilerplate, Future Work. Omit them entirely.
103
+ - You NEVER invent schema, validation, precedence, or fallback policy the request did not establish, unless it is required to prevent a concrete implementation mistake.
104
+ - You NEVER present alternatives in the final plan — choose. Record a discarded option only when it is a live tradeoff the user should confirm, and put it under Assumptions.
107
105
  </directives>
108
106
 
107
+ <caution>
108
+ The approval selector offers:
109
+ - **Approve and execute** — execution starts in fresh context (session cleared).
110
+ - **Approve and compact context** — distills this discussion into a summary, then executes in this session.
111
+ - **Approve and keep context** — executes in this session, preserving exploration history.
112
+
113
+ All three rely on the plan file being self-contained.
114
+ </caution>
115
+
109
116
  <critical>
117
+ You MUST use `{{askToolName}}` only to clarify requirements or choose between approaches.
118
+
110
119
  Your turn ends ONLY by:
111
120
  1. Using `{{askToolName}}` to gather information, OR
112
- 2. Calling `resolve` with `action: "apply"`, `reason`, and `extra: { title: "<PLAN_TITLE>" }` when ready — this triggers user approval, then implementation with full tool access
121
+ 2. Calling `resolve` with `action: "apply"`, `reason`, and `extra: { title: "<slug>" }` (the slug of your `local://<slug>-plan.md`) when ready — this triggers user approval, then implementation with full tool access.
113
122
 
114
- You NEVER ask plan approval via text or `{{askToolName}}`; you MUST use `resolve`.
115
- You MUST keep going until complete.
123
+ You NEVER ask for plan approval via text or `{{askToolName}}`; you MUST use `resolve`.
124
+ You MUST keep going until the plan is decision-complete.
116
125
  </critical>
@@ -16,7 +16,7 @@ The plan path is for subagent handoff only. You already have the plan; NEVER rea
16
16
 
17
17
  The full plan is injected below. You MUST execute it now:
18
18
 
19
- <plan path="{{finalPlanFilePath}}">
19
+ <plan path="{{planFilePath}}">
20
20
  {{planContent}}
21
21
  </plan>
22
22
 
package/src/sdk.ts CHANGED
@@ -10,7 +10,6 @@ import {
10
10
  } from "@oh-my-pi/pi-agent-core";
11
11
  import {
12
12
  type CredentialDisabledEvent,
13
- isUsageLimitError,
14
13
  type Message,
15
14
  type Model,
16
15
  type SimpleStreamOptions,
@@ -24,7 +23,6 @@ import type { Component } from "@oh-my-pi/pi-tui";
24
23
  import {
25
24
  $env,
26
25
  $flag,
27
- extractRetryHint,
28
26
  getAgentDbPath,
29
27
  getAgentDir,
30
28
  getAuthBrokerSnapshotCachePath,
@@ -39,6 +37,7 @@ import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from ".
39
37
  import { loadCapability } from "./capability";
40
38
  import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
41
39
  import { bucketRules } from "./capability/rule-buckets";
40
+ import { createApiKeyResolver } from "./config/api-key-resolver";
42
41
  import { shouldEnableAppendOnlyContext } from "./config/append-only-context-mode";
43
42
  import { ModelRegistry } from "./config/model-registry";
44
43
  import {
@@ -280,6 +279,8 @@ export interface CreateAgentSessionOptions {
280
279
  /** Optional provider-facing session identifier for prompt caches and sticky auth selection.
281
280
  * Keeps persisted session files isolated while reusing provider-side caches. */
282
281
  providerSessionId?: string;
282
+ /** Optional provider-facing prompt cache key, distinct from request lineage. */
283
+ providerPromptCacheKey?: string;
283
284
 
284
285
  /** Custom tools to register (in addition to built-in tools). Accepts both CustomTool and ToolDefinition. */
285
286
  customTools?: (CustomTool | ToolDefinition)[];
@@ -2001,6 +2002,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2001
2002
  onPayload,
2002
2003
  onResponse,
2003
2004
  sessionId: providerSessionId,
2005
+ promptCacheKey: options.providerPromptCacheKey,
2004
2006
  transformContext,
2005
2007
  steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
2006
2008
  followUpMode: settings.get("followUpMode") ?? "one-at-a-time",
@@ -2017,9 +2019,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2017
2019
  kimiApiFormat: settings.get("providers.kimiApiFormat") ?? "anthropic",
2018
2020
  preferWebsockets: preferOpenAICodexWebsockets,
2019
2021
  getToolContext: tc => toolContextStore.getContext(tc),
2020
- getApiKey: async provider => {
2022
+ getApiKey: async (provider, ctx) => {
2021
2023
  // Read agent.sessionId at call time so credential selection stays aligned
2022
2024
  // with metadataResolver after /new, fork, resume, or branch switches.
2025
+ // Retry steps (ctx carries an auth error) drive the central a/b/c
2026
+ // policy — force-refresh the same account, then rotate to a sibling —
2027
+ // and may legitimately yield no key when every account is exhausted.
2028
+ if (ctx?.error !== undefined) {
2029
+ return createApiKeyResolver(modelRegistry, provider, { sessionId: agent.sessionId })(ctx);
2030
+ }
2023
2031
  const key = await modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
2024
2032
  if (!key) {
2025
2033
  throw new Error(`No API key found for provider "${provider}"`);
@@ -2033,40 +2041,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2033
2041
  return streamSimple(streamModel, context, {
2034
2042
  ...streamOptions,
2035
2043
  openrouterVariant: streamOptions?.openrouterVariant ?? openrouterVariant,
2036
- onAuthError: async (provider, oldKey, error) => {
2037
- const message = error instanceof Error ? error.message : String(error);
2038
- // streamSimple invokes this for both 401 auth failures AND
2039
- // rotatable usage-limit errors (Codex usage_limit_reached,
2040
- // Anthropic usage_limit_reached, etc.). The two need
2041
- // different storage actions: a real 401 means the credential
2042
- // is bad and should be marked suspect; a usage limit just
2043
- // means this account is parked until reset and should be
2044
- // temporarily blocked so a sibling can pick the request up.
2045
- if (isUsageLimitError(message)) {
2046
- const retryAfterMs = extractRetryHint(undefined, message);
2047
- const switched = await modelRegistry.authStorage.markUsageLimitReached(provider, agent.sessionId, {
2048
- retryAfterMs,
2049
- signal: streamOptions?.signal,
2050
- });
2051
- logger.debug("Retrying provider request after usage-limit block", {
2052
- provider,
2053
- switched,
2054
- retryAfterMs,
2055
- error: message,
2056
- });
2057
- if (!switched) return undefined;
2058
- return modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
2059
- }
2060
- await modelRegistry.authStorage.invalidateCredentialMatching(provider, oldKey, {
2061
- signal: streamOptions?.signal,
2062
- sessionId: agent.sessionId,
2063
- });
2064
- logger.debug("Retrying provider request after credential invalidation", {
2065
- provider,
2066
- error: message,
2067
- });
2068
- return modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
2069
- },
2070
2044
  });
2071
2045
  },
2072
2046
  cursorExecHandlers,
@@ -471,6 +471,12 @@ export interface SessionStats {
471
471
  cost: number;
472
472
  }
473
473
 
474
+ export interface FreshSessionResult {
475
+ previousSessionId: string;
476
+ sessionId: string;
477
+ closedProviderSessions: number;
478
+ }
479
+
474
480
  /** Internal marker for hook messages queued through the agent loop */
475
481
  // ============================================================================
476
482
  // Constants
@@ -922,6 +928,7 @@ export class AgentSession {
922
928
  #agentId: string | undefined;
923
929
  #agentRegistry: AgentRegistry | undefined;
924
930
  #providerSessionId: string | undefined;
931
+ #freshProviderSessionId: string | undefined;
925
932
  #isDisposed = false;
926
933
  // Extension system
927
934
  #extensionRunner: ExtensionRunner | undefined = undefined;
@@ -1275,6 +1282,14 @@ export class AgentSession {
1275
1282
  return this.#modelRegistry;
1276
1283
  }
1277
1284
 
1285
+ get asyncJobManager(): AsyncJobManager | undefined {
1286
+ return this.#asyncJobManager;
1287
+ }
1288
+
1289
+ getAgentId(): string | undefined {
1290
+ return this.#agentId;
1291
+ }
1292
+
1278
1293
  /** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
1279
1294
  nextToolChoice(): ToolChoice | undefined {
1280
1295
  return this.#toolChoiceQueue.nextToolChoice();
@@ -1681,7 +1696,7 @@ export class AgentSession {
1681
1696
  // Abort the stream immediately — do not gate on extension callbacks
1682
1697
  this.#ttsrAbortPending = true;
1683
1698
  this.#ensureTtsrResumePromise();
1684
- this.agent.abort();
1699
+ this.agent.abort(this.#formatTtsrAbortReason(matches));
1685
1700
  // Notify extensions (fire-and-forget, does not block abort)
1686
1701
  this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
1687
1702
  // Schedule retry after a short delay
@@ -2162,6 +2177,12 @@ export class AgentSession {
2162
2177
  }
2163
2178
  }
2164
2179
 
2180
+ #formatTtsrAbortReason(rules: Rule[]): string {
2181
+ const label = rules.length === 1 ? "rule" : "rules";
2182
+ const ruleNames = rules.map(rule => rule.name).join(", ");
2183
+ return `TTSR matched ${label}: ${ruleNames}`;
2184
+ }
2185
+
2165
2186
  /** Get TTSR injection payload and clear pending injections. */
2166
2187
  #getTtsrInjectionContent(): { content: string; rules: Rule[] } | undefined {
2167
2188
  if (this.#pendingTtsrInjections.length === 0) return undefined;
@@ -2185,13 +2206,20 @@ export class AgentSession {
2185
2206
  * project, `~`-relative when it lives under home, else the raw path.
2186
2207
  */
2187
2208
  #displayRulePath(rulePath: string): string {
2188
- const cwdRel = relativePathWithinRoot(this.sessionManager.getCwd(), rulePath);
2209
+ const cwdRel =
2210
+ relativePathWithinRoot(this.sessionManager.getCwd(), rulePath) ??
2211
+ this.#displayPathWithinRoot(this.sessionManager.getCwd(), rulePath);
2189
2212
  if (cwdRel) return cwdRel;
2190
2213
  const homeRel = relativePathWithinRoot(os.homedir(), rulePath);
2191
2214
  if (homeRel) return `~/${homeRel}`;
2192
2215
  return rulePath;
2193
2216
  }
2194
2217
 
2218
+ #displayPathWithinRoot(root: string, candidate: string): string | null {
2219
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
2220
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : null;
2221
+ }
2222
+
2195
2223
  #addPendingTtsrInjections(rules: Rule[]): void {
2196
2224
  const seen = new Set(this.#pendingTtsrInjections.map(rule => rule.name));
2197
2225
  for (const rule of rules) {
@@ -2946,6 +2974,10 @@ export class AgentSession {
2946
2974
  this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
2947
2975
  }
2948
2976
 
2977
+ #activeProviderSessionId(sessionId?: string): string {
2978
+ return this.#freshProviderSessionId ?? this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
2979
+ }
2980
+
2949
2981
  /**
2950
2982
  * Set agent.sessionId from the session manager and install a dynamic
2951
2983
  * metadata resolver so every Anthropic API request carries
@@ -2958,7 +2990,7 @@ export class AgentSession {
2958
2990
  * `#syncAgentSessionId()` on every such event.
2959
2991
  */
2960
2992
  #syncAgentSessionId(sessionId?: string): void {
2961
- const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
2993
+ const sid = this.#activeProviderSessionId(sessionId);
2962
2994
  this.agent.sessionId = sid;
2963
2995
  this.agent.setMetadataResolver((provider: string) =>
2964
2996
  buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
@@ -3088,6 +3120,23 @@ export class AgentSession {
3088
3120
  this.#providerSessionState.clear();
3089
3121
  }
3090
3122
 
3123
+ freshSession(): FreshSessionResult | undefined {
3124
+ if (this.isStreaming) return undefined;
3125
+ const previousSessionId = this.sessionId;
3126
+ const closedProviderSessions = this.#providerSessionState.size;
3127
+ this.#closeAllProviderSessions("fresh session");
3128
+ this.#freshProviderSessionId = Bun.randomUUIDv7();
3129
+ this.#syncAgentSessionId();
3130
+ this.#rekeyHindsightMemoryForCurrentSessionId();
3131
+ this.#rekeyMnemopiMemoryForCurrentSessionId();
3132
+ this.agent.appendOnlyContext?.invalidateForModelChange();
3133
+ return {
3134
+ previousSessionId,
3135
+ sessionId: this.sessionId,
3136
+ closedProviderSessions,
3137
+ };
3138
+ }
3139
+
3091
3140
  // =========================================================================
3092
3141
  // Read-only State Access
3093
3142
  // =========================================================================
@@ -3992,7 +4041,7 @@ export class AgentSession {
3992
4041
 
3993
4042
  /** Current session ID */
3994
4043
  get sessionId(): string {
3995
- return this.#providerSessionId ?? this.sessionManager.getSessionId();
4044
+ return this.#activeProviderSessionId();
3996
4045
  }
3997
4046
  getEvalSessionId(): string | null {
3998
4047
  if (this.#parentEvalSessionId !== undefined) return this.#parentEvalSessionId;
@@ -5091,8 +5140,13 @@ export class AgentSession {
5091
5140
 
5092
5141
  /**
5093
5142
  * Abort current operation and wait for agent to become idle.
5143
+ *
5144
+ * `reason` (e.g. `USER_INTERRUPT_LABEL`) rides the agent's `AbortController`
5145
+ * and surfaces verbatim on the aborted assistant message's `errorMessage`, so
5146
+ * the transcript can distinguish a deliberate user interrupt from an opaque
5147
+ * abort. Omit it for internal/lifecycle aborts.
5094
5148
  */
5095
- async abort(options?: { goalReason?: "interrupted" | "internal" }): Promise<void> {
5149
+ async abort(options?: { goalReason?: "interrupted" | "internal"; reason?: string }): Promise<void> {
5096
5150
  this.abortRetry();
5097
5151
  this.#promptGeneration++;
5098
5152
  this.#scheduledHiddenNextTurnGeneration = undefined;
@@ -5101,7 +5155,7 @@ export class AgentSession {
5101
5155
  this.abortBash();
5102
5156
  this.abortEval();
5103
5157
  const postPromptDrain = this.#cancelPostPromptTasks();
5104
- this.agent.abort();
5158
+ this.agent.abort(options?.reason);
5105
5159
  await postPromptDrain;
5106
5160
  await this.agent.waitForIdle();
5107
5161
  await this.#goalRuntime.onTaskAborted({ reason: options?.goalReason ?? "interrupted" });
@@ -5118,6 +5172,19 @@ export class AgentSession {
5118
5172
  }
5119
5173
  }
5120
5174
 
5175
+ /**
5176
+ * Abort active work, then immediately resume the agent so queued steer/follow-up
5177
+ * messages drain instead of waiting for another natural turn boundary.
5178
+ */
5179
+ async interruptAndFlushQueuedMessages(options?: { reason?: string }): Promise<void> {
5180
+ if (!this.agent.hasQueuedMessages()) return;
5181
+ await this.abort({ reason: options?.reason });
5182
+ if (!this.agent.hasQueuedMessages()) return;
5183
+ if (this.isCompacting || this.isGeneratingHandoff) return;
5184
+ await this.#maybeRestoreRetryFallbackPrimary();
5185
+ await this.agent.continue();
5186
+ }
5187
+
5121
5188
  /**
5122
5189
  * Start a new session, optionally with initial messages and parent tracking.
5123
5190
  * Clears all messages and starts a new session.
@@ -5162,6 +5229,7 @@ export class AgentSession {
5162
5229
  }
5163
5230
  await this.sessionManager.newSession(options);
5164
5231
  this.setTodoPhases([]);
5232
+ this.#freshProviderSessionId = undefined;
5165
5233
  this.#syncAgentSessionId();
5166
5234
  this.#rekeyHindsightMemoryForCurrentSessionId();
5167
5235
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -5259,6 +5327,7 @@ export class AgentSession {
5259
5327
  }
5260
5328
 
5261
5329
  // Update agent session ID
5330
+ this.#freshProviderSessionId = undefined;
5262
5331
  this.#syncAgentSessionId();
5263
5332
  this.#rekeyHindsightMemoryForCurrentSessionId();
5264
5333
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -6226,6 +6295,7 @@ export class AgentSession {
6226
6295
  this.#cancelOwnAsyncJobs();
6227
6296
  await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
6228
6297
  this.agent.reset();
6298
+ this.#freshProviderSessionId = undefined;
6229
6299
  this.#syncAgentSessionId();
6230
6300
  this.#rekeyHindsightMemoryForCurrentSessionId();
6231
6301
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -8941,6 +9011,7 @@ export class AgentSession {
8941
9011
  const previousTools = [...this.agent.state.tools];
8942
9012
  const previousBaseSystemPrompt = this.#baseSystemPrompt;
8943
9013
  const previousSystemPrompt = this.agent.state.systemPrompt;
9014
+ const previousFreshProviderSessionId = this.#freshProviderSessionId;
8944
9015
  const previousFallbackSelectedMCPToolNames = previousSessionFile
8945
9016
  ? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
8946
9017
  : undefined;
@@ -8952,6 +9023,9 @@ export class AgentSession {
8952
9023
 
8953
9024
  try {
8954
9025
  await this.sessionManager.setSessionFile(sessionPath);
9026
+ if (switchingToDifferentSession) {
9027
+ this.#freshProviderSessionId = undefined;
9028
+ }
8955
9029
  this.#syncAgentSessionId();
8956
9030
  this.#rekeyHindsightMemoryForCurrentSessionId();
8957
9031
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -9061,6 +9135,7 @@ export class AgentSession {
9061
9135
  return true;
9062
9136
  } catch (error) {
9063
9137
  this.sessionManager.restoreState(previousSessionState);
9138
+ this.#freshProviderSessionId = previousFreshProviderSessionId;
9064
9139
  this.#syncAgentSessionId(previousSessionState.sessionId);
9065
9140
  this.#rekeyHindsightMemoryForCurrentSessionId();
9066
9141
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -9159,6 +9234,7 @@ export class AgentSession {
9159
9234
  this.sessionManager.createBranchedSession(selectedEntry.parentId);
9160
9235
  }
9161
9236
  this.#syncTodoPhasesFromBranch();
9237
+ this.#freshProviderSessionId = undefined;
9162
9238
  this.#syncAgentSessionId();
9163
9239
  this.#rekeyHindsightMemoryForCurrentSessionId();
9164
9240
  this.#rekeyMnemopiMemoryForCurrentSessionId();
@@ -70,6 +70,32 @@ export function isSilentAbort(errorMessage: string | undefined): boolean {
70
70
  return errorMessage === SILENT_ABORT_MARKER;
71
71
  }
72
72
 
73
+ /** Reason threaded through `AbortController.abort(reason)` when the user aborts
74
+ * the turn with Esc (see `AgentSession.abort`). The agent surfaces it verbatim
75
+ * on the aborted assistant message's `errorMessage`, so the transcript reads as
76
+ * a deliberate user interrupt instead of an opaque failure. */
77
+ export const USER_INTERRUPT_LABEL = "Interrupted by user";
78
+
79
+ /** Sentinel `errorMessage` the agent stamps on any abort that carried no custom
80
+ * reason (bare `abort()`). Renderers treat it as "no specific reason given". */
81
+ const GENERIC_ABORT_SENTINEL = "Request was aborted";
82
+
83
+ /** Resolve the operator-facing label for an aborted assistant turn. A custom
84
+ * abort reason (e.g. `USER_INTERRUPT_LABEL`) threaded onto `errorMessage` is
85
+ * shown verbatim; aborts with no threaded reason fall back to the retry-aware
86
+ * generic label. Centralizes the live-stream (`EventController`), replay
87
+ * (`ui-helpers`), and component (`AssistantMessageComponent`) render paths so
88
+ * they stay in lockstep. */
89
+ export function resolveAbortLabel(errorMessage: string | undefined, retryAttempt = 0): string {
90
+ if (errorMessage && errorMessage !== GENERIC_ABORT_SENTINEL && !isSilentAbort(errorMessage)) {
91
+ return errorMessage;
92
+ }
93
+ if (retryAttempt > 0) {
94
+ return `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`;
95
+ }
96
+ return "Operation aborted";
97
+ }
98
+
73
99
  /** Extract the optional `__pendingDisplayTag` field from a CustomMessage's
74
100
  * `details` blob. Safe over `unknown`; returns undefined when the field is
75
101
  * absent or non-string. */
@@ -1967,6 +1967,7 @@ export class SessionManager {
1967
1967
  #inMemoryArtifacts: Map<string, string> | null = null;
1968
1968
  #inMemoryArtifactCounter = 0;
1969
1969
  readonly #blobStore: BlobStore;
1970
+ #suppressBreadcrumb = false;
1970
1971
 
1971
1972
  private constructor(
1972
1973
  private cwd: string,
@@ -1981,6 +1982,11 @@ export class SessionManager {
1981
1982
  // Note: call _initSession() or _initSessionFile() after construction
1982
1983
  }
1983
1984
 
1985
+ #maybeWriteBreadcrumb(cwd: string, sessionFile: string): void {
1986
+ if (this.#suppressBreadcrumb) return;
1987
+ writeTerminalBreadcrumb(cwd, sessionFile);
1988
+ }
1989
+
1984
1990
  /** Puts a binary blob into the blob store and returns the blob reference */
1985
1991
  async putBlob(data: Buffer, options?: BlobPutOptions): Promise<BlobPutResult> {
1986
1992
  return this.#blobStore.put(data, options);
@@ -2027,7 +2033,7 @@ export class SessionManager {
2027
2033
  this.#adoptedArtifactManager = null;
2028
2034
  this.#buildIndex();
2029
2035
  if (this.#sessionFile) {
2030
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2036
+ this.#maybeWriteBreadcrumb(this.cwd, this.#sessionFile);
2031
2037
  }
2032
2038
  }
2033
2039
 
@@ -2047,7 +2053,7 @@ export class SessionManager {
2047
2053
  this.#persistError = undefined;
2048
2054
  this.#persistErrorReported = false;
2049
2055
  this.#sessionFile = path.resolve(sessionFile);
2050
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2056
+ this.#maybeWriteBreadcrumb(this.cwd, this.#sessionFile);
2051
2057
  this.#fileEntries = await loadEntriesFromFile(this.#sessionFile, this.storage);
2052
2058
  if (this.#fileEntries.length > 0) {
2053
2059
  const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
@@ -2064,7 +2070,7 @@ export class SessionManager {
2064
2070
  if (headerCwd && headerCwd !== this.cwd) {
2065
2071
  this.cwd = headerCwd;
2066
2072
  this.sessionDir = path.resolve(this.#sessionFile, "..");
2067
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2073
+ this.#maybeWriteBreadcrumb(this.cwd, this.#sessionFile);
2068
2074
  }
2069
2075
 
2070
2076
  this.#needsFullRewriteOnNextPersist = migrateToCurrentVersion(this.#fileEntries);
@@ -2245,7 +2251,7 @@ export class SessionManager {
2245
2251
 
2246
2252
  // Update terminal breadcrumb
2247
2253
  if (this.#sessionFile) {
2248
- writeTerminalBreadcrumb(resolvedCwd, this.#sessionFile);
2254
+ this.#maybeWriteBreadcrumb(resolvedCwd, this.#sessionFile);
2249
2255
  }
2250
2256
  }
2251
2257
 
@@ -2280,7 +2286,7 @@ export class SessionManager {
2280
2286
  if (this.persist) {
2281
2287
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
2282
2288
  this.#sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.#sessionId}.jsonl`);
2283
- writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
2289
+ this.#maybeWriteBreadcrumb(this.cwd, this.#sessionFile);
2284
2290
  }
2285
2291
  return this.#sessionFile;
2286
2292
  }
@@ -3429,9 +3435,11 @@ export class SessionManager {
3429
3435
  cwd: string,
3430
3436
  sessionDir?: string,
3431
3437
  storage: SessionStorage = new FileSessionStorage(),
3438
+ options?: { suppressBreadcrumb?: boolean },
3432
3439
  ): Promise<SessionManager> {
3433
3440
  const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
3434
3441
  const manager = new SessionManager(cwd, dir, true, storage);
3442
+ manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
3435
3443
  const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
3436
3444
  migrateToCurrentVersion(forkEntries);
3437
3445
  await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);