stageflow 0.42.0 → 0.46.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 (315) hide show
  1. package/README.md +12 -3
  2. package/dist/agent/activity.d.ts +18 -0
  3. package/dist/agent/claudeAdapter.js +7 -0
  4. package/dist/agent/claudeTools.d.ts +27 -18
  5. package/dist/agent/claudeTools.js +54 -15
  6. package/dist/agent/compactionEvents.d.ts +34 -0
  7. package/dist/agent/compactionEvents.js +37 -0
  8. package/dist/agent/openRouterRouting.d.ts +7 -0
  9. package/dist/agent/openRouterRouting.js +23 -0
  10. package/dist/agent/piAdapter.d.ts +6 -0
  11. package/dist/agent/piAdapter.js +128 -27
  12. package/dist/agent/piSettings.d.ts +7 -0
  13. package/dist/agent/piSettings.js +9 -0
  14. package/dist/agent/sandboxPiTools.d.ts +3 -1
  15. package/dist/agent/sandboxPiTools.js +8 -2
  16. package/dist/agent/stageTurnDriver.d.ts +11 -2
  17. package/dist/agent/stageTurnDriver.js +45 -4
  18. package/dist/agent/toolOutputBudget.d.ts +15 -1
  19. package/dist/agent/toolOutputBudget.js +18 -0
  20. package/dist/agent/toolOutputBudgetExtension.d.ts +2 -0
  21. package/dist/agent/toolOutputBudgetExtension.js +40 -3
  22. package/dist/agents/authorChat.d.ts +1 -0
  23. package/dist/agents/authorChat.js +7 -0
  24. package/dist/agents/authorSessionStore.d.ts +3 -0
  25. package/dist/agents/authorSessionStore.js +17 -2
  26. package/dist/agents/evalGate.js +1 -0
  27. package/dist/agents/hostContext.d.ts +1 -1
  28. package/dist/agents/runtime/piRuntime.d.ts +5 -1
  29. package/dist/agents/runtime/piRuntime.js +46 -12
  30. package/dist/agents/service.js +3 -2
  31. package/dist/agents/sessions.js +3 -1
  32. package/dist/cli/evalCommand.d.ts +1 -1
  33. package/dist/cli/evalCommand.js +39 -2
  34. package/dist/cli/hostClient.d.ts +6 -0
  35. package/dist/cli/hostClient.js +4 -0
  36. package/dist/compaction/directives.d.ts +18 -0
  37. package/dist/compaction/directives.js +119 -0
  38. package/dist/compaction/extension.d.ts +20 -0
  39. package/dist/compaction/extension.js +78 -0
  40. package/dist/compaction/mask.d.ts +18 -0
  41. package/dist/compaction/mask.js +155 -0
  42. package/dist/compaction/pin.d.ts +21 -0
  43. package/dist/compaction/pin.js +270 -0
  44. package/dist/compaction/scrub.d.ts +7 -0
  45. package/dist/compaction/scrub.js +26 -0
  46. package/dist/compaction/sessionWiring.d.ts +22 -0
  47. package/dist/compaction/sessionWiring.js +26 -0
  48. package/dist/compaction/summarize.d.ts +79 -0
  49. package/dist/compaction/summarize.js +171 -0
  50. package/dist/compaction/toolPolicy.d.ts +3 -0
  51. package/dist/compaction/toolPolicy.js +33 -0
  52. package/dist/compaction/types.d.ts +45 -0
  53. package/dist/compaction/types.js +7 -0
  54. package/dist/compaction/workingState.d.ts +56 -0
  55. package/dist/compaction/workingState.js +198 -0
  56. package/dist/config/createPipeline.js +2 -0
  57. package/dist/config/createTrigger.js +7 -1
  58. package/dist/config/ensureCatalogScanRoot.d.ts +2 -0
  59. package/dist/config/ensureCatalogScanRoot.js +6 -0
  60. package/dist/config/evalCatalog.js +30 -2
  61. package/dist/config/githubTriggerAction.d.ts +8 -0
  62. package/dist/config/githubTriggerAction.js +29 -0
  63. package/dist/config/hostConfig.js +2 -0
  64. package/dist/config/loadEval.d.ts +3 -1
  65. package/dist/config/loadEval.js +96 -10
  66. package/dist/config/pipelineNeeds.d.ts +2 -0
  67. package/dist/config/pipelineNeeds.js +16 -3
  68. package/dist/config/pipelineRoute.js +5 -4
  69. package/dist/config/validateCatalog.d.ts +1 -1
  70. package/dist/config/validateCatalog.js +12 -0
  71. package/dist/config/validateEval.js +60 -3
  72. package/dist/config/writeEval.js +2 -2
  73. package/dist/envelope/assertEmittedArtifacts.d.ts +12 -0
  74. package/dist/envelope/assertEmittedArtifacts.js +43 -0
  75. package/dist/envelope/preEmitChecks.d.ts +3 -0
  76. package/dist/evals/calibration.d.ts +21 -0
  77. package/dist/evals/calibration.js +69 -0
  78. package/dist/evals/calibrationSuggest.d.ts +29 -0
  79. package/dist/evals/calibrationSuggest.js +72 -0
  80. package/dist/evals/calibrator.d.ts +46 -0
  81. package/dist/evals/calibrator.js +148 -0
  82. package/dist/evals/compare.d.ts +1 -1
  83. package/dist/evals/compare.js +3 -0
  84. package/dist/evals/constants.d.ts +7 -3
  85. package/dist/evals/constants.js +7 -1
  86. package/dist/evals/engine/FakeEngine.d.ts +2 -1
  87. package/dist/evals/engine/FakeEngine.js +9 -3
  88. package/dist/evals/engine/promptfoo/PromptfooEngine.d.ts +1 -1
  89. package/dist/evals/engine/promptfoo/PromptfooEngine.js +4 -2
  90. package/dist/evals/engine/promptfoo/generate.d.ts +3 -1
  91. package/dist/evals/engine/promptfoo/generate.js +43 -23
  92. package/dist/evals/engine/promptfoo/judgeProviders.js +2 -1
  93. package/dist/evals/engine/promptfoo/normalize.d.ts +4 -1
  94. package/dist/evals/engine/promptfoo/normalize.js +16 -3
  95. package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +3 -1
  96. package/dist/evals/engine/promptfoo/rubricPrompt.js +15 -5
  97. package/dist/evals/errors.d.ts +7 -0
  98. package/dist/evals/errors.js +10 -0
  99. package/dist/evals/judgeProviders.d.ts +6 -0
  100. package/dist/evals/judgeProviders.js +14 -0
  101. package/dist/evals/judged.d.ts +7 -0
  102. package/dist/evals/judged.js +7 -0
  103. package/dist/evals/manager.d.ts +27 -8
  104. package/dist/evals/manager.js +73 -12
  105. package/dist/evals/plan.d.ts +10 -1
  106. package/dist/evals/plan.js +33 -13
  107. package/dist/evals/planInternalAgent.d.ts +19 -0
  108. package/dist/evals/planInternalAgent.js +201 -0
  109. package/dist/evals/read/api.d.ts +18 -0
  110. package/dist/evals/read/api.js +10 -1
  111. package/dist/evals/read/labelQueue.d.ts +45 -0
  112. package/dist/evals/read/labelQueue.js +83 -0
  113. package/dist/evals/read/list.js +10 -1
  114. package/dist/evals/read/runs.d.ts +5 -0
  115. package/dist/evals/read/runs.js +7 -1
  116. package/dist/evals/runner/caseRunner.d.ts +2 -1
  117. package/dist/evals/runner/caseRunner.js +16 -5
  118. package/dist/evals/runner/internalAgentCaseRunner.d.ts +6 -0
  119. package/dist/evals/runner/internalAgentCaseRunner.js +250 -0
  120. package/dist/evals/snapshot.d.ts +10 -2
  121. package/dist/evals/snapshot.js +8 -3
  122. package/dist/evals/stats.d.ts +40 -0
  123. package/dist/evals/stats.js +69 -0
  124. package/dist/evals/storeTypes.d.ts +42 -0
  125. package/dist/evals/types.d.ts +25 -2
  126. package/dist/evals/types.js +4 -1
  127. package/dist/evals/votes.d.ts +10 -0
  128. package/dist/evals/votes.js +60 -0
  129. package/dist/guide/cardContract.d.ts +133 -0
  130. package/dist/guide/cardContract.js +156 -0
  131. package/dist/guide/chat.d.ts +45 -0
  132. package/dist/guide/chat.js +378 -0
  133. package/dist/guide/knowledge/index.d.ts +14 -0
  134. package/dist/guide/knowledge/index.js +109 -0
  135. package/dist/guide/knowledge/topics/agents.d.ts +2 -0
  136. package/dist/guide/knowledge/topics/agents.js +58 -0
  137. package/dist/guide/knowledge/topics/bestPractices.d.ts +2 -0
  138. package/dist/guide/knowledge/topics/bestPractices.js +67 -0
  139. package/dist/guide/knowledge/topics/connections.d.ts +2 -0
  140. package/dist/guide/knowledge/topics/connections.js +66 -0
  141. package/dist/guide/knowledge/topics/consoleMap.d.ts +2 -0
  142. package/dist/guide/knowledge/topics/consoleMap.js +55 -0
  143. package/dist/guide/knowledge/topics/evals.d.ts +2 -0
  144. package/dist/guide/knowledge/topics/evals.js +72 -0
  145. package/dist/guide/knowledge/topics/limitations.d.ts +2 -0
  146. package/dist/guide/knowledge/topics/limitations.js +84 -0
  147. package/dist/guide/knowledge/topics/mcpSkillsBrowser.d.ts +2 -0
  148. package/dist/guide/knowledge/topics/mcpSkillsBrowser.js +57 -0
  149. package/dist/guide/knowledge/topics/memories.d.ts +2 -0
  150. package/dist/guide/knowledge/topics/memories.js +63 -0
  151. package/dist/guide/knowledge/topics/modelsAndProviders.d.ts +2 -0
  152. package/dist/guide/knowledge/topics/modelsAndProviders.js +56 -0
  153. package/dist/guide/knowledge/topics/overview.d.ts +2 -0
  154. package/dist/guide/knowledge/topics/overview.js +56 -0
  155. package/dist/guide/knowledge/topics/pipelines.d.ts +2 -0
  156. package/dist/guide/knowledge/topics/pipelines.js +93 -0
  157. package/dist/guide/knowledge/topics/stages.d.ts +2 -0
  158. package/dist/guide/knowledge/topics/stages.js +54 -0
  159. package/dist/guide/knowledge/topics/tasks.d.ts +2 -0
  160. package/dist/guide/knowledge/topics/tasks.js +52 -0
  161. package/dist/guide/knowledge/topics/triggers.d.ts +2 -0
  162. package/dist/guide/knowledge/topics/triggers.js +68 -0
  163. package/dist/guide/knowledge/topics/troubleshooting.d.ts +2 -0
  164. package/dist/guide/knowledge/topics/troubleshooting.js +85 -0
  165. package/dist/guide/knowledgeTools.d.ts +2 -0
  166. package/dist/guide/knowledgeTools.js +56 -0
  167. package/dist/guide/showUi.d.ts +6 -0
  168. package/dist/guide/showUi.js +546 -0
  169. package/dist/guide/threadStore.d.ts +48 -0
  170. package/dist/guide/threadStore.js +189 -0
  171. package/dist/guide/writeRoot.d.ts +11 -0
  172. package/dist/guide/writeRoot.js +70 -0
  173. package/dist/hosted/translate.js +2 -0
  174. package/dist/mcp/agentTools.js +12 -0
  175. package/dist/mcp/catalogTools.js +18 -20
  176. package/dist/mcp/catalogWriteTools.d.ts +8 -0
  177. package/dist/mcp/catalogWriteTools.js +225 -0
  178. package/dist/mcp/connectionTools.d.ts +3 -0
  179. package/dist/mcp/connectionTools.js +85 -0
  180. package/dist/mcp/controlTools.js +34 -1
  181. package/dist/mcp/deps.d.ts +9 -0
  182. package/dist/mcp/evalTools.js +723 -21
  183. package/dist/mcp/memoryHostTools.d.ts +3 -0
  184. package/dist/mcp/memoryHostTools.js +72 -0
  185. package/dist/mcp/pipelineAuthorTools.js +14 -1
  186. package/dist/mcp/server.js +5 -0
  187. package/dist/mcp/tools.js +6 -0
  188. package/dist/mcp/triggerTools.js +77 -31
  189. package/dist/operatorAgent/evalGate.d.ts +22 -0
  190. package/dist/operatorAgent/evalGate.js +77 -0
  191. package/dist/operatorAgent/fakeHost.js +10 -2
  192. package/dist/operatorAgent/host.js +18 -2
  193. package/dist/operatorAgent/index.d.ts +1 -0
  194. package/dist/operatorAgent/index.js +1 -0
  195. package/dist/operatorAgent/internalAgents.d.ts +11 -0
  196. package/dist/operatorAgent/internalAgents.js +13 -0
  197. package/dist/operatorAgent/piModel.d.ts +5 -1
  198. package/dist/operatorAgent/piModel.js +15 -10
  199. package/dist/operatorAgent/profiles/agentAuthor.js +66 -22
  200. package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -1
  201. package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +1 -1
  202. package/dist/operatorAgent/profiles/guideConfirm.d.ts +21 -0
  203. package/dist/operatorAgent/profiles/guideConfirm.js +100 -0
  204. package/dist/operatorAgent/profiles/guideTools.d.ts +7 -0
  205. package/dist/operatorAgent/profiles/guideTools.js +123 -0
  206. package/dist/operatorAgent/profiles/stageflowGuide.d.ts +7 -0
  207. package/dist/operatorAgent/profiles/stageflowGuide.js +133 -0
  208. package/dist/operatorAgent/profiles/workshopAuthor.js +23 -0
  209. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
  210. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +9 -1
  211. package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +2 -0
  212. package/dist/operatorAgent/profiles/workshopAuthorTools.js +197 -9
  213. package/dist/operatorAgent/types.d.ts +23 -0
  214. package/dist/package-meta.d.ts +1 -1
  215. package/dist/package-meta.js +1 -1
  216. package/dist/runstore/port.d.ts +13 -1
  217. package/dist/runstore/retention.d.ts +2 -1
  218. package/dist/runstore/retention.js +3 -7
  219. package/dist/runstore/runStatusClock.d.ts +17 -0
  220. package/dist/runstore/runStatusClock.js +19 -0
  221. package/dist/runstore/sqlite/SqliteRunStore.d.ts +11 -1
  222. package/dist/runstore/sqlite/SqliteRunStore.js +70 -45
  223. package/dist/runstore/sqlite/calibrationStore.d.ts +10 -0
  224. package/dist/runstore/sqlite/calibrationStore.js +106 -0
  225. package/dist/runstore/sqlite/evalStore.d.ts +5 -0
  226. package/dist/runstore/sqlite/evalStore.js +7 -0
  227. package/dist/runstore/sqlite/migrations/018-eval-grader-labels.d.ts +11 -0
  228. package/dist/runstore/sqlite/migrations/018-eval-grader-labels.js +43 -0
  229. package/dist/runstore/sqlite/migrations/index.js +2 -0
  230. package/dist/runstore/workspaceLayout.js +6 -1
  231. package/dist/runtime/artifactDiskInspect.d.ts +14 -0
  232. package/dist/runtime/artifactDiskInspect.js +52 -0
  233. package/dist/runtime/artifactPath.d.ts +24 -0
  234. package/dist/runtime/artifactPath.js +84 -0
  235. package/dist/runtime/completionCheckRunner.js +15 -43
  236. package/dist/runtime/githubPollSource.d.ts +22 -4
  237. package/dist/runtime/githubPollSource.js +93 -8
  238. package/dist/runtime/priorArtifactGrants.d.ts +8 -0
  239. package/dist/runtime/priorArtifactGrants.js +105 -0
  240. package/dist/runtime/runActionEligibility.d.ts +46 -0
  241. package/dist/runtime/runActionEligibility.js +123 -0
  242. package/dist/runtime/runManager.d.ts +4 -1
  243. package/dist/runtime/runManager.js +52 -10
  244. package/dist/runtime/runRetryCoordinator.d.ts +2 -14
  245. package/dist/runtime/runRetryCoordinator.js +3 -67
  246. package/dist/runtime/sandboxContainer.d.ts +7 -0
  247. package/dist/runtime/sandboxContainer.js +48 -6
  248. package/dist/runtime/sandboxDesk.d.ts +2 -1
  249. package/dist/runtime/sandboxDesk.js +11 -12
  250. package/dist/runtime/stageEnvironment.js +1 -0
  251. package/dist/runtime/stageRoots.d.ts +12 -0
  252. package/dist/runtime/stageSandbox.d.ts +2 -1
  253. package/dist/runtime/stageSandbox.js +18 -2
  254. package/dist/runtime/stageWorker.js +20 -0
  255. package/dist/server/agentAuthorRoutes.js +12 -3
  256. package/dist/server/bootstrap.d.ts +5 -0
  257. package/dist/server/bootstrap.js +14 -1
  258. package/dist/server/evalRoutes.js +251 -12
  259. package/dist/server/guideRoutes.d.ts +5 -0
  260. package/dist/server/guideRoutes.js +166 -0
  261. package/dist/server/http.d.ts +10 -0
  262. package/dist/server/http.js +35 -1
  263. package/dist/server/internalAgentRoutes.d.ts +6 -0
  264. package/dist/server/internalAgentRoutes.js +12 -0
  265. package/dist/server/mcpHost.js +2 -0
  266. package/dist/toolkit/meta.d.ts +1 -1
  267. package/dist/toolkit/meta.js +41 -1
  268. package/dist/toolkit/profiles.d.ts +5 -0
  269. package/dist/toolkit/profiles.js +12 -0
  270. package/dist/tools/askOperator.js +2 -1
  271. package/dist/tools/emitStageEnvelope.d.ts +17 -16
  272. package/dist/tools/emitStageEnvelope.js +46 -32
  273. package/dist/tools/stageToolNames.d.ts +4 -0
  274. package/dist/tools/stageToolNames.js +4 -0
  275. package/dist/tools/workingState.d.ts +40 -0
  276. package/dist/tools/workingState.js +108 -0
  277. package/dist/tools/writeStageArtifact.d.ts +0 -9
  278. package/dist/tools/writeStageArtifact.js +2 -1
  279. package/dist/ui/assets/{MarkdownText-BTrOqu5J.js → MarkdownText-BS2BBbCx.js} +2 -2
  280. package/dist/ui/assets/adapters-DPD-cAsy.js +17 -0
  281. package/dist/ui/assets/chat-BD1-VTT6.js +2 -0
  282. package/dist/ui/assets/chat-DXnQv9st.css +2 -0
  283. package/dist/ui/assets/main-AX5XNLtX.js +278 -0
  284. package/dist/ui/assets/{main-CHlmTNJe.css → main-CFX0_n8J.css} +1 -1
  285. package/dist/ui/chat.html +3 -4
  286. package/dist/ui/index.html +3 -4
  287. package/dist/workshop/chatTurn.js +1 -1
  288. package/dist/workshop/workshopPin.d.ts +3 -3
  289. package/dist/workshop/workshopPin.js +16 -15
  290. package/examples/compaction-stress/README.md +30 -0
  291. package/examples/compaction-stress/check-run.mjs +53 -0
  292. package/examples/compaction-stress/collect-evidence.yaml +60 -0
  293. package/examples/compaction-stress/compaction-stress.pipeline.yaml +141 -0
  294. package/examples/compaction-stress/compaction-stress.task.yaml +15 -0
  295. package/examples/compaction-stress/generate-evidence.mjs +110 -0
  296. package/examples/compaction-stress/root-cause.yaml +26 -0
  297. package/examples/compaction-stress/stageflow.yaml +9 -0
  298. package/examples/compaction-stress/write-postmortem.yaml +40 -0
  299. package/examples/evals/README.md +18 -0
  300. package/examples/evals/guide-behavior.dataset.jsonl +15 -0
  301. package/examples/evals/guide-behavior.eval.yaml +35 -0
  302. package/examples/evals/guide-depth.dataset.jsonl +35 -0
  303. package/examples/evals/guide-depth.eval.yaml +169 -0
  304. package/examples/memory/evals/guide-behavior.dataset.jsonl +15 -0
  305. package/examples/memory/evals/guide-behavior.eval.yaml +35 -0
  306. package/examples/memory/evals/guide-depth.dataset.jsonl +35 -0
  307. package/examples/memory/evals/guide-depth.eval.yaml +169 -0
  308. package/package.json +1 -1
  309. package/skills/stageflow/references/control-surface.md +1 -1
  310. package/skills/stageflow/references/evals.md +1 -0
  311. package/dist/ui/assets/AssistantRuntimeProvider-qIX1xNoH.js +0 -9
  312. package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +0 -6
  313. package/dist/ui/assets/chat-Df_wCpak.js +0 -5
  314. package/dist/ui/assets/chat-P7KQznHm.css +0 -2
  315. package/dist/ui/assets/main-CvVfxmMd.js +0 -271
