@bastani/atomic 0.8.27 → 0.8.28-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +2 -2
  4. package/dist/builtin/subagents/package.json +1 -1
  5. package/dist/builtin/web-access/package.json +1 -1
  6. package/dist/builtin/workflows/CHANGELOG.md +14 -0
  7. package/dist/builtin/workflows/README.md +11 -9
  8. package/dist/builtin/workflows/package.json +1 -1
  9. package/dist/builtin/workflows/src/authoring.d.ts +5 -2
  10. package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +3 -1
  11. package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +17 -25
  12. package/dist/builtin/workflows/src/extension/index.ts +133 -18
  13. package/dist/builtin/workflows/src/extension/render-result.ts +22 -2
  14. package/dist/builtin/workflows/src/extension/workflow-schema.ts +3 -3
  15. package/dist/builtin/workflows/src/runs/foreground/executor.ts +210 -16
  16. package/dist/builtin/workflows/src/sdk-surface.ts +1 -1
  17. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +42 -5
  18. package/dist/builtin/workflows/src/shared/store-types.ts +8 -2
  19. package/dist/builtin/workflows/src/shared/store.ts +51 -0
  20. package/dist/builtin/workflows/src/shared/types.ts +14 -4
  21. package/dist/builtin/workflows/src/tui/graph-view.ts +4 -1
  22. package/dist/builtin/workflows/src/tui/prompt-card.ts +6 -0
  23. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +11 -1
  24. package/dist/core/agent-session.d.ts +4 -4
  25. package/dist/core/agent-session.d.ts.map +1 -1
  26. package/dist/core/agent-session.js +147 -31
  27. package/dist/core/agent-session.js.map +1 -1
  28. package/dist/core/auth-guidance.d.ts +10 -1
  29. package/dist/core/auth-guidance.d.ts.map +1 -1
  30. package/dist/core/auth-guidance.js +26 -1
  31. package/dist/core/auth-guidance.js.map +1 -1
  32. package/dist/core/compaction/branch-summarization.d.ts +2 -2
  33. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  34. package/dist/core/compaction/branch-summarization.js +7 -7
  35. package/dist/core/compaction/branch-summarization.js.map +1 -1
  36. package/dist/core/compaction/compaction.d.ts +4 -84
  37. package/dist/core/compaction/compaction.d.ts.map +1 -1
  38. package/dist/core/compaction/compaction.js +3 -479
  39. package/dist/core/compaction/compaction.js.map +1 -1
  40. package/dist/core/compaction/context-compaction.d.ts.map +1 -1
  41. package/dist/core/compaction/context-compaction.js +39 -82
  42. package/dist/core/compaction/context-compaction.js.map +1 -1
  43. package/dist/core/compaction/index.d.ts +1 -1
  44. package/dist/core/compaction/index.d.ts.map +1 -1
  45. package/dist/core/compaction/index.js +1 -1
  46. package/dist/core/compaction/index.js.map +1 -1
  47. package/dist/core/extensions/types.d.ts +10 -8
  48. package/dist/core/extensions/types.d.ts.map +1 -1
  49. package/dist/core/extensions/types.js.map +1 -1
  50. package/dist/core/index.d.ts +1 -1
  51. package/dist/core/index.d.ts.map +1 -1
  52. package/dist/core/index.js.map +1 -1
  53. package/dist/core/messages.d.ts +1 -11
  54. package/dist/core/messages.d.ts.map +1 -1
  55. package/dist/core/messages.js +10 -25
  56. package/dist/core/messages.js.map +1 -1
  57. package/dist/core/session-manager.d.ts +5 -8
  58. package/dist/core/session-manager.d.ts.map +1 -1
  59. package/dist/core/session-manager.js +12 -76
  60. package/dist/core/session-manager.js.map +1 -1
  61. package/dist/core/settings-manager.d.ts +0 -3
  62. package/dist/core/settings-manager.d.ts.map +1 -1
  63. package/dist/core/settings-manager.js +0 -4
  64. package/dist/core/settings-manager.js.map +1 -1
  65. package/dist/index.d.ts +3 -3
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +3 -3
  68. package/dist/index.js.map +1 -1
  69. package/dist/modes/interactive/components/chat-message-renderer.d.ts +1 -5
  70. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
  71. package/dist/modes/interactive/components/chat-message-renderer.js +5 -9
  72. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
  73. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  74. package/dist/modes/interactive/components/chat-session-host.js +0 -3
  75. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  76. package/dist/modes/interactive/components/index.d.ts +0 -1
  77. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  78. package/dist/modes/interactive/components/index.js +0 -1
  79. package/dist/modes/interactive/components/index.js.map +1 -1
  80. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  81. package/dist/modes/interactive/interactive-mode.js +4 -27
  82. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  83. package/dist/modes/rpc/rpc-client.d.ts +1 -1
  84. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  85. package/dist/modes/rpc/rpc-client.js +2 -2
  86. package/dist/modes/rpc/rpc-client.js.map +1 -1
  87. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  88. package/dist/modes/rpc/rpc-mode.js +1 -1
  89. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  90. package/dist/modes/rpc/rpc-types.d.ts +0 -1
  91. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  92. package/dist/modes/rpc/rpc-types.js.map +1 -1
  93. package/docs/compaction.md +210 -181
  94. package/docs/extensions.md +31 -20
  95. package/docs/json.md +3 -4
  96. package/docs/session-format.md +12 -21
  97. package/docs/sessions.md +3 -1
  98. package/docs/settings.md +2 -5
  99. package/docs/workflows.md +11 -9
  100. package/examples/extensions/README.md +1 -1
  101. package/examples/extensions/custom-compaction.ts +43 -106
  102. package/examples/extensions/handoff.ts +6 -44
  103. package/examples/extensions/trigger-compact.ts +5 -4
  104. package/package.json +5 -5
  105. package/dist/modes/interactive/components/compaction-summary-message.d.ts +0 -16
  106. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +0 -1
  107. package/dist/modes/interactive/components/compaction-summary-message.js +0 -43
  108. package/dist/modes/interactive/components/compaction-summary-message.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,12 +2,34 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.8.28-alpha.1] - 2026-06-09
