stageflow 0.33.0 → 0.35.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 (313) hide show
  1. package/README.md +3 -3
  2. package/dist/agent/adapterSpill.d.ts +14 -0
  3. package/dist/agent/adapterSpill.js +83 -0
  4. package/dist/agent/agentBackend.d.ts +2 -2
  5. package/dist/agent/agentBackend.js +2 -2
  6. package/dist/agent/claudeAdapter.d.ts +13 -2
  7. package/dist/agent/claudeAdapter.js +50 -6
  8. package/dist/agent/cursorProvider.d.ts +3 -0
  9. package/dist/agent/cursorProvider.js +23 -6
  10. package/dist/agent/fakeAgent.d.ts +3 -3
  11. package/dist/agent/piAdapter.d.ts +2 -2
  12. package/dist/agent/piAdapter.js +116 -15
  13. package/dist/agent/piIsolatedMcp.d.ts +1 -0
  14. package/dist/agent/piIsolatedMcp.js +19 -1
  15. package/dist/agent/piIsolatedMcpProbe.d.ts +11 -0
  16. package/dist/agent/piIsolatedMcpProbe.js +20 -0
  17. package/dist/agent/port.d.ts +2 -2
  18. package/dist/agent/providerAuth.js +36 -3
  19. package/dist/agent/{resolveAgentPort.d.ts → resolveStagePort.d.ts} +8 -8
  20. package/dist/agent/{resolveAgentPort.js → resolveStagePort.js} +11 -11
  21. package/dist/agent/stageTurnDriver.d.ts +33 -0
  22. package/dist/agent/stageTurnDriver.js +64 -0
  23. package/dist/agent/toolOutputBudget.d.ts +46 -0
  24. package/dist/agent/toolOutputBudget.js +97 -0
  25. package/dist/agent/toolOutputBudgetExtension.d.ts +22 -0
  26. package/dist/agent/toolOutputBudgetExtension.js +141 -0
  27. package/dist/agent/toolOutputSpill.d.ts +34 -0
  28. package/dist/agent/toolOutputSpill.js +85 -0
  29. package/dist/browser/agentBrowserLiveViewRelay.d.ts +41 -0
  30. package/dist/browser/agentBrowserLiveViewRelay.js +801 -0
  31. package/dist/browser/anchor.d.ts +2 -0
  32. package/dist/browser/anchor.js +2 -1
  33. package/dist/browser/auditSink.d.ts +8 -0
  34. package/dist/browser/browserHost.d.ts +29 -0
  35. package/dist/browser/browserSweep.d.ts +4 -1
  36. package/dist/browser/browserSweep.js +7 -0
  37. package/dist/browser/browserTeardown.d.ts +1 -0
  38. package/dist/browser/browserTeardown.js +32 -2
  39. package/dist/browser/containerBrowserEndpoint.d.ts +11 -0
  40. package/dist/browser/containerBrowserEndpoint.js +57 -0
  41. package/dist/browser/containerBrowserHost.d.ts +24 -0
  42. package/dist/browser/containerBrowserHost.js +147 -0
  43. package/dist/browser/dockerSandboxOrchestrator.d.ts +26 -0
  44. package/dist/browser/dockerSandboxOrchestrator.js +142 -0
  45. package/dist/browser/fakeLiveViewRelay.d.ts +15 -0
  46. package/dist/browser/fakeLiveViewRelay.js +94 -0
  47. package/dist/browser/fakeLiveViewSource.d.ts +6 -0
  48. package/dist/browser/fakeLiveViewSource.js +16 -0
  49. package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -1
  50. package/dist/browser/fakeRemoteBrowserHost.js +2 -1
  51. package/dist/browser/fakeSandboxOrchestrator.d.ts +9 -0
  52. package/dist/browser/fakeSandboxOrchestrator.js +44 -0
  53. package/dist/browser/gateHandoff.d.ts +0 -2
  54. package/dist/browser/gateHandoff.js +5 -11
  55. package/dist/browser/hostCapabilities.d.ts +23 -0
  56. package/dist/browser/hostCapabilities.js +23 -0
  57. package/dist/browser/humanLogin.d.ts +2 -2
  58. package/dist/browser/humanLogin.js +11 -2
  59. package/dist/browser/liveViewPersisted.d.ts +11 -0
  60. package/dist/browser/liveViewPersisted.js +22 -0
  61. package/dist/browser/liveViewRelay.d.ts +76 -0
  62. package/dist/browser/liveViewRelay.js +1 -0
  63. package/dist/browser/liveViewSessions.d.ts +25 -0
  64. package/dist/browser/liveViewSessions.js +139 -0
  65. package/dist/browser/liveViewSource.d.ts +25 -0
  66. package/dist/browser/liveViewSource.js +7 -0
  67. package/dist/browser/liveViewTickets.d.ts +46 -0
  68. package/dist/browser/liveViewTickets.js +122 -0
  69. package/dist/browser/localBrowserHost.d.ts +10 -0
  70. package/dist/browser/localBrowserHost.js +53 -7
  71. package/dist/browser/ownerScope.d.ts +10 -0
  72. package/dist/browser/ownerScope.js +6 -0
  73. package/dist/browser/profileStore.d.ts +5 -2
  74. package/dist/browser/profileStore.js +3 -5
  75. package/dist/browser/sandboxOrchestrator.d.ts +60 -0
  76. package/dist/browser/sandboxOrchestrator.js +8 -0
  77. package/dist/browser/sessionApiSandboxOrchestrator.d.ts +34 -0
  78. package/dist/browser/sessionApiSandboxOrchestrator.js +108 -0
  79. package/dist/browser/stageBrowserEnv.d.ts +7 -0
  80. package/dist/browser/stageBrowserEnv.js +53 -15
  81. package/dist/browser/stageHandoff.d.ts +47 -0
  82. package/dist/browser/stageHandoff.js +101 -0
  83. package/dist/browser/stageProfileLock.d.ts +1 -0
  84. package/dist/browser/stageProfileLock.js +1 -3
  85. package/dist/browser/volumeProfileStore.d.ts +7 -0
  86. package/dist/browser/volumeProfileStore.js +35 -0
  87. package/dist/cli/browserCommand.d.ts +3 -1
  88. package/dist/cli/browserCommand.js +25 -20
  89. package/dist/cli/resolveCliProjectRoot.d.ts +7 -0
  90. package/dist/cli/resolveCliProjectRoot.js +27 -0
  91. package/dist/cli/runCommand.d.ts +1 -1
  92. package/dist/cli/runCommand.js +59 -17
  93. package/dist/cli/skillsCommand.d.ts +20 -0
  94. package/dist/cli/skillsCommand.js +74 -31
  95. package/dist/cli/validateCommand.d.ts +1 -1
  96. package/dist/cli/validateCommand.js +22 -4
  97. package/dist/cli.js +4 -4
  98. package/dist/config/browseCatalog.d.ts +2 -0
  99. package/dist/config/browseCatalog.js +6 -0
  100. package/dist/config/catalogRelativePath.d.ts +4 -0
  101. package/dist/config/catalogRelativePath.js +37 -9
  102. package/dist/config/createStage.d.ts +1 -0
  103. package/dist/config/createStage.js +11 -2
  104. package/dist/config/createTask.d.ts +30 -0
  105. package/dist/config/createTask.js +161 -0
  106. package/dist/config/createTrigger.d.ts +15 -0
  107. package/dist/config/createTrigger.js +32 -25
  108. package/dist/config/draftPackage.d.ts +41 -0
  109. package/dist/config/draftPackage.js +216 -23
  110. package/dist/config/ensureCatalogScanRoot.d.ts +2 -1
  111. package/dist/config/ensureCatalogScanRoot.js +6 -2
  112. package/dist/config/findTaskInCatalog.d.ts +33 -0
  113. package/dist/config/findTaskInCatalog.js +69 -0
  114. package/dist/config/hostConfig.d.ts +21 -0
  115. package/dist/config/hostConfig.js +113 -1
  116. package/dist/config/importTask.d.ts +19 -0
  117. package/dist/config/importTask.js +133 -0
  118. package/dist/config/installSkillRequest.d.ts +40 -0
  119. package/dist/config/installSkillRequest.js +286 -0
  120. package/dist/config/loadOutcome.d.ts +11 -6
  121. package/dist/config/loadPipeline.js +0 -1
  122. package/dist/config/loadStage.js +2 -0
  123. package/dist/config/loadStageflowManifest.js +4 -1
  124. package/dist/config/loadTask.js +2 -0
  125. package/dist/config/loadTrigger.js +2 -0
  126. package/dist/config/mergePipelineIncludes.js +3 -0
  127. package/dist/config/normalizePipelineStageEntry.js +10 -1
  128. package/dist/config/printTargetYaml.js +0 -1
  129. package/dist/config/publishDraftPackage.d.ts +5 -1
  130. package/dist/config/publishDraftPackage.js +4 -1
  131. package/dist/config/resolveCatalogRoots.d.ts +4 -1
  132. package/dist/config/resolveCatalogRoots.js +18 -1
  133. package/dist/config/skillUsageIndex.d.ts +6 -0
  134. package/dist/config/skillUsageIndex.js +39 -0
  135. package/dist/config/stageBrowser.js +13 -0
  136. package/dist/config/triggerAdapterStatus.d.ts +11 -0
  137. package/dist/config/triggerAdapterStatus.js +90 -0
  138. package/dist/config/triggerCatalog.d.ts +41 -2
  139. package/dist/config/triggerCatalog.js +107 -15
  140. package/dist/config/updateTask.d.ts +30 -0
  141. package/dist/config/updateTask.js +119 -0
  142. package/dist/config/updateTrigger.d.ts +38 -0
  143. package/dist/config/updateTrigger.js +220 -0
  144. package/dist/config/validateCatalog.d.ts +6 -2
  145. package/dist/config/validateCatalog.js +48 -7
  146. package/dist/config/workshopCatalogFilenames.d.ts +37 -0
  147. package/dist/config/workshopCatalogFilenames.js +125 -0
  148. package/dist/config/workshopCatalogRepair.d.ts +7 -0
  149. package/dist/config/workshopCatalogRepair.js +144 -0
  150. package/dist/config/yamlParsePosition.d.ts +4 -0
  151. package/dist/config/yamlParsePosition.js +9 -0
  152. package/dist/hosted/runtime.d.ts +2 -2
  153. package/dist/hosted/types.d.ts +1 -1
  154. package/dist/hosted/types.js +1 -1
  155. package/dist/index.d.ts +3 -2
  156. package/dist/index.js +2 -1
  157. package/dist/mcp/getStartedTool.js +15 -6
  158. package/dist/mcp/triggerTools.js +2 -8
  159. package/dist/operatorAgent/agentPort.d.ts +11 -0
  160. package/dist/operatorAgent/agentPort.js +12 -0
  161. package/dist/operatorAgent/index.d.ts +1 -0
  162. package/dist/operatorAgent/index.js +1 -0
  163. package/dist/operatorAgent/piModel.js +26 -6
  164. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
  165. package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +4 -2
  166. package/dist/operatorAgent/profiles/workshopAuthorTools.js +103 -16
  167. package/dist/package-meta.d.ts +1 -1
  168. package/dist/package-meta.js +1 -1
  169. package/dist/project/ensureProjectRootsFromEnv.d.ts +3 -0
  170. package/dist/project/ensureProjectRootsFromEnv.js +22 -0
  171. package/dist/project/globalHome.d.ts +1 -0
  172. package/dist/project/globalHome.js +18 -0
  173. package/dist/project/workspaceCatalog.d.ts +10 -0
  174. package/dist/project/workspaceCatalog.js +91 -0
  175. package/dist/runstore/backup.js +14 -4
  176. package/dist/runstore/port.d.ts +11 -1
  177. package/dist/runstore/restore.js +5 -2
  178. package/dist/runstore/sqlite/SqliteRunStore.d.ts +8 -0
  179. package/dist/runstore/sqlite/SqliteRunStore.js +29 -9
  180. package/dist/runstore/sqlite/migrations/012-trigger-fires.d.ts +7 -0
  181. package/dist/runstore/sqlite/migrations/012-trigger-fires.js +18 -0
  182. package/dist/runstore/sqlite/migrations/index.js +2 -0
  183. package/dist/runstore/stageSnapshot.d.ts +3 -1
  184. package/dist/runstore/stageSnapshot.js +19 -0
  185. package/dist/runstore/workspaceLayout.js +27 -0
  186. package/dist/runstore/writeContainedArtifact.d.ts +1 -0
  187. package/dist/runstore/writeContainedArtifact.js +52 -0
  188. package/dist/runtime/credentialBinding.js +14 -6
  189. package/dist/runtime/emailSource.js +20 -28
  190. package/dist/runtime/githubPollSource.js +20 -28
  191. package/dist/runtime/pipelineRunner.d.ts +4 -4
  192. package/dist/runtime/pipelineScheduler.d.ts +2 -2
  193. package/dist/runtime/pipelineScheduler.js +3 -0
  194. package/dist/runtime/resumeReconstruct.d.ts +2 -2
  195. package/dist/runtime/resumeTimedOut.d.ts +2 -2
  196. package/dist/runtime/runManager.d.ts +18 -2
  197. package/dist/runtime/runManager.js +95 -30
  198. package/dist/runtime/runRetryCoordinator.d.ts +2 -2
  199. package/dist/runtime/scheduleSource.js +15 -24
  200. package/dist/runtime/stageAttemptBootstrap.d.ts +2 -2
  201. package/dist/runtime/stageAttemptBootstrap.js +10 -1
  202. package/dist/runtime/stageProcessLauncher.js +2 -2
  203. package/dist/runtime/stageRoots.js +8 -1
  204. package/dist/runtime/stageRunner.d.ts +2 -2
  205. package/dist/runtime/stageRunner.js +8 -2
  206. package/dist/runtime/stageWorker.js +2 -2
  207. package/dist/runtime/triggerPort.d.ts +5 -3
  208. package/dist/runtime/triggerRunner.d.ts +1 -0
  209. package/dist/runtime/triggerRunner.js +27 -20
  210. package/dist/runtime/webhookAccept.d.ts +25 -0
  211. package/dist/runtime/webhookAccept.js +56 -0
  212. package/dist/server/bootstrap.d.ts +7 -2
  213. package/dist/server/bootstrap.js +15 -1
  214. package/dist/server/controlToken.d.ts +1 -1
  215. package/dist/server/controlToken.js +18 -2
  216. package/dist/server/healthSurfaces.d.ts +12 -0
  217. package/dist/server/healthSurfaces.js +27 -6
  218. package/dist/server/http.d.ts +19 -10
  219. package/dist/server/http.js +605 -85
  220. package/dist/server/liveViewPath.d.ts +9 -0
  221. package/dist/server/liveViewPath.js +8 -0
  222. package/dist/server/liveViewRoutes.d.ts +58 -0
  223. package/dist/server/liveViewRoutes.js +565 -0
  224. package/dist/server/mcpHost.d.ts +4 -2
  225. package/dist/server/mcpHost.js +2 -0
  226. package/dist/server/readCatalogFile.d.ts +29 -0
  227. package/dist/server/readCatalogFile.js +215 -0
  228. package/dist/server/triggerRoutes.d.ts +1 -0
  229. package/dist/server/triggerRoutes.js +200 -73
  230. package/dist/tools/readEmail.d.ts +3 -3
  231. package/dist/tools/writeStageArtifact.d.ts +9 -0
  232. package/dist/tools/writeStageArtifact.js +2 -47
  233. package/dist/types/pipeline.d.ts +1 -1
  234. package/dist/types/stage.d.ts +1 -1
  235. package/dist/types/stageflowManifest.d.ts +1 -1
  236. package/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  237. package/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  238. package/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  239. package/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  240. package/dist/ui/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  241. package/dist/ui/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  242. package/dist/ui/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  243. package/dist/ui/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  244. package/dist/ui/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  245. package/dist/ui/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  246. package/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  247. package/dist/ui/assets/index-BoFPkKFg.js +256 -0
  248. package/dist/ui/assets/{index-Bv4p1aPK.css → index-CIPbbBUi.css} +1 -1
  249. package/dist/ui/index.html +4 -4
  250. package/dist/workshop/autosave.d.ts +2 -2
  251. package/dist/workshop/autosave.js +4 -6
  252. package/dist/workshop/buildStore.d.ts +2 -0
  253. package/dist/workshop/chatAttachments.d.ts +27 -0
  254. package/dist/workshop/chatAttachments.js +98 -0
  255. package/dist/workshop/chatTurn.d.ts +5 -2
  256. package/dist/workshop/chatTurn.js +22 -18
  257. package/dist/workshop/sessionMutations.d.ts +17 -0
  258. package/dist/workshop/sessionMutations.js +107 -0
  259. package/dist/workshop/sessionStore.d.ts +10 -0
  260. package/dist/workshop/sessionStore.js +52 -0
  261. package/dist/workshop/workshopPin.d.ts +4 -3
  262. package/dist/workshop/workshopPin.js +26 -6
  263. package/dist/workshop/workshopTranscriptTurn.d.ts +8 -0
  264. package/dist/workshop/workshopTranscriptTurn.js +46 -0
  265. package/examples/README.md +1 -1
  266. package/examples/archify-on-pr/README.md +1 -1
  267. package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +0 -1
  268. package/examples/archify-on-pr/author-diagrams.yaml +1 -0
  269. package/examples/conditional-fork/branch-a.yaml +1 -1
  270. package/examples/conditional-fork/decide.yaml +2 -1
  271. package/examples/feature-loop/{implement.yaml → feature-implement.yaml} +1 -1
  272. package/examples/feature-loop/feature-loop.pipeline.yaml +10 -10
  273. package/examples/feature-loop/{plan.yaml → feature-plan.yaml} +1 -1
  274. package/examples/feature-loop/{review.yaml → feature-review.yaml} +1 -1
  275. package/examples/feedback-loop/{implement.yaml → feedback-implement.yaml} +1 -1
  276. package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +9 -9
  277. package/examples/feedback-loop/feedback-loop.pipeline.yaml +9 -9
  278. package/examples/feedback-loop/{plan.yaml → feedback-plan.yaml} +1 -1
  279. package/examples/feedback-loop/{review.yaml → feedback-review.yaml} +1 -1
  280. package/examples/generic-fan-in/{research.yaml → fan-in-research.yaml} +1 -1
  281. package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +3 -3
  282. package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +3 -3
  283. package/examples/hello-world/{research.yaml → hello-research.yaml} +1 -1
  284. package/examples/hello-world/hello.pipeline.yaml +2 -2
  285. package/examples/mcp-hitl-tour/{review.yaml → mcp-hitl-review.yaml} +1 -1
  286. package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +3 -3
  287. package/examples/oss-issue-contribution/oss-explain-fix.yaml +1 -0
  288. package/examples/oss-issue-contribution/oss-explain-issue.yaml +1 -0
  289. package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +0 -2
  290. package/examples/stageflow.yaml +6 -0
  291. package/examples/triggers/sample-github-pr.trigger.yaml +12 -0
  292. package/examples/triggers/sample-inbox-email.trigger.yaml +14 -0
  293. package/examples/triggers/sample-manual-hello.trigger.yaml +5 -0
  294. package/examples/triggers/sample-nightly-hello.trigger.yaml +8 -0
  295. package/examples/triggers/sample-signed-webhook.trigger.yaml +11 -0
  296. package/examples/triggers-readiness/README.md +48 -0
  297. package/examples/triggers-readiness/readiness-emit.yaml +18 -0
  298. package/examples/triggers-readiness/readiness.pipeline.yaml +4 -0
  299. package/examples/triggers-readiness/readiness.task.yaml +6 -0
  300. package/examples/triggers-readiness/stageflow.yaml +13 -0
  301. package/examples/triggers-readiness/triggers/readiness-dynamic-manual.trigger.yaml +4 -0
  302. package/examples/triggers-readiness/triggers/readiness-email.trigger.yaml +12 -0
  303. package/examples/triggers-readiness/triggers/readiness-github-pr.trigger.yaml +12 -0
  304. package/examples/triggers-readiness/triggers/readiness-manual.trigger.yaml +5 -0
  305. package/examples/triggers-readiness/triggers/readiness-schedule.trigger.yaml +8 -0
  306. package/examples/triggers-readiness/triggers/readiness-webhook.trigger.yaml +11 -0
  307. package/package.json +1 -1
  308. package/skills/stageflow/references/control-surface.md +1 -1
  309. package/skills/stageflow-author/references/catalog-write-conventions.md +2 -2
  310. package/skills/stageflow-run/SKILL.md +1 -1
  311. package/skills/stageflow-run/references/mcp-call.md +1 -1
  312. package/skills/stageflow-setup/SKILL.md +1 -1
  313. package/dist/ui/assets/index-59cjF0OA.js +0 -133
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Pi implementation of AgentPort.
2
+ * Pi implementation of StagePort.
3
3
  *
