@stigmer/runner 3.40.0 → 3.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +3 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
  4. package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
  5. package/dist/activities/call-agent.js +1 -1
  6. package/dist/activities/discover-mcp-server.d.ts +7 -1
  7. package/dist/activities/discover-mcp-server.js +13 -29
  8. package/dist/activities/discover-mcp-server.js.map +1 -1
  9. package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
  10. package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
  11. package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
  12. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
  13. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  14. package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
  15. package/dist/activities/execute-cursor/approval-policy.js +11 -35
  16. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  17. package/dist/activities/execute-cursor/boundary-rows.js +4 -4
  18. package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
  19. package/dist/activities/execute-cursor/translator.d.ts +12 -17
  20. package/dist/activities/execute-cursor/translator.js +13 -30
  21. package/dist/activities/execute-cursor/translator.js.map +1 -1
  22. package/dist/activities/execute-cursor/turn-stream.js +4 -2
  23. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  24. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
  25. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
  26. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
  28. package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
  29. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
  31. package/dist/activities/execute-deep-agent/platform-route.js +87 -19
  32. package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
  34. package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
  35. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  36. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
  37. package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
  38. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
  40. package/dist/activities/execute-deep-agent/translator.js +5 -4
  41. package/dist/activities/execute-deep-agent/translator.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
  43. package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
  44. package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
  45. package/dist/activities/execute-deep-agent/turn-setup.js +42 -36
  46. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  47. package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
  48. package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
  49. package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
  50. package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
  51. package/dist/activities/execute-deep-agent/turn.js +15 -6
  52. package/dist/activities/execute-deep-agent/turn.js.map +1 -1
  53. package/dist/activities/workflow-event-activities.js +2 -0
  54. package/dist/activities/workflow-event-activities.js.map +1 -1
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +2 -0
  57. package/dist/config.js.map +1 -1
  58. package/dist/exit-backstop.d.ts +64 -0
  59. package/dist/exit-backstop.js +79 -0
  60. package/dist/exit-backstop.js.map +1 -0
  61. package/dist/harness/terminal-table.d.ts +12 -5
  62. package/dist/harness/terminal-table.js +13 -13
  63. package/dist/harness/terminal-table.js.map +1 -1
  64. package/dist/harness/transcript/builder.d.ts +21 -0
  65. package/dist/harness/transcript/builder.js +53 -31
  66. package/dist/harness/transcript/builder.js.map +1 -1
  67. package/dist/harness/transcript/events.d.ts +22 -26
  68. package/dist/harness/transcript/events.js +6 -5
  69. package/dist/harness/transcript/events.js.map +1 -1
  70. package/dist/harness/turn-context.d.ts +3 -1
  71. package/dist/harness/turn-context.js +5 -3
  72. package/dist/harness/turn-context.js.map +1 -1
  73. package/dist/main.js +10 -4
  74. package/dist/main.js.map +1 -1
  75. package/dist/middleware/cost-advisory.d.ts +6 -3
  76. package/dist/middleware/cost-advisory.js +6 -3
  77. package/dist/middleware/cost-advisory.js.map +1 -1
  78. package/dist/middleware/execution-budget.d.ts +22 -7
  79. package/dist/middleware/execution-budget.js +57 -35
  80. package/dist/middleware/execution-budget.js.map +1 -1
  81. package/dist/middleware/index.d.ts +10 -7
  82. package/dist/middleware/index.js +10 -7
  83. package/dist/middleware/index.js.map +1 -1
  84. package/dist/middleware/types.d.ts +6 -1
  85. package/dist/runner-manager.js +3 -2
  86. package/dist/runner-manager.js.map +1 -1
  87. package/dist/runner.d.ts +25 -0
  88. package/dist/runner.js +41 -14
  89. package/dist/runner.js.map +1 -1
  90. package/dist/shared/approval-policy.d.ts +23 -13
  91. package/dist/shared/approval-policy.js +47 -14
  92. package/dist/shared/approval-policy.js.map +1 -1
  93. package/dist/shared/caller-identity.d.ts +4 -5
  94. package/dist/shared/caller-identity.js +4 -5
  95. package/dist/shared/caller-identity.js.map +1 -1
  96. package/dist/shared/channel-attachment.js +3 -2
  97. package/dist/shared/channel-attachment.js.map +1 -1
  98. package/dist/shared/connect-backfill.d.ts +2 -1
  99. package/dist/shared/connect-backfill.js +2 -2
  100. package/dist/shared/connect-backfill.js.map +1 -1
  101. package/dist/shared/mcp-resolver.d.ts +10 -1
  102. package/dist/shared/mcp-resolver.js +11 -2
  103. package/dist/shared/mcp-resolver.js.map +1 -1
  104. package/dist/shared/memory-attachment.js +3 -2
  105. package/dist/shared/memory-attachment.js.map +1 -1
  106. package/dist/shared/model-error.d.ts +25 -3
  107. package/dist/shared/model-error.js +78 -10
  108. package/dist/shared/model-error.js.map +1 -1
  109. package/dist/shared/platform-server-address.d.ts +71 -0
  110. package/dist/shared/platform-server-address.js +103 -0
  111. package/dist/shared/platform-server-address.js.map +1 -0
  112. package/dist/shared/prompt-sections.d.ts +6 -28
  113. package/dist/shared/prompt-sections.js +6 -35
  114. package/dist/shared/prompt-sections.js.map +1 -1
  115. package/dist/shared/skill-resolver.d.ts +2 -3
  116. package/dist/shared/skill-resolver.js +2 -3
  117. package/dist/shared/skill-resolver.js.map +1 -1
  118. package/dist/shared/synthesized-attachment.d.ts +0 -5
  119. package/dist/shared/synthesized-attachment.js +0 -12
  120. package/dist/shared/synthesized-attachment.js.map +1 -1
  121. package/dist/shared/tool-rounds.d.ts +51 -23
  122. package/dist/shared/tool-rounds.js +67 -24
  123. package/dist/shared/tool-rounds.js.map +1 -1
  124. package/dist/shared/workspace/platform-dir.d.ts +15 -0
  125. package/dist/shared/workspace/platform-dir.js +18 -1
  126. package/dist/shared/workspace/platform-dir.js.map +1 -1
  127. package/dist/shared/workspace/sources/git.js +13 -6
  128. package/dist/shared/workspace/sources/git.js.map +1 -1
  129. package/dist/shared/workspace/stigmer-link.d.ts +21 -14
  130. package/dist/shared/workspace/stigmer-link.js +91 -29
  131. package/dist/shared/workspace/stigmer-link.js.map +1 -1
  132. package/dist/tools/url-guard.d.ts +5 -1
  133. package/dist/tools/url-guard.js +9 -2
  134. package/dist/tools/url-guard.js.map +1 -1
  135. package/dist/worker.d.ts +11 -2
  136. package/dist/worker.js +1 -1
  137. package/dist/worker.js.map +1 -1
  138. package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
  139. package/dist/workflow-engine/tasks/human-input.js +79 -37
  140. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  141. package/dist/workflow-engine/types.d.ts +14 -4
  142. package/dist/workflow-engine/types.js.map +1 -1
  143. package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
  144. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  145. package/dist/workflows/engine-core.d.ts +25 -0
  146. package/dist/workflows/engine-core.js.map +1 -1
  147. package/dist/workflows/execute-from-execution.d.ts +3 -0
  148. package/dist/workflows/execute-from-execution.js.map +1 -1
  149. package/dist/workflows/human-input-orchestrator.d.ts +9 -12
  150. package/dist/workflows/human-input-orchestrator.js +12 -44
  151. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  152. package/package.json +5 -5
  153. package/src/__test-utils__/config-fixture.ts +1 -0
  154. package/src/__test-utils__/execution-record-fixture.ts +2 -1
  155. package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
  156. package/src/__test-utils__/test-gate.ts +8 -0
  157. package/src/__tests__/exit-backstop.test.ts +91 -0
  158. package/src/__tests__/golden-e2e.test.ts +80 -62
  159. package/src/__tests__/history-encryption-e2e.test.ts +51 -27
  160. package/src/__tests__/runner.test.ts +60 -2
  161. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  162. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
  163. package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
  164. package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
  165. package/src/activities/call-agent.ts +1 -1
  166. package/src/activities/discover-mcp-server.ts +27 -37
  167. package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
  168. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
  169. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
  170. package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
  171. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
  172. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
  173. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  174. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
  175. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
  176. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
  177. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
  178. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
  179. package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
  180. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
  181. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
  182. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
  183. package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
  184. package/src/activities/execute-cursor/approval-policy.ts +11 -36
  185. package/src/activities/execute-cursor/boundary-rows.ts +3 -3
  186. package/src/activities/execute-cursor/translator.ts +15 -32
  187. package/src/activities/execute-cursor/turn-stream.ts +4 -1
  188. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
  189. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
  190. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
  191. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
  192. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
  193. package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
  194. package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
  195. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
  196. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
  197. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
  198. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
  199. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  200. package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
  201. package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
  202. package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
  203. package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
  204. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
  205. package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
  206. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
  207. package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
  208. package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
  209. package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
  210. package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
  211. package/src/activities/execute-deep-agent/platform-route.ts +109 -20
  212. package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
  213. package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
  214. package/src/activities/execute-deep-agent/translator.ts +5 -4
  215. package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
  216. package/src/activities/execute-deep-agent/turn-setup.ts +43 -38
  217. package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
  218. package/src/activities/execute-deep-agent/turn.ts +16 -7
  219. package/src/activities/workflow-event-activities.ts +2 -0
  220. package/src/config.ts +11 -0
  221. package/src/exit-backstop.ts +97 -0
  222. package/src/harness/README.md +5 -5
  223. package/src/harness/terminal-table.ts +13 -13
  224. package/src/harness/transcript/__tests__/builder.test.ts +93 -21
  225. package/src/harness/transcript/builder.ts +53 -30
  226. package/src/harness/transcript/events.ts +22 -27
  227. package/src/harness/turn-context.ts +5 -3
  228. package/src/main.ts +9 -5
  229. package/src/middleware/__tests__/execution-budget.test.ts +181 -103
  230. package/src/middleware/cost-advisory.ts +6 -3
  231. package/src/middleware/execution-budget.ts +60 -34
  232. package/src/middleware/index.ts +10 -7
  233. package/src/middleware/types.ts +6 -1
  234. package/src/runner-manager.ts +3 -2
  235. package/src/runner.ts +57 -15
  236. package/src/shared/__tests__/approval-policy.test.ts +41 -0
  237. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  238. package/src/shared/__tests__/connect-backfill.test.ts +15 -11
  239. package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
  240. package/src/shared/__tests__/model-error.test.ts +71 -3
  241. package/src/shared/__tests__/platform-server-address.test.ts +122 -0
  242. package/src/shared/__tests__/prompt-sections.test.ts +2 -54
  243. package/src/shared/__tests__/tool-rounds.test.ts +64 -14
  244. package/src/shared/approval-policy.ts +57 -14
  245. package/src/shared/caller-identity.ts +4 -5
  246. package/src/shared/channel-attachment.ts +2 -2
  247. package/src/shared/connect-backfill.ts +5 -1
  248. package/src/shared/mcp-resolver.ts +11 -1
  249. package/src/shared/memory-attachment.ts +2 -2
  250. package/src/shared/model-error.ts +87 -9
  251. package/src/shared/platform-server-address.ts +123 -0
  252. package/src/shared/prompt-sections.ts +6 -55
  253. package/src/shared/skill-resolver.ts +2 -3
  254. package/src/shared/synthesized-attachment.ts +0 -12
  255. package/src/shared/tool-rounds.ts +70 -24
  256. package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
  257. package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
  258. package/src/shared/workspace/platform-dir.ts +19 -1
  259. package/src/shared/workspace/sources/git.ts +14 -8
  260. package/src/shared/workspace/stigmer-link.ts +99 -29
  261. package/src/tools/__tests__/url-guard.test.ts +19 -1
  262. package/src/tools/url-guard.ts +9 -1
  263. package/src/worker.ts +12 -2
  264. package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
  265. package/src/workflow-engine/__tests__/loader.test.ts +1 -1
  266. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
  267. package/src/workflow-engine/tasks/human-input.ts +92 -42
  268. package/src/workflow-engine/types.ts +16 -4
  269. package/src/workflows/call-agent-orchestrator.ts +7 -0
  270. package/src/workflows/engine-core.ts +18 -0
  271. package/src/workflows/execute-from-execution.ts +2 -1
  272. package/src/workflows/human-input-orchestrator.ts +15 -56
  273. package/dist/shared/skill-relevance.d.ts +0 -65
  274. package/dist/shared/skill-relevance.js +0 -175
  275. package/dist/shared/skill-relevance.js.map +0 -1
  276. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
  277. package/src/shared/__tests__/skill-relevance.test.ts +0 -292
  278. package/src/shared/skill-relevance.ts +0 -222
