@bastani/atomic 0.8.28-alpha.4 → 0.8.29-alpha.2

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 (134) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +27 -0
  3. package/dist/builtin/cursor/LICENSE +26 -0
  4. package/dist/builtin/cursor/README.md +22 -0
  5. package/dist/builtin/cursor/index.ts +9 -0
  6. package/dist/builtin/cursor/package.json +46 -0
  7. package/dist/builtin/cursor/src/auth.ts +352 -0
  8. package/dist/builtin/cursor/src/catalog-cache.ts +155 -0
  9. package/dist/builtin/cursor/src/config.ts +123 -0
  10. package/dist/builtin/cursor/src/conversation-state.ts +135 -0
  11. package/dist/builtin/cursor/src/cursor-models-raw.json +583 -0
  12. package/dist/builtin/cursor/src/model-mapper.ts +270 -0
  13. package/dist/builtin/cursor/src/models.ts +54 -0
  14. package/dist/builtin/cursor/src/native-loader.ts +71 -0
  15. package/dist/builtin/cursor/src/proto/README.md +34 -0
  16. package/dist/builtin/cursor/src/proto/agent_pb.ts +15294 -0
  17. package/dist/builtin/cursor/src/proto/protobuf-codec.ts +717 -0
  18. package/dist/builtin/cursor/src/provider.ts +301 -0
  19. package/dist/builtin/cursor/src/stream.ts +564 -0
  20. package/dist/builtin/cursor/src/transport.ts +791 -0
  21. package/dist/builtin/intercom/CHANGELOG.md +10 -0
  22. package/dist/builtin/intercom/package.json +2 -2
  23. package/dist/builtin/intercom/skills/intercom/SKILL.md +5 -5
  24. package/dist/builtin/mcp/CHANGELOG.md +10 -0
  25. package/dist/builtin/mcp/package.json +3 -3
  26. package/dist/builtin/subagents/CHANGELOG.md +18 -0
  27. package/dist/builtin/subagents/README.md +7 -3
  28. package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -24
  29. package/dist/builtin/subagents/agents/debugger.md +3 -5
  30. package/dist/builtin/subagents/package.json +4 -4
  31. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +2 -1
  32. package/dist/builtin/subagents/src/runs/foreground/execution.ts +2 -1
  33. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
  34. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +19 -2
  35. package/dist/builtin/subagents/src/runs/shared/structured-output.ts +271 -10
  36. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +12 -39
  37. package/dist/builtin/subagents/src/shared/types.ts +1 -0
  38. package/dist/builtin/subagents/src/shared/utils.ts +50 -10
  39. package/dist/builtin/subagents/src/slash/saved-chain-mapping.ts +77 -0
  40. package/dist/builtin/subagents/src/slash/slash-commands.ts +1 -55
  41. package/dist/builtin/web-access/CHANGELOG.md +11 -1
  42. package/dist/builtin/web-access/README.md +1 -1
  43. package/dist/builtin/web-access/github-extract.ts +1 -1
  44. package/dist/builtin/web-access/package.json +3 -3
  45. package/dist/builtin/workflows/CHANGELOG.md +44 -0
  46. package/dist/builtin/workflows/README.md +19 -1
  47. package/dist/builtin/workflows/package.json +2 -2
  48. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +17 -3
  49. package/dist/builtin/workflows/src/extension/wiring.ts +17 -1
  50. package/dist/builtin/workflows/src/extension/workflow-schema.ts +34 -0
  51. package/dist/builtin/workflows/src/runs/foreground/executor.ts +13 -2
  52. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +86 -14
  53. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +11 -3
  54. package/dist/builtin/workflows/src/shared/types.ts +8 -4
  55. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +64 -2
  56. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
  57. package/dist/builtin/workflows/src/tui/workflow-status.ts +2 -0
  58. package/dist/core/builtin-packages.d.ts.map +1 -1
  59. package/dist/core/builtin-packages.js +6 -0
  60. package/dist/core/builtin-packages.js.map +1 -1
  61. package/dist/core/extensions/index.d.ts +1 -1
  62. package/dist/core/extensions/index.d.ts.map +1 -1
  63. package/dist/core/extensions/index.js.map +1 -1
  64. package/dist/core/extensions/types.d.ts +20 -0
  65. package/dist/core/extensions/types.d.ts.map +1 -1
  66. package/dist/core/extensions/types.js.map +1 -1
  67. package/dist/core/model-resolver.d.ts +1 -0
  68. package/dist/core/model-resolver.d.ts.map +1 -1
  69. package/dist/core/model-resolver.js +17 -8
  70. package/dist/core/model-resolver.js.map +1 -1
  71. package/dist/core/package-manager.d.ts +11 -9
  72. package/dist/core/package-manager.d.ts.map +1 -1
  73. package/dist/core/package-manager.js +55 -10
  74. package/dist/core/package-manager.js.map +1 -1
  75. package/dist/core/project-trust.d.ts +1 -0
  76. package/dist/core/project-trust.d.ts.map +1 -1
  77. package/dist/core/project-trust.js +3 -3
  78. package/dist/core/project-trust.js.map +1 -1
  79. package/dist/core/resource-loader.d.ts +9 -0
  80. package/dist/core/resource-loader.d.ts.map +1 -1
  81. package/dist/core/resource-loader.js +72 -9
  82. package/dist/core/resource-loader.js.map +1 -1
  83. package/dist/core/sdk.d.ts +3 -3
  84. package/dist/core/sdk.d.ts.map +1 -1
  85. package/dist/core/sdk.js +5 -5
  86. package/dist/core/sdk.js.map +1 -1
  87. package/dist/core/tools/index.d.ts +1 -0
  88. package/dist/core/tools/index.d.ts.map +1 -1
  89. package/dist/core/tools/index.js +1 -0
  90. package/dist/core/tools/index.js.map +1 -1
  91. package/dist/core/tools/structured-output.d.ts +39 -0
  92. package/dist/core/tools/structured-output.d.ts.map +1 -0
  93. package/dist/core/tools/structured-output.js +141 -0
  94. package/dist/core/tools/structured-output.js.map +1 -0
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +1 -1
  98. package/dist/index.js.map +1 -1
  99. package/dist/main.d.ts.map +1 -1
  100. package/dist/main.js +36 -14
  101. package/dist/main.js.map +1 -1
  102. package/dist/modes/interactive/components/login-dialog.d.ts +3 -0
  103. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  104. package/dist/modes/interactive/components/login-dialog.js +16 -0
  105. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  106. package/dist/modes/interactive/interactive-mode.d.ts +11 -0
  107. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  108. package/dist/modes/interactive/interactive-mode.js +158 -11
  109. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  110. package/dist/modes/print-mode.d.ts.map +1 -1
  111. package/dist/modes/print-mode.js +39 -0
  112. package/dist/modes/print-mode.js.map +1 -1
  113. package/docs/custom-provider.md +1 -0
  114. package/docs/extensions.md +2 -2
  115. package/docs/models.md +2 -0
  116. package/docs/packages.md +3 -1
  117. package/docs/providers.md +15 -0
  118. package/docs/sdk.md +61 -0
  119. package/docs/security.md +1 -1
  120. package/docs/subagents.md +21 -0
  121. package/docs/usage.md +2 -0
  122. package/docs/workflows.md +10 -7
  123. package/examples/extensions/README.md +1 -1
  124. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  125. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  126. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  127. package/examples/extensions/gondolin/package-lock.json +2 -2
  128. package/examples/extensions/gondolin/package.json +1 -1
  129. package/examples/extensions/sandbox/package-lock.json +2 -2
  130. package/examples/extensions/sandbox/package.json +1 -1
  131. package/examples/extensions/structured-output.ts +22 -53
  132. package/examples/extensions/with-deps/package-lock.json +2 -2
  133. package/examples/extensions/with-deps/package.json +1 -1
  134. package/package.json +12 -9