4
4
  * One stage = one `createAgentSession` (single-shot SDK path, not
5
5
  * AgentSessionRuntime / interactive / RPC). The session is sealed from host
@@ -35,19 +35,24 @@ import path from "node:path";
35
35
  import { BROWSER_SKILL_NAME } from "../config/builtinSkills.js";
36
36
  import { defineTool, ModelRuntime, resolveCliModel, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
37
37
  import { StageMcpError } from "../config/resolveStageMcpServers.js";
38
- import { attachIsolatedMcp, STAGEFLOW_PI_MCP_EXTENSION_NAME, } from "./piIsolatedMcp.js";
38
+ import { attachIsolatedMcp, STAGEFLOW_PI_MCP_EXTENSION_NAME, UNREQUESTED_MCP_ADAPTER_TOOLS, } from "./piIsolatedMcp.js";
39
+ import { warmIsolatedMcpMetadata } from "./piIsolatedMcpProbe.js";
40
+ import { createToolOutputBudgetExtension, STAGEFLOW_TOOL_OUTPUT_EXTENSION_NAME, } from "./toolOutputBudgetExtension.js";
41
+ import { TOOL_OUTPUT_ARTIFACT_DIR } from "./toolOutputSpill.js";
39
42
  import { createPiAgentSession, createSealedResourceLoader, } from "./piSessionFactory.js";
40
43
  import { isAdvancingEnvelope } from "../envelope/check.js";
41
44
  import { formatFeedbackLoopContext } from "../prompt/feedbackLoopContext.js";
42
45
  import { formatPriorEnvelope } from "../prompt/priorEnvelope.js";
43
46
  import { attemptContext, noAttemptContext, } from "../runtime/stageAttemptContext.js";
44
47
  import { createEmitStageEnvelopeTool, } from "../tools/emitStageEnvelope.js";
48
+ import { remindUntilEmitted } from "./stageTurnDriver.js";
45
49
  import { createAskOperatorTool, } from "../tools/askOperator.js";
46
50
  import { createWriteStageArtifactTool } from "../tools/writeStageArtifact.js";
47
51
  import { createSendEmailTool } from "../tools/sendEmail.js";
48
52
  import { createReplyEmailTool } from "../tools/replyEmail.js";
49
53
  import { createGetEmailTool, createSearchEmailTool, createDownloadEmailAttachmentTool } from "../tools/readEmail.js";
50
54
  import "./cursorProvider.js";
55
+ import { isCursorModelRef, readCursorApiKey } from "./cursorProvider.js";
51
56
  import { findProviderSupport } from "./providerSupport.js";
52
57
  import { mapSessionEventToActivity, readActivityVerbose } from "./activity.js";
53
58
  import { createStageActivityObserver, } from "./activityObserver.js";
@@ -55,6 +60,8 @@ import { DEFAULT_STAGE_TIMEOUT_MS, runtimeStageId, runStageViaOpen } from "./por
55
60
  import { addModelUsage, emptyStageUsage } from "../types/usage.js";
56
61
  import { TIMEOUT_ABORT_TOOL_RESULT, composeTimeoutResumePrompt, stageTimeoutReason, } from "./stageTimeout.js";
57
62
  import { globalStageflowHome } from "../project/globalHome.js";
63
+ import { isUsableAuthFile, STAGEFLOW_AGENT_AUTH_PATH_ENV, } from "../runtime/credentialBinding.js";
64
+ import { SF_STAGE_WORKER } from "../runtime/stageWorkerProtocol.js";
58
65
  import { durableRootFileToolDenial, STAGEFLOW_PATH_DENIED, } from "../runstore/workspaceLayout.js";
59
66
  export { STAGEFLOW_PATH_DENIED };
60
67
  export { createSealedResourceLoader } from "./piSessionFactory.js";
@@ -462,6 +469,10 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
462
469
  ].filter((dir) => dir !== undefined);