@@ -33,7 +33,7 @@ import { existsSync } from "node:fs";
33
33
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
34
34
  import path from "node:path";
35
35
  import { BROWSER_SKILL_NAME } from "../config/builtinSkills.js";
36
- import { defineTool, ModelRuntime, resolveCliModel, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
36
+ import { defineTool, ModelRuntime, resolveCliModel, SessionManager, } from "@earendil-works/pi-coding-agent";
37
37
  import { StageMcpError } from "../config/resolveStageMcpServers.js";
38
38
  import { attachIsolatedMcp, STAGEFLOW_PI_MCP_EXTENSION_NAME, UNREQUESTED_MCP_ADAPTER_TOOLS, } from "./piIsolatedMcp.js";
39
39
  import { warmIsolatedMcpMetadata } from "./piIsolatedMcpProbe.js";
@@ -61,6 +61,7 @@ import { findProviderSupport } from "./providerSupport.js";
61
61
  import { mapSessionEventToActivity, readActivityVerbose } from "./activity.js";
62
62
  import { createStageActivityObserver, } from "./activityObserver.js";
63
63
  import { DEFAULT_STAGE_TIMEOUT_MS, runtimeStageId, runStageViaOpen } from "./port.js";
64
+ import { addCompactionUsage, compactionActivityEvent, } from "./compactionEvents.js";
64
65
  import { addModelUsage, emptyStageUsage } from "../types/usage.js";
65
66
  import { TIMEOUT_ABORT_TOOL_RESULT, composeTimeoutResumePrompt, stageTimeoutReason, } from "./stageTimeout.js";
66
67
  import { globalStageflowHome } from "../project/globalHome.js";
@@ -68,6 +69,10 @@ import { isUsableAuthFile, STAGEFLOW_AGENT_AUTH_PATH_ENV, } from "../runtime/cre
68
69
  import { SF_STAGE_WORKER } from "../runtime/stageWorkerProtocol.js";
69
70
  import { durableRootFileToolDenial, STAGEFLOW_PATH_DENIED, } from "../runstore/workspaceLayout.js";
70
71
  export { STAGEFLOW_PATH_DENIED };
72
+ import { createPiSettingsManager } from "./piSettings.js";
73
+ import { createSessionContextWiring } from "../compaction/sessionWiring.js";
74
+ import { ASK_OPERATOR_TOOL_NAME } from "../tools/stageToolNames.js";
75
+ import { WORKING_STATE_GUIDANCE } from "../compaction/workingState.js";
71
76
  export { createSealedResourceLoader } from "./piSessionFactory.js";
72
77
  export const STAGEFLOW_PATH_DENY_EXTENSION_NAME = "stageflow-path-deny";
73
78
  const PATH_DENY_FILE_TOOLS = new Set(["read", "write", "edit"]);
@@ -102,7 +107,7 @@ export function createDurableRootPathDenyExtension(options) {
102
107
  * `write_stage_artifact` when registered (always, for bound and unbound —
103
108
  * bound/unbound only gates cwd / env bind).
104
109
  */
105
- export function resolveStageToolNames(emitToolName, artifactToolName, askOperatorToolName = "ask_operator", gateKinds) {
110
+ export function resolveStageToolNames(emitToolName, artifactToolName, askOperatorToolName = ASK_OPERATOR_TOOL_NAME, gateKinds) {
106
111
  const tools = [
107
112
  "read",
108
113
  "bash",
@@ -425,11 +430,37 @@ function attachStageProgress(session, onActivity, usage, onAssistantTextDelta) {
425
430
  writeStderr: true,
426
431
  });
427
432
  const verbose = readActivityVerbose();
433
+ let lastContextPercent = -1;
428
434
  const unsubscribe = session.subscribe((event) => {
429
- routeSessionEventToProgress(event, {
430
- observer,
431
- verbose,
432
- usage,
435
+ const raw = event;
436
+ routeSessionEventToProgress(raw, { observer, verbose, usage });
437
+ if (raw.type === "compaction_end") {
438
+ lastContextPercent = -1;
439
+ const end = raw;
440
+ if (usage)
441
+ addCompactionUsage(usage, session.model?.id, end);
442
+ observer.onActivity(compactionActivityEvent(end, session.getContextUsage()?.tokens ?? null));
443
+ const after = end.result?.estimatedTokensAfter;
444
+ const window = session.getContextUsage()?.contextWindow ?? 0;
445
+ if (session.getContextUsage()?.tokens == null && typeof after === "number" && Number.isFinite(after) && window > 0) {
446
+ lastContextPercent = Math.min(100, Math.round((after / window) * 100));
447
+ observer.onActivity({ event: "context_usage", tokens: after, contextWindow: window, percent: lastContextPercent });
448
+ }
449
+ }
450
+ if (raw.type !== "message_end" && raw.type !== "compaction_end")
451
+ return;
452
+ const context = session.getContextUsage();
453
+ if (context?.tokens == null || context.contextWindow <= 0)
454
+ return;
455
+ const percent = Math.min(100, Math.round((context.tokens / context.contextWindow) * 100));
456
+ if (percent === lastContextPercent)
457
+ return;
458
+ lastContextPercent = percent;
459
+ observer.onActivity({
460
+ event: "context_usage",
461
+ tokens: context.tokens,
462
+ contextWindow: context.contextWindow,
463
+ percent,
433
464
  });
434
465
  });
435
466
  return () => {
@@ -437,6 +468,20 @@ function attachStageProgress(session, onActivity, usage, onAssistantTextDelta) {
437
468
  observer.dispose();
438
469
  };
439
470
  }
471
+ function formatGrantedArtifacts(grants) {
472
+ if (grants === undefined || grants.length === 0) {
473
+ return "No prior artifacts are granted in this sandbox. Do not open stages/<id>/attempts/... host layouts.";
474
+ }
475
+ const lines = [
476
+ "Prior artifacts are read-only. Read the sandbox path on each line. A file grant is a file at that path. A directory grant is a directory. Do not open stages/<id>/attempts/... host layouts.",
477
+ ];
478
+ for (const grant of grants) {
479
+ const name = grant.listedPath.split("/").pop() || grant.listedPath;
480
+ lines.push(`- ${name} (${grant.kind}) → ${grant.containerPath}`);
481
+ lines.push(` from ${grant.listedPath}`);
482
+ }
483
+ return lines.join("\n");
484
+ }
440
485
  function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName) {
441
486
  let emitHint = emitHintOverride ??
442
487
  `Required final action: call ${emitToolName} exactly once with status, summary, and artifacts (run-relative paths).`;
@@ -480,14 +525,18 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
480
525
  : "";
481
526
  const artifactGuidance = sandboxed
482
527
  ? [
483
- "Sandbox desk: /workspace. read, bash, write, and edit run only inside that desk.",
528
+ "Sandbox desk: /workspace. read, write, and edit run only inside that desk.",
529
+ input.roots.readonlyDir
530
+ ? `One folder is mounted read-only at ${input.roots.readonlyDir}. Read the files there. If a file you were given is not in that folder, stop and say so. Do not create stand-in documents.`
531
+ : "The host filesystem is not mounted.",
484
532
  input.roots.checkoutRoot
485
533
  ? `Checkout: /workspace/checkout. The host checkout path is not mounted.`
486
534
  : "This stage has no checkout. Scratch files go in /workspace/work.",
487
- "Attempt artifacts are at /workspace/artifacts. Scratch space is /workspace/work.",
535
+ "Scratch space is /workspace/work. Desk /workspace/artifacts is not durable — bash or builtin writes there do not persist for emit or the next stage.",
488
536
  artifactToolName !== undefined
489
- ? `Create factory stage artifacts only with ${artifactToolName} (path relative to ${attemptArtifactsPath}). Use the returned run-relative path in ${emitToolName}.`
490
- : `Create factory stage artifacts under ${attemptArtifactsPath} relative to the run folder.`,
537
+ ? `Durable stage artifacts only via ${artifactToolName} (path relative to ${attemptArtifactsPath}). Use the returned run-relative path in ${emitToolName}.`
538
+ : `Durable stage artifacts belong under ${attemptArtifactsPath} relative to the run folder.`,
539
+ formatGrantedArtifacts(input.roots.artifactGrants),
491
540
  ].join("\n")
492
541
  : input.roots.mode === "bound" && artifactToolName !== undefined
493
542
  ? [
@@ -569,6 +618,30 @@ export function composeStageUserPrompt(input, emitToolName, emitHintOverride, ar
569
618
  }
570
619
  return body;
571
620
  }
621
+ /**
622
+ * Pi stages carry the whole assignment (task, prior envelopes, envelope
623
+ * contract, guidance) in the system prompt: compaction never summarizes it.
624
+ */
625
+ export function composeStagePiSystemPrompt(input, emitToolName, emitHintOverride, artifactToolName) {
626
+ return [
627
+ input.stage.system_prompt,
628
+ "",
629
+ "# Working state",
630
+ WORKING_STATE_GUIDANCE,
631
+ "",
632
+ "# Stage assignment",
633
+ buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName),
634
+ ].join("\n");
635
+ }
636
+ export function composeStagePiKickoffPrompt(input) {
637
+ const body = "Begin this stage. Your task, inputs, constraints and the required final action are in your system prompt under \"Stage assignment\".";
638
+ if (input.stage.skill !== undefined)
639
+ return `/skill:${input.stage.skill} ${body}`;
640
+ if (input.browserSkillFilePath !== undefined && input.stage.browser !== undefined) {
641
+ return `/skill:${BROWSER_SKILL_NAME} ${body}`;
642
+ }
643
+ return body;
644
+ }
572
645
  export function composeFeedbackResumePrompt(input) {
573
646
  if (input.feedbackLoopContext === undefined) {
574
647
  throw new Error("feedback_resume requires feedbackLoopContext");
@@ -744,7 +817,7 @@ export async function repairPrematureAskOperatorClosure(sessionFile) {
744
817
  const message = entry.message;
745
818
  if (!message || message.role !== "toolResult")
746
819
  continue;
747
- if (message.toolName !== "ask_operator" || message.isError !== true)
820
+ if (message.toolName !== ASK_OPERATOR_TOOL_NAME || message.isError !== true)
748
821
  continue;
749
822
  const content = message.content;
750
823
  if (!Array.isArray(content))
@@ -876,8 +949,16 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
876
949
  const emitDef = createEmitStageEnvelopeTool(capture, input.stage.payload_schema, input.forkEmitContext, input.cloneEmitContext, {
877
950
  checks: input.stage.pre_emit_checks,
878
951
  readQaTrail: input.readQaTrail,
952
+ artifactPresence: {
953
+ runWorkspaceDir: roots.runWorkspaceDir,
954
+ stageId: runtimeStageId(input),
955
+ attempt: roots.attempt ?? 1,
956
+ },
879
957
  }, input.feedbackLoopEmitContext);
880
- const emitTool = defineTool(emitDef);
958
+ const emitTool = defineTool({
959
+ ...emitDef,
960
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
961
+ });
881
962
  const gateKinds = input.stage.gate_kinds;
882
963
  const includeAsk = Array.isArray(gateKinds) && gateKinds.length > 0;
883
964
  const askDef = includeAsk
@@ -886,13 +967,21 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
886
967
  allowedKinds: gateKinds,
887
968
  })
888
969
  : undefined;
889
- const askTool = askDef ? defineTool(askDef) : undefined;
970
+ const askTool = askDef
971
+ ? defineTool({
972
+ ...askDef,
973
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
974
+ })
975
+ : undefined;
890
976
  const artifactDef = createWriteStageArtifactTool({
891
977
  runWorkspaceDir: roots.runWorkspaceDir,
892
978
  stageId: runtimeStageId(input),
893
979
  attempt: roots.attempt ?? 1,
894
980
  });
895
- const artifactTool = defineTool(artifactDef);
981
+ const artifactTool = defineTool({
982
+ ...artifactDef,
983
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
984
+ });
896
985
  if (input.stage.skill !== undefined && input.skillFilePath === undefined) {
897
986
  return {
898
987
  ok: false,
@@ -926,9 +1015,7 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
926
1015
  modelsPath: path.join(path.dirname(roots.authPath), "models.json"),
927
1016
  }
928
1017
  : undefined);
929
- const settingsManager = SettingsManager.inMemory({
930
- compaction: { enabled: false },
931
- });
1018
+ const settingsManager = createPiSettingsManager();
932
1019
  attached = await attachIsolatedMcp(input.resolvedMcpServers, {
933
1020
  lifecycle: "eager",
934
1021
  });
@@ -937,6 +1024,7 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
937
1024
  restoreProvider?.();
938
1025
  return { ok: false, reason };
939
1026
  };
1027
+ const contextWiring = createSessionContextWiring({ workingState: true });
940
1028
  const extensionFactories = [
941
1029
  {
942
1030
  name: STAGEFLOW_PATH_DENY_EXTENSION_NAME,
@@ -960,22 +1048,25 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
960
1048
  runWorkspaceDir: roots.runWorkspaceDir,
961
1049
  stageId: runtimeStageId(input),
962
1050
  attempt: roots.attempt ?? 1,
1051
+ turnCapped: new Set(["read", "bash"]),
963
1052
  exempt: new Set([
964
1053
  "read",
965
1054
  "bash",
1055
+ ...contextWiring.budgetExemptNames,
966
1056
  emitDef.name,
967
1057
  artifactDef.name,
968
- askDef?.name ?? "ask_operator",
1058
+ askDef?.name ?? ASK_OPERATOR_TOOL_NAME,
969
1059
  ]),
970
1060
  }),
971
1061
  },
1062
+ ...contextWiring.extensions,
972
1063
  ...(attached.extensionFactories ?? []),
973
1064
  ];
974
1065
  const loader = createSealedResourceLoader({
975
1066
  cwd: roots.cwd,
976
1067
  agentDir: roots.agentDir,
977
1068
  settingsManager,
978
- systemPrompt: input.stage.system_prompt,
1069
+ systemPrompt: composeStagePiSystemPrompt(input, emitDef.name, provider?.emitToolHint?.(emitDef.name), artifactDef.name),
979
1070
  additionalExtensionPaths,
980
1071
  ...(additionalSkillPaths.length > 0 ? { additionalSkillPaths } : {}),
981
1072
  extensionFactories,
@@ -1020,7 +1111,8 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
1020
1111
  const customTools = askTool
1021
1112
  ? [emitTool, askTool, artifactTool]
1022
1113
  : [emitTool, artifactTool];
1023
- const tools = resolveStageToolNames(emitDef.name, artifactDef.name, askDef?.name ?? "ask_operator", gateKinds);
1114
+ const tools = resolveStageToolNames(emitDef.name, artifactDef.name, askDef?.name ?? ASK_OPERATOR_TOOL_NAME, gateKinds);
1115
+ tools.push(...contextWiring.toolNames);
1024
1116
  if (attached.extensionFactories !== undefined) {
1025
1117
  const mcpTools = collectMcpExtensionToolNames(loader);
1026
1118
  if (!mcpTools.ok) {
@@ -1030,12 +1122,18 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
1030
1122
  }
1031
1123
  if (input.channels && input.stage.channels?.some(channel => channel.operations.includes("send"))) {
1032
1124
  const definition = createSendMessageTool(input.channels);
1033
- customTools.push(defineTool(definition));
1125
+ customTools.push(defineTool({
1126
+ ...definition,
1127
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
1128
+ }));
1034
1129
  tools.push(definition.name);
1035
1130
  }
1036
1131
  if (input.memory) {
1037
1132
  for (const definition of memorySession(input.memory).tools) {
1038
- customTools.push(defineTool(definition));
1133
+ customTools.push(defineTool({
1134
+ ...definition,
1135
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
1136
+ }));
1039
1137
  tools.push(definition.name);
1040
1138
  }
1041
1139
  }
@@ -1050,7 +1148,10 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
1050
1148
  ];
1051
1149
  for (const definition of definitions)
1052
1150
  if (definition) {
1053
- customTools.push(defineTool(definition));
1151
+ customTools.push(defineTool({
1152
+ ...definition,
1153
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
1154
+ }));
1054
1155
  tools.push(definition.name);
1055
1156
  }
1056
1157
  }
@@ -1060,8 +1161,10 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
1060
1161
  ...(roots.containerDockerBin !== undefined
1061
1162
  ? { dockerBin: roots.containerDockerBin }
1062
1163
  : {}),
1164
+ ...(roots.readonlyDir !== undefined ? { readonlyDir: roots.readonlyDir } : {}),
1063
1165
  };
1064
- customTools.push(defineTool(createSandboxReadTool(sandboxTools)), defineTool(createSandboxWriteTool(sandboxTools)), defineTool(createSandboxEditTool(sandboxTools)), defineTool(createSandboxBashTool(sandboxTools)));
1166
+ const sampling = { type: "json_schema", strict: "prefer" };
1167
+ customTools.push(defineTool({ ...createSandboxReadTool(sandboxTools), constrainedSampling: sampling }), defineTool({ ...createSandboxWriteTool(sandboxTools), constrainedSampling: sampling }), defineTool({ ...createSandboxEditTool(sandboxTools), constrainedSampling: sampling }), defineTool({ ...createSandboxBashTool(sandboxTools), constrainedSampling: sampling }));
1065
1168
  }
1066
1169
  return {
1067
1170
  sessionManager,
@@ -1073,7 +1176,6 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
1073
1176
  emitDefName: emitDef.name,
1074
1177
  ...(askDef ? { askOperatorDefName: askDef.name } : {}),
1075
1178
  artifactDefName: artifactDef.name,
1076
- providerEmitHint: provider?.emitToolHint?.(emitDef.name),
1077
1179
  restoreProvider,
1078
1180
  capture,
1079
1181
  usage,
@@ -1415,8 +1517,7 @@ export class PiAgentAdapter {
1415
1517
  await session.prompt(composeTimeoutResumePrompt());
1416
1518
  return;
1417
1519
  }
1418
- const userPrompt = composeStageUserPrompt(input, wiring.emitDefName, wiring.providerEmitHint, wiring.artifactDefName);
1419
- await session.prompt(userPrompt);
1520
+ await session.prompt(composeStagePiKickoffPrompt(input));
1420
1521
  });
1421
1522
  },