@@ -1,8 +1,6 @@
1
1
  /**
2
- * The shared prompt glue (`shared/prompt-sections.ts`): the standing order,
3
- * the input-files lines, the skill selection and the also-available sentence
4
- * (the sentence's arms came from `skill-writer.test.ts`, retired with its
5
- * module in #1096). What each harness renders AROUND these is pinned whole by
2
+ * The shared prompt glue (`shared/prompt-sections.ts`): the standing order
3
+ * and the input-files lines. What each harness renders AROUND these is pinned whole by
6
4
  * the two builders' prompt goldens (`execute-deep-agent/__tests__/
7
5
  * prompt-goldens.test.ts`, `execute-cursor/__tests__/prompt-goldens.test.ts`);
8
6
  * this file pins the glue's own contract, harness-free.
@@ -10,16 +8,12 @@
10
8
 
11
9
  import { describe, it, expect } from "vitest";
12
10
  import {
13
- alsoAvailableSkillsNote,
14
11
  inputFileLines,
15
- selectSkillsForPrompt,
16
12
  standingContextSections,
17
13
  visionPromptInfoOf,
18
14
  type StandingSectionKind,
19
15
  } from "../prompt-sections.js";
20
16
  import type { ResolvedAttachment } from "../attachment-resolver.js";
21
- import type { SkillMetadata } from "../skill-resolver.js";
22
- import { SKILL_COUNT_THRESHOLD } from "../skill-relevance.js";
23
17
  import { formatDeclaredPreferencesText } from "../declared-preferences.js";
24
18
 
25
19
  describe("standingContextSections", () => {
@@ -119,49 +113,3 @@ describe("visionPromptInfoOf", () => {
119
113
  });
120
114
  });
121
115
  });
122
-
123
- describe("selectSkillsForPrompt", () => {
124
- const skill = (name: string, description: string): SkillMetadata => ({ name, description, path: `.stigmer/skills/${name}/SKILL.md` });
125
-
126
- it("highlights every skill below the threshold, in mount order, and names none as also-available", () => {
127
- const skills = [skill("alpha", "Deploy to kubernetes"), skill("beta", "Reshape CSV exports")];
128
- const selection = selectSkillsForPrompt("deploy the service", skills);
129
- expect(selection.highlighted).toEqual(skills);
130
- expect(selection.alsoAvailable).toEqual([]);
131
- });
132
-
133
- it("at the threshold, highlights the skills the message reaches and names the rest", () => {
134
- const skills = Array.from({ length: SKILL_COUNT_THRESHOLD }, (_, i) =>
135
- i === 0 ? skill("k8s-deploy", "Deploy services to kubernetes clusters") : skill(`other-${i}`, `Unrelated thing number ${i}`),
136
- );
137
- const selection = selectSkillsForPrompt("Deploy the payments service to kubernetes", skills);
138
- expect(selection.highlighted.map((s) => s.name)).toContain("k8s-deploy");
139
- expect(selection.alsoAvailable.length).toBeGreaterThan(0);
140
- expect(selection.highlighted.length + selection.alsoAvailable.length).toBe(SKILL_COUNT_THRESHOLD);
141
- });
142
-
143
- it("selects nothing from nothing", () => {
144
- expect(selectSkillsForPrompt("anything", [])).toEqual({ highlighted: [], alsoAvailable: [] });
145
- });
146
- });
147
-
148
- describe("alsoAvailableSkillsNote", () => {
149
- it("returns empty for no excluded names", () => {
150
- expect(alsoAvailableSkillsNote([])).toBe("");
151
- });
152
-
153
- it("lists excluded skills with backtick formatting", () => {
154
- const note = alsoAvailableSkillsNote(["alpha", "beta", "gamma"]);
155
- expect(note).toContain("`alpha`");
156
- expect(note).toContain("`beta`");
157
- expect(note).toContain("`gamma`");
158
- });
159
-
160
- it("includes activation instructions that name no tool, so any harness can carry it", () => {
161
- const note = alsoAvailableSkillsNote(["some-skill"]);
162
- expect(note).toContain(".stigmer/skills/<name>/SKILL.md");
163
- expect(note).toContain("relevant to your task");
164
- expect(note).not.toMatch(/\b(read|Read) tool\b/);
165
- expect(note.startsWith("#")).toBe(false);
166
- });
167
- });
@@ -1,35 +1,52 @@
1
+ /**
2
+ * max_tool_rounds resolution and the budget's stop signal.
3
+ *
4
+ * Pins the proto contract's arithmetic (0/unset = unlimited, the 10–1000
5
+ * clamp and its warnings), the backstop that must never bind before the
6
+ * round count, and the classifier the native turn uses to read a thrown
7
+ * error as the `tool_call_limit` outcome. The classifier cases use
8
+ * LangChain's and LangGraph's own error classes, so an engine upgrade that
9
+ * changes how a middleware's throw reaches the turn fails here, not in a
10
+ * production run that would otherwise read the stop as a failure.
11
+ */
12
+
1
13
  import { describe, expect, it, vi, afterEach } from "vitest";