463
470
  const skillBaseDir = skillBaseDirs.length > 0 ? skillBaseDirs.join(", ") : undefined;
464
471
  const browserGuidance = formatBrowserGuidance(input);
472
+ const toolOutputGuidance = Object.keys(input.resolvedMcpServers ?? {}).length > 0 ||
473
+ (input.stage.email?.length ?? 0) > 0
474
+ ? `Large tool results are saved automatically as stage artifacts under ${attemptArtifactsPath}${TOOL_OUTPUT_ARTIFACT_DIR}/. The tool result names the path. Read or grep that file; do not copy tool output into ${artifactToolName ?? "an artifact"}.`
475
+ : "";
465
476
  const artifactGuidance = input.roots.mode === "bound" && artifactToolName !== undefined
466
477
  ? [
467
478
  `Project checkout (agent cwd): ${input.roots.checkoutRoot}`,
@@ -499,6 +510,7 @@ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName
499
510
  : "",
500
511
  "",
501
512
  artifactGuidance,
513
+ toolOutputGuidance,
502
514
  browserGuidance,
503
515
  emitHint,
504
516
  ]
@@ -543,15 +555,26 @@ export function composeFeedbackResumePrompt(input) {
543
555
  formatFeedbackLoopContext(input.feedbackLoopContext, input.sessionMode ?? "feedback_resume"),
544
556
  ].join("\n\n");