6
+
7
+ ### Changed
8
+
9
+ - Changed Atomic compaction to be verbatim-only across manual `/compact`, automatic threshold/overflow compaction, SDK/RPC compaction, and extension-triggered compaction. All compaction now records validated `context_compaction` deletion targets and rebuilds active context with retained transcript content verbatim and unchanged. Retained file paths, exact commands, error strings, and line numbers are never paraphrased or rewritten.
10
+ - Changed compaction extension hooks (`session_before_compact`, `session_compact`) to receive verbatim context-compaction preparations/results and allow cancellation or locally validated deletion requests instead of custom generated summaries. The before-compact hook now yields `ContextCompactionPreparation` and accepts `{ cancel: true }` or `{ deletionRequest }` returns; the after-compact hook now receives `ContextCompactionResult` and `contextCompactionEntry`.
11
+ - Changed the verbatim compaction critical-overflow recovery prompt to evict in an explicit priority order when context still exceeds the token budget after compaction: removable reasoning traces are evicted first, then removable user/custom/summary context. Existing safety/retention rules (recent entries, unresolved errors, failed commands, and at least one task-bearing entry) are preserved ([#1308](https://github.com/bastani-inc/atomic/issues/1308)).
12
+
13
+ ### Fixed
14
+
15
+ - Fixed `AgentSession.prompt` surfacing the confusing `No API key found for undefined` error when a model never resolved to a real provider (for example an unknown/unresolved model id reaching the prompt path as a bare string). The prompt path now fails fast with a clear `Unknown model: "<id>" did not resolve to an available provider` message, and `No API key found` guidance no longer renders a literal `undefined` provider.
16
+
17
+ ### Removed
18
+
19
+ - Removed the legacy summary-compaction runtime path, summary prompts, `CompactionEntry` active-context injection, `CompactionSummaryMessage` active message type, custom compaction instructions (`CompactOptions.customInstructions`, RPC `compact.customInstructions`, `/compact [instructions]`), `compaction.keepRecentTokens` setting, summary-compaction public exports (`CompactionResult`, `CompactionPreparation`, `appendCompaction()`, `prepareCompaction()`, `generateSummary()`, summary `compact()`), and summary-compaction docs and examples. Historical `type:"compaction"` JSONL lines on disk are inert and are not injected into active LLM context.
20
+
5
21
  ## [0.8.27] - 2026-06-08
6
22
 
7
23
  ### Fixed
8
24
 
9
25
  - Fixed `/compact` and auto-compaction regressions by removing the native `better-sqlite3` dependency from transcript-bound deletion tools and preserving the currently selected reasoning level for the compaction planner ([#1310](https://github.com/bastani-inc/atomic/issues/1310)).
10
26
 
27
+ ## [0.8.27-alpha.1] - 2026-06-08
28
+
29
+ ### Fixed
30
+
31
+ - Fixed `/compact` and auto-compaction regressions by removing the native `better-sqlite3` dependency from transcript-bound deletion tools and preserving the currently selected reasoning level for the compaction planner ([#1310](https://github.com/bastani-inc/atomic/issues/1310)).
32
+
11
33
  ## [0.8.26] - 2026-06-08
12
34
 
13
35
  ### Added
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.8.27",
3
+ "version": "0.8.28-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.8.27",
3
+ "version": "0.8.28-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
6
6
  "contributors": [
@@ -50,7 +50,7 @@
50
50
  "dependencies": {
51
51
  "@modelcontextprotocol/ext-apps": "^1.7.2",
52
52
  "@modelcontextprotocol/sdk": "^1.25.1",
53
- "open": "^10.2.0",
53
+ "open": "^11.0.0",
54
54
  "typebox": "^1.1.24",
55
55
  "zod": "^3.25.0 || ^4.0.0"
56
56
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.8.27",
3
+ "version": "0.8.28-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.8.27",
3
+ "version": "0.8.28-alpha.1",
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": [
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.28-alpha.1] - 2026-06-09
10
+
11
+ ### Added
12
+
13
+ - 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, keep labels display-only/outside replay identity, honor prompt/run abort signals, and reject clearly in headless/unavailable UI modes. Iteration 1 supports inline graph rendering; `overlay: true` and non-TUI `workflow send` answers for arbitrary custom widget results return clear unsupported errors rather than silently degrading ([#1309](https://github.com/bastani-inc/atomic/issues/1309)).
14
+
15
+ ### Changed
16
+
17
+ - Changed workflow transcript introspection to return `sessionFile`/`transcriptPath` metadata with a lazy-read prompt by default when a transcript path exists, while keeping bounded inline previews behind explicit `tail`/`limit` requests and falling back to a small preview when no path is available ([#1314](https://github.com/bastani-inc/atomic/issues/1314)).
18
+
19
+ ### Fixed
20
+
21
+ - Fixed a workflow kill/abort race that could crash the entire CLI with a process-level uncaught exception (for example `No API key found for ...`). When a workflow was killed mid-prompt, the executor's `raceAbort` left the already-in-flight stage prompt promise unobserved; its later rejection escaped every workflow error boundary and became an unhandled rejection. `raceAbort` now always observes the in-flight promise in the already-aborted branch so a killed run can no longer orphan a rejecting prompt.
22
+
9
23
  ## [0.8.27] - 2026-06-08
10
24
 
11
25
  ### Changed
@@ -139,7 +139,9 @@ export default defineWorkflow("review-and-merge")
139
139
  .compile();
140
140
  ```
141
141
 
142
- Human input is runtime-only: call `ctx.ui.input`, `ctx.ui.confirm`, `ctx.ui.select`, or `ctx.ui.editor` at the point where the workflow actually needs a decision. No builder-level declaration is required or supported.
142
+ Human input is runtime-only: call `ctx.ui.input`, `ctx.ui.confirm`, `ctx.ui.select`, `ctx.ui.editor`, or `ctx.ui.custom<T>` at the point where the workflow actually needs a decision. No builder-level declaration is required or supported.
143
+
144
+ `ctx.ui.custom<T>(factory, options?)` mounts an arbitrary focused TUI component in the attached workflow graph/stage UI and resolves with the value passed to `done(value)`. The factory uses the same real TUI/theme/keybinding/component types as Atomic extension `ctx.ui.custom`. Use `options.label` for a safe display-only graph/status label and `options.replayIdentity` (do not include secrets) when the widget's semantics can change without the callsite changing; label text is not part of replay identity. Custom widget prompts require an interactive workflow graph; they are not answerable through non-TUI `workflow send` in iteration 1. Inline graph rendering is supported; `overlay: true` is rejected clearly because nested workflow graph overlays are not safely supported yet.
143
145
 
144
146
  ### Example 4 — Compose workflows
145
147
 
@@ -458,11 +460,11 @@ Tradeoff: `Type.Unsafe<T>()` does not deeply validate at runtime — it trusts t
458
460
 
459
461
  Input overrides are bare `key=value` tokens (no leading `--`). Values are JSON-parsed when possible, so numbers, booleans, and quoted strings work as expected (e.g. `count=3`, `flag=true`, `prompt="multi word value"`). A whole-object override can be passed as a single JSON token (e.g. `{"prompt":"...","count":3}`). Runtime validation is strict: unknown input keys, missing required values, type mismatches, and invalid `select` choices fail before a named workflow run starts.
460
462
 
461
- Workflows always run as **background tasks** in interactive sessions — the chat editor stays free while a run executes. Press **F2** (or `/workflow connect <run-id>`) to attach to the live graph viewer; HIL prompts (`ctx.ui.input/confirm/select/editor`) appear as awaiting-input graph nodes. Press Enter on the node to answer locally, never as a modal dialog over the chat. Human input is detected when those runtime `ctx.ui.*` calls execute; workflows no longer have a declaration-time HIL flag.
463
+ Workflows always run as **background tasks** in interactive sessions — the chat editor stays free while a run executes. Press **F2** (or `/workflow connect <run-id>`) to attach to the live graph viewer; HIL prompts (`ctx.ui.input/confirm/select/editor/custom`) appear as awaiting-input graph nodes. Press Enter on the node to answer locally, never as a modal dialog over the chat. Human input is detected when those runtime `ctx.ui.*` calls execute; workflows no longer have a declaration-time HIL flag.
462
464
 
463
465
  Nested `ctx.workflow(...)` calls are displayed as an expanded graph within the top-level run. `/workflow status` and run pickers list only top-level user-launched workflows, not implementation-owned child runs. The `workflow` tool's `stages`, `stage`, `transcript`, `send`, `pause`, `interrupt`, and `resume` actions can still target visible child stage ids, prefixes, or names from the expanded graph; Atomic routes the control action to the owning nested run internally. (`stages`, `stage`, `transcript`, and `send` are `workflow` tool actions, not `/workflow` slash subcommands; the slash command exposes `connect`, `attach`, `pause`, `list`, `status`, `interrupt`, `kill`, `resume`, `reload`, and `inputs`.)
464
466
 
465
- Prompt answer replay is live-memory only. `StageSnapshot.promptAnswerState` reports whether continuation can replay a prompt answer (`available`), must ask again because the private ledger entry is gone (`unavailable`), or must ask again because multiple matching prompt nodes are ambiguous (`ambiguous`). Raw answers stay in a private `PromptAnswerRecord` ledger, are never serialized to snapshots or persistence, and remain resident in memory until the answer is cleared, the run is removed, or the store is cleared. Replay keys include prompt kind, message text, select choices, input/editor initial value, and hashed author callsite, so changing any of those inputs may intentionally re-ask on continuation. Empty `ctx.ui.select(..., [])` calls throw before creating a prompt node.
467
+ Prompt answer replay is live-memory only. `StageSnapshot.promptAnswerState` reports whether continuation can replay a prompt answer (`available`), must ask again because the private ledger entry is gone (`unavailable`), or must ask again because multiple matching prompt nodes are ambiguous (`ambiguous`). Raw answers stay in a private `PromptAnswerRecord` ledger, are never serialized to snapshots or persistence, and remain resident in memory until the answer is cleared, the run is removed, or the store is cleared. Replay keys include prompt kind, message text, select choices, input/editor initial value, custom prompt identity hash, and hashed author callsite, so changing any of those inputs may intentionally re-ask on continuation. Empty `ctx.ui.select(..., [])` calls throw before creating a prompt node. Arbitrary custom-widget answers cannot be supplied with `workflow send`; focus the `custom` awaiting-input node in the interactive graph instead.
466
468
 
467
469
  ### `workflow` tool (LLM-callable)
468
470
 
@@ -471,7 +473,7 @@ Prompt answer replay is live-memory only. `StageSnapshot.promptAnswerState` repo
471
473
  ```json
472
474
  {
473
475
  "name": "workflow",
474
- "description": "Run named workflows or direct one-off task/tasks/chain workflows; discover with list/get/inputs, inspect status/stages/stage details, send prompt answers or steering, pause/resume/interrupt/kill runs, and reload workflow resources. For large stage handoffs, write context to files/artifacts, pass paths via reads, and prompt downstream agents to 'Read the file at <path>...' instead of injecting large previous text. For transcripts, prefer status/stages/stage to get sessionFile/transcriptPath, quote the exact path without rewriting separators (Windows backslashes are valid), search it with rg/grep, and read small ranges; transcript defaults to at most 5 recent entries and explicit tail/limit overrides that preview.",
476
+ "description": "Run named workflows or direct one-off task/tasks/chain workflows; discover with list/get/inputs, inspect status/stages/stage details, send prompt answers or steering, pause/resume/interrupt/kill runs, and reload workflow resources. For large stage handoffs, write context to files/artifacts, pass paths via reads, and prompt downstream agents to 'Read the file at <path>...' instead of injecting large previous text. For transcripts, prefer status/stages/stage to get sessionFile/transcriptPath, quote the exact path without rewriting separators (Windows backslashes are valid), then search it with rg/grep and read small ranges; transcript is path-only by default when sessionFile/transcriptPath exists, explicit tail/limit returns bounded previews, and missing transcript paths fall back to a small preview.",
475
477
  "parameters": {
476
478
  "workflow": "string (optional) — workflow ID or normalized name",
477
479
  "inputs": "object (optional) — key/value map of workflow inputs",
@@ -480,9 +482,9 @@ Prompt answer replay is live-memory only. `StageSnapshot.promptAnswerState` repo
480
482
  "stageId": "optional stage id, prefix, or name for stage-scoped actions; cannot be combined with all:true",
481
483
  "statusFilter": "optional stages filter: pending/running/awaiting_input/paused/blocked/completed/failed/skipped/all",
482
484
  "format": "optional agent-facing output format: text or json",
483
- "limit": "transcript-only explicit maximum number of recent entries; omitted with tail omitted uses the default 5-entry preview plus metadata/path",
485
+ "limit": "transcript-only explicit maximum number of recent entries; omitted with tail omitted uses the path-only default when sessionFile/transcriptPath exists",
484
486
  "tail": "transcript-only explicit last-N entry count; overrides limit for quick recent-context checks",
485
- "includeToolOutput": "transcript-only flag for explicit snapshot tool-event output; prefer rg/grep on the exact quoted sessionFile/transcriptPath for large outputs",
487
+ "includeToolOutput": "transcript-only flag for inlined snapshot preview/fallback tool-event output; does not bypass the path-only default; prefer rg/grep on the exact quoted sessionFile/transcriptPath for large outputs",
486
488
  "text": "optional string payload for send/resume; explicit empty text answers pending prompts",
487
489
  "response": "optional structured payload for answering pending prompts; explicit empty response is valid",
488
490
  "message": "optional string payload for send/resume when text is not provided",
@@ -506,8 +508,8 @@ Prompt answer replay is live-memory only. `StageSnapshot.promptAnswerState` repo
506
508
 
507
509
  - **`renderCall`** — renders a compact workflow call summary in the chat scroll.
508
510
  - **`renderResult`** — renders the result or dispatch banner; live progress continues through the widget and graph viewer. Named workflow runs are background-oriented.
509
- - **`transcript`** — reference-first with a small preview by default: use `status`, `stages`, or `stage` to identify the stage and its `sessionFile`/`transcriptPath`, quote the exact path without changing platform separators (for example, preserve Windows backslashes), then search that file with `rg`/`grep` for targeted terms and read only small surrounding ranges. Text results include JSON-escaped `sessionFileJson`/`transcriptPathJson` lines for copy-safe path literals plus up to 5 recent entries by default. Passing explicit `tail` or `limit` overrides that preview for quick context checks. A registered live stage handle is used when one exists, even before live messages arrive; otherwise the action falls back to stored stage snapshots. Snapshot entries are ordered chronologically before `tail`/`limit` is applied, with terminal result/error entries kept after tool entries when timestamps are missing or tied. `includeToolOutput` applies to snapshot tool-event results; live session transcripts may not expose tool output.
510
- - **`send`** — answers pending stage prompts only when `text`, `response`, or `message` is present; an explicit empty string is a valid answer, while an omitted payload is a no-op. `delivery: "auto"` answers pending prompts first, then resumes paused stages, steers streaming stages, or queues a follow-up.
511
+ - **`transcript`** — path-only by default when a transcript file exists: use `status`, `stages`, or `stage` to identify the stage and its `sessionFile`/`transcriptPath`, quote the exact path without changing platform separators (for example, preserve Windows backslashes), then search that file with `rg`/`grep` for targeted terms and read only small surrounding ranges. Default text results include JSON-escaped `sessionFileJson`/`transcriptPathJson` lines for copy-safe path literals plus a `lazyReadPrompt`, with `entries: not inlined` so transcript bodies and tool outputs stay out of model context. Passing explicit `tail` or `limit` opts into a bounded inline preview for quick context checks. If no transcript path is available, the action falls back to a bounded preview of up to 5 recent entries with a `fallbackNote`. A registered live stage handle is used when one exists, even before live messages arrive; otherwise the action falls back to stored stage snapshots. Snapshot entries are ordered chronologically before `tail`/`limit` is applied, with terminal result/error entries kept after tool entries when timestamps are missing or tied. `includeToolOutput` applies only to inlined snapshot previews or no-path fallback previews; live session transcripts may not expose tool output.
512
+ - **`send`** — answers pending primitive/structured stage prompts only when `text`, `response`, or `message` is present; an explicit empty string is a valid answer, while an omitted payload is a no-op. Arbitrary `ctx.ui.custom<T>` widget prompts require the interactive workflow graph and return a clear unsupported message when targeted through `send`. `delivery: "auto"` answers pending prompts first, then resumes paused stages, steers streaming stages, or queues a follow-up.
511
513
  - **`reload`** — refreshes workflow resources directly in-process instead of queuing a literal `/workflow reload` chat follow-up.
512
514
 
513
515
  ### F2 keyboard shortcut
@@ -520,7 +522,7 @@ Press **F2** while a workflow is running to open the DAG overlay for the active
520
522
 
521
523
  For interactive use, run workflows through `/workflow <name> [key=value ...]` or let the LLM call the `workflow` tool. In non-interactive (`-p` / `--print` / `--mode json`) sessions, `/workflow <name> key=value` and LLM calls to the `workflow` tool remain available for deterministic workflows. The input picker and graph picker are disabled, top-level `ctx.ui.*` is unavailable, and stage child sessions exclude `ask_user_question`. Named workflow dispatch waits for the terminal run snapshot before returning.
522
524
 
523
- Because human input is runtime-only and workflows no longer carry a declaration-time HIL marker, headless dispatch does not reject a workflow just because its source contains `ctx.ui.*`. If you copy the HIL example above into a non-interactive session, it can pass dispatch and then fail when execution reaches the prompt with an error such as `atomic-workflows: HIL ctx.ui.confirm is unavailable because Atomic runtime did not provide a UI adapter` (the primitive name varies). Run those workflows interactively, or guard/remove runtime `ctx.ui.*` calls before using headless mode.
525
+ Because human input is runtime-only and workflows no longer carry a declaration-time HIL marker, headless dispatch does not reject a workflow just because its source contains `ctx.ui.*`. If you copy the HIL example above into a non-interactive session, it can pass dispatch and then fail when execution reaches the prompt with an error such as `atomic-workflows: HIL ctx.ui.confirm is unavailable because Atomic runtime did not provide a UI adapter` (the primitive name varies, including `ctx.ui.custom`). Run those workflows interactively, or guard/remove runtime `ctx.ui.*` calls before using headless mode.
524
526
 
525
527
  For library or package authoring, define reusable workflows with the builder and export the compiled definition. Hand-written objects with `__piWorkflow: true` are rejected by discovery and composition; `defineWorkflow(...).compile()` is the public authoring surface. Standalone TypeScript workflow packages can import `defineWorkflow` and `Type` from `@bastani/workflows` directly with no local `.d.ts` file or `declare module` shim. The former imperative `runWorkflow` object-form API is removed; use compiled workflow definitions with the exported `run()` / registry helpers for programmatic execution.
526
528
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.8.27",
3
+ "version": "0.8.28-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -57,7 +57,7 @@ export declare const Type: Omit<typeof TypeboxType, "Any" | "Array" | "BigInt" |
57
57
  Void(): TVoid;
58
58
  };
59
59
  export type { Static, TSchema } from "typebox";
60
- export type { AgentSessionAdapter, CompleteAdapter, CompleteStageOpts, GitWorktreeSetupOptions, GitWorktreeSetupResult, PromptAdapter, PromptOptions, ResolvedInputs, RunResult, RunStatus, StageAdapters, StageStatus, StageOptions, StageContext, StageSnapshot, StageExecutionMeta, StageMcpOptions, StageOutputOptions, StagePromptOptions, StageSessionCreateOptions, StageSessionCreateResult, StageSessionRuntime, WorkflowAction, WorkflowArtifact, WorkflowChainOptions, WorkflowChainStep, WorkflowChildResult, WorkflowContextMode, WorkflowControlEvent, WorkflowCustomToolDefinition, WorkflowDetails, WorkflowDetailsMode, WorkflowDetailsStatus, WorkflowDirectOptions, WorkflowDirectTaskItem, WorkflowExecutionMode, WorkflowExecutionPolicy, WorkflowInputBindings, WorkflowInputSchema, WorkflowInputSchemaMap, WorkflowInputValues, WorkflowIntercomSummary, WorkflowMaxOutput, WorkflowMcpPort, WorkflowModelAttempt, WorkflowModelCatalogPort, WorkflowModelFallbackFields, WorkflowModelInfo, WorkflowModelUsage, WorkflowModelValue, WorkflowOutputMode, WorkflowOutputSchema, WorkflowOutputSchemaMap, WorkflowOutputValues, WorkflowParallelChainStep, WorkflowParallelOptions, WorkflowPersistencePort, WorkflowProgressSummary, WorkflowRunChildOptions, WorkflowRunOutput, WorkflowRuntimeConfig, WorkflowSerializableObject, WorkflowSerializablePrimitive, WorkflowSerializableValue, WorkflowSharedTaskDefaults, WorkflowTaskContext, WorkflowTaskContextInput, WorkflowTaskOptions, WorkflowTaskResult, WorkflowTaskSessionFields, WorkflowTaskSessionOptions, WorkflowTaskStep, WorkflowThinkingLevel, WorkflowUIAdapter, WorkflowUIContext, WorkflowWorktreeInputBinding, } from "./shared/authoring-contract.js";
60
+ export type { AgentSessionAdapter, CompleteAdapter, CompleteStageOpts, GitWorktreeSetupOptions, GitWorktreeSetupResult, PromptAdapter, PromptOptions, ResolvedInputs, RunResult, RunStatus, StageAdapters, StageStatus, StageOptions, StageContext, StageSnapshot, StageExecutionMeta, StageMcpOptions, StageOutputOptions, StagePromptOptions, StageSessionCreateOptions, StageSessionCreateResult, StageSessionRuntime, WorkflowAction, WorkflowArtifact, WorkflowChainOptions, WorkflowChainStep, WorkflowChildResult, WorkflowContextMode, WorkflowControlEvent, WorkflowCustomToolDefinition, WorkflowCustomUiComponent, WorkflowCustomUiFactory, WorkflowCustomUiKeybindings, WorkflowCustomUiOptions, WorkflowCustomUiOverlayHandle, WorkflowCustomUiOverlayOptions, WorkflowCustomUiTheme, WorkflowCustomUiTui, WorkflowDetails, WorkflowDetailsMode, WorkflowDetailsStatus, WorkflowDirectOptions, WorkflowDirectTaskItem, WorkflowExecutionMode, WorkflowExecutionPolicy, WorkflowInputBindings, WorkflowInputSchema, WorkflowInputSchemaMap, WorkflowInputValues, WorkflowIntercomSummary, WorkflowMaxOutput, WorkflowMcpPort, WorkflowModelAttempt, WorkflowModelCatalogPort, WorkflowModelFallbackFields, WorkflowModelInfo, WorkflowModelUsage, WorkflowModelValue, WorkflowOutputMode, WorkflowOutputSchema, WorkflowOutputSchemaMap, WorkflowOutputValues, WorkflowParallelChainStep, WorkflowParallelOptions, WorkflowPersistencePort, WorkflowProgressSummary, WorkflowRunChildOptions, WorkflowRunOutput, WorkflowRuntimeConfig, WorkflowSerializableObject, WorkflowSerializablePrimitive, WorkflowSerializableValue, WorkflowSharedTaskDefaults, WorkflowTaskContext, WorkflowTaskContextInput, WorkflowTaskOptions, WorkflowTaskResult, WorkflowTaskSessionFields, WorkflowTaskSessionOptions, WorkflowTaskStep, WorkflowThinkingLevel, WorkflowUIAdapter, WorkflowUIContext, WorkflowWorktreeInputBinding, } from "./shared/authoring-contract.js";
61
61
  import type * as AuthoringContract from "./shared/authoring-contract.js";
62
62
  import type { GitWorktreeSetupOptions, GitWorktreeSetupResult, ResolvedInputs, RunResult, RunStatus, StageSnapshot, WorkflowDefinition as WorkflowContractDefinition, WorkflowDetails, WorkflowDirectOptions, WorkflowDirectTaskItem, WorkflowExecutionPolicy, WorkflowInputValues, WorkflowOutputValues, WorkflowSerializableObject, WorkflowChainStep } from "./shared/authoring-contract.js";
63
63
  declare const workflowDefinitionBrand: unique symbol;
@@ -118,13 +118,16 @@ export interface StageNode extends WorkflowSerializableObject {
118
118
  readonly parentIds: readonly string[];
119
119
  }
120
120
  export type NoticeLevel = "info" | "warning" | "error";
121
- export type PromptKind = "input" | "confirm" | "select" | "editor";
121
+ export type PromptKind = "input" | "confirm" | "select" | "editor" | "custom";
122
+ export type CustomPromptIdentitySource = "caller" | "factory" | "callsite";
122
123
  export interface PendingPrompt extends WorkflowSerializableObject {
123
124
  readonly id: string;
124
125
  readonly kind: PromptKind;
125
126
  readonly message: string;
126
127
  readonly choices?: readonly string[];
127
128
  readonly initial?: string;
129
+ readonly customIdentityHash?: string;
130
+ readonly customIdentitySource?: CustomPromptIdentitySource;
128
131
  readonly createdAt: number;
129
132
  }
130
133
  export interface ToolEvent {
@@ -36,8 +36,10 @@ import type {
36
36
  } from "../shared/store-types.js";
37
37
  import type { WorkflowUIAdapter } from "../shared/types.js";
38
38
 
39
+ type BackgroundPromptKind = Exclude<PromptKind, "custom">;
40
+
39
41
  interface PromptDescriptor {
40
- readonly kind: PromptKind;
42
+ readonly kind: BackgroundPromptKind;
41
43
  readonly message: string;
42
44
  readonly choices?: readonly string[];
43
45
  readonly initial?: string;
@@ -76,7 +76,6 @@ export function installWorkflowHilAnswerNotifications(
76
76
  if (typeof send !== "function") return () => undefined;
77
77
 
78
78
  const state = options.state ?? createWorkflowHilAnswerNotificationState();
79
- let previousSnapshot = options.store.snapshot();
80
79
 
81
80
  const emitOnce = (details: WorkflowHilAnswerNoticeDetails): void => {
82
81
  const key = answerNoticeKey(details.runId, details.stageId, details.promptId, details.promptKind);
@@ -86,23 +85,21 @@ export function installWorkflowHilAnswerNotifications(
86
85
  sendHilAnswerNotice(send, details);
87
86
  };
88
87
 
89
- const inspectSimplePromptAnswers = (snapshot: StoreSnapshot): void => {
90
- for (const previousRun of previousSnapshot.runs) {
91
- const currentRun = snapshot.runs.find((run) => run.id === previousRun.id);
92
- if (currentRun === undefined) continue;
93
-
94
- for (const previousStage of previousRun.stages) {
95
- const answeredPrompt = simplePromptAnswer(previousStage, currentRun);
96
- if (answeredPrompt === undefined) continue;
97
- const answerRecord = options.store.getStagePromptAnswer(currentRun.id, answeredPrompt.stage.id);
98
- if (answerRecord?.answerSource === "workflow_tool") continue;
99
- emitOnce(makeSimplePromptAnswerNotice(currentRun, answeredPrompt.stage, answeredPrompt.prompt, answerRecord?.value));
88
+ const inspectWorkflowPromptAnswers = (snapshot: StoreSnapshot): void => {
89
+ for (const currentRun of snapshot.runs) {
90
+ for (const currentStage of currentRun.stages) {
91
+ const prompt = workflowPromptAnswerCandidate(currentStage);
92
+ if (prompt === undefined) continue;
93
+ const answerRecord = options.store.getStagePromptAnswer(currentRun.id, currentStage.id);
94
+ if (answerRecord === undefined) continue;
95
+ if (answerRecord.promptId !== prompt.id) continue;
96
+ if (answerRecord.answerSource === "workflow_tool") continue;
97
+ emitOnce(makeSimplePromptAnswerNotice(currentRun, currentStage, prompt, answerRecord.value, answerRecord.answeredAt));
100
98
  }
101
99
  }
102
- previousSnapshot = snapshot;
103
100
  };
104
101
 
105
- const unsubscribeStore = options.store.subscribe(inspectSimplePromptAnswers);
102
+ const unsubscribeStore = options.store.subscribe(inspectWorkflowPromptAnswers);
106
103
  const unsubscribeBroker = options.stageUiBroker?.onStagePromptResolved((event) => {
107
104
  if (event.answerSource === "workflow_tool") return;
108
105
  const answeredStage = findStageSnapshot(options.store.snapshot(), event.runId, event.stageId);
@@ -180,17 +177,11 @@ function sendHilAnswerNotice(
180
177
  }
181
178
  }
182
179
 
183
- function simplePromptAnswer(
184
- previousStage: StageSnapshot,
185
- currentRun: RunSnapshot,
186
- ): { stage: StageSnapshot; prompt: PendingPrompt } | undefined {
187
- const prompt = previousStage.pendingPrompt;
180
+ function workflowPromptAnswerCandidate(stage: StageSnapshot): PendingPrompt | undefined {
181
+ const prompt = stage.promptFootprint;
188
182
  if (prompt === undefined) return undefined;
189
- const currentStage = currentRun.stages.find((stage) => stage.id === previousStage.id);
190
- if (currentStage === undefined) return undefined;
191
- if (currentStage.pendingPrompt !== undefined) return undefined;
192
- if (currentStage.promptAnswerState !== "available") return undefined;
193
- return { stage: currentStage, prompt };
183
+ if (stage.promptAnswerState !== "available") return undefined;
184
+ return prompt;
194
185
  }
195
186
 
196
187
  function findStageSnapshot(
@@ -215,6 +206,7 @@ function makeSimplePromptAnswerNotice(
215
206
  stage: StageSnapshot,
216
207
  prompt: PendingPrompt,
217
208
  answer: unknown,
209
+ answeredAt: number,
218
210
  ): WorkflowHilAnswerNoticeDetails {
219
211
  return {
220
212
  kind: "hil_answered",
@@ -226,7 +218,7 @@ function makeSimplePromptAnswerNotice(
226
218
  promptId: prompt.id,
227
219
  promptKind: prompt.kind,
228
220
  promptMessage: truncateAnswerSnippet(prompt.message),
229
- answeredAt: Date.now(),
221
+ answeredAt,
230
222
  answerAvailable: true,
231
223
  answerIncluded: true,
232
224
  answerSummary: formatAnswerSummary(answer),
@@ -129,7 +129,7 @@ export const WORKFLOW_TOOL_DESCRIPTION =
129
129
  "For large stage handoffs, write context to files/artifacts, pass paths via reads, and prompt downstream agents to 'Read the file at <path>...' instead of injecting large previous text. " +
130
130
  "For transcripts, prefer status/stages/stage to get sessionFile/transcriptPath, " +
131
131
  "quote the exact path without rewriting separators (Windows backslashes are valid), " +
132
- "search it with rg/grep, and read small ranges; transcript defaults to at most 5 recent entries and explicit tail/limit overrides that preview.";
132
+ "then search it with rg/grep and read small ranges; transcript is path-only by default when sessionFile/transcriptPath exists, explicit tail/limit returns bounded previews, and missing transcript paths fall back to a small preview.";
133
133
 
134
134
  // ---------------------------------------------------------------------------
135
135
  // Minimal ExtensionAPI structural types
@@ -644,8 +644,10 @@ function renderTranscriptToolContent(
644
644
  if (result.transcriptPath) lines.push(`transcriptPathJson: ${JSON.stringify(result.transcriptPath)}`);
645
645
  if (result.entryCount !== undefined) lines.push(`availableEntries: ${result.entryCount}`);
646
646
  if (result.entryLimit !== undefined) lines.push(`entryLimit: ${result.entryLimit}`);
647
+ if (result.lazyReadPrompt) lines.push(`lazyReadPrompt: ${result.lazyReadPrompt}`);
648
+ if (result.fallbackNote) lines.push(`fallbackNote: ${result.fallbackNote}`);
647
649
  if (result.entries.length === 0) {
648
- lines.push("entries: none");
650
+ lines.push(result.inlineMode === "path_only" || result.lazyReadPrompt ? "entries: not inlined" : "entries: none");
649
651
  return lines.join("\n");
650
652
  }
651
653
  lines.push("entries:");
@@ -702,6 +704,10 @@ function renderStagesToolContent(
702
704
  lines.push("inputRequest:");
703
705
  lines.push(JSON.stringify(stage.inputRequest, null, 2));
704
706
  }
707
+ if (stage.promptFootprint !== undefined) {
708
+ lines.push("promptFootprint:");
709
+ lines.push(JSON.stringify(stage.promptFootprint, null, 2));
710
+ }
705
711
  });
706
712
  return lines.join("\n");
707
713
  }
@@ -800,6 +806,7 @@ type WorkflowStageSummary = {
800
806
  awaitingInputSince?: number;
801
807
  pendingPrompt?: StageSnapshot["pendingPrompt"];
802
808
  inputRequest?: StageSnapshot["inputRequest"];
809
+ promptFootprint?: StageSnapshot["promptFootprint"];
803
810
  };
804
811
 
805
812
  type WorkflowTranscriptEntry = {
@@ -842,6 +849,9 @@ function summarizeStage(stage: StageSnapshot): WorkflowStageSummary {
842
849
  inputRequest: stage.inputRequest === undefined
843
850
  ? undefined
844
851
  : structuredClone(stage.inputRequest),
852
+ promptFootprint: stage.promptFootprint === undefined
853
+ ? undefined
854
+ : structuredClone(stage.promptFootprint),
845
855
  };
846
856
  }
847
857
 
@@ -854,6 +864,12 @@ type TranscriptEntrySelection = {
854
864
  entryLimit?: number;
855
865
  };
856
866
 
867
+ type WorkflowTranscriptResult = Extract<WorkflowToolResult, { action: "transcript" }>;
868
+
869
+ function isTranscriptPreviewExplicit(args: WorkflowToolArgs): boolean {
870
+ return args.tail !== undefined || args.limit !== undefined;
871
+ }
872
+
857
873
  function requestedTranscriptEntryLimit(args: WorkflowToolArgs): number {
858
874
  const raw = args.tail ?? args.limit;
859
875
  if (raw === undefined) return DEFAULT_TRANSCRIPT_LIMIT;
@@ -891,6 +907,80 @@ function selectTranscriptEntries(
891
907
  };
892
908
  }
893
909
 
910
+ function transcriptLazyReadPrompt(path: string): string {
911
+ return `Transcript not inlined to protect context. Read it lazily from ${path} with your file read tools (read small ranges; rg/grep for targeted lookups).`;
912
+ }
913
+
914
+ function transcriptFallbackNote(limit: number): string {
915
+ return `No transcript file path is available for this stage; falling back to a bounded inline preview of up to ${limit} recent ${limit === 1 ? "entry" : "entries"}.`;
916
+ }
917
+
918
+ /**
919
+ * Shape a transcript tool result, keeping the context-safe path-only default
920
+ * the cheap hot path for the large runs #1314 protects against.
921
+ *
922
+ * `buildEntries` is a thunk so the default case (a transcript file path exists
923
+ * and no explicit `tail`/`limit` was requested) never materializes entry bodies
924
+ * just to discard them. Only the caller-provided `entryCount` is needed for the
925
+ * advisory count, which matches what `buildEntries()` would yield. The thunk is
926
+ * invoked solely for the explicit-preview and no-path fallback branches.
927
+ */
928
+ function shapeTranscriptResult(input: {
929
+ runId: string;
930
+ stageId: string;
931
+ source: "live" | "snapshot";
932
+ entryCount: number;
933
+ buildEntries: () => readonly WorkflowTranscriptEntry[];
934
+ args: WorkflowToolArgs;
935
+ sessionId?: string | undefined;
936
+ sessionFile?: string | undefined;
937
+ transcriptPath?: string | undefined;
938
+ }): WorkflowTranscriptResult {
939
+ // `transcriptPath` already falls back to `sessionFile`, so it is the single
940
+ // resolved path the agent should lazily read.
941
+ const transcriptPath = input.transcriptPath ?? input.sessionFile;
942
+ if (transcriptPath !== undefined && !isTranscriptPreviewExplicit(input.args)) {
943
+ const result: WorkflowTranscriptResult = {
944
+ action: "transcript",
945
+ runId: input.runId,
946
+ stageId: input.stageId,
947
+ source: input.source,
948
+ entries: [],
949
+ // `truncated` here means "more transcript exists on disk than was inlined"
950
+ // (everything, since the default inlines nothing), not "an explicit limit
951
+ // clipped results". It only drives the cosmetic "(truncated)" notice
952
+ // suffix; no consumer re-fetches on it.
953
+ truncated: input.entryCount > 0,
954
+ entryCount: input.entryCount,
955
+ entryLimit: 0,
956
+ lazyReadPrompt: transcriptLazyReadPrompt(transcriptPath),
957
+ inlineMode: "path_only",
958
+ };
959
+ if (input.sessionId !== undefined) result.sessionId = input.sessionId;
960
+ if (input.sessionFile !== undefined) result.sessionFile = input.sessionFile;
961
+ result.transcriptPath = transcriptPath;
962
+ return result;
963
+ }
964
+
965
+ const limited = selectTranscriptEntries(input.buildEntries(), input.args);
966
+ const result: WorkflowTranscriptResult = {
967
+ action: "transcript",
968
+ runId: input.runId,
969
+ stageId: input.stageId,
970
+ source: input.source,
971
+ entries: limited.entries,
972
+ truncated: limited.truncated,
973
+ entryCount: limited.entryCount,
974
+ entryLimit: limited.entryLimit,
975
+ inlineMode: transcriptPath === undefined ? "fallback_preview" : "preview",
976
+ };
977
+ if (input.sessionId !== undefined) result.sessionId = input.sessionId;
978
+ if (input.sessionFile !== undefined) result.sessionFile = input.sessionFile;
979
+ if (transcriptPath !== undefined) result.transcriptPath = transcriptPath;
980
+ if (transcriptPath === undefined) result.fallbackNote = transcriptFallbackNote(limited.entryLimit ?? DEFAULT_TRANSCRIPT_LIMIT);
981
+ return result;
982
+ }
983
+
894
984
  function messageText(content: MessageLike["content"]): string | undefined {
895
985
  if (typeof content === "string") return content;
896
986
  if (!Array.isArray(content)) return undefined;
@@ -1463,33 +1553,40 @@ export function makeExecuteWorkflowTool(
1463
1553
  if (liveHandle !== undefined) {
1464
1554
  const sessionFile = liveHandle.sessionFile ?? snapshot?.sessionFile;
1465
1555
  const sessionId = liveHandle.sessionId ?? snapshot?.sessionId;
1466
- const limited = selectTranscriptEntries(
1467
- liveHandle.messages.map((m) => transcriptEntryFromMessage(m as MessageLike)),
1468
- args,
1469
- );
1470
- return {
1471
- action: "transcript",
1556
+ return shapeTranscriptResult({
1472
1557
  runId: stageRunId,
1473
1558
  stageId: stage.stageId,
1474
1559
  source: "live",
1475
- ...limited,
1560
+ entryCount: liveHandle.messages.length,
1561
+ buildEntries: () =>
1562
+ liveHandle.messages.map((m) => transcriptEntryFromMessage(m as MessageLike)),
1563
+ args,
1476
1564
  sessionId,
1477
1565
  sessionFile,
1478
1566
  transcriptPath: sessionFile,
1479
- };
1567
+ });
1480
1568
  }
1481
- const fallback = snapshotTranscriptEntries(snapshot, args.includeToolOutput === true);
1482
- const limited = selectTranscriptEntries(fallback, args);
1483
- return {
1484
- action: "transcript",
1569
+ const snapshotSessionFile = snapshot?.sessionFile;
1570
+ const includeSnapshotOutput = args.includeToolOutput === true && (
1571
+ isTranscriptPreviewExplicit(args) || snapshotSessionFile === undefined
1572
+ );
1573
+ // Cheap count matches `snapshotTranscriptEntries(...).length` (one entry
1574
+ // per tool event plus the optional terminal result/error entries)
1575
+ // without building bodies for the path-only default.
1576
+ const snapshotEntryCount = (snapshot?.toolEvents?.length ?? 0)
1577
+ + (snapshot?.result !== undefined ? 1 : 0)
1578
+ + (snapshot?.error !== undefined ? 1 : 0);
1579
+ return shapeTranscriptResult({
1485
1580
  runId: stageRunId,
1486
1581
  stageId: stage.stageId,
1487
1582
  source: "snapshot",
1488
- ...limited,
1583
+ entryCount: snapshotEntryCount,
1584
+ buildEntries: () => snapshotTranscriptEntries(snapshot, includeSnapshotOutput),
1585
+ args,
1489
1586
  sessionId: snapshot?.sessionId,
1490
- sessionFile: snapshot?.sessionFile,
1491
- transcriptPath: snapshot?.sessionFile,
1492
- };
1587
+ sessionFile: snapshotSessionFile,
1588
+ transcriptPath: snapshotSessionFile,
1589
+ });
1493
1590
  }
1494
1591
 
1495
1592
  case "send": {
@@ -1543,6 +1640,24 @@ export function makeExecuteWorkflowTool(
1543
1640
  ok ? `Answered input request ${brokerPrompt.id}.` : `No matching pending input request ${brokerPrompt.id}.`,
1544
1641
  );
1545
1642
  }
1643
+ const customPrompt = snapshot?.status === "awaiting_input" && snapshot.promptFootprint?.kind === "custom"
1644
+ ? snapshot.promptFootprint
1645
+ : undefined;
1646
+ const targetsCustomPrompt =
1647
+ customPrompt !== undefined &&
1648
+ (args.promptId === undefined || args.promptId === customPrompt.id) &&
1649
+ (requestedDelivery === "answer" ||
1650
+ args.promptId !== undefined ||
1651
+ requestedDelivery === "auto");
1652
+ if (targetsCustomPrompt && customPrompt !== undefined) {
1653
+ return workflowSendResult(
1654
+ stageRunId,
1655
+ stage.stageId,
1656
+ "answer",
1657
+ "noop",
1658
+ `Custom UI prompt ${customPrompt.id} requires the interactive workflow graph; arbitrary ctx.ui.custom<T> results cannot be answered through workflow send.`,
1659
+ );
1660
+ }
1546
1661
  const targetsPrompt =
1547
1662
  requestedDelivery === "answer" ||
1548
1663
  args.promptId !== undefined ||
@@ -110,11 +110,13 @@ type StageListItem = {
110
110
  awaitingInputSince?: number;
111
111
  pendingPrompt?: PendingPrompt;
112
112
  inputRequest?: StageInputRequest;
113
+ promptFootprint?: PendingPrompt;
113
114
  };
114
115
  type StageListResult = { action: "stages"; runId: string; filter: string; stages: StageListItem[]; error?: string };
115
116
  type StageDetailItem = StageSnapshot & { transcriptPath?: string };
116
117
  type StageDetailResult = { action: "stage"; runId: string; stage?: StageDetailItem; error?: string };
117
118
  type TranscriptEntry = { role: string; text?: string; toolName?: string; output?: string; timestamp?: number };
119
+ type TranscriptInlineMode = "path_only" | "preview" | "fallback_preview";
118
120
  type TranscriptResult = {
119
121
  action: "transcript";
120
122
  runId: string;
@@ -127,6 +129,9 @@ type TranscriptResult = {
127
129
  sessionId?: string;
128
130
  sessionFile?: string;
129
131
  transcriptPath?: string;
132
+ lazyReadPrompt?: string;
133
+ fallbackNote?: string;
134
+ inlineMode?: TranscriptInlineMode;
130
135
  };
131
136
  type SendResult = { action: "send"; runId: string; stageId: string; delivery: string; status: "ok" | "noop"; message: string };
132
137
  type PauseResult = { action: "pause"; runId: string; status: string; message: string };
@@ -213,7 +218,7 @@ function renderNotice(
213
218
  const TRANSCRIPT_NOTICE_ENTRY_LIMIT = 5;
214
219
  const TRANSCRIPT_NOTICE_CHAR_LIMIT = 240;
215
220
 
216
- function transcriptNoticeText(entries: readonly TranscriptEntry[]): string {
221
+ function transcriptEntriesNoticeText(entries: readonly TranscriptEntry[]): string {
217
222
  if (entries.length === 0) return "no transcript entries";
218
223
  const shown = entries.slice(0, TRANSCRIPT_NOTICE_ENTRY_LIMIT);
219
224
  const text = shown
@@ -225,6 +230,21 @@ function transcriptNoticeText(entries: readonly TranscriptEntry[]): string {
225
230
  return fitLine(`${text}${entrySuffix}`, TRANSCRIPT_NOTICE_CHAR_LIMIT);
226
231
  }
227
232
 
233
+ function transcriptNoticeText(result: TranscriptResult): string {
234
+ if ((result.inlineMode === "path_only" || result.lazyReadPrompt !== undefined) && result.entries.length === 0) {
235
+ const path = result.transcriptPath ?? result.sessionFile ?? "transcript file";
236
+ const count = result.entryCount === undefined
237
+ ? ""
238
+ : ` (${result.entryCount} ${result.entryCount === 1 ? "entry" : "entries"})`;
239
+ return fitLine(`not inlined; read ${path}${count}`, TRANSCRIPT_NOTICE_CHAR_LIMIT);
240
+ }
241
+ const entriesText = transcriptEntriesNoticeText(result.entries);
242
+ if (result.inlineMode === "fallback_preview" || result.fallbackNote !== undefined) {
243
+ return fitLine(`no session file; preview: ${entriesText}`, TRANSCRIPT_NOTICE_CHAR_LIMIT);
244
+ }
245
+ return entriesText;
246
+ }
247
+
228
248
  export function renderResult(result: WorkflowToolResult, opts?: RenderResultOpts): string {
229
249
  const partial = opts?.isPartial === true;
230
250
  const themed = opts?.plain !== true;
@@ -350,7 +370,7 @@ export function renderResult(result: WorkflowToolResult, opts?: RenderResultOpts
350
370
 
351
371
  case "transcript": {
352
372
  const r = result as TranscriptResult;
353
- const text = transcriptNoticeText(r.entries);
373
+ const text = transcriptNoticeText(r);
354
374
  const suffix = r.truncated ? " (truncated)" : "";
355
375
  return renderNotice("WORKFLOW TRANSCRIPT", `${r.runId}/${r.stageId.slice(0, 12)} ${r.source}: ${text}${suffix}`, opts, themed);
356
376
  }