@@ -78,7 +78,7 @@ function loadGitHubConfig(): GitHubCloneConfig {
78
78
  enabled: true,
79
79
  maxRepoSizeMB: 350,
80
80
  cloneTimeoutSeconds: 30,
81
- clonePath: "/tmp/pi-github-repos",
81
+ clonePath: "/tmp/atomic-github-repos",
82
82
  };
83
83
 
84
84
  if (!existsSync(CONFIG_PATH)) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.8.28-alpha.4",
3
+ "version": "0.8.29-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@bastani/atomic": "*",
33
- "@earendil-works/pi-tui": "^0.78.1"
33
+ "@earendil-works/pi-tui": "^0.79.3"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "@bastani/atomic": {
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@mozilla/readability": "^0.6.0",
45
45
  "linkedom": "^0.18.12",
46
- "p-limit": "^6.1.0",
46
+ "p-limit": "^7.3.0",
47
47
  "turndown": "^7.2.0",
48
48
  "unpdf": "^1.6.2"
49
49
  }
@@ -6,6 +6,50 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Added
10
+
11
+ - Added opt-in schema-backed workflow item results: `ctx.stage(..., { schema })`, `ctx.task(..., { schema })`, `ctx.chain` items, and `ctx.parallel` items now receive a schema-specific `structured_output` tool only for that item, require the final tool call, return the parsed value from `ctx.stage().prompt(...)`, and expose parsed task values as `result.structured` while preserving formatted JSON handoff text ([#1350](https://github.com/bastani-inc/atomic/issues/1350)).
12
+
13
+ ### Changed
14
+
15
+ - Aligned the workflows extension with upstream pi TUI `^0.79.3` so workflow graph, custom UI, and prompt-broker integrations inherit the latest shared TUI compatibility fixes.
16
+ - Documented the opt-in `structured_output` workflow path and clarified that ordinary workflow stages do not receive `structured_output` from the default tool registry; schema-enabled items auto-add the runtime tool to explicit `tools` allowlists ([#1350](https://github.com/bastani-inc/atomic/issues/1350)).
17
+ - Clarified that workflow `structured_output` gate schemas must be top-level object tool-argument schemas, with arrays and primitives wrapped in object fields before being returned through the terminating tool, and documented the one-`prompt()` limit for schema-backed `StageContext` result contracts ([#1350](https://github.com/bastani-inc/atomic/issues/1350)).
18
+ - Documented that terminating workflow-stage `structured_output` JSON stays inline even when large, while artifact-sized handoffs should still be saved to files when downstream stages do not need the full payload in context ([#1350](https://github.com/bastani-inc/atomic/issues/1350)).
19
+
20
+ ### Fixed
21
+
22
+ - Fixed direct workflow tool validation so schema-enabled `task`, `tasks`, `chain`, and `parallel` items reject array or primitive structured-output schemas at argument-validation time while accepting the same object-root contracts as runtime validation, including object-only `allOf` schemas ([#1350](https://github.com/bastani-inc/atomic/issues/1350)).
23
+ - Fixed schema-backed workflow stages to fail with a clear stage-level error when `prompt()` is called more than once on the same `StageContext`, rather than surfacing the lower-level structured-output single-use guard ([#1350](https://github.com/bastani-inc/atomic/issues/1350)).
24
+ - Fixed schema-backed workflow model fallback so an attempt that already captured a valid terminating `structured_output` result is treated as successful instead of retrying against fallback models and tripping the single-use result guard ([#1350](https://github.com/bastani-inc/atomic/issues/1350)).
25
+ - Fixed the workflow graph overlay remaining interactive when the parent/main-chat agent opens `ask_user_question`: the graph keeps focus, the parent question stays pending behind it with a clear “Main chat needs input — exit graph to answer.” status hint, hiding/exiting the graph focuses the pending question, and host custom-UI state changes no longer hide, restore, remount, or repaint the overlay ([#1353](https://github.com/bastani-inc/atomic/issues/1353)).
26
+
27
+ ## [0.8.28] - 2026-06-11
28
+
29
+ ### Added
30
+
31
+ - Added workflow `ctx.ui.custom<T>(factory, options?)` for graph-visible custom TUI human-in-the-loop prompts. Custom prompts create `awaiting_input` prompt nodes, reuse the stage UI broker/attached stage chat component path, expose the same real TUI/theme/keybinding/component types as Atomic extension custom UI, participate in live-memory prompt replay through hashed custom identities, honor prompt/run abort signals, and reject clearly in headless/unavailable UI modes ([#1309](https://github.com/bastani-inc/atomic/issues/1309)).
32
+ - Added workflow authoring `ctx.exit(options?)` for intentional early terminal runs from any call depth, supporting `completed`, `skipped`, `cancelled`, and `blocked` terminal statuses, optional persisted/displayed reasons, and partial declared outputs with strict validation for provided output keys. Public run/detail/child status unions widen with `skipped`, `cancelled`, and `blocked`, and child workflow results are discriminated by `exited`.
33
+ - Added workflow stage/task `bashPolicy` wiring so individual workflow stages can constrain the built-in `bash` tool with command-level allow/deny rules, command-string glob matching, fail-closed invalid-policy validation, and default-allow no-rule compatibility.
34
+
35
+ ### Changed
36
+
37
+ - Changed the builtin `deep-research-codebase`, `goal`, `ralph`, and `open-claude-design` workflows to use `anthropic/claude-fable-5:xhigh` as the primary planner/reviewer/design model, demoting each previous primary to the head of the fallback chain ([#1345](https://github.com/bastani-inc/atomic/pull/1345)).
38
+ - Changed workflow transcript introspection to return `sessionFile`/`transcriptPath` metadata with a lazy-read prompt by default when a transcript path exists, keeping bounded inline previews behind explicit `tail`/`limit` requests ([#1314](https://github.com/bastani-inc/atomic/issues/1314)).
39
+
40
+ ### Fixed
41
+
42
+ - Fixed a workflow kill/abort race that could crash the entire CLI with a process-level uncaught exception when a workflow was killed mid-prompt; `raceAbort` now always observes the in-flight promise in the already-aborted branch so a killed run can no longer orphan a rejecting prompt.
43
+ - Fixed `ctx.exit(...)` cleanup races across the executor: the selected exit is a level-triggered gate so delayed `ctx.stage`/`ctx.task`/`ctx.chain`/`ctx.parallel`/`ctx.workflow`/graph-backed `ctx.ui.*` calls and retained `StageContext` session-control methods no longer create artifacts after exit, queued `ctx.parallel` work stops after exit, parent exits cancel linked hidden child workflows with typed parent-exit abort reasons and exactly-once stage-end ordering, and prompt-node abort handling preserves `workflow-exit` skipped reasons.
44
+ - Fixed terminal run-end reconciliation after `ctx.exit(...)` so when an external kill or another terminal writer wins `Store.recordRunEnd(...)`, the returned `RunResult` and `onRunEnd` callback report the canonical store status and only the winning run-end write is persisted.
45
+ - Fixed workflow-boundary child-edge metadata cleanup for `ctx.exit(...)` and continuation replay: skipped/failed boundaries clear `workflowChild`/`workflowChildRun`, stage-end persistence only emits child replay metadata for completed boundary stages, and expanded graph views no longer flatten stale child stages.
46
+ - Fixed `ctx.exit({ outputs })` payload capture to snapshot outputs by value at the first selected exit call, and deep-froze the thrown exit signal so author code cannot rewrite the terminal status, reason, or outputs after the fact.
47
+ - Fixed continuation replay races where replayed stage `prompt`/`complete` or prompt-node finalizers could complete after a concurrent `ctx.exit(...)`; pending replay finalizers now re-check the exit gate so resumed runs skip those stages instead of writing misleading completed stage-end entries.
48
+ - Fixed control-signal probing for arbitrary workflow-thrown values and abort reasons to use non-throwing reads, so throwing or inaccessible author accessors no longer leak from the executor catch path.
49
+ - Fixed interactive `ctx.ui.*` handling so workflow runs degrade gracefully: every primitive is guarded against method-less UI adapters with a clear per-method error, and headless (non-interactive) runs without a UI adapter reject with an explicit actionable message ([#1339](https://github.com/bastani-inc/atomic/issues/1339)).
50
+ - Fixed the builtin `open-claude-design` workflow not installing the browser skill's `browse` CLI before it is needed: a deterministic best-effort setup step probes `PATH` and installs the CLI when missing, per-run bootstrap guidance is injected into every browser-using stage, the install outcome is exposed via a new `browse_cli_status` output, and read-only `read`/`grep`/`ls` tools are granted to the refinement and pre-export decision gates ([#1327](https://github.com/bastani-inc/atomic/issues/1327)).
51
+ - Fixed paused workflow runs being counted as running in `/workflow status` (now shown separately as `❚❚ paused`) and run detail cards to surface the natural `workflow resume` action hint ([#1283](https://github.com/bastani-inc/atomic/issues/1283)).
52
+
9
53
  ## [0.8.28-alpha.4] - 2026-06-11
10
54
 
11
55
  ### Changed
@@ -264,6 +264,24 @@ Worktree semantics:
264
264
 
265
265
  For advanced integrations, the SDK also exports `setupGitWorktree(options)`, which returns `{ worktreeRoot, cwd, repositoryRoot, created }` and uses the same validation/path behavior as the executor.
266
266
 
267
+ ### Structured stage results
268
+
269
+ `structured_output` is opt-in for workflow items. Add `schema` to `ctx.stage`, `ctx.task`, `ctx.chain` items, or `ctx.parallel` items when the stage must finish with machine-readable JSON:
270
+
271
+ ```typescript
272
+ const Decision = Type.Object({
273
+ approved: Type.Boolean(),
274
+ findings: Type.Array(Type.String()),
275
+ }, { additionalProperties: false });
276
+
277
+ const decision = await ctx.stage("review-gate", { schema: Decision }).prompt(
278
+ "Review the artifact and return the decision.",
279
+ );
280
+ // decision.approved is typed from the schema.
281
+ ```
282
+
283
+ Atomic registers the canonical `structured_output` tool only for schema-enabled items, automatically adds it to explicit `tools` allowlists, and fails the item if the model completes without the final tool call. The schema is used directly as the tool argument contract, so wrap arrays or primitives in an object field such as `{ items: [...] }` or `{ value: ... }`. A schema-backed `StageContext` supports one `prompt()` call because the final-answer tool is an exact-once result contract; create another `ctx.stage(..., { schema })` for another structured prompt. `ctx.task`/`ctx.chain`/`ctx.parallel` results expose the parsed value as `result.structured` and keep `result.text` as formatted JSON for handoffs.
284
+
267
285
  ### Model fallbacks
268
286
 
269
287
  Stages and high-level task helpers can retry transient provider/model failures with an ordered `fallbackModels` list. The primary `model` is tried first, then each fallback, and finally the current Atomic-selected model when available. Fallbacks are only used for retryable model/provider failures such as rate limits, quota/auth/provider outages, unavailable models, network timeouts, and 5xx errors — ordinary tool, shell, validation, cancellation, and workflow-code failures are not retried.
@@ -501,7 +519,7 @@ Prompt answer replay is live-memory only. `StageSnapshot.promptAnswerState` repo
501
519
  "async": "optional boolean to dispatch a run in the background",
502
520
  "intercom": "optional intercom coordination options",
503
521
  "chainDir": "optional directory for direct chain artifacts",
504
- "session/task options": "per-stage overrides also accepted at the top level and on direct task items — model, thinkingLevel, fallbackModels, tools, noTools, customTools, mcp, context, cwd, output, outputMode, reads, worktree, gitWorktreeDir, baseBranch, maxOutput, artifacts, and more"
522
+ "session/task options": "per-stage overrides also accepted at the top level and on direct task items — schema, model, thinkingLevel, fallbackModels, tools, noTools, customTools, mcp, context, cwd, output, outputMode, reads, worktree, gitWorktreeDir, baseBranch, maxOutput, artifacts, and more"
505
523
  }
506
524
  }
507
525
  ```
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.8.28-alpha.4",
3
+ "version": "0.8.29-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -83,7 +83,7 @@
83
83
  },
84
84
  "peerDependencies": {
85
85
  "@bastani/atomic": "*",
86
- "@earendil-works/pi-tui": "^0.78.1"
86
+ "@earendil-works/pi-tui": "^0.79.3"
87
87
  },
88
88
  "peerDependenciesMeta": {
89
89
  "@bastani/atomic": {
@@ -65,10 +65,24 @@ The user's research question/request is: **$ARGUMENTS**
65
65
  - The agent fetches live web content using the **browser** skill's `browse` CLI (or `npx browse` / `curl`). Instruct it to apply the token-efficient fetch order: (1) try `curl https://<site>/llms.txt` for an AI-friendly index (see [llmstxt.org](https://llmstxt.org/llms.txt)), (2) try `curl <url> -H "Accept: text/markdown"` to get pre-converted Markdown (supported on Cloudflare-hosted docs via [Markdown for Agents](https://developers.cloudflare.com/fundamentals/reference/markdown-for-agents/)), (3) fall back to HTML parsing via `browse`
66
66
  - Instruct the agent to return LINKS with their findings and INCLUDE those links in the research document
67
67
  - The agent should persist reusable source documents under `research/web/<YYYY-MM-DD>-<kebab-case-topic>.md` (with frontmatter noting `source_url`, `fetched_at`, and `fetch_method`) so future research can reuse them without re-fetching
68
- - Output directory for the synthesized research artifact: `research/docs/`
68
+ - Output directory for the synthesized web research artifacts: `research/web/`:
69
+
70
+ When you fetch a document that is worth keeping for future sessions (reference docs, API schemas, SDK guides, release notes, troubleshooting writeups, architecture articles), `write` it to `research/web/<YYYY-MM-DD>-<kebab-case-topic>.md` with frontmatter capturing:
71
+
72
+ ```markdown
73
+ ---
74
+ source_url: <original URL>
75
+ fetched_at: <YYYY-MM-DD>
76
+ fetch_method: read | llms.txt | markdown-accept-header | browser | browse
77
+ topic: <short description>
78
+ ---
79
+ ```
80
+
81
+ - Followed by the extracted content (trimmed of nav chrome, ads, and irrelevant boilerplate). This lets future work reuse the lookup without re-fetching. Before fetching anything, quickly `find research/web/` for an existing, recent copy.
82
+
69
83
  - Examples:
70
- - If researching `Redis` locks usage, the agent might find relevant usage and create a document `research/docs/2024-01-15-redis-locks-usage.md` with internal links to Redis docs and code references (and cache the fetched Redis docs under `research/web/`)
71
- - If researching `OAuth` flows, the agent might find relevant external articles and create a document `research/docs/2024-01-16-oauth-flows.md` with links to those articles
84
+ - If researching `Redis` locks usage, the agent might find relevant usage and create a document `research/web/2024-01-15-redis-locks-usage.md` with internal links to Redis docs and code references (and cache the fetched Redis docs under `research/web/`)
85
+ - If researching `OAuth` flows, the agent might find relevant external articles and create a document `research/web/2024-01-16-oauth-flows.md` with links to those articles
72
86
 
73
87
  The key is to use these agents intelligently:
74
88
  - Start with locator agents to find what exists
@@ -253,7 +253,7 @@ async function createTestAgentSession(_options?: CreateAgentSessionOptions): Pro
253
253
  function stripWorkflowOnlyOptions(options: (StageOptions | CreateAgentSessionOptions) | undefined): CreateAgentSessionOptions | undefined {
254
254
  if (!options) return options;
255
255
  const maybeWorkflowOptions = options as StageOptions;
256
- const { mcp: _mcp, fallbackModels: _fallbackModels, ...sessionOptions } = maybeWorkflowOptions;
256
+ const { schema: _schema, mcp: _mcp, fallbackModels: _fallbackModels, ...sessionOptions } = maybeWorkflowOptions;
257
257
  return sessionOptions as CreateAgentSessionOptions;
258
258
  }
259
259
 
@@ -499,6 +499,14 @@ export interface PiOverlayHandle {
499
499
  * (`overlay-adapter.ts`); inline pickers leave it unset and dismiss
500
500
  * via the factory `done()` callback.
501
501
  */
502
+ export interface PiHostCustomUiState {
503
+ blockingInlineCustomUiDepth: number;
504
+ blockingInlineCustomUiActive: boolean;
505
+ blockingInlineCustomUiFocusDeferred?: boolean;
506
+ }
507
+
508
+ export type PiHostCustomUiStateListener = (state: PiHostCustomUiState) => void;
509
+
502
510
  export interface PiCustomOverlayOptions {
503
511
  /**
504
512
  * `true` mounts a floating popup; `false` mounts a focused
@@ -506,6 +514,8 @@ export interface PiCustomOverlayOptions {
506
514
  * place of the editor until the factory's `done()` callback fires.
507
515
  */
508
516
  overlay: boolean;
517
+ /** Keep host inline custom UI pending in the background while this overlay is visible. */
518
+ deferInlineCustomUiFocus?: boolean;
509
519
  /**
510
520
  * Geometry / anchoring intended for pi-tui's `resolveOverlayLayout`.
511
521
  * NOT forwarded by current pi interactive `custom()` — see
@@ -636,6 +646,12 @@ export interface PiUISurface {
636
646
  setTitle?: (title: string) => void;
637
647
  /** Show a custom component or overlay. */
638
648
  custom?: PiCustomOverlayFunction;
649
+ /** Get host-owned inline custom UI focus state, if exposed by the host. */
650
+ getHostCustomUiState?: () => PiHostCustomUiState;
651
+ /** Observe host-owned inline custom UI focus state changes, if exposed by the host. */
652
+ onHostCustomUiStateChange?: (listener: PiHostCustomUiStateListener) => () => void;
653
+ /** Move focus to a mounted host-owned inline custom UI, if one is pending. */
654
+ focusHostInlineCustomUi?: () => boolean;
639
655
  pasteToEditor?: (text: string) => void;
640
656
  setEditorText?: (text: string) => void;
641
657
  getEditorText?: () => string;
@@ -37,6 +37,39 @@ const McpOptionsSchema = Type.Object({
37
37
  deny: Type.Optional(Type.Array(Type.String())),
38
38
  });
39
39
 
40
+ const JsonSchemaObjectTypeValue = {
41
+ anyOf: [
42
+ { const: "object" },
43
+ { type: "array", minItems: 1, maxItems: 1, items: { const: "object" } },
44
+ ],
45
+ };
46
+
47
+ const JsonSchemaExplicitObjectDescriptor = {
48
+ type: "object",
49
+ required: ["type"],
50
+ properties: { type: JsonSchemaObjectTypeValue },
51
+ additionalProperties: true,
52
+ };
53
+
54
+ const JsonSchemaObject = Type.Unsafe<Record<string, unknown>>({
55
+ description: "Top-level object JSON Schema used as structured_output tool arguments for this workflow item.",
56
+ anyOf: [
57
+ JsonSchemaExplicitObjectDescriptor,
58
+ {
59
+ type: "object",
60
+ required: ["allOf"],
61
+ properties: {
62
+ allOf: {
63
+ type: "array",
64
+ minItems: 1,
65
+ items: JsonSchemaExplicitObjectDescriptor,
66
+ },
67
+ },
68
+ additionalProperties: true,
69
+ },
70
+ ],
71
+ });
72
+
40
73
  const BashCommandRuleSchema = Type.Union([
41
74
  Type.String(),
42
75
  Type.Object({ prefix: Type.String() }, { additionalProperties: false }),
@@ -55,6 +88,7 @@ const BashCommandPolicySchema = Type.Object({
55
88
  }, { additionalProperties: false });
56
89
 
57
90
  const StageSessionOptionProperties = {
91
+ schema: Type.Optional(JsonSchemaObject),
58
92
  cwd: Type.Optional(Type.String()),
59
93
  agentDir: Type.Optional(Type.String()),
60
94
  authStorage: Type.Optional(SdkSessionOptionSchema("authStorage")),
@@ -1267,6 +1267,15 @@ function truncateByBytes(text: string, maxBytes: number): { text: string; trunca
1267
1267
  return { text: text.slice(0, low), truncated: true };
1268
1268
  }
1269
1269
 
1270
+ function structuredTaskOutputText(value: unknown): string {
1271
+ if (typeof value === "string") return value;
1272
+ try {
1273
+ return JSON.stringify(value, null, 2);
1274
+ } catch (error) {
1275
+ throw new Error(`atomic-workflows: structured task output is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`);
1276
+ }
1277
+ }
1278
+
1270
1279
  function truncateTaskOutput(text: string, maxOutput: WorkflowMaxOutput | undefined): string {
1271
1280
  const limits = normalizeMaxOutput(maxOutput);
1272
1281
  const byLines = truncateByLines(text, limits.lines);
@@ -4827,11 +4836,12 @@ export async function run<TInputs extends WorkflowInputValues>(
4827
4836
  taskStageOptions(resolvedTaskOptions),
4828
4837
  stageFailFastScope,
4829
4838
  );
4830
- const rawText = await stage.prompt(
4839
+ const rawOutput = await stage.prompt(
4831
4840
  applyTaskContext(`${taskReadInstruction(resolvedTaskOptions)}${taskPrompt(resolvedTaskOptions)}`, taskPrevious(resolvedTaskOptions)),
4832
4841
  taskPromptOptions(resolvedTaskOptions),
4833
4842
  );
4834
- const text = truncateTaskOutput(rawText, resolvedTaskOptions.maxOutput);
4843
+ const structured = typeof rawOutput === "string" ? undefined : rawOutput;
4844
+ const text = truncateTaskOutput(structuredTaskOutputText(rawOutput), resolvedTaskOptions.maxOutput);
4835
4845
  const sessionId = (() => {
4836
4846
  try {
4837
4847
  return stage.sessionId;
@@ -4844,6 +4854,7 @@ export async function run<TInputs extends WorkflowInputValues>(
4844
4854
  name,
4845
4855
  stageName: name,
4846
4856
  text,
4857
+ ...(structured !== undefined ? { structured: structured as WorkflowSerializableValue } : {}),
4847
4858
  ...(sessionId !== undefined ? { sessionId } : {}),
4848
4859
  ...(stage.sessionFile !== undefined ? { sessionFile: stage.sessionFile } : {}),
4849
4860
  ...(stageMeta.model !== undefined ? { model: stageMeta.model } : {}),
@@ -10,11 +10,14 @@
10
10
  import { mkdir, writeFile } from "node:fs/promises";
11
11
  import { dirname, isAbsolute, resolve } from "node:path";
12
12
  import {
13
+ createStructuredOutputCapture,
14
+ createStructuredOutputTool,
13
15
  shouldApplyCodexFastModeForScope,
14
16
  SessionManager,
15
17
  type AgentSession,
16
18
  type CreateAgentSessionOptions,
17
19
  type PromptOptions,
20
+ type StructuredOutputCapture,
18
21
  } from "@bastani/atomic";
19
22
  import type {
20
23
  CompleteStageOpts,
@@ -28,6 +31,7 @@ import type {
28
31
  WorkflowExecutionMode,
29
32
  WorkflowModelCatalogPort,
30
33
  } from "../../shared/types.js";
34
+ import type { Static, TSchema } from "typebox";
31
35
  import {
32
36
  buildModelCandidatesFromCatalog,
33
37
  errorMessage,
@@ -167,6 +171,7 @@ export interface InternalStageContext extends StageContext {
167
171
  function stripWorkflowOnlyOptions(options: StageOptions | undefined): CreateAgentSessionOptions {
168
172
  if (!options) return {};
169
173
  const {
174
+ schema: _schema,
170
175
  mcp: _mcp,
171
176
  fallbackModels: _fallbackModels,
172
177
  fallbackThinkingLevels: _fallbackThinkingLevels,
@@ -530,6 +535,43 @@ function splitPromptOptions(options: StagePromptOptions | undefined): {
530
535
  };
531
536
  }
532
537
 
538
+ const STRUCTURED_OUTPUT_TOOL_NAME = "structured_output";
539
+
540
+ function structuredOutputPrompt(text: string): string {
541
+ return `${text}\n\nFinal output contract:\n- Your final action MUST be a structured_output tool call.\n- Pass the schema fields directly as tool arguments; do not wrap them in { value: ... } unless the schema explicitly defines a top-level value field.\n- Do not emit a prose final answer instead of structured_output.\n- If you need to inspect files or run commands first, do so, then call structured_output exactly once.`;
542
+ }
543
+
544
+ function stringifyStructuredOutputValue(value: unknown): string {
545
+ try {
546
+ return JSON.stringify(value, null, 2);
547
+ } catch (error) {
548
+ throw new Error(`atomic-workflows: structured_output returned a non-serializable value: ${error instanceof Error ? error.message : String(error)}`);
549
+ }
550
+ }
551
+
552
+ function stageOptionsWithStructuredOutput(
553
+ options: StageOptions | undefined,
554
+ capture: StructuredOutputCapture<unknown> | undefined,
555
+ ): StageOptions | undefined {
556
+ if (!options?.schema || !capture) return options;
557
+ const tools = options.tools === undefined
558
+ ? undefined
559
+ : Array.from(new Set([...options.tools, STRUCTURED_OUTPUT_TOOL_NAME]));
560
+ const excludedTools = options.excludedTools?.filter((toolName) => toolName !== STRUCTURED_OUTPUT_TOOL_NAME);
561
+ return {
562
+ ...options,
563
+ ...(tools !== undefined ? { tools } : {}),
564
+ ...(excludedTools !== undefined ? { excludedTools } : {}),
565
+ customTools: [
566
+ ...(options.customTools ?? []),
567
+ createStructuredOutputTool({
568
+ schema: options.schema as TSchema,
569
+ capture: capture as StructuredOutputCapture<Static<TSchema>>,
570
+ }),
571
+ ],
572
+ };
573
+ }
574
+
533
575
  function validatePromptOutputOptions(outputOptions: StageOutputOptions): void {
534
576
  if (outputOptions.outputMode === "file-only" && (typeof outputOptions.output !== "string" || outputOptions.output.length === 0)) {
535
577
  throw new Error(
@@ -564,7 +606,9 @@ async function finalizePromptOutput(
564
606
 
565
607
  export function createStageContext(opts: StageRunnerOpts): InternalStageContext {
566
608
  const { stageId, stageName, adapters, runId, signal, stageOptions, executionMode } = opts;
567
- const meta: StageExecutionMeta = { runId, stageId, stageName, signal, stageOptions, executionMode };
609
+ const structuredOutputCapture = stageOptions?.schema ? createStructuredOutputCapture<unknown>() : undefined;
610
+ const effectiveStageOptions = stageOptionsWithStructuredOutput(stageOptions, structuredOutputCapture);
611
+ const meta: StageExecutionMeta = { runId, stageId, stageName, signal, stageOptions: effectiveStageOptions, executionMode };
568
612
  let session: StageSessionRuntime | undefined;
569
613
  let sessionPromise: Promise<StageSessionRuntime> | undefined;
570
614
  let lastAssistantText: string | undefined;
@@ -633,7 +677,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
633
677
  }
634
678
 
635
679
  const hasExplicitModelFallbackConfig =
636
- stageOptions?.model !== undefined || (stageOptions?.fallbackModels?.length ?? 0) > 0;
680
+ effectiveStageOptions?.model !== undefined || (effectiveStageOptions?.fallbackModels?.length ?? 0) > 0;
637
681
  let candidatesPromise: Promise<WorkflowResolvedModelCandidate[]> | undefined;
638
682
  let activeCandidateIndex: number | undefined;
639
683
  let selectedModel: string | undefined;
@@ -653,9 +697,9 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
653
697
  function modelCandidates(): Promise<WorkflowResolvedModelCandidate[]> {
654
698
  if (!candidatesPromise) {
655
699
  candidatesPromise = buildModelCandidatesFromCatalog({
656
- primaryModel: stageOptions?.model,
657
- fallbackModels: stageOptions?.fallbackModels,
658
- fallbackThinkingLevels: stageOptions?.fallbackThinkingLevels,
700
+ primaryModel: effectiveStageOptions?.model,
701
+ fallbackModels: effectiveStageOptions?.fallbackModels,
702
+ fallbackThinkingLevels: effectiveStageOptions?.fallbackThinkingLevels,
659
703
  catalog: modelCatalog,
660
704
  });
661
705
  }
@@ -663,9 +707,9 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
663
707
  }
664
708
 
665
709
  function stageOptionsForCandidate(candidate: WorkflowResolvedModelCandidate | undefined): StageOptions | undefined {
666
- if (candidate === undefined) return stageOptions;
710
+ if (candidate === undefined) return effectiveStageOptions;
667
711
  return {
668
- ...(stageOptions ?? {}),
712
+ ...(effectiveStageOptions ?? {}),
669
713
  model: candidate.value,
670
714
  ...(candidate.reasoningLevel !== undefined ? { thinkingLevel: candidate.reasoningLevel } : {}),
671
715
  fallbackModels: undefined,
@@ -677,7 +721,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
677
721
 
678
722
  function isWorkflowFastModeEnabled(): boolean | undefined {
679
723
  const model = session?.model;
680
- const settingsManager = sessionSettingsManager ?? stageOptions?.settingsManager;
724
+ const settingsManager = sessionSettingsManager ?? effectiveStageOptions?.settingsManager;
681
725
  if (model === undefined || settingsManager === undefined) return undefined;
682
726
  return shouldApplyCodexFastModeForScope(model, settingsManager.getCodexFastModeSettings(), "workflow");
683
727
  }
@@ -705,7 +749,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
705
749
  }
706
750
 
707
751
  function effectiveCandidateReasoning(candidate: WorkflowResolvedModelCandidate): StageOptions["thinkingLevel"] | undefined {
708
- return candidate.reasoningLevel ?? stageOptions?.thinkingLevel;
752
+ return candidate.reasoningLevel ?? effectiveStageOptions?.thinkingLevel;
709
753
  }
710
754
 
711
755
  function modelAttemptReasoning(candidate: WorkflowResolvedModelCandidate): Pick<WorkflowModelAttempt, "reasoningLevel"> {
@@ -715,7 +759,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
715
759
 
716
760
  function applyCandidateThinking(candidate: WorkflowResolvedModelCandidate | undefined): void {
717
761
  pendingThinkingLevel = candidate === undefined
718
- ? stageOptions?.thinkingLevel
762
+ ? effectiveStageOptions?.thinkingLevel
719
763
  : effectiveCandidateReasoning(candidate);
720
764
  }
721
765
 
@@ -843,6 +887,13 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
843
887
  }
844
888
 
845
889
  let index = activeCandidateIndex ?? 0;
890
+ const capturedStructuredOutputForAttempt = (): boolean =>
891
+ structuredOutputCapture?.called === true && signal?.aborted !== true;
892
+ const recordSuccessfulAttempt = (candidate: WorkflowResolvedModelCandidate): void => {
893
+ modelAttempts.push({ model: candidate.id, success: true, ...modelAttemptReasoning(candidate) });
894
+ pendingFallbackWarnings.length = 0;
895
+ };
896
+
846
897
  while (index < candidates.length) {
847
898
  const candidate = candidates[index]!;
848
899
  const activeSession = session && activeCandidateIndex === index
@@ -855,13 +906,20 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
855
906
  const { terminalScanStartIndex } = await promptWithPauseResume(activeSession, text, sdkOptions);
856
907
  const terminalFailure = latestTerminalAssistantFailureSince(activeSession.messages, terminalScanStartIndex);
857
908
  if (terminalFailure !== undefined) {
909
+ if (capturedStructuredOutputForAttempt()) {
910
+ recordSuccessfulAttempt(candidate);
911
+ return;
912
+ }
858
913
  throw new WorkflowPromptModelFailure(terminalFailure);
859
914
  }
860
- modelAttempts.push({ model: candidate.id, success: true, ...modelAttemptReasoning(candidate) });
861
- pendingFallbackWarnings.length = 0;
915
+ recordSuccessfulAttempt(candidate);
862
916
  return;
863
917
  } catch (err) {
864
918
  const message = errorMessage(err);
919
+ if (capturedStructuredOutputForAttempt() && isRetryableModelFailure(err)) {
920
+ recordSuccessfulAttempt(candidate);
921
+ return;
922
+ }
865
923
  modelAttempts.push({ model: candidate.id, success: false, ...modelAttemptReasoning(candidate), error: message });
866
924
  if (signal?.aborted || !isRetryableModelFailure(err) || index === candidates.length - 1) {
867
925
  modelWarnings.push(...pendingFallbackWarnings);
@@ -887,15 +945,29 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
887
945
 
888
946
  async prompt(text, options) {
889
947
  const { sdkOptions, outputOptions } = splitPromptOptions(options);
890
- const runtimeCwd = typeof stageOptions?.cwd === "string" ? stageOptions.cwd : process.cwd();
948
+ const runtimeCwd = typeof effectiveStageOptions?.cwd === "string" ? effectiveStageOptions.cwd : process.cwd();
891
949
  validatePromptOutputOptions(outputOptions);
950
+ if (structuredOutputCapture?.called) {
951
+ throw new Error("atomic-workflows: stage schema supports one prompt() call per stage context because structured_output may be called exactly once. Create a new ctx.stage(...) for each additional schema-backed prompt.");
952
+ }
892
953
  if (adapters.prompt) {
954
+ if (structuredOutputCapture) {
955
+ throw new Error("atomic-workflows: stage schema requires an AgentSessionAdapter so the structured_output tool can be registered.");
956
+ }
893
957
  const rawText = await adapters.prompt.prompt(text, meta);
894
958
  lastAssistantText = await finalizePromptOutput(rawText, outputOptions, runtimeCwd);
895
959
  adapterMessages = assistantMessage(lastAssistantText);
896
960
  return lastAssistantText;
897
961
  }
898
- await promptWithFallback(text, sdkOptions);
962
+ await promptWithFallback(structuredOutputCapture ? structuredOutputPrompt(text) : text, sdkOptions);
963
+ if (structuredOutputCapture) {
964
+ if (!structuredOutputCapture.called) {
965
+ throw new Error("atomic-workflows: stage configured with schema must finish by calling structured_output.");
966
+ }
967
+ const rawStructuredText = stringifyStructuredOutputValue(structuredOutputCapture.value);
968
+ lastAssistantText = await finalizePromptOutput(rawStructuredText, outputOptions, runtimeCwd);
969
+ return structuredOutputCapture.value as never;
970
+ }
899
971
  const rawText = lastAssistantTextFromSession(session, lastAssistantText, terminatingToolCallIds) ?? "";
900
972
  lastAssistantText = await finalizePromptOutput(rawText, outputOptions, runtimeCwd);
901
973
  return lastAssistantText;
@@ -48,6 +48,7 @@ export interface WorkflowModelFallbackFields {
48
48
  readonly fallbackThinkingLevels?: readonly string[];
49
49
  }
50
50
  export type WorkflowModelValue = string | object;
51
+ export type WorkflowStageResult<TSchemaDef extends TSchema | undefined = undefined> = [TSchemaDef] extends [TSchema] ? Static<TSchemaDef> : string;
51
52
  export interface WorkflowModelUsage extends WorkflowSerializableObject {
52
53
  readonly input?: number;
53
54
  readonly output?: number;
@@ -111,7 +112,9 @@ export interface WorkflowFastModeSettings extends WorkflowSerializableObject {
111
112
  export interface WorkflowFastModeSettingsManager {
112
113
  getCodexFastModeSettings(): WorkflowFastModeSettings;
113
114
  }
114
- export interface StageOptions extends WorkflowModelFallbackFields {
115
+ export interface StageOptions<TSchemaDef extends TSchema | undefined = TSchema | undefined> extends WorkflowModelFallbackFields {
116
+ /** Optional structured final-answer schema. When set, the stage receives a schema-specific `structured_output` tool and must finish by calling it. */
117
+ readonly schema?: TSchemaDef;
115
118
  readonly model?: WorkflowModelValue;
116
119
  readonly mcp?: StageMcpOptions;
117
120
  readonly tools?: readonly string[];
@@ -231,9 +234,9 @@ export interface StageAdapters {
231
234
  readonly prompt?: PromptAdapter;
232
235
  readonly complete?: CompleteAdapter;
233
236
  }
234
- export interface StageContext {
237
+ export interface StageContext<TSchemaDef extends TSchema | undefined = undefined> {
235
238
  readonly name: string;
236
- prompt(text: string, options?: StagePromptOptions): Promise<string>;
239
+ prompt(text: string, options?: StagePromptOptions): Promise<WorkflowStageResult<TSchemaDef>>;
237
240
  complete(text: string, options?: CompleteStageOpts): Promise<string>;
238
241
  steer(text: string): Promise<void>;
239
242
  followUp(text: string): Promise<void>;
@@ -279,6 +282,8 @@ export interface WorkflowTaskContext extends WorkflowSerializableObject {
279
282
  export type WorkflowTaskContextInput = string | WorkflowTaskContext | WorkflowTaskResult;
280
283
  export interface WorkflowTaskResult extends WorkflowTaskContext {
281
284
  readonly stageName: string;
285
+ /** Parsed structured value when the task/stage was configured with `schema`. */
286
+ readonly structured?: WorkflowSerializableValue;
282
287
  readonly sessionId?: string;
283
288
  readonly sessionFile?: string;
284
289
  readonly artifacts?: readonly WorkflowArtifact[];
@@ -400,6 +405,9 @@ export interface WorkflowRunContext<TInputs extends WorkflowInputValues = Workfl
400
405
  readonly inputs: Readonly<TInputs>;
401
406
  readonly cwd?: string;
402
407
  exit(options?: WorkflowExitOptions<TOutputs>): never;
408
+ stage<TSchemaDef extends TSchema>(name: string, options: StageOptions<TSchemaDef> & {
409
+ readonly schema: TSchemaDef;
410
+ }): StageContext<TSchemaDef>;
403
411
  stage(name: string, options?: StageOptions): StageContext;
404
412
  task(name: string, options: WorkflowTaskOptions): Promise<WorkflowTaskResult>;
405
413
  chain(steps: readonly WorkflowTaskStep[], options?: WorkflowChainOptions): Promise<WorkflowTaskResult[]>;
@@ -153,9 +153,11 @@ export interface StageMcpOptions extends AuthoringContract.StageMcpOptions {
153
153
  * All pi SDK createAgentSession options are forwarded to the stage session;
154
154
  * workflow-owned options such as `mcp` and `gitWorktreeDir` are stripped before SDK session creation.
155
155
  */
156
- export interface StageOptions
156
+ export interface StageOptions<TSchemaDef extends TSchema | undefined = TSchema | undefined>
157
157
  extends Omit<CreateAgentSessionOptions, "model" | keyof AuthoringContract.StageOptions>,
158
- Omit<Mutable<AuthoringContract.StageOptions>, "sessionManager" | "settingsManager"> {
158
+ Omit<Mutable<AuthoringContract.StageOptions<TSchemaDef>>, "sessionManager" | "settingsManager"> {
159
+ /** Optional structured final-answer schema. When set, the stage receives a schema-specific `structured_output` tool and must finish by calling it. */
160
+ schema?: TSchemaDef;
159
161
  /** Model id or pi SDK model object used as the primary stage model. */
160
162
  model?: WorkflowModelValue;
161
163
  /** Per-stage MCP server gating. No-op when no WorkflowMcpPort is configured. */
@@ -231,6 +233,7 @@ export interface WorkflowPersistencePort {
231
233
  export type WorkflowTaskContext = AuthoringContract.WorkflowTaskContext;
232
234
  export type WorkflowTaskContextInput = AuthoringContract.WorkflowTaskContextInput;
233
235
  export type WorkflowTaskResult = AuthoringContract.WorkflowTaskResult;
236
+ export type WorkflowStageResult<TSchemaDef extends TSchema | undefined = undefined> = AuthoringContract.WorkflowStageResult<TSchemaDef>;
234
237
 
235
238
  /**
236
239
  * Higher-level task API: create a tracked stage, optionally inject prior task
@@ -276,12 +279,12 @@ export interface WorkflowDirectOptions extends StageOptions, Omit<Mutable<Author
276
279
  * This exposes the supported subset of pi's SDK AgentSession. The workflow
277
280
  * executor owns disposal and wraps prompt() with stage lifecycle tracking.
278
281
  */
279
- export interface StageContext {
282
+ export interface StageContext<TSchemaDef extends TSchema | undefined = undefined> {
280
283
  /** Human-readable name for this stage (used in TUI + persistence). */
281
284
  readonly name: string;
282
285
 
283
286
  /** Send a prompt and wait for completion. */
284
- prompt(text: string, options?: StagePromptOptions): Promise<string>;
287
+ prompt(text: string, options?: StagePromptOptions): Promise<WorkflowStageResult<TSchemaDef>>;
285
288
  complete(text: string, options?: CompleteStageOpts): Promise<string>;
286
289
 
287
290
  /** Queue messages during streaming. */
@@ -344,6 +347,7 @@ export interface WorkflowRunContext<
344
347
  * @param name Human-readable stage name (used in TUI + persistence).
345
348
  * @param options Optional per-stage configuration (mcp allow/deny, etc.).
346
349
  */
350
+ stage<TSchemaDef extends TSchema>(name: string, options: StageOptions<TSchemaDef> & { schema: TSchemaDef }): StageContext<TSchemaDef>;
347
351
  stage(name: string, options?: StageOptions): StageContext;
348
352
  /**
349
353
  * Safe high-level task primitive. Equivalent to creating a named stage and