1422
1523
  resume: resumeWaiting
@@ -0,0 +1,7 @@
1
+ import { SettingsManager } from "@earendil-works/pi-coding-agent";
2
+ /**
3
+ * Settings for every Pi session Stageflow starts. Auto-compaction is always on
4
+ * and not configurable: a long session compacts at the threshold, and recovers
5
+ * once from a provider context-overflow, instead of failing.
6
+ */
7
+ export declare function createPiSettingsManager(): SettingsManager;
@@ -0,0 +1,9 @@
1
+ import { SettingsManager } from "@earendil-works/pi-coding-agent";
2
+ /**
3
+ * Settings for every Pi session Stageflow starts. Auto-compaction is always on
4
+ * and not configurable: a long session compacts at the threshold, and recovers
5
+ * once from a provider context-overflow, instead of failing.
6
+ */
7
+ export function createPiSettingsManager() {
8
+ return SettingsManager.inMemory({ compaction: { enabled: true } });
9
+ }
@@ -1,6 +1,8 @@
1
1
  import { Type } from "typebox";
2
2
  import { type SandboxFileTarget } from "../runtime/sandboxFiles.js";
3
- export type SandboxPiToolOptions = SandboxFileTarget;
3
+ export type SandboxPiToolOptions = SandboxFileTarget & {
4
+ readonlyDir?: string;
5
+ };
4
6
  export declare function createSandboxReadTool(options: SandboxPiToolOptions): {
5
7
  name: string;
6
8
  label: string;
@@ -89,9 +89,15 @@ export function createSandboxBashTool(options) {
89
89
  return {
90
90
  name: "bash",
91
91
  label: "bash",
92
- description: "Execute a bash command in this stage's sandbox. The working directory is /workspace. The host filesystem is not mounted.",
92
+ description: options.readonlyDir
93
+ ? `Execute a bash command in this stage's sandbox. The working directory is /workspace. One folder is mounted read-only at ${options.readonlyDir}. Read files there. If a file is missing, stop. Do not create a stand-in.`
94
+ : "Execute a bash command in this stage's sandbox. The working directory is /workspace. The host filesystem is not mounted.",
93
95
  promptSnippet: "Execute bash commands in /workspace",
94
- promptGuidelines: ["The shell starts in /workspace. Do not use host paths."],
96
+ promptGuidelines: [
97
+ options.readonlyDir
98
+ ? `The shell starts in /workspace. The only host folder you can read is ${options.readonlyDir}, and it is read-only. If a file is not there, stop. Do not create stand-in documents.`
99
+ : "The shell starts in /workspace. Do not use host paths.",
100
+ ],
95
101
  parameters: Type.Object({
96
102
  command: Type.String({ description: "Shell command to execute" }),
97
103
  timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional)" })),
@@ -1,6 +1,13 @@
1
1
  import type { EmitCapture } from "../tools/emitStageEnvelope.js";
2
- /** Follow-up turns sent when the agent ends its turn without an accepted envelope. */
2
+ /**
3
+ * Follow-up turns in a row, with no progress between them, sent when the agent ends its turn
4
+ * without an accepted envelope. A reminder turn that runs a tool call counts as progress and
5
+ * resets this budget.
6
+ */
3
7
  export declare const DEFAULT_EMIT_REMINDERS = 2;
8
+ /** Hard ceiling on reminders in one stage attempt, however much progress each one made. */
9
+ export declare const MAX_TOTAL_EMIT_REMINDERS = 20;
10
+ export declare const LEAKED_TOOL_CALL_ERROR = "the model wrote its tool call out as plain text instead of making it, even after reminders. This is usually a provider-side tool-call parsing problem (for example one OpenRouter backend serving the model). Run again, pin a different provider for this model, or choose another model.";
4
11
  /** The slice of a Pi session the driver needs; a fake in tests. */
5
12
  export type StageTurnSession = {
6
13
  prompt(text: string): Promise<void>;
@@ -24,7 +31,9 @@ export declare function formatProviderError(text: string | undefined): string;
24
31
  * model unavailable, or a transient error Pi already gave up retrying). Undefined otherwise.
25
32
  */
26
33
  export declare function lastTurnProviderError(messages: readonly unknown[]): string | undefined;
27
- export declare function emitReminderPrompt(emitToolName: string, attempt: number, max: number, rejectedEmit?: string): string;
34
+ /** True when the last assistant turn wrote a tool call out as plain text instead of making a real one. */
35
+ export declare function lastTurnLeakedToolCall(messages: readonly unknown[]): boolean;
36
+ export declare function emitReminderPrompt(emitToolName: string, attempt: number, max: number, rejectedEmit?: string, leakedToolCall?: boolean): string;
28
37
  /**
29
38
  * Called after a turn returns. While no envelope was accepted, nudge the agent with a short
30
39
  * follow-up turn, up to `maxReminders` times. Stops at once when an envelope is captured,
@@ -1,6 +1,13 @@
1
- /** Follow-up turns sent when the agent ends its turn without an accepted envelope. */
1
+ /**
2
+ * Follow-up turns in a row, with no progress between them, sent when the agent ends its turn
3
+ * without an accepted envelope. A reminder turn that runs a tool call counts as progress and
4
+ * resets this budget.
5
+ */
2
6
  export const DEFAULT_EMIT_REMINDERS = 2;
7
+ /** Hard ceiling on reminders in one stage attempt, however much progress each one made. */
8
+ export const MAX_TOTAL_EMIT_REMINDERS = 20;
3
9
  const PROVIDER_ERROR_MAX_CHARS = 500;
10
+ export const LEAKED_TOOL_CALL_ERROR = "the model wrote its tool call out as plain text instead of making it, even after reminders. This is usually a provider-side tool-call parsing problem (for example one OpenRouter backend serving the model). Run again, pin a different provider for this model, or choose another model.";
4
11
  function lastAssistant(messages) {
5
12
  for (let i = messages.length - 1; i >= 0; i--) {
6
13
  const m = messages[i];
@@ -27,10 +34,37 @@ export function lastTurnProviderError(messages) {
27
34
  function lastTurnAborted(messages) {
28
35
  return lastAssistant(messages)?.stopReason === "aborted";
29
36
  }
30
- export function emitReminderPrompt(emitToolName, attempt, max, rejectedEmit) {
37
+ const LEAKED_TOOL_CALL = /<[||]?\s*tool[▁_ ]?calls?[▁_ ]?(begin|start)|<tool_call>|<function_calls?>/i;
38
+ /** True when the last assistant turn wrote a tool call out as plain text instead of making a real one. */
39
+ export function lastTurnLeakedToolCall(messages) {
40
+ const m = lastAssistant(messages);
41
+ if (!m || !Array.isArray(m.content))
42
+ return false;
43
+ return m.content.some((block) => {
44
+ if (block.type === "toolCall")
45
+ return false;
46
+ const text = typeof block.text === "string" ? block.text : typeof block.thinking === "string" ? block.thinking : "";
47
+ return LEAKED_TOOL_CALL.test(text);
48
+ });
49
+ }
50
+ /** Tool calls the agent made in the messages after `fromIndex`. */
51
+ function toolCallsSince(messages, fromIndex) {
52
+ let count = 0;
53
+ for (const raw of messages.slice(fromIndex)) {
54
+ const m = raw;
55
+ if (m?.role !== "assistant" || !Array.isArray(m.content))
56
+ continue;
57
+ count += m.content.filter((block) => block.type === "toolCall").length;
58
+ }
59
+ return count;
60
+ }
61
+ export function emitReminderPrompt(emitToolName, attempt, max, rejectedEmit, leakedToolCall = false) {
31
62
  const lines = [
32
63
  `Reminder ${attempt} of ${max}: your turn ended without an accepted ${emitToolName} call, so this stage has not finished.`,
33
64
  ];
65
+ if (leakedToolCall) {
66
+ lines.push(`Your last tool call was written out as plain text, so it was NOT run. Make it again as a real tool call.`);
67
+ }
34
68
  if (rejectedEmit !== undefined) {
35
69
  lines.push(`Your last ${emitToolName} call was rejected: ${rejectedEmit}`);
36
70
  lines.push(`Fix that and call ${emitToolName} again.`);
@@ -49,16 +83,23 @@ export function emitReminderPrompt(emitToolName, attempt, max, rejectedEmit) {
49
83
  export async function remindUntilEmitted(session, capture, options) {
50
84
  const max = options.maxReminders ?? DEFAULT_EMIT_REMINDERS;
51
85
  let reminders = 0;
86
+ let withoutProgress = 0;
52
87
  for (;;) {
53
88
  if (capture.envelope)
54
89
  return { reminders };
55
90
  const providerError = lastTurnProviderError(session.messages);
56
91
  if (providerError !== undefined)
57
92
  return { reminders, providerError };
58
- if (options.shouldStop() || lastTurnAborted(session.messages) || reminders >= max) {
93
+ if (options.shouldStop() || lastTurnAborted(session.messages))
59
94
  return { reminders };
95
+ if (withoutProgress >= max || reminders >= MAX_TOTAL_EMIT_REMINDERS) {
96
+ return lastTurnLeakedToolCall(session.messages) ? { reminders, providerError: LEAKED_TOOL_CALL_ERROR } : { reminders };
60
97
  }
61
98
  reminders++;
62
- await session.prompt(emitReminderPrompt(options.emitToolName, reminders, max, capture.error));
99
+ withoutProgress++;
100
+ const before = session.messages.length;
101
+ await session.prompt(emitReminderPrompt(options.emitToolName, withoutProgress, max, capture.error, lastTurnLeakedToolCall(session.messages)));
102
+ if (toolCallsSince(session.messages, before) > 0)
103
+ withoutProgress = 0;
63
104
  }
64
105
  }
@@ -16,7 +16,7 @@ export type BudgetInput = {
16
16
  knownTokens: number | null;
17
17
  turnLedgerTokens: number;
18
18
  };
19
- export type SpillReason = "exceeds_remaining" | "exceeds_per_result_cap" | "unknown_usage_fallback" | "adapter_truncated";
19
+ export type SpillReason = "exceeds_remaining" | "exceeds_per_result_cap" | "unknown_usage_fallback" | "adapter_truncated" | "turn_cap_deferred";
20
20
  export type BudgetDecision = {
21
21
  kind: "inline";
22
22
  estTokens: number;
@@ -27,6 +27,20 @@ export type BudgetDecision = {
27
27
  };
28
28
  export declare function estimateTokens(bytes: number): number;
29
29
  export declare function decideToolOutput(input: BudgetInput): BudgetDecision;
30
+ export declare const TURN_TOTAL_MAX_FRACTION = 0.4;
31
+ export type TurnCapInput = {
32
+ resultTokens: number;
33
+ contextWindow: number;
34
+ knownTokens: number | null;
35
+ turnLedgerTokens: number;
36
+ };
37
+ /**
38
+ * Turn cap for tools whose output is kept inline (read, bash): the first result
39
+ * of a turn always passes so a lone call can make progress; later results in
40
+ * the same turn are deferred once the turn's total would pass the smaller of
41
+ * the remaining window (after the reserve) and TURN_TOTAL_MAX_FRACTION of it.
42
+ */
43
+ export declare function decideTurnCap(input: TurnCapInput): "inline" | "defer";
30
44
  export declare function utf8ByteLength(text: string): number;
31
45
  export declare function truncateUtf8(text: string, maxBytes: number): string;
32
46
  export declare function tryParseJson(text: string): {
@@ -31,6 +31,24 @@ export function decideToolOutput(input) {
31
31
  }
32
32
  return { kind: "inline", estTokens };
33
33
  }
34
+ export const TURN_TOTAL_MAX_FRACTION = 0.4;
35
+ /**
36
+ * Turn cap for tools whose output is kept inline (read, bash): the first result
37
+ * of a turn always passes so a lone call can make progress; later results in
38
+ * the same turn are deferred once the turn's total would pass the smaller of
39
+ * the remaining window (after the reserve) and TURN_TOTAL_MAX_FRACTION of it.
40
+ */
41
+ export function decideTurnCap(input) {
42
+ const window = input.contextWindow;
43
+ if (input.knownTokens === null || !Number.isFinite(window) || window <= 0)
44
+ return "inline";
45
+ if (input.turnLedgerTokens <= 0)
46
+ return "inline";
47
+ const reserve = Math.max(RESERVE_MIN_TOKENS, RESERVE_FRACTION * window);
48
+ const remaining = window - input.knownTokens - reserve;
49
+ const allowed = Math.min(remaining, TURN_TOTAL_MAX_FRACTION * window);
50
+ return input.turnLedgerTokens + input.resultTokens > allowed ? "defer" : "inline";
51
+ }
34
52
  export function utf8ByteLength(text) {
35
53
  return Buffer.byteLength(text, "utf8");
36
54
  }
@@ -16,6 +16,8 @@ export type ToolOutputBudgetOptions = {
16
16
  stageId: string;
17
17
  attempt: number;
18
18
  exempt: ReadonlySet<string>;
19
+ /** Exempt tools that keep output inline but share the per-turn token cap (read, bash). */
20
+ turnCapped?: ReadonlySet<string>;
19
21
  onSpill?: (event: ToolOutputSpillEvent) => void;
20
22
  };
21
23
  export declare function formatSpillNotice(spilled: SpilledToolOutput, estTokens: number, lostReason?: string): string;
@@ -1,4 +1,4 @@
1
- import { buildPreview, decideToolOutput, estimateTokens, IMAGE_BLOCK_EST_TOKENS, truncateUtf8, UNKNOWN_USAGE_INLINE_MAX_BYTES, utf8ByteLength, } from "./toolOutputBudget.js";
1
+ import { buildPreview, decideToolOutput, decideTurnCap, estimateTokens, IMAGE_BLOCK_EST_TOKENS, truncateUtf8, UNKNOWN_USAGE_INLINE_MAX_BYTES, utf8ByteLength, } from "./toolOutputBudget.js";
2
2
  import { spillToolOutput } from "./toolOutputSpill.js";
3
3
  import { discardAdapterSpill, readAdapterSpill } from "./adapterSpill.js";
4
4
  export const STAGEFLOW_TOOL_OUTPUT_EXTENSION_NAME = "stageflow-tool-output";
@@ -37,11 +37,48 @@ export function createToolOutputBudgetExtension(options) {
37
37
  }
38
38
  return undefined;
39
39
  });
40
- pi.on("tool_result", async (event, ctx) => {
41
- if (options.exempt.has(event.toolName))
40
+ const capTurnTotal = (event, ctx) => {
41
+ const images = event.content.filter((block) => block.type === "image");
42
+ const resultTokens = estimateTokens(utf8ByteLength(joinText(event.content))) +
43
+ images.length * IMAGE_BLOCK_EST_TOKENS;
44
+ const usage = ctx.getContextUsage();
45
+ const verdict = decideTurnCap({
46
+ resultTokens,
47
+ contextWindow: usage?.contextWindow ?? 0,
48
+ knownTokens: usage?.tokens ?? null,
49
+ turnLedgerTokens,
50
+ });
51
+ if (verdict === "inline") {
52
+ turnLedgerTokens += resultTokens;
42
53
  return undefined;
54
+ }
55
+ const notice = event.toolName === "bash"
56
+ ? `[bash output not returned: this turn's tool results already fill the context budget (this one is ~${resultTokens} tokens). The command already ran; do not run it again. Re-read its output with a read-only command (for example write it to a file earlier, or use grep/head/tail on the files it produced) in a later turn, and keep to one large call per turn.]`
57
+ : `[${event.toolName} output not returned: this turn's tool results already fill the context budget (this one is ~${resultTokens} tokens). Nothing was lost. Call ${event.toolName} again for this one on its own, in a later turn, and keep to one large ${event.toolName} call per turn.]`;
58
+ turnLedgerTokens += estimateTokens(utf8ByteLength(notice));
59
+ options.onSpill?.({
60
+ toolName: event.toolName,
61
+ toolCallId: event.toolCallId,
62
+ reason: "turn_cap_deferred",
63
+ originalBytes: utf8ByteLength(joinText(event.content)),
64
+ estTokens: resultTokens,
65
+ });
66
+ const previousDetails = event.details !== null && typeof event.details === "object"
67
+ ? event.details
68
+ : {};
69
+ return {
70
+ content: [{ type: "text", text: notice }],
71
+ details: { ...previousDetails, stageflowToolOutput: { deferred: true, estTokens: resultTokens } },
72
+ };
73
+ };
74
+ pi.on("tool_result", async (event, ctx) => {
43
75
  if (event.parentToolCallId !== undefined)
44
76
  return undefined;
77
+ if (options.exempt.has(event.toolName)) {
78
+ if (!options.turnCapped?.has(event.toolName))
79
+ return undefined;
80
+ return capTurnTotal(event, ctx);
81
+ }
45
82
  const images = event.content.filter((block) => block.type === "image");
46
83
  turnLedgerTokens += images.length * IMAGE_BLOCK_EST_TOKENS;
47
84
  const visible = joinText(event.content);
@@ -9,6 +9,7 @@ export type AgentAuthorTurnResult = {
9
9
  summaryLines: SummaryLine[];
10
10
  };
11
11
  export declare function resetAgentAuthorSessionsForTests(): void;
12
+ export declare function forgetAgentAuthorSession(sessionId: string): void;
12
13
  export declare function runAgentAuthorTurn(input: {
13
14
  host: OperatorAgentHost;
14
15
  projectRoot: string;
@@ -53,6 +53,13 @@ export function resetAgentAuthorSessionsForTests() {
53
53
  session.close();
54
54
  sessions.clear();
55
55
  }
56
+ export function forgetAgentAuthorSession(sessionId) {
57
+ const session = sessions.get(sessionId);
58
+ if (!session)
59
+ return;
60
+ session.close();
61
+ sessions.delete(sessionId);
62
+ }
56
63
  export async function runAgentAuthorTurn(input) {
57
64
  const message = input.message.trim();
58
65
  if (!message)
@@ -25,11 +25,14 @@ export declare function createAuthorSession(input?: {
25
25
  }, storeRoot?: string): AuthorSessionRecord;
26
26
  export declare function readAuthorSession(sessionId: string, storeRoot?: string): AuthorSessionRecord | null;
27
27
  export declare function saveAuthorSession(record: AuthorSessionRecord, storeRoot?: string): AuthorSessionRecord;
28
+ /** Removes the session directory. Returns false when the id is invalid or missing. */
29
+ export declare function deleteAuthorSession(sessionId: string, storeRoot?: string): boolean;
28
30
  export type AuthorDraftSummary = {
29
31
  id: string;
30
32
  name: string;
31
33
  updatedAt: string;
32
34
  messages: number;
33
35
  };
36
+ export declare function draftHasWork(record: AuthorSessionRecord): boolean;
34
37
  /** Unsaved author sessions, newest first. Empty shells with no name and no chat are left out. */
35
38
  export declare function listAuthorDrafts(storeRoot?: string): AuthorDraftSummary[];