14
+ import { MiddlewareError } from "langchain";
15
+ import { GraphRecursionError } from "@langchain/langgraph";
2
16
  import {
3
17
  MAX_TOOL_ROUNDS,
4
18
  MIN_TOOL_ROUNDS,
5
- SUPER_STEPS_PER_ROUND,
6
- resolveRecursionLimit,
19
+ SUPER_STEPS_BACKSTOP_PER_ROUND,
20
+ ToolRoundLimitError,
21
+ backstopRecursionLimit,
22
+ isToolCallBudgetStop,
23
+ resolveToolRoundLimit,
7
24
  } from "../tool-rounds.js";
8
25
 
9
- describe("resolveRecursionLimit", () => {
26
+ describe("resolveToolRoundLimit", () => {
10
27
  afterEach(() => {
11
28
  vi.restoreAllMocks();
12
29
  });
13
30
 
14
31
  it("returns null for unset (proto contract: 0 = unlimited)", () => {
15
- expect(resolveRecursionLimit(undefined)).toBeNull();
16
- expect(resolveRecursionLimit(0)).toBeNull();
32
+ expect(resolveToolRoundLimit(undefined)).toBeNull();
33
+ expect(resolveToolRoundLimit(0)).toBeNull();
17
34
  });
18
35
 
19
36
  it("returns null for negative values (treated as unset, never a tiny limit)", () => {
20
- expect(resolveRecursionLimit(-5)).toBeNull();
37
+ expect(resolveToolRoundLimit(-5)).toBeNull();
21
38
  });
22
39
 
23
- it("converts rounds to super-steps at the contract's x6 factor", () => {
24
- expect(resolveRecursionLimit(10)).toBe(60);
25
- expect(resolveRecursionLimit(100)).toBe(600);
26
- expect(resolveRecursionLimit(MAX_TOOL_ROUNDS)).toBe(MAX_TOOL_ROUNDS * SUPER_STEPS_PER_ROUND);
40
+ it("returns an in-range value as the round count itself", () => {
41
+ expect(resolveToolRoundLimit(10)).toBe(10);
42
+ expect(resolveToolRoundLimit(100)).toBe(100);
43
+ expect(resolveToolRoundLimit(MAX_TOOL_ROUNDS)).toBe(MAX_TOOL_ROUNDS);
27
44
  });
28
45
 
29
46
  it("clamps below-range values up to the minimum with a warning", () => {
30
47
  const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
31
48
 
32
- expect(resolveRecursionLimit(3)).toBe(MIN_TOOL_ROUNDS * SUPER_STEPS_PER_ROUND);
49
+ expect(resolveToolRoundLimit(3)).toBe(MIN_TOOL_ROUNDS);
33
50
 
34
51
  expect(warn).toHaveBeenCalledOnce();
35
52
  expect(warn.mock.calls[0][0]).toContain("max_tool_rounds=3");
@@ -39,7 +56,7 @@ describe("resolveRecursionLimit", () => {
39
56
  it("clamps above-range values down to the maximum with a warning", () => {
40
57
  const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
41
58
 
42
- expect(resolveRecursionLimit(5000)).toBe(MAX_TOOL_ROUNDS * SUPER_STEPS_PER_ROUND);
59
+ expect(resolveToolRoundLimit(5000)).toBe(MAX_TOOL_ROUNDS);
43
60
 
44
61
  expect(warn).toHaveBeenCalledOnce();
45
62
  expect(warn.mock.calls[0][0]).toContain("max_tool_rounds=5000");
@@ -49,9 +66,42 @@ describe("resolveRecursionLimit", () => {
49
66
  it("does not warn for in-range values", () => {
50
67
  const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
51
68
 
52
- resolveRecursionLimit(MIN_TOOL_ROUNDS);
53
- resolveRecursionLimit(MAX_TOOL_ROUNDS);
69
+ resolveToolRoundLimit(MIN_TOOL_ROUNDS);
70
+ resolveToolRoundLimit(MAX_TOOL_ROUNDS);
54
71
 
55
72
  expect(warn).not.toHaveBeenCalled();
56
73
  });
57
74
  });
75
+
76
+ describe("backstopRecursionLimit", () => {
77
+ it("allows the backstop's super-steps per round, far above what one round costs", () => {
78
+ expect(backstopRecursionLimit(MIN_TOOL_ROUNDS)).toBe(MIN_TOOL_ROUNDS * SUPER_STEPS_BACKSTOP_PER_ROUND);
79
+ expect(backstopRecursionLimit(MAX_TOOL_ROUNDS)).toBe(MAX_TOOL_ROUNDS * SUPER_STEPS_BACKSTOP_PER_ROUND);
80
+ });
81
+ });
82
+
83
+ describe("isToolCallBudgetStop", () => {
84
+ it("reads the round count's own error as the budget's stop", () => {
85
+ expect(isToolCallBudgetStop(new ToolRoundLimitError(10))).toBe(true);
86
+ });
87
+
88
+ it("reads it through LangChain's MiddlewareError wrapping, once per wrapModelCall layer", () => {
89
+ let wrapped: unknown = new ToolRoundLimitError(10);
90
+ for (const layer of ["ExecutionBudgetMiddleware", "todoListMiddleware", "SummarizationMiddleware"]) {
91
+ wrapped = MiddlewareError.wrap(wrapped, layer);
92
+ }
93
+ expect(wrapped).toBeInstanceOf(MiddlewareError);
94
+ expect(isToolCallBudgetStop(wrapped)).toBe(true);
95
+ });
96
+
97
+ it("reads the backstop's recursion error as the same stop", () => {
98
+ expect(isToolCallBudgetStop(new GraphRecursionError("Recursion limit of 200 reached without hitting a stop condition."))).toBe(true);
99
+ });
100
+
101
+ it("does not read any other failure as the budget's stop, wrapped or not", () => {
102
+ expect(isToolCallBudgetStop(new Error("upstream 529"))).toBe(false);
103
+ expect(isToolCallBudgetStop(MiddlewareError.wrap(new Error("upstream 529"), "ExecutionBudgetMiddleware"))).toBe(false);
104
+ expect(isToolCallBudgetStop("Recursion limit")).toBe(false);
105
+ expect(isToolCallBudgetStop(undefined)).toBe(false);
106
+ });
107
+ });
@@ -20,6 +20,7 @@ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexe
20
20
  import { ApprovalAction, ApprovalMode, ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
21
21
  import { SENSITIVE_ARG_KEYS } from "./args-preview.js";
22
22
  import { toolApprovalCategory, type ToolApprovalCategory } from "./tool-kind.js";
23
+ import { extractFilePath } from "./file-tools.js";
23
24
  import type { ResolvedMcpServer } from "./mcp-resolver.js";
24
25
 
25
26
  /**
@@ -480,18 +481,15 @@ export function resolveApprovalMessage(
480
481
  * THE approval-message template per mutating built-in category — the one
481
482
  * table both harnesses word their approval cards from (since #1097).
482
483
  * Keyed by category (not raw tool name) so every alias of a mutation renders
483
- * one message; placeholders resolve against the stream arg shape
484
- * (`path`/`command`; `resolveApprovalMessage`). The native gate decides with
485
- * it (`resolveToolApproval`), the Cursor translator words its `gate` from it
486
- * (`execute-cursor/approval-policy.ts` `getBuiltInApprovalMessage`), and the
487
- * Cursor boundary resolves a denied call's message through the same path.
484
+ * one message. It is read only through {@link resolveBuiltInApprovalMessage},
485
+ * which the native gate (`resolveToolApproval`), the Cursor translator's `gate`
486
+ * and the Cursor boundary's denied-call message all call, so the three can
487
+ * never word a card differently.
488
488
  *
489
489
  * Until then the Cursor harness carried a table of its own and the two differed
490
490
  * by a word — `shell` read "Run command" there and "Execute command" here.
491
491
  * Unified on the plainer word: a user reads "Run command" and knows
492
- * what is being asked. Known and deliberately left (#1112):
493
- * `write` resolves `{{args.path}}`, while deepagents' file tools send
494
- * `file_path`, so a gated native write reads "Write file: <unknown>".
492
+ * what is being asked.
495
493
  */
496
494
  export const CATEGORY_APPROVAL_MESSAGE: Record<ToolApprovalCategory, string> = {
497
495
  write: "Write file: {{args.path}}",
@@ -499,6 +497,50 @@ export const CATEGORY_APPROVAL_MESSAGE: Record<ToolApprovalCategory, string> = {
499
497
  shell: "Run command: {{args.command}}",
500
498
  };
501
499
 
500
+ /**
501
+ * THE approval-card message for a built-in tool of either harness taxonomy, or
502
+ * `undefined` when the tool is not a gated built-in.
503
+ *
504
+ * The `{{args.path}}` a file card reads is filled from `extractFilePath`
505
+ * (`file-tools.ts`), the one home of "which argument names the file" across
506
+ * both taxonomies: deepagents' file tools and the Cursor hook send
507
+ * `file_path`, the Cursor stream sends `path`. Resolving the raw args instead
508
+ * rendered a gated native write as "Write file: <unknown>" (#1112). The
509
+ * template keeps its one placeholder, so the table stays harness-neutral.
510
+ */
511
+ export function resolveBuiltInApprovalMessage(
512
+ toolName: string,
513
+ args: Record<string, unknown>,
514
+ ): string | undefined {
515
+ const category = toolApprovalCategory(toolName);
516
+ return category ? resolveCategoryApprovalMessage(category, toolName, args) : undefined;
517
+ }
518
+
519
+ function resolveCategoryApprovalMessage(
520
+ category: ToolApprovalCategory,
521
+ toolName: string,
522
+ args: Record<string, unknown>,
523
+ ): string {
524
+ return resolveApprovalMessage(CATEGORY_APPROVAL_MESSAGE[category], toolName, categoryTemplateArgs(category, args));
525
+ }
526
+
527
+ /** The args a category's template resolves against: a file card reads the file from wherever its taxonomy names it. */
528
+ function categoryTemplateArgs(category: ToolApprovalCategory, args: Record<string, unknown>): Record<string, unknown> {
529
+ switch (category) {
530
+ case "write":
531
+ case "delete": {
532
+ const path = extractFilePath(args);
533
+ return path === null ? args : { ...args, path };
534
+ }
535
+ case "shell":
536
+ return args;
537
+ default: {
538
+ const exhaustive: never = category;
539
+ throw new Error(`categoryTemplateArgs: unknown approval category ${String(exhaustive)}`);
540
+ }
541
+ }
542
+ }
543
+
502
544
  /** What the gate decides for one call: whether it waits, the message the card shows, and which layer said so. */
503
545
  export interface ApprovalRequirement {
504
546
  readonly requiresApproval: boolean;
@@ -508,11 +550,12 @@ export interface ApprovalRequirement {
508
550
  }
509
551
 
510
552
  /**
511
- * THE approval decision for one tool call — the same function the native gate
512
- * (`middleware/approval-gate.ts`) interrupts on and the native translator
513
- * reports as `tool_started.gate`, so the row can never disagree with the gate
514
- * about whether a call waits (since #1097; until then the transcript
515
- * builder carried its own weaker copy that gated MCP tools only). Its
553
+ * THE approval decision for one tool call — the function the native gate
554
+ * (`middleware/approval-gate.ts`) interrupts on. A held call reaches the
555
+ * transcript only through that interrupt (`approval_proposed`), so the row can
556
+ * never disagree with the gate about whether a call waits (since #1097; until
557
+ * then the transcript builder carried its own weaker copy that gated MCP tools
558
+ * only). Its
516
559
  * read-side twin is {@link resolveApprovalProvenance}, which answers "which
517
560
  * layer governs this call" for EVERY call, gated or not.
518
561
  *
@@ -561,7 +604,7 @@ export function resolveToolApproval(
561
604
  }
562
605
  return {
563
606
  requiresApproval: true,
564
- message: resolveApprovalMessage(CATEGORY_APPROVAL_MESSAGE[category], toolName, args),
607
+ message: resolveCategoryApprovalMessage(category, toolName, args),
565
608
  source: "builtin_category",
566
609
  };
567
610
  }
@@ -139,9 +139,8 @@ export function resolveCallerIdentity(
139
139
 
140
140
  /**
141
141
  * Return a NEW env map with the reserved caller-identity keys set —
142
- * platform values are authoritative over same-named user entries (the
143
- * injectPlatformEnv precedent: a user env var must never be able to
144
- * impersonate a caller).
142
+ * platform values are authoritative over same-named user entries (a user
143
+ * env var must never be able to impersonate a caller).
145
144
  *
146
145
  * Call this on the env map handed to MCP resolution ONLY — never on the
147
146
  * map that reaches agent subprocess environments. Per-server opt-in is
@@ -174,8 +173,8 @@ export function injectCallerIdentityEnv(
174
173
  * Discovery-context injection: the connect workflow resolves the same
175
174
  * header templates with no session, so every declared reserved key gets
176
175
  * the anonymous sentinel — otherwise a caller-identity-templating server
177
- * can never be discovered. Gated on the server's declared keys, matching
178
- * injectPlatformEnv's contract in the discovery activity.
176
+ * can never be discovered. Gated on the server's declared keys, as every
177
+ * platform key is (platform-server-address.ts).
179
178
  */
180
179
  export function injectAnonymousCallerIdentityForDiscovery(
181
180
  declaredEnvKeys: ReadonlySet<string>,
@@ -45,8 +45,8 @@ import type {
45
45
  } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
46
46
  import type { StigmerClient } from "../client/stigmer-client.js";
47
47
  import type { ResolvedMcpServer } from "./mcp-resolver.js";
48
+ import { grpcTarget, SERVER_ADDRESS_ENV_KEY } from "./platform-server-address.js";
48
49
  import {
49
- grpcTarget,
50
50
  stdioCredentialEnv,
51
51
  type SynthesizedAttachmentOptions,
52
52
  } from "./synthesized-attachment.js";
@@ -157,7 +157,7 @@ export function synthesizeChannelAttachment(
157
157
  args: ["mcp-server"],
158
158
  env: {
159
159
  STIGMER_MCP_ROSTER: "channels",
160
- STIGMER_SERVER_ADDRESS: grpcTarget(options.backendEndpoint),
160
+ [SERVER_ADDRESS_ENV_KEY]: grpcTarget(options.backendEndpoint),
161
161
  ...stdioCredentialEnv(options.credential),
162
162
  },
163
163
  };
@@ -21,6 +21,7 @@
21
21
  import type { ResolvedMcpServer } from "./mcp-resolver.js";
22
22
  import { resolveMcpServers } from "./mcp-resolver.js";
23
23
  import type { McpTransportPosture } from "./mcp-transport-guard.js";
24
+ import type { PlatformEndpoints } from "./platform-server-address.js";
24
25
  import { withTimeout } from "./with-timeout.js";
25
26
  import type { StigmerClient } from "../client/stigmer-client.js";
26
27
  import type { McpServerUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
@@ -67,6 +68,7 @@ export async function backfillMcpServersIfNeeded(
67
68
  envVars: Record<string, string>,
68
69
  org: string,
69
70
  transportPosture: McpTransportPosture,
71
+ platformEndpoints: PlatformEndpoints,
70
72
  onHeartbeat?: () => void,
71
73
  secretKeys?: ReadonlySet<string>,
72
74
  ): Promise<ResolvedMcpServer[]> {
@@ -131,7 +133,9 @@ export async function backfillMcpServersIfNeeded(
131
133
 
132
134
  // Same posture as the initial resolution: every server here already
133
135
  // passed the transport guard once, so re-resolving cannot newly reject.
134
- const refreshed = await resolveMcpServers(client, usages, envVars, transportPosture);
136
+ const refreshed = await resolveMcpServers(
137
+ client, usages, envVars, transportPosture, platformEndpoints,
138
+ );
135
139
  return refreshed.resolvedServers;
136
140
  }
137
141
 
@@ -12,6 +12,11 @@
12
12
  * toCursorMcpConfig (execute-cursor/cursor-mcp-config.ts) for the Cursor
13
13
  * SDK — so a behavioral change here (like the transport guard) lands in
14
14
  * both execution paths by construction.
15
+ *
16
+ * The one platform key filled here is STIGMER_SERVER_ADDRESS
17
+ * (platform-server-address.ts), before the declared-key filter so the filter
18
+ * sees the key present; discovery applies the same fill itself because it
19
+ * resolves a single server without a usage.
15
20
  */
16
21
 
17
22
  import type { McpServerUsage, ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
@@ -30,6 +35,7 @@ import {
30
35
  PlaceholderResolutionError,
31
36
  } from "./placeholder-resolver.js";
32
37
  import { effectiveEnabledTools } from "./mcp-enabled-tools.js";
38
+ import { fillPlatformServerAddress, type PlatformEndpoints } from "./platform-server-address.js";
33
39
 
34
40
  /**
35
41
  * Harness-agnostic intermediate representation of a resolved MCP server.
@@ -86,12 +92,16 @@ export interface McpResolutionResult {
86
92
  * resolveMcpTransportPosture(config.mode)). A stdio server under a
87
93
  * forbidding posture throws {@link McpTransportError} and fails the
88
94
  * whole resolution — never degraded to a skipped server.
95
+ * @param platformEndpoints The runner's endpoints a missing
96
+ * STIGMER_SERVER_ADDRESS is filled from (the runner Config satisfies
97
+ * it). Required so no resolution path can forget the fill.
89
98
  */
90
99
  export async function resolveMcpServers(
91
100
  client: StigmerClient,
92
101
  usages: McpServerUsage[],
93
102
  envVars: Record<string, string>,
94
103
  transportPosture: McpTransportPosture,
104
+ platformEndpoints: PlatformEndpoints,
95
105
  ): Promise<McpResolutionResult> {
96
106
  const resolved: ResolvedMcpServer[] = [];
97
107
 
@@ -103,7 +113,7 @@ export async function resolveMcpServers(
103
113
  const mcpServer = await client.getMcpServerByReference(ref);
104
114
  const serverEnv = filterEnvToDeclaredKeys(
105
115
  mcpServer.spec?.env,
106
- envVars,
116
+ fillPlatformServerAddress(mcpServer, envVars, platformEndpoints),
107
117
  ref.slug,
108
118
  );
109
119
  const server = mcpServerToResolved(
@@ -51,8 +51,8 @@
51
51
 
52
52
  import type { RecalledMemories } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
53
53
  import type { ResolvedMcpServer } from "./mcp-resolver.js";
54
+ import { grpcTarget, SERVER_ADDRESS_ENV_KEY } from "./platform-server-address.js";
54
55
  import {
55
- grpcTarget,
56
56
  stdioCredentialEnv,
57
57
  type SynthesizedAttachmentOptions,
58
58
  } from "./synthesized-attachment.js";
@@ -159,7 +159,7 @@ export function synthesizeMemoryAttachment(
159
159
  args: ["mcp-server"],
160
160
  env: {
161
161
  STIGMER_MCP_ROSTER: "memory",
162
- STIGMER_SERVER_ADDRESS: grpcTarget(options.backendEndpoint),
162
+ [SERVER_ADDRESS_ENV_KEY]: grpcTarget(options.backendEndpoint),
163
163
  ...stdioCredentialEnv(options.credential),
164
164
  ...nonEmptyEntries([
165
165
  [MEMORY_ORG_ENV, context.org],
@@ -19,6 +19,14 @@
19
19
  * (BYO-key) mode, attributes billing errors to the user's own account
20
20
  * with actionable wording.
21
21
  *
22
+ * 3. On Stigmer Cloud an organization may bring its own provider key, which
23
+ * the proxy injects upstream in place of the platform's. A failure on
24
+ * that key is the organization's, not the platform's, yet it reaches the
25
+ * runner in proxy mode. The proxy keeps the provider's status and message
26
+ * and marks the message with ORGANIZATION_PROVIDER_KEY_SENTINEL; this
27
+ * module words a marked failure as the organization's key, never as a
28
+ * platform fault.
29
+ *
22
30
  * Classification order is load-bearing: the sentinel check must precede
23
31
  * status mapping, otherwise the proxy's 503 would classify as a retryable
24
32
  * 5xx and waste Temporal retries against a dead platform account.
@@ -43,6 +51,14 @@ import {
43
51
  */
44
52
  export const PLATFORM_CAPACITY_SENTINEL = "STIGMER_PLATFORM_MODEL_CAPACITY";
45
53
 
54
+ /**
55
+ * Machine-readable code the cloud proxy embeds in a provider failure it
56
+ * relays from a call served on the organization's own provider key. The
57
+ * message text carries it, as it carries PLATFORM_CAPACITY_SENTINEL.
58
+ * Duplicated in stigmer-cloud's LLM proxy lane; change both or neither.
59
+ */
60
+ export const ORGANIZATION_PROVIDER_KEY_SENTINEL = "STIGMER_ORGANIZATION_PROVIDER_KEY";
61
+
46
62
  export interface ClassifiedModelError {
47
63
  /** Stable machine-readable code (doubles as the Temporal failure type). */
48
64
  readonly code: string;
@@ -72,17 +88,22 @@ export interface ModelErrorContext {
72
88
 
73
89
  /**
74
90
  * Walk the `cause` chain to the root error. LangChain's MiddlewareError (and
75
- * anything else that chains causes) preserves the original SDK error there.
76
- * Depth-capped so a pathological cause cycle cannot spin forever.
91
+ * anything else that chains causes) preserves the original SDK error there,
92
+ * and the root is the one link carrying the provider's `.status`.
93
+ *
94
+ * The chain is as long as the agent's middleware stack: LangChain wraps a
95
+ * model call's failure once per middleware that implements `wrapModelCall`
96
+ * (`MiddlewareError.wrap` in its AgentNode), and a native turn stacks
97
+ * deepagents' own middleware, the to-do list and Stigmer's, well past ten
98
+ * layers. So the walk has no depth bound; a cause cycle ends it instead, at
99
+ * the first error seen twice.
77
100
  */
78
101
  export function unwrapModelError(err: unknown): unknown {
102
+ const seen = new Set<unknown>([err]);
79
103
  let current = err;
80
- for (let depth = 0; depth < 10; depth++) {
81
- if (current instanceof Error && current.cause instanceof Error) {
82
- current = current.cause;
83
- } else {
84
- return current;
85
- }
104
+ while (current instanceof Error && current.cause instanceof Error && !seen.has(current.cause)) {
105
+ current = current.cause;
106
+ seen.add(current);
86
107
  }
87
108
  return current;
88
109
  }
@@ -99,10 +120,21 @@ export function unwrapModelError(err: unknown): unknown {
99
120
  */
100
121
  export function classifyModelCallError(
101
122
  err: unknown,
102
- ctx: ModelErrorContext,
123
+ callerCtx: ModelErrorContext,
103
124
  ): ClassifiedModelError | undefined {
104
125
  const root = unwrapModelError(err);
105
126
  const message = root instanceof Error ? root.message : String(root);
127
+
128
+ // 0. Organization-key sentinel — before everything that reads proxyMode:
129
+ // the call went through the proxy but on the organization's own key,
130
+ // so its failures are the organization's. The key-specific failures
131
+ // get key-specific wording; the rest classify as direct mode would.
132
+ const organizationKey = message.includes(ORGANIZATION_PROVIDER_KEY_SENTINEL);
133
+ if (organizationKey) {
134
+ const classified = classifyOrganizationKeyError(root, message, callerCtx);
135
+ if (classified !== undefined) return classified;
136
+ }
137
+ const ctx = organizationKey ? { ...callerCtx, proxyMode: false } : callerCtx;
106
138
  const backend = resolveDirectBackend(ctx);
107
139
 
108
140
  // 1. Platform sentinel — before status mapping (see module doc).
@@ -235,6 +267,52 @@ export function classifyModelCallError(
235
267
  return undefined;
236
268
  }
237
269
 
270
+ /**
271
+ * The failures an organization's own provider key causes, worded for the
272
+ * organization's admin: out of credit or quota at the provider, the key
273
+ * rejected, or the key's account not permitted the model. Undefined for
274
+ * every other failure, which classifies as direct mode would.
275
+ */
276
+ function classifyOrganizationKeyError(
277
+ root: unknown,
278
+ message: string,
279
+ ctx: ModelErrorContext,
280
+ ): ClassifiedModelError | undefined {
281
+ const provider = providerSubject(ctx);
282
+ if (isProviderBillingMessage(message)) {
283
+ return {
284
+ code: "LLM_PROVIDER_BILLING",
285
+ retryable: false,
286
+ message:
287
+ `Your organization's own ${providerLabel(ctx)} key is out of credits or over quota. ` +
288
+ `Add credits to that provider account, or update the key in Settings → Provider keys. ` +
289
+ `Provider message: ${message}`,
290
+ };
291
+ }
292
+ const status = typeof (root as { status?: unknown }).status === "number"
293
+ ? (root as { status: number }).status
294
+ : undefined;
295
+ if (status === 401) {
296
+ return {
297
+ code: "LLM_AUTHENTICATION_ERROR",
298
+ retryable: false,
299
+ message:
300
+ `${provider} rejected your organization's own key (authentication, HTTP 401) for ${modelLabel(ctx)}. ` +
301
+ `Update the key in Settings → Provider keys.`,
302
+ };
303
+ }
304
+ if (status === 403) {
305
+ return {
306
+ code: "LLM_PERMISSION_DENIED",
307
+ retryable: false,
308
+ message:
309
+ `${provider} denied your organization's own key (authorization, HTTP 403) for ${modelLabel(ctx)}. ` +
310
+ `Check that the key's account may use this model, or update the key in Settings → Provider keys.`,
311
+ };
312
+ }
313
+ return undefined;
314
+ }
315
+
238
316
  /**
239
317
  * Retryability policy (unchanged from the original call-llm classifier):
240
318
  * - 4xx (except 429): nonRetryable — client/config errors won't self-heal