545
557
  }
546
- function collectMcpExtensionToolNames(loader) {
558
+ function mcpExtensionTools(loader) {
547
559
  const inlinePath = `<inline:${STAGEFLOW_PI_MCP_EXTENSION_NAME}>`;
560
+ return loader
561
+ .getExtensions()
562
+ .extensions.filter((ext) => ext.path === inlinePath)
563
+ .flatMap((ext) => [...ext.tools.keys()]);
564
+ }
565
+ function mcpToolMetadataMissing(loader) {
566
+ return mcpExtensionTools(loader).includes("mcp");
567
+ }
568
+ function collectMcpExtensionToolNames(loader) {
548
569
  const names = [];
549
- for (const ext of loader.getExtensions().extensions) {
550
- if (ext.path !== inlinePath)
551
- continue;
552
- names.push(...ext.tools.keys());
553
- }
554
- return names;
570
+ const unrequested = [];
571
+ for (const name of mcpExtensionTools(loader)) {
572
+ if (UNREQUESTED_MCP_ADAPTER_TOOLS.has(name))
573
+ unrequested.push(name);
574
+ else
575
+ names.push(name);
576
+ }
577
+ return unrequested.length > 0 ? { ok: false, unrequested } : { ok: true, names };
555
578
  }
556
579
  async function shutdownSession(session) {
557
580
  if (!session) {
@@ -574,7 +597,10 @@ async function shutdownSession(session) {
574
597
  // ignore dispose errors during teardown
575
598
  }
576
599
  }
577
- function resultFromCapture(capture) {
600
+ function resultFromCapture(capture, providerError) {
601
+ if (providerError !== undefined && !capture.envelope) {
602
+ return { ok: false, reason: `provider error: ${providerError}` };
603
+ }
578
604
  if (capture.error && !capture.envelope) {
579
605
  return { ok: false, reason: capture.error };
580
606
  }
@@ -800,6 +826,26 @@ export function injectOpaqueAnswerIntoSession(sessionManager, answer) {
800
826
  async function prepareStageSessionWiring(input, sessionManager, existingAskWaitChannel) {
801
827
  const { roots } = input;
802
828
  const provider = findProviderSupport(input.stage.model);
829
+ if (process.env[SF_STAGE_WORKER] === "1" && !provider) {
830
+ if (!roots.authPath || !isUsableAuthFile(roots.authPath)) {
831
+ const authPath = roots.authPath;
832
+ return {
833
+ ok: false,
834
+ reason: authPath
835
+ ? `Stage provider auth is not configured (auth file missing or empty: ${authPath}).`
836
+ : `Stage provider auth is not configured (${STAGEFLOW_AGENT_AUTH_PATH_ENV} is unset).`,
837
+ };
838
+ }
839
+ }
840
+ if (process.env[SF_STAGE_WORKER] === "1" &&
841
+ provider &&
842
+ isCursorModelRef(input.stage.model) &&
843
+ !readCursorApiKey()) {
844
+ return {
845
+ ok: false,
846
+ reason: "Stage provider auth is not configured (Cursor API key missing from the credential root).",
847
+ };
848
+ }
803
849
  const capture = {};
804
850
  const usage = emptyStageUsage();
805
851
  const askWaitChannel = existingAskWaitChannel ?? new AskOperatorWaitChannel();
@@ -859,7 +905,9 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
859
905
  const settingsManager = SettingsManager.inMemory({
860
906
  compaction: { enabled: false },
861
907
  });
862
- attached = await attachIsolatedMcp(input.resolvedMcpServers);
908
+ attached = await attachIsolatedMcp(input.resolvedMcpServers, {
909
+ lifecycle: "eager",
910
+ });
863
911
  const failAfterAttach = (reason) => {
864
912
  attached?.cancel?.();
865
913
  restoreProvider?.();
@@ -876,6 +924,21 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
876
924
  : {}),
877
925
  }),
878
926
  },
927
+ {
928
+ name: STAGEFLOW_TOOL_OUTPUT_EXTENSION_NAME,
929
+ factory: createToolOutputBudgetExtension({
930
+ runWorkspaceDir: roots.runWorkspaceDir,
931
+ stageId: runtimeStageId(input),
932
+ attempt: roots.attempt ?? 1,
933
+ exempt: new Set([
934
+ "read",
935
+ "bash",
936
+ emitDef.name,
937
+ artifactDef.name,
938
+ askDef?.name ?? "ask_operator",
939
+ ]),
940
+ }),
941
+ },
879
942
  ...(attached.extensionFactories ?? []),
880
943
  ];
881
944
  const loader = createSealedResourceLoader({
@@ -889,6 +952,23 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
889
952
  ...(attached.eventBus !== undefined ? { eventBus: attached.eventBus } : {}),
890
953
  });
891
954
  await loader.reload();
955
+ if (attached.extensionFactories !== undefined &&
956
+ input.resolvedMcpServers !== undefined &&
957
+ mcpToolMetadataMissing(loader)) {
958
+ // A cold adapter metadata cache registers only the `mcp` gateway at load,
959
+ // and the session allowlist is fixed at load. Warm the cache, then reload
960
+ // so the declared servers' direct tools exist when the allowlist is built.
961
+ try {
962
+ await warmIsolatedMcpMetadata(input.resolvedMcpServers, { cwd: roots.cwd });
963
+ }
964
+ catch (err) {
965
+ return failAfterAttach(err instanceof Error ? err.message : String(err));
966
+ }
967
+ await loader.reload();
968
+ if (mcpToolMetadataMissing(loader)) {
969
+ return failAfterAttach(`MCP tool metadata unavailable for: ${Object.keys(input.resolvedMcpServers).join(", ")}`);
970
+ }
971
+ }
892
972
  const extensionErrors = loader.getExtensions().errors;
893
973
  if (extensionErrors.length > 0) {
894
974
  return failAfterAttach(`Failed to load Pi extension(s): ${extensionErrors
@@ -912,7 +992,11 @@ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitC
912
992
  : [emitTool, artifactTool];
913
993
  const tools = resolveStageToolNames(emitDef.name, artifactDef.name, askDef?.name ?? "ask_operator", gateKinds);
914
994
  if (attached.extensionFactories !== undefined) {
915
- tools.push(...collectMcpExtensionToolNames(loader));
995
+ const mcpTools = collectMcpExtensionToolNames(loader);
996
+ if (!mcpTools.ok) {
997
+ return failAfterAttach(`MCP adapter registered unrequested tool(s): ${mcpTools.unrequested.join(", ")}`);
998
+ }
999
+ tools.push(...mcpTools.names);
916
1000
  }
917
1001
  if (input.email) {
918
1002
  const permitted = new Set(input.stage.email?.flatMap(permission => permission.operations));
@@ -1178,6 +1262,7 @@ export class PiAgentAdapter {
1178
1262
  let wiring;
1179
1263
  let unsubscribeProgress;
1180
1264
  let prepareError;
1265
+ let closing = false;
1181
1266
  const preparePromise = (async () => {
1182
1267
  if (forceNewSession) {
1183
1268
  await mkdir(path.dirname(sessionFile), { recursive: true });
@@ -1213,15 +1298,30 @@ export class PiAgentAdapter {
1213
1298
  const controller = new AbortController();
1214
1299
  const timer = setTimeout(() => controller.abort(), timeoutMs);
1215
1300
  try {
1216
- const workPromise = work();
1301
+ const live = session;
1302
+ const capture = wiring.capture;
1303
+ const emitToolName = wiring.emitDefName;
1304
+ const turns = {
1305
+ prompt: (text) => live.prompt(text),
1306
+ get messages() {
1307
+ return live.messages ?? live.agent?.state?.messages ?? [];
1308
+ },
1309
+ };
1310
+ const workPromise = (async () => {
1311
+ await work();
1312
+ return remindUntilEmitted(turns, capture, {
1313
+ emitToolName,
1314
+ shouldStop: () => closing || controller.signal.aborted,
1315
+ });
1316
+ })();
1217
1317
  const abortPromise = new Promise((_, reject) => {
1218
1318
  controller.signal.addEventListener("abort", () => {
1219
1319
  void session?.abort();
1220
1320
  reject(new Error(stageTimeoutReason(timeoutMs)));
1221
1321
  });
1222
1322
  });
1223
- await Promise.race([workPromise, abortPromise]);
1224
- return { ...resultFromCapture(wiring.capture), usage: wiring.usage };
1323
+ const outcome = await Promise.race([workPromise, abortPromise]);
1324
+ return { ...resultFromCapture(capture, outcome.providerError), usage: wiring.usage };
1225
1325
  }
1226
1326
  catch (err) {
1227
1327
  if (sessionManager) {
@@ -1292,6 +1392,7 @@ export class PiAgentAdapter {
1292
1392
  }
1293
1393
  : undefined,
1294
1394
  onClose: async (closeOptions) => {
1395
+ closing = true;
1295
1396
  await preparePromise.catch(() => undefined);
1296
1397
  if (closeOptions?.park) {
1297
1398
  if (sessionManager) {
@@ -17,6 +17,7 @@ export type IsolatedMcpAttach = {
17
17
  connecting: Promise<void> | undefined;
18
18
  cancel?: () => void;
19
19
  };
20
+ export declare const UNREQUESTED_MCP_ADAPTER_TOOLS: ReadonlySet<string>;
20
21
  export declare function emitIsolatedMcpStatus(eventBus: EventBus, servers: ReadonlyArray<{
21
22
  name: string;
22
23
  status: string;
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { createEventBus, } from "@earendil-works/pi-coding-agent";
3
3
  import { StageMcpError, } from "../config/resolveStageMcpServers.js";
4
4
  import { globalStageflowHome } from "../project/globalHome.js";
5
+ import { TOOL_OUTPUT_HARD_CEILING_BYTES } from "./toolOutputBudget.js";
5
6
  export const STAGEFLOW_PI_MCP_EXTENSION_NAME = "stageflow-mcp";
6
7
  const MCP_STATUS_EVENT = "pi-mcp-adapter/status/v1";
7
8
  const DEFAULT_ISOLATED_MCP_CONNECT_TIMEOUT_MS = 30_000;
@@ -13,11 +14,25 @@ export function jitiFsCacheDir() {
13
14
  export function resetCreateMcpAdapterCacheForTests() {
14
15
  cachedCreateMcpAdapter = undefined;
15
16
  }
17
+ // Stages get only the declared servers' direct tools: no mcpScript sandbox and
18
+ // no `mcp` gateway. The adapter's output guard is a backstop; the stage budget
19
+ // lives in toolOutputBudgetExtension.
16
20
  const ISOLATED_MCP_SETTINGS = {
17
21
  directTools: true,
18
22
  elicitation: false,
19
23
  hostConfigDiscovery: "off",
24
+ scriptMode: false,
25
+ disableProxyTool: true,
26
+ outputGuard: {
27
+ maxBytes: TOOL_OUTPUT_HARD_CEILING_BYTES,
28
+ maxLines: Number.MAX_SAFE_INTEGER,
29
+ detailsMaxBytes: 16 * 1024,
30
+ },
20
31
  };
32
+ export const UNREQUESTED_MCP_ADAPTER_TOOLS = new Set([
33
+ "mcpScript",
34
+ "mcp",
35
+ ]);
21
36
  const SUCCESS_STATUSES = new Set([
22
37
  "connected",
23
38
  "cached",
@@ -48,7 +63,10 @@ function toIsolatedMcpConfig(snapshot, lifecycle = "lazy") {
48
63
  }
49
64
  return {
50
65
  mcpServers,
51
- settings: { ...ISOLATED_MCP_SETTINGS },
66
+ settings: {
67
+ ...ISOLATED_MCP_SETTINGS,
68
+ outputGuard: { ...ISOLATED_MCP_SETTINGS.outputGuard },
69
+ },
52
70
  };
53
71
  }
54
72
  async function loadCreateMcpAdapter() {
@@ -1,3 +1,4 @@
1
+ import { type ResolvedMcpServers } from "../config/resolveStageMcpServers.js";
1
2
  export declare const PROJECT_MCP_PROBE_TIMEOUT_MS = 30000;
2
3
  export type ProjectMcpProbeStatus = "connected" | "needs_auth" | "connect_failed" | "unresolved_var" | "invalid_config" | "missing_catalog" | "cancelled";
3
4
  export type ProjectMcpProbeResult = {
@@ -13,3 +14,13 @@ export type ProbeProjectMcpServerOptions = {
13
14
  timeoutMs?: number;
14
15
  };
15
16
  export declare function probeProjectMcpServer(options: ProbeProjectMcpServerOptions): Promise<ProjectMcpProbeResult>;
17
+ /**
18
+ * Connects the snapshot's servers headlessly so the adapter writes its tool
19
+ * metadata cache into the current Pi agent dir. A sealed loader reloaded after
20
+ * this registers the servers' direct tools at load time, which is when the
21
+ * stage tool allowlist is computed.
22
+ */
23
+ export declare function warmIsolatedMcpMetadata(snapshot: ResolvedMcpServers, options: {
24
+ cwd: string;
25
+ timeoutMs?: number;
26
+ }): Promise<void>;
@@ -290,3 +290,23 @@ export async function probeProjectMcpServer(options) {
290
290
  await dispose();
291
291
  }
292
292
  }
293
+ /**
294
+ * Connects the snapshot's servers headlessly so the adapter writes its tool
295
+ * metadata cache into the current Pi agent dir. A sealed loader reloaded after
296
+ * this registers the servers' direct tools at load time, which is when the
297
+ * stage tool allowlist is computed.
298
+ */
299
+ export async function warmIsolatedMcpMetadata(snapshot, options) {
300
+ const attached = await piIsolatedMcp.attachIsolatedMcp(snapshot, {
301
+ lifecycle: "eager",
302
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
303
+ });
304
+ const shutdown = instantiateProbeFactories(attached, options.cwd);
305
+ try {
306
+ await attached.connecting;
307
+ }
308
+ finally {
309
+ attached.cancel?.();
310
+ await shutdown();
311
+ }
312
+ }
@@ -122,12 +122,12 @@ export interface StageHandle {
122
122
  deliverAnswer(answer: OpaqueAnswer): void;
123
123
  close(options?: StageHandleCloseOptions): Promise<void>;
124
124
  }
125
- export interface AgentPort {
125
+ export interface StagePort {
126
126
  openStage(input: StageRunInput): StageHandle;
127
127
  runStage(input: StageRunInput): Promise<StageRunResult>;
128
128
  }
129
129
  /** Thin non-HITL wrapper: open → next; unexpected wait fails closed. */
130
- export declare function runStageViaOpen(port: Pick<AgentPort, "openStage">, input: StageRunInput): Promise<StageRunResult>;
130
+ export declare function runStageViaOpen(port: Pick<StagePort, "openStage">, input: StageRunInput): Promise<StageRunResult>;
131
131
  /** Wrap a single-shot runner as a handle that never waits (Pi U2 shim). */
132
132
  export declare function createCompletedOnlyStageHandle(options: {
133
133
  stageId: string;
@@ -1,6 +1,8 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { mkdir, writeFile, chmod, access } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { CredentialSynchronizationError, ModelRuntime, } from "@earendil-works/pi-coding-agent";
5
+ import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
4
6
  import { parseCredentialSource, resolveCredentialBinding, writeCredentialSourceToContext, isUsableAuthFile, } from "../runtime/credentialBinding.js";
5
7
  import { getCursorHostStatus } from "./cursorProvider.js";
6
8
  import { readPersistedCredentialSourceFromContext, } from "../runtime/settingsFile.js";
@@ -23,7 +25,13 @@ export class ProviderAuthError extends Error {
23
25
  this.status = status;
24
26
  }
25
27
  }
26
- const defaultCreateRuntime = async (authPath) => ModelRuntime.create({ authPath, refreshOnCreate: false });
28
+ const defaultCreateRuntime = async (authPath) => ModelRuntime.create({
29
+ authPath,
30
+ // ModelRuntime otherwise reads models.json from PI_CODING_AGENT_DIR,
31
+ // which stays on the process data directory.
32
+ modelsPath: path.join(path.dirname(authPath), "models.json"),
33
+ refreshOnCreate: false,
34
+ });
27
35
  async function ensurePiAuthFile(authPath) {
28
36
  const resolved = path.resolve(authPath);
29
37
  await mkdir(path.dirname(resolved), { recursive: true });
@@ -49,6 +57,31 @@ export async function openRuntime(cwd, ctx = defaultContext) {
49
57
  const binding = resolveCredentialBinding(cwd);
50
58
  return ctx.createRuntime(binding.authPath);
51
59
  }
60
+ function catalogRuntime() {
61
+ const providers = builtinProviders();
62
+ return {
63
+ getProviders: () => providers,
64
+ getProvider: (id) => providers.find((provider) => provider.id === id),
65
+ getProviderAuthStatus: () => ({ configured: false }),
66
+ listCredentials: async () => [],
67
+ checkAuth: async () => undefined,
68
+ login: async () => {
69
+ throw new ProviderAuthError("Provider login failed", 500);
70
+ },
71
+ logout: async () => {
72
+ throw new ProviderAuthError("Provider logout failed", 500);
73
+ },
74
+ };
75
+ }
76
+ async function openReadRuntime(cwd, ctx) {
77
+ const binding = resolveCredentialBinding(cwd);
78
+ // ModelRuntime.create writes auth.json when that file is missing.
79
+ if (ctx.createRuntime === defaultCreateRuntime &&
80
+ !existsSync(binding.authPath)) {
81
+ return catalogRuntime();
82
+ }
83
+ return ctx.createRuntime(binding.authPath);
84
+ }
52
85
  export function isLoginCapable(provider) {
53
86
  return (provider.auth?.oauth != null || provider.auth?.apiKey?.login != null);
54
87
  }
@@ -133,7 +166,7 @@ export async function statusForProvider(runtime, providerId) {
133
166
  return mapped;
134
167
  }
135
168
  export async function listProviders(cwd, ctx = defaultContext) {
136
- const runtime = await openRuntime(cwd, ctx);
169
+ const runtime = await openReadRuntime(cwd, ctx);
137
170
  return {
138
171
  authShell: "pi",
139
172
  via: "pi",
@@ -144,7 +177,7 @@ export async function listProviders(cwd, ctx = defaultContext) {
144
177
  };
145
178
  }
146
179
  export async function getAuthStatus(cwd, providerId, ctx = defaultContext) {
147
- const runtime = await openRuntime(cwd, ctx);
180
+ const runtime = await openReadRuntime(cwd, ctx);
148
181
  if (providerId !== undefined) {
149
182
  if (!runtime.getProvider(providerId)) {
150
183
  throw new ProviderAuthError("Provider not found", 404);
@@ -1,5 +1,5 @@
1
1
  import { type AgentBackendId } from "./agentBackend.js";
2
- import type { AgentPort } from "./port.js";
2
+ import type { StagePort } from "./port.js";
3
3
  import type { LoadedManifest } from "../types/stageflowManifest.js";
4
4
  export type AgentSelection = {
5
5
  /** stageflow.yaml top-level `agent`. */
@@ -10,14 +10,14 @@ export type AgentSelection = {
10
10
  stage?: AgentBackendId;
11
11
  };
12
12
  /** stage > pipeline > global > "pi", with stage gated off for now. */
13
- export declare function resolveAgentBackend(selection?: AgentSelection): AgentBackendId;
14
- /** Existing Pi AgentPort. Hosted runs pass this into createHostedRuntime. */
15
- export declare function createPiAgentPort(): AgentPort;
16
- export declare function resolveAgentPort(selection?: AgentSelection): AgentPort;
13
+ export declare function resolveStageBackend(selection?: AgentSelection): AgentBackendId;
14
+ /** Existing Pi StagePort. Hosted runs pass this into createHostedRuntime. */
15
+ export declare function createPiStagePort(): StagePort;
16
+ export declare function resolveStagePort(selection?: AgentSelection): StagePort;
17
17
  /** The global tier's backend id from an already-loaded manifest, or undefined for missing/invalid/unset. */
18
- export declare function globalAgentBackendFromManifest(manifest: LoadedManifest | null): AgentBackendId | undefined;
18
+ export declare function globalStageBackendFromManifest(manifest: LoadedManifest | null): AgentBackendId | undefined;
19
19
  /**
20
- * Resolves an AgentPort using only the global (stageflow.yaml) tier — for
20
+ * Resolves an StagePort using only the global (stageflow.yaml) tier — for
21
21
  * the three call sites that construct an agent before any pipeline is
22
22
  * known (`cli.ts`'s ui/mcp servers, `runCommand.ts`, `runsCommand.ts`).
23
23
  * Named explicitly so it's clear these sites are scoped to the global tier
@@ -25,4 +25,4 @@ export declare function globalAgentBackendFromManifest(manifest: LoadedManifest
25
25
  * through `stageWorker.ts`, which resolves the full stage > pipeline >
26
26
  * global chain once it has loaded the specific pipeline and stage.
27
27
  */
28
- export declare function resolveGlobalOnlyAgentPort(projectRoot: string): Promise<AgentPort>;
28
+ export declare function resolveGlobalOnlyStagePort(projectRoot: string): Promise<StagePort>;
@@ -3,31 +3,31 @@ import { PiAgentAdapter } from "./piAdapter.js";
3
3
  import { ClaudeAgentAdapter } from "./claudeAdapter.js";
4
4
  import { loadStageflowManifestOutcome } from "../config/loadStageflowManifest.js";
5
5
  /** stage > pipeline > global > "pi", with stage gated off for now. */
6
- export function resolveAgentBackend(selection = {}) {
6
+ export function resolveStageBackend(selection = {}) {
7
7
  if (STAGE_LEVEL_AGENT_OVERRIDE_ENABLED && selection.stage !== undefined) {
8
8
  return selection.stage;
9
9
  }
10
10
  return selection.pipeline ?? selection.global ?? "pi";
11
11
  }
12
- /** Existing Pi AgentPort. Hosted runs pass this into createHostedRuntime. */
13
- export function createPiAgentPort() {
12
+ /** Existing Pi StagePort. Hosted runs pass this into createHostedRuntime. */
13
+ export function createPiStagePort() {
14
14
  return new PiAgentAdapter();
15
15
  }
16
- export function resolveAgentPort(selection = {}) {
17
- const backend = resolveAgentBackend(selection);
16
+ export function resolveStagePort(selection = {}) {
17
+ const backend = resolveStageBackend(selection);
18
18
  switch (backend) {
19
19
  case "pi":
20
- return createPiAgentPort();
20
+ return createPiStagePort();
21
21
  case "claude":
22
22
  return new ClaudeAgentAdapter();
23
23
  }
24
24
  }
25
25
  /** The global tier's backend id from an already-loaded manifest, or undefined for missing/invalid/unset. */
26
- export function globalAgentBackendFromManifest(manifest) {
26
+ export function globalStageBackendFromManifest(manifest) {
27
27
  return manifest ? asAgentBackendId(manifest.manifest.agent) : undefined;
28
28
  }
29
29
  /**
30
- * Resolves an AgentPort using only the global (stageflow.yaml) tier — for
30
+ * Resolves an StagePort using only the global (stageflow.yaml) tier — for
31
31
  * the three call sites that construct an agent before any pipeline is
32
32
  * known (`cli.ts`'s ui/mcp servers, `runCommand.ts`, `runsCommand.ts`).
33
33
  * Named explicitly so it's clear these sites are scoped to the global tier
@@ -35,9 +35,9 @@ export function globalAgentBackendFromManifest(manifest) {
35
35
  * through `stageWorker.ts`, which resolves the full stage > pipeline >
36
36
  * global chain once it has loaded the specific pipeline and stage.
37
37
  */
38
- export async function resolveGlobalOnlyAgentPort(projectRoot) {
38
+ export async function resolveGlobalOnlyStagePort(projectRoot) {
39
39
  const outcome = await loadStageflowManifestOutcome(projectRoot);
40
- return resolveAgentPort({
41
- global: outcome.ok ? globalAgentBackendFromManifest(outcome.value) : undefined,
40
+ return resolveStagePort({
41
+ global: outcome.ok ? globalStageBackendFromManifest(outcome.value) : undefined,
42
42
  });
43
43
  }
@@ -0,0 +1,33 @@
1
+ import type { EmitCapture } from "../tools/emitStageEnvelope.js";
2
+ /** Follow-up turns sent when the agent ends its turn without an accepted envelope. */
3
+ export declare const DEFAULT_EMIT_REMINDERS = 2;
4
+ /** The slice of a Pi session the driver needs; a fake in tests. */
5
+ export type StageTurnSession = {
6
+ prompt(text: string): Promise<void>;
7
+ readonly messages: readonly unknown[];
8
+ };
9
+ export type RemindOptions = {
10
+ emitToolName: string;
11
+ maxReminders?: number;
12
+ /** True once the stage is closing, parked, or timed out: never start another turn then. */
13
+ shouldStop: () => boolean;
14
+ };
15
+ export type RemindOutcome = {
16
+ reminders: number;
17
+ /** Set when the last turn ended on a provider error; no reminder is sent after one. */
18
+ providerError?: string;
19
+ };
20
+ /** Trimmed, length-capped provider error text for a stage failure reason. */
21
+ export declare function formatProviderError(text: string | undefined): string;
22
+ /**
23
+ * Error text of the last assistant turn when it ended on a provider error (bad key, auth,
24
+ * model unavailable, or a transient error Pi already gave up retrying). Undefined otherwise.
25
+ */
26
+ export declare function lastTurnProviderError(messages: readonly unknown[]): string | undefined;
27
+ export declare function emitReminderPrompt(emitToolName: string, attempt: number, max: number, rejectedEmit?: string): string;
28
+ /**
29
+ * Called after a turn returns. While no envelope was accepted, nudge the agent with a short
30
+ * follow-up turn, up to `maxReminders` times. Stops at once when an envelope is captured,
31
+ * the stage is stopping, the turn was aborted, or the turn ended on a provider error.
32
+ */
33
+ export declare function remindUntilEmitted(session: StageTurnSession, capture: EmitCapture, options: RemindOptions): Promise<RemindOutcome>;
@@ -0,0 +1,64 @@
1
+ /** Follow-up turns sent when the agent ends its turn without an accepted envelope. */
2
+ export const DEFAULT_EMIT_REMINDERS = 2;
3
+ const PROVIDER_ERROR_MAX_CHARS = 500;
4
+ function lastAssistant(messages) {
5
+ for (let i = messages.length - 1; i >= 0; i--) {
6
+ const m = messages[i];
7
+ if (m && typeof m === "object" && m.role === "assistant")
8
+ return m;
9
+ }
10
+ return undefined;
11
+ }
12
+ /** Trimmed, length-capped provider error text for a stage failure reason. */
13
+ export function formatProviderError(text) {
14
+ const t = text?.trim() ? text.trim() : "the model provider returned an error";
15
+ return t.length > PROVIDER_ERROR_MAX_CHARS ? `${t.slice(0, PROVIDER_ERROR_MAX_CHARS)}…` : t;
16
+ }
17
+ /**
18
+ * Error text of the last assistant turn when it ended on a provider error (bad key, auth,
19
+ * model unavailable, or a transient error Pi already gave up retrying). Undefined otherwise.
20
+ */
21
+ export function lastTurnProviderError(messages) {
22
+ const m = lastAssistant(messages);
23
+ if (!m || m.stopReason !== "error")
24
+ return undefined;
25
+ return formatProviderError(typeof m.errorMessage === "string" ? m.errorMessage : undefined);
26
+ }
27
+ function lastTurnAborted(messages) {
28
+ return lastAssistant(messages)?.stopReason === "aborted";
29
+ }
30
+ export function emitReminderPrompt(emitToolName, attempt, max, rejectedEmit) {
31
+ const lines = [
32
+ `Reminder ${attempt} of ${max}: your turn ended without an accepted ${emitToolName} call, so this stage has not finished.`,
33
+ ];
34
+ if (rejectedEmit !== undefined) {
35
+ lines.push(`Your last ${emitToolName} call was rejected: ${rejectedEmit}`);
36
+ lines.push(`Fix that and call ${emitToolName} again.`);
37
+ }
38
+ else {
39
+ lines.push(`Continue the stage's work if any is left, then call ${emitToolName}.`);
40
+ }
41
+ lines.push(`If the stage cannot be completed, call ${emitToolName} with status "failure" and a summary that says why. Do not end your turn without calling it.`);
42
+ return lines.join("\n");
43
+ }
44
+ /**
45
+ * Called after a turn returns. While no envelope was accepted, nudge the agent with a short
46
+ * follow-up turn, up to `maxReminders` times. Stops at once when an envelope is captured,
47
+ * the stage is stopping, the turn was aborted, or the turn ended on a provider error.
48
+ */
49
+ export async function remindUntilEmitted(session, capture, options) {
50
+ const max = options.maxReminders ?? DEFAULT_EMIT_REMINDERS;
51
+ let reminders = 0;
52
+ for (;;) {
53
+ if (capture.envelope)
54
+ return { reminders };
55
+ const providerError = lastTurnProviderError(session.messages);
56
+ if (providerError !== undefined)
57
+ return { reminders, providerError };
58
+ if (options.shouldStop() || lastTurnAborted(session.messages) || reminders >= max) {
59
+ return { reminders };
60
+ }
61
+ reminders++;
62
+ await session.prompt(emitReminderPrompt(options.emitToolName, reminders, max, capture.error));
63
+ }
64
+ }