@bastani/atomic 0.8.21 → 0.8.22
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.
- package/CHANGELOG.md +66 -9
- package/dist/builtin/intercom/broker/broker.ts +3 -3
- package/dist/builtin/intercom/config.ts +3 -3
- package/dist/builtin/intercom/index.ts +1 -1
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/ui/compose.ts +2 -2
- package/dist/builtin/mcp/host-html-template.ts +0 -3
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +20 -4
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -9
- package/dist/builtin/subagents/agents/debugger.md +6 -6
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/builtin/subagents/skills/browser-use/SKILL.md +234 -0
- package/dist/builtin/subagents/skills/browser-use/references/cdp-python.md +76 -0
- package/dist/builtin/subagents/skills/browser-use/references/multi-session.md +92 -0
- package/dist/builtin/subagents/skills/subagent/SKILL.md +4 -4
- package/dist/builtin/subagents/src/agents/skills.ts +19 -1
- package/dist/builtin/subagents/src/extension/index.ts +24 -22
- package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +7 -1
- package/dist/builtin/subagents/src/runs/background/async-execution.ts +23 -7
- package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +98 -3
- package/dist/builtin/subagents/src/runs/background/async-status.ts +3 -1
- package/dist/builtin/subagents/src/runs/background/run-status.ts +1 -1
- package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +3 -0
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +37 -12
- package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +15 -15
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +26 -2
- package/dist/builtin/subagents/src/runs/shared/nested-render.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +7 -0
- package/dist/builtin/subagents/src/runs/shared/pi-args.ts +28 -1
- package/dist/builtin/subagents/src/shared/fast-mode.ts +80 -0
- package/dist/builtin/subagents/src/shared/formatters.ts +4 -2
- package/dist/builtin/subagents/src/shared/types.ts +4 -2
- package/dist/builtin/subagents/src/shared/utils.ts +3 -61
- package/dist/builtin/subagents/src/tui/render.ts +303 -157
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +113 -35
- package/dist/builtin/workflows/README.md +228 -41
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +535 -541
- package/dist/builtin/workflows/builtin/goal.ts +39 -25
- package/dist/builtin/workflows/builtin/open-claude-design.ts +66 -69
- package/dist/builtin/workflows/builtin/ralph.ts +21 -21
- package/dist/builtin/workflows/package.json +6 -5
- package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
- package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +2 -2
- package/dist/builtin/workflows/src/extension/discovery.ts +25 -146
- package/dist/builtin/workflows/src/extension/dispatcher.ts +72 -24
- package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +363 -0
- package/dist/builtin/workflows/src/extension/index.ts +690 -352
- package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +99 -62
- package/dist/builtin/workflows/src/extension/render-call.ts +2 -1
- package/dist/builtin/workflows/src/extension/render-result.ts +9 -3
- package/dist/builtin/workflows/src/extension/renderers.ts +5 -3
- package/dist/builtin/workflows/src/extension/runtime.ts +68 -33
- package/dist/builtin/workflows/src/extension/status-writer.ts +1 -1
- package/dist/builtin/workflows/src/extension/wiring.ts +34 -13
- package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +142 -0
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +4 -4
- package/dist/builtin/workflows/src/index.ts +2 -0
- package/dist/builtin/workflows/src/intercom/result-intercom.ts +1 -1
- package/dist/builtin/workflows/src/runs/background/runner.ts +6 -4
- package/dist/builtin/workflows/src/runs/background/status.ts +45 -21
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +624 -52
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +1 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +80 -24
- package/dist/builtin/workflows/src/runs/shared/validate-inputs.ts +61 -24
- package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +32 -10
- package/dist/builtin/workflows/src/sdk-surface.ts +6 -0
- package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +178 -0
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +92 -12
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +21 -3
- package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +1 -2
- package/dist/builtin/workflows/src/shared/run-visibility.ts +9 -0
- package/dist/builtin/workflows/src/shared/schema-introspection.ts +121 -0
- package/dist/builtin/workflows/src/shared/serializable.ts +132 -0
- package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +91 -9
- package/dist/builtin/workflows/src/shared/store-types.ts +31 -3
- package/dist/builtin/workflows/src/shared/store.ts +58 -14
- package/dist/builtin/workflows/src/shared/types.ts +105 -40
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +129 -13
- package/dist/builtin/workflows/src/tui/chat-surface.ts +6 -1
- package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +3 -2
- package/dist/builtin/workflows/src/tui/graph-canvas.ts +1 -1
- package/dist/builtin/workflows/src/tui/graph-view.ts +91 -65
- package/dist/builtin/workflows/src/tui/inline-form-card.ts +1 -1
- package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +3 -2
- package/dist/builtin/workflows/src/tui/inputs-overlay.ts +3 -2
- package/dist/builtin/workflows/src/tui/inputs-picker.ts +8 -7
- package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
- package/dist/builtin/workflows/src/tui/node-card.ts +34 -8
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -11
- package/dist/builtin/workflows/src/tui/prompt-card.ts +98 -50
- package/dist/builtin/workflows/src/tui/session-list.ts +7 -2
- package/dist/builtin/workflows/src/tui/session-picker.ts +2 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +226 -55
- package/dist/builtin/workflows/src/tui/status-helpers.ts +2 -0
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +37 -158
- package/dist/builtin/workflows/src/tui/toast.ts +2 -2
- package/dist/builtin/workflows/src/tui/widget.ts +53 -12
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +270 -19
- package/dist/builtin/workflows/src/tui/workflow-notice-card.ts +184 -0
- package/dist/builtin/workflows/src/workflows/define-workflow.ts +138 -43
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +45 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +27 -9
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +196 -17
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +2 -2
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/codex-fast-mode.d.ts +36 -0
- package/dist/core/codex-fast-mode.d.ts.map +1 -0
- package/dist/core/codex-fast-mode.js +117 -0
- package/dist/core/codex-fast-mode.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +1 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +1 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/index.d.ts +4 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -0
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +7 -2
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +23 -8
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/reactive-widget.d.ts +58 -0
- package/dist/core/extensions/reactive-widget.d.ts.map +1 -0
- package/dist/core/extensions/reactive-widget.js +182 -0
- package/dist/core/extensions/reactive-widget.js.map +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +26 -12
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/messages.d.ts +1 -1
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +8 -2
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +11 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/resource-loader.d.ts +9 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +49 -21
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +22 -13
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +7 -5
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +5 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +16 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +64 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +7 -4
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.js +2 -2
- package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/chat-input-actions.d.ts.map +1 -1
- package/dist/modes/interactive/chat-input-actions.js.map +1 -1
- package/dist/modes/interactive/components/diff.d.ts.map +1 -1
- package/dist/modes/interactive/components/diff.js +0 -1
- package/dist/modes/interactive/components/diff.js.map +1 -1
- package/dist/modes/interactive/components/fast-mode-selector.d.ts +27 -0
- package/dist/modes/interactive/components/fast-mode-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/fast-mode-selector.js +105 -0
- package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +7 -12
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +132 -30
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +53 -6
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/custom-provider.md +2 -2
- package/docs/development.md +2 -2
- package/docs/docs.json +2 -2
- package/docs/extensions.md +18 -13
- package/docs/providers.md +5 -1
- package/docs/quickstart.md +5 -3
- package/docs/rpc.md +5 -5
- package/docs/sdk.md +12 -12
- package/docs/settings.md +18 -0
- package/docs/themes.md +6 -6
- package/docs/tui.md +20 -18
- package/docs/usage.md +2 -0
- package/docs/workflows.md +403 -39
- package/examples/extensions/qna.ts +2 -2
- package/package.json +4 -4
- package/dist/builtin/subagents/skills/playwright-cli/SKILL.md +0 -392
- package/dist/builtin/subagents/skills/playwright-cli/references/element-attributes.md +0 -23
- package/dist/builtin/subagents/skills/playwright-cli/references/playwright-tests.md +0 -39
- package/dist/builtin/subagents/skills/playwright-cli/references/request-mocking.md +0 -87
- package/dist/builtin/subagents/skills/playwright-cli/references/running-code.md +0 -241
- package/dist/builtin/subagents/skills/playwright-cli/references/session-management.md +0 -225
- package/dist/builtin/subagents/skills/playwright-cli/references/spec-driven-testing.md +0 -305
- package/dist/builtin/subagents/skills/playwright-cli/references/storage-state.md +0 -275
- package/dist/builtin/subagents/skills/playwright-cli/references/test-generation.md +0 -134
- package/dist/builtin/subagents/skills/playwright-cli/references/tracing.md +0 -139
- package/dist/builtin/subagents/skills/playwright-cli/references/video-recording.md +0 -143
|
@@ -6,10 +6,11 @@ import type {
|
|
|
6
6
|
WorkflowToolResult,
|
|
7
7
|
} from "./render-result.js";
|
|
8
8
|
import { renderInputsSchema } from "../shared/render-inputs-schema.js";
|
|
9
|
+
import { deriveInputFields, schemaIsRequired, schemaChoices, schemaFieldKind, schemaDescription } from "../shared/schema-introspection.js";
|
|
9
10
|
import { WorkflowParametersSchema } from "./workflow-schema.js";
|
|
10
11
|
import { renderRunBanner, renderRunSummary } from "./renderers.js";
|
|
11
12
|
import type { RunEndPayload, RunStartPayload } from "./renderers.js";
|
|
12
|
-
import type { StageSnapshot, StageStatus, ToolEvent } from "../shared/store-types.js";
|
|
13
|
+
import type { RunStatus, StageSnapshot, StageStatus, ToolEvent } from "../shared/store-types.js";
|
|
13
14
|
import { store } from "../shared/store.js";
|
|
14
15
|
import { stageUiBroker } from "../shared/stage-ui-broker.js";
|
|
15
16
|
import {
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
interruptRun,
|
|
30
31
|
interruptAllRuns,
|
|
31
32
|
inspectRun,
|
|
33
|
+
type RunDetail,
|
|
32
34
|
} from "../runs/background/status.js";
|
|
33
35
|
import { cancellationRegistry } from "../runs/background/cancellation-registry.js";
|
|
34
36
|
import { stageControlRegistry } from "../runs/foreground/stage-control-registry.js";
|
|
@@ -45,6 +47,7 @@ import type { OverlayPiSurface } from "../tui/overlay-adapter.js";
|
|
|
45
47
|
import type { GraphOverlayPort } from "../tui/overlay-adapter.js";
|
|
46
48
|
import { renderSessionList } from "../tui/session-list.js";
|
|
47
49
|
import { selectRunsForPicker } from "../tui/session-picker.js";
|
|
50
|
+
import { renderRunDetail } from "../tui/run-detail.js";
|
|
48
51
|
|
|
49
52
|
import { openSessionPicker, openKillConfirm } from "../tui/session-overlays.js";
|
|
50
53
|
import {
|
|
@@ -77,8 +80,15 @@ import {
|
|
|
77
80
|
resetWorkflowLifecycleNotificationState,
|
|
78
81
|
seedWorkflowLifecycleNotificationState,
|
|
79
82
|
withWorkflowLifecycleNotificationsSuppressed,
|
|
83
|
+
withWorkflowLifecycleNotificationsSuppressedAsync,
|
|
80
84
|
} from "./lifecycle-notifications.js";
|
|
81
85
|
import type { WorkflowLifecycleNotificationConfig } from "./lifecycle-notifications.js";
|
|
86
|
+
import {
|
|
87
|
+
createWorkflowHilAnswerNotificationState,
|
|
88
|
+
installWorkflowHilAnswerNotifications,
|
|
89
|
+
registerHilAnswerNoticeRenderer,
|
|
90
|
+
resetWorkflowHilAnswerNotificationState,
|
|
91
|
+
} from "./hil-answer-notifications.js";
|
|
82
92
|
import type { ConfigLoadResult } from "./config-loader.js";
|
|
83
93
|
import type {
|
|
84
94
|
WorkflowPersistencePort,
|
|
@@ -91,7 +101,11 @@ import type {
|
|
|
91
101
|
WorkflowModelCatalogPort,
|
|
92
102
|
WorkflowModelInfo,
|
|
93
103
|
StageOptions,
|
|
104
|
+
WorkflowExecutionPolicy,
|
|
105
|
+
WorkflowInputValues,
|
|
106
|
+
WorkflowSerializableValue,
|
|
94
107
|
} from "../shared/types.js";
|
|
108
|
+
import { INTERACTIVE_WORKFLOW_POLICY, NON_INTERACTIVE_WORKFLOW_POLICY } from "../shared/types.js";
|
|
95
109
|
import { buildRuntimeAdapters } from "./wiring.js";
|
|
96
110
|
import type { PiUISurface } from "./wiring.js";
|
|
97
111
|
import { createStatusWriter } from "./status-writer.js";
|
|
@@ -99,8 +113,22 @@ import type { StatusWriter } from "./status-writer.js";
|
|
|
99
113
|
import { setMcpScope, clearMcpScope } from "./mcp.js";
|
|
100
114
|
import type { PiMcpExtensionAPI, PiEventBus } from "./mcp.js";
|
|
101
115
|
import type { StageSessionRuntime } from "../runs/foreground/stage-runner.js";
|
|
116
|
+
import {
|
|
117
|
+
expandWorkflowGraph,
|
|
118
|
+
expandedStageLabel,
|
|
119
|
+
stageMatchesExpandedIdentifier,
|
|
120
|
+
} from "../shared/expanded-workflow-graph.js";
|
|
121
|
+
import { topLevelWorkflowRuns } from "../shared/run-visibility.js";
|
|
102
122
|
import { WORKFLOW_STAGE_SUBAGENT_GUARD_ENV, getEnvValue, type CreateAgentSessionOptions } from "@bastani/atomic";
|
|
103
123
|
|
|
124
|
+
export const WORKFLOW_TOOL_DESCRIPTION =
|
|
125
|
+
"Run named workflows or direct one-off task/tasks/chain workflows; " +
|
|
126
|
+
"discover with list/get/inputs, inspect status/stages/stage details, " +
|
|
127
|
+
"send prompt answers or steering, pause/resume/interrupt/kill runs, and reload workflow resources. " +
|
|
128
|
+
"For transcripts, prefer status/stages/stage to get sessionFile/transcriptPath, " +
|
|
129
|
+
"quote the exact path without rewriting separators (Windows backslashes are valid), " +
|
|
130
|
+
"search it with rg/grep, and read small ranges; transcript defaults to at most 5 recent entries and explicit tail/limit overrides that preview.";
|
|
131
|
+
|
|
104
132
|
// ---------------------------------------------------------------------------
|
|
105
133
|
// Minimal ExtensionAPI structural types
|
|
106
134
|
// No `any`; all optional fields use explicit union with undefined.
|
|
@@ -133,12 +161,17 @@ export interface PiMessageRenderComponent {
|
|
|
133
161
|
invalidate?: () => void;
|
|
134
162
|
}
|
|
135
163
|
|
|
136
|
-
export
|
|
137
|
-
|
|
138
|
-
function textRenderComponent(text: string): PiRenderComponent {
|
|
139
|
-
return dynamicTextRenderComponent(() => text);
|
|
164
|
+
export interface PiMessageRenderOptions {
|
|
165
|
+
expanded: boolean;
|
|
140
166
|
}
|
|
141
167
|
|
|
168
|
+
export type PiMessageRendererResult = string | PiMessageRenderComponent | undefined;
|
|
169
|
+
export type PiMessageRenderer = (
|
|
170
|
+
payload: unknown,
|
|
171
|
+
options?: PiMessageRenderOptions,
|
|
172
|
+
theme?: unknown,
|
|
173
|
+
) => PiMessageRendererResult;
|
|
174
|
+
|
|
142
175
|
function dynamicTextRenderComponent(renderText: (width: number) => string): PiRenderComponent {
|
|
143
176
|
return {
|
|
144
177
|
render(width: number): string[] {
|
|
@@ -300,7 +333,7 @@ export interface ExtensionAPI {
|
|
|
300
333
|
registerCommand?: (name: string, options: PiCommandOptions) => void;
|
|
301
334
|
registerMessageRenderer?: (
|
|
302
335
|
event: string,
|
|
303
|
-
renderer:
|
|
336
|
+
renderer: PiMessageRenderer,
|
|
304
337
|
) => void;
|
|
305
338
|
/**
|
|
306
339
|
* Inject a custom message into chat history. Used by inline workflow surfaces
|
|
@@ -316,12 +349,16 @@ export interface ExtensionAPI {
|
|
|
316
349
|
},
|
|
317
350
|
options?: {
|
|
318
351
|
triggerTurn?: boolean;
|
|
319
|
-
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
352
|
+
deliverAs?: "steer" | "followUp" | "nextTurn" | "interrupt";
|
|
353
|
+
excludeFromContext?: boolean;
|
|
354
|
+
interruptAbortMessage?: string;
|
|
320
355
|
},
|
|
321
356
|
) => void | Promise<void>;
|
|
322
357
|
registerFlag?: (name: string, opts: PiFlagNamedOpts) => void;
|
|
323
358
|
/** Return package-provided workflow files discovered by Atomic's package loader. */
|
|
324
359
|
getWorkflowResources?: () => readonly WorkflowResourceInfo[];
|
|
360
|
+
/** Refresh package-provided workflow files before rediscovery, when supported by host. */
|
|
361
|
+
refreshWorkflowResources?: () => Promise<readonly WorkflowResourceInfo[]>;
|
|
325
362
|
/**
|
|
326
363
|
* Register a keyboard shortcut.
|
|
327
364
|
* Present on pi >= 1.x; absent on older runtimes.
|
|
@@ -339,9 +376,8 @@ export interface ExtensionAPI {
|
|
|
339
376
|
*/
|
|
340
377
|
getActiveTools?: () => string[];
|
|
341
378
|
/**
|
|
342
|
-
* Replace the model's active tool set by name.
|
|
343
|
-
*
|
|
344
|
-
* older runtimes.
|
|
379
|
+
* Replace the model's active tool set by name. Present on pi's ExtensionAPI;
|
|
380
|
+
* absent on older runtimes.
|
|
345
381
|
*/
|
|
346
382
|
setActiveTools?: (toolNames: string[]) => void;
|
|
347
383
|
/**
|
|
@@ -432,7 +468,7 @@ export interface ExtensionAPI {
|
|
|
432
468
|
export interface WorkflowToolArgs extends StageOptions {
|
|
433
469
|
/** Canonical named workflow identifier. */
|
|
434
470
|
workflow?: string;
|
|
435
|
-
inputs?:
|
|
471
|
+
inputs?: WorkflowInputValues;
|
|
436
472
|
action?:
|
|
437
473
|
| "run"
|
|
438
474
|
| "list"
|
|
@@ -601,6 +637,11 @@ function renderTranscriptToolContent(
|
|
|
601
637
|
];
|
|
602
638
|
if (result.sessionId) lines.push(`sessionId: ${result.sessionId}`);
|
|
603
639
|
if (result.sessionFile) lines.push(`sessionFile: ${result.sessionFile}`);
|
|
640
|
+
if (result.sessionFile) lines.push(`sessionFileJson: ${JSON.stringify(result.sessionFile)}`);
|
|
641
|
+
if (result.transcriptPath) lines.push(`transcriptPath: ${result.transcriptPath}`);
|
|
642
|
+
if (result.transcriptPath) lines.push(`transcriptPathJson: ${JSON.stringify(result.transcriptPath)}`);
|
|
643
|
+
if (result.entryCount !== undefined) lines.push(`availableEntries: ${result.entryCount}`);
|
|
644
|
+
if (result.entryLimit !== undefined) lines.push(`entryLimit: ${result.entryLimit}`);
|
|
604
645
|
if (result.entries.length === 0) {
|
|
605
646
|
lines.push("entries: none");
|
|
606
647
|
return lines.join("\n");
|
|
@@ -644,6 +685,9 @@ function renderStagesToolContent(
|
|
|
644
685
|
lines.push(`[${index + 1}] ${stage.name} (${stage.id}) ${stage.status}`);
|
|
645
686
|
if (stage.sessionId) lines.push(`sessionId: ${stage.sessionId}`);
|
|
646
687
|
if (stage.sessionFile) lines.push(`sessionFile: ${stage.sessionFile}`);
|
|
688
|
+
if (stage.sessionFile) lines.push(`sessionFileJson: ${JSON.stringify(stage.sessionFile)}`);
|
|
689
|
+
if (stage.transcriptPath) lines.push(`transcriptPath: ${stage.transcriptPath}`);
|
|
690
|
+
if (stage.transcriptPath) lines.push(`transcriptPathJson: ${JSON.stringify(stage.transcriptPath)}`);
|
|
647
691
|
if (stage.error) lines.push(`error: ${stage.error}`);
|
|
648
692
|
if (stage.awaitingInputSince !== undefined) {
|
|
649
693
|
lines.push(`awaitingInputSince: ${stage.awaitingInputSince}`);
|
|
@@ -670,6 +714,10 @@ function renderStageToolContent(
|
|
|
670
714
|
}
|
|
671
715
|
lines.push("stage:");
|
|
672
716
|
lines.push(JSON.stringify(result.stage, null, 2));
|
|
717
|
+
if (result.stage.sessionFile) {
|
|
718
|
+
lines.push(`transcriptPath: ${result.stage.sessionFile}`);
|
|
719
|
+
lines.push(`transcriptPathJson: ${JSON.stringify(result.stage.sessionFile)}`);
|
|
720
|
+
}
|
|
673
721
|
return lines.join("\n");
|
|
674
722
|
}
|
|
675
723
|
|
|
@@ -716,14 +764,7 @@ function workflowGetResult(
|
|
|
716
764
|
error: `Workflow not found: "${workflow}"`,
|
|
717
765
|
};
|
|
718
766
|
}
|
|
719
|
-
const inputs =
|
|
720
|
-
name,
|
|
721
|
-
type: schema.type,
|
|
722
|
-
description: schema.description,
|
|
723
|
-
required: schema.required,
|
|
724
|
-
default: "default" in schema ? schema.default : undefined,
|
|
725
|
-
choices: schema.type === "select" ? schema.choices : undefined,
|
|
726
|
-
}));
|
|
767
|
+
const inputs = deriveInputFields(def.inputs);
|
|
727
768
|
return {
|
|
728
769
|
action: "get",
|
|
729
770
|
workflow: def.normalizedName,
|
|
@@ -735,7 +776,7 @@ function workflowGetResult(
|
|
|
735
776
|
workflow: def.normalizedName,
|
|
736
777
|
name: def.name,
|
|
737
778
|
description: def.description,
|
|
738
|
-
inputs,
|
|
779
|
+
inputs: inputs as unknown as WorkflowSerializableValue[],
|
|
739
780
|
},
|
|
740
781
|
progress: { completed: 0, total: 0 },
|
|
741
782
|
},
|
|
@@ -752,6 +793,7 @@ type WorkflowStageSummary = {
|
|
|
752
793
|
status: StageStatus;
|
|
753
794
|
sessionId?: string;
|
|
754
795
|
sessionFile?: string;
|
|
796
|
+
transcriptPath?: string;
|
|
755
797
|
error?: string;
|
|
756
798
|
awaitingInputSince?: number;
|
|
757
799
|
pendingPrompt?: StageSnapshot["pendingPrompt"];
|
|
@@ -776,8 +818,10 @@ type MessageLike = {
|
|
|
776
818
|
readonly createdAt?: number;
|
|
777
819
|
};
|
|
778
820
|
|
|
779
|
-
function cloneStage(stage: StageSnapshot): StageSnapshot {
|
|
780
|
-
|
|
821
|
+
function cloneStage(stage: StageSnapshot): StageSnapshot & { transcriptPath?: string } {
|
|
822
|
+
const cloned = structuredClone(stage) as StageSnapshot & { transcriptPath?: string };
|
|
823
|
+
if (cloned.sessionFile !== undefined) cloned.transcriptPath = cloned.sessionFile;
|
|
824
|
+
return cloned;
|
|
781
825
|
}
|
|
782
826
|
|
|
783
827
|
function summarizeStage(stage: StageSnapshot): WorkflowStageSummary {
|
|
@@ -787,6 +831,7 @@ function summarizeStage(stage: StageSnapshot): WorkflowStageSummary {
|
|
|
787
831
|
status: stage.status,
|
|
788
832
|
sessionId: stage.sessionId,
|
|
789
833
|
sessionFile: stage.sessionFile,
|
|
834
|
+
transcriptPath: stage.sessionFile,
|
|
790
835
|
error: stage.error,
|
|
791
836
|
awaitingInputSince: stage.awaitingInputSince,
|
|
792
837
|
pendingPrompt: stage.pendingPrompt === undefined
|
|
@@ -798,27 +843,50 @@ function summarizeStage(stage: StageSnapshot): WorkflowStageSummary {
|
|
|
798
843
|
};
|
|
799
844
|
}
|
|
800
845
|
|
|
801
|
-
const DEFAULT_TRANSCRIPT_LIMIT =
|
|
846
|
+
const DEFAULT_TRANSCRIPT_LIMIT = 5;
|
|
847
|
+
|
|
848
|
+
type TranscriptEntrySelection = {
|
|
849
|
+
entries: WorkflowTranscriptEntry[];
|
|
850
|
+
truncated: boolean;
|
|
851
|
+
entryCount: number;
|
|
852
|
+
entryLimit?: number;
|
|
853
|
+
};
|
|
802
854
|
|
|
803
|
-
function
|
|
855
|
+
function requestedTranscriptEntryLimit(args: WorkflowToolArgs): number {
|
|
804
856
|
const raw = args.tail ?? args.limit;
|
|
805
857
|
if (raw === undefined) return DEFAULT_TRANSCRIPT_LIMIT;
|
|
806
858
|
if (!Number.isFinite(raw) || raw <= 0) return 0;
|
|
807
859
|
return Math.floor(raw);
|
|
808
860
|
}
|
|
809
861
|
|
|
810
|
-
function
|
|
811
|
-
entries: readonly
|
|
862
|
+
function selectTranscriptEntries(
|
|
863
|
+
entries: readonly WorkflowTranscriptEntry[],
|
|
812
864
|
args: WorkflowToolArgs,
|
|
813
|
-
):
|
|
814
|
-
const count =
|
|
865
|
+
): TranscriptEntrySelection {
|
|
866
|
+
const count = requestedTranscriptEntryLimit(args);
|
|
867
|
+
const entryCount = entries.length;
|
|
815
868
|
if (count === 0) {
|
|
816
|
-
return {
|
|
869
|
+
return {
|
|
870
|
+
entries: [],
|
|
871
|
+
truncated: false,
|
|
872
|
+
entryCount,
|
|
873
|
+
entryLimit: count,
|
|
874
|
+
};
|
|
817
875
|
}
|
|
818
876
|
if (entries.length <= count) {
|
|
819
|
-
return {
|
|
877
|
+
return {
|
|
878
|
+
entries: [...entries],
|
|
879
|
+
truncated: false,
|
|
880
|
+
entryCount,
|
|
881
|
+
entryLimit: count,
|
|
882
|
+
};
|
|
820
883
|
}
|
|
821
|
-
return {
|
|
884
|
+
return {
|
|
885
|
+
entries: entries.slice(entries.length - count),
|
|
886
|
+
truncated: true,
|
|
887
|
+
entryCount,
|
|
888
|
+
entryLimit: count,
|
|
889
|
+
};
|
|
822
890
|
}
|
|
823
891
|
|
|
824
892
|
function messageText(content: MessageLike["content"]): string | undefined {
|
|
@@ -981,7 +1049,15 @@ function stageFailureMessage(
|
|
|
981
1049
|
}
|
|
982
1050
|
|
|
983
1051
|
function inFlightRunCount(): number {
|
|
984
|
-
return store.runs().filter((run) => run.endedAt === undefined).length;
|
|
1052
|
+
return topLevelWorkflowRuns(store.runs()).filter((run) => run.endedAt === undefined).length;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function topLevelExpandedSnapshots() {
|
|
1056
|
+
const snapshot = store.snapshot();
|
|
1057
|
+
return topLevelWorkflowRuns(snapshot.runs).map((run) => ({
|
|
1058
|
+
...structuredClone(run),
|
|
1059
|
+
stages: expandWorkflowGraph(snapshot, run.id).stages.map((stage) => structuredClone(stage)),
|
|
1060
|
+
}));
|
|
985
1061
|
}
|
|
986
1062
|
|
|
987
1063
|
function reloadBlockedMessage(count = inFlightRunCount()): string {
|
|
@@ -1012,39 +1088,152 @@ function isWorkflowStageToolContext(ctx: PiExecuteContext): boolean {
|
|
|
1012
1088
|
return hasWorkflowStageSubagentGuardEnv() || ctx.orchestrationContext?.kind === "workflow-stage";
|
|
1013
1089
|
}
|
|
1014
1090
|
|
|
1015
|
-
/** Tool name registered for workflow execution; shared by the de-advertise guard. */
|
|
1016
|
-
const WORKFLOW_TOOL_NAME = "workflow";
|
|
1017
|
-
|
|
1018
1091
|
/**
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
* pickers, the graph overlay, or human-in-the-loop prompts.
|
|
1023
|
-
*
|
|
1024
|
-
* The `workflow` tool is removed from the model's tool set in these sessions
|
|
1025
|
-
* (see `deAdvertiseWorkflowToolWhenHeadless`). The slash command has no
|
|
1026
|
-
* advertise layer and is still reachable via `atomic -p "/workflow …"` (print
|
|
1027
|
-
* mode dispatches leading-slash commands through `session.prompt`), so it is
|
|
1028
|
-
* refused here instead.
|
|
1092
|
+
* Legacy message retained for consumers that imported the old refusal string.
|
|
1093
|
+
* Non-interactive sessions now keep the workflow tool and `/workflow` command
|
|
1094
|
+
* available; policy gates interactive pickers and runtime human-input APIs.
|
|
1029
1095
|
*/
|
|
1030
1096
|
export const WORKFLOW_NON_INTERACTIVE_MESSAGE =
|
|
1031
|
-
"Workflows are
|
|
1097
|
+
"Workflows are policy-gated in non-interactive (-p) mode; deterministic workflows can run headlessly while runtime human input remains unavailable.";
|
|
1032
1098
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1099
|
+
export function workflowPolicyFromContext(ctx?: { readonly hasUI?: boolean }): WorkflowExecutionPolicy {
|
|
1100
|
+
if (ctx?.hasUI === false) {
|
|
1101
|
+
return NON_INTERACTIVE_WORKFLOW_POLICY;
|
|
1102
|
+
}
|
|
1103
|
+
return INTERACTIVE_WORKFLOW_POLICY;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function isRunStatus(value: string): value is RunStatus {
|
|
1107
|
+
switch (value) {
|
|
1108
|
+
case "pending":
|
|
1109
|
+
case "running":
|
|
1110
|
+
case "paused":
|
|
1111
|
+
case "completed":
|
|
1112
|
+
case "failed":
|
|
1113
|
+
case "killed":
|
|
1114
|
+
return true;
|
|
1115
|
+
default:
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function fallbackRunDetailFromResult(
|
|
1121
|
+
workflowName: string,
|
|
1122
|
+
inputs: Readonly<WorkflowInputValues>,
|
|
1123
|
+
result: Extract<WorkflowToolResult, { action: "run"; runId: string }>,
|
|
1124
|
+
): RunDetail {
|
|
1125
|
+
const now = Date.now();
|
|
1126
|
+
const stages = result.stages?.map((stage) => structuredClone(stage)) ?? [];
|
|
1127
|
+
// This path is a degraded last-resort view used only when the retained run
|
|
1128
|
+
// snapshot has disappeared before output rendering. Timestamps are synthetic,
|
|
1129
|
+
// so prefer a conservative failed status over fabricating success if the tool
|
|
1130
|
+
// result status is not one of the known run states.
|
|
1131
|
+
return {
|
|
1132
|
+
runId: result.runId,
|
|
1133
|
+
name: result.name ?? workflowName,
|
|
1134
|
+
status: isRunStatus(result.status) ? result.status : "failed",
|
|
1135
|
+
mode: stages.length > 1 ? "chain" : "single",
|
|
1136
|
+
startedAt: now,
|
|
1137
|
+
endedAt: now,
|
|
1138
|
+
durationMs: 0,
|
|
1139
|
+
inputs,
|
|
1140
|
+
stages,
|
|
1141
|
+
result: result.result,
|
|
1142
|
+
error: result.error,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function emitTerminalRunDetailSurface(
|
|
1147
|
+
pi: ExtensionAPI,
|
|
1148
|
+
workflowName: string,
|
|
1149
|
+
inputs: Readonly<WorkflowInputValues>,
|
|
1150
|
+
result: Extract<WorkflowToolResult, { action: "run"; runId: string }>,
|
|
1151
|
+
): void {
|
|
1152
|
+
const inspected = inspectRun(result.runId);
|
|
1153
|
+
const detail = inspected.ok
|
|
1154
|
+
? inspected.detail
|
|
1155
|
+
: fallbackRunDetailFromResult(workflowName, inputs, result);
|
|
1156
|
+
emitChatSurface(
|
|
1157
|
+
pi,
|
|
1158
|
+
{ kind: "detail", detail },
|
|
1159
|
+
{ content: renderRunDetail(detail, { width: 100 }) },
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
export const WORKFLOW_COMMAND_OUTPUT_CUSTOM_TYPE = "workflows:command-output";
|
|
1164
|
+
|
|
1165
|
+
interface WorkflowCommandOutputDetails {
|
|
1166
|
+
readonly command: string;
|
|
1167
|
+
readonly workflowName?: string;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function emitWorkflowCommandOutput(
|
|
1171
|
+
pi: ExtensionAPI,
|
|
1172
|
+
content: string,
|
|
1173
|
+
details: WorkflowCommandOutputDetails,
|
|
1174
|
+
): void {
|
|
1175
|
+
if (typeof pi.sendMessage !== "function") return;
|
|
1176
|
+
void pi.sendMessage<WorkflowCommandOutputDetails>({
|
|
1177
|
+
customType: WORKFLOW_COMMAND_OUTPUT_CUSTOM_TYPE,
|
|
1178
|
+
content,
|
|
1179
|
+
display: true,
|
|
1180
|
+
details,
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
interface WorkflowCommandReporter {
|
|
1185
|
+
info(message: string): void;
|
|
1186
|
+
error(message: string): void;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function formatAvailableWorkflowNames(names: readonly string[]): string {
|
|
1190
|
+
return names.length > 0 ? names.join(", ") : "(none)";
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const ASK_USER_QUESTION_TOOL_NAME = "ask_user_question";
|
|
1194
|
+
|
|
1195
|
+
function deAdvertiseAskUserQuestionWhenHeadless(
|
|
1196
|
+
pi: ExtensionAPI,
|
|
1197
|
+
hasUI: boolean | undefined,
|
|
1198
|
+
): void {
|
|
1043
1199
|
if (hasUI !== false) return;
|
|
1044
1200
|
if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1201
|
+
|
|
1202
|
+
const activeTools = pi.getActiveTools();
|
|
1203
|
+
if (!activeTools.includes(ASK_USER_QUESTION_TOOL_NAME)) return;
|
|
1204
|
+
|
|
1205
|
+
pi.setActiveTools(activeTools.filter((toolName) => toolName !== ASK_USER_QUESTION_TOOL_NAME));
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
class WorkflowHeadlessCommandError extends Error {
|
|
1209
|
+
constructor(message: string) {
|
|
1210
|
+
super(message);
|
|
1211
|
+
this.name = "WorkflowHeadlessCommandError";
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function createWorkflowCommandReporter(
|
|
1216
|
+
ctx: PiCommandContext,
|
|
1217
|
+
policy: WorkflowExecutionPolicy = workflowPolicyFromContext(ctx),
|
|
1218
|
+
pi?: ExtensionAPI,
|
|
1219
|
+
): WorkflowCommandReporter {
|
|
1220
|
+
return {
|
|
1221
|
+
info(message: string): void {
|
|
1222
|
+
if (policy.mode === "non_interactive") {
|
|
1223
|
+
if (pi) {
|
|
1224
|
+
emitWorkflowCommandOutput(pi, message, { command: "message" });
|
|
1225
|
+
}
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
ctx.ui.notify(message, "info");
|
|
1229
|
+
},
|
|
1230
|
+
error(message: string): void {
|
|
1231
|
+
if (policy.mode === "non_interactive") {
|
|
1232
|
+
throw new WorkflowHeadlessCommandError(message);
|
|
1233
|
+
}
|
|
1234
|
+
ctx.ui.notify(message, "error");
|
|
1235
|
+
},
|
|
1236
|
+
};
|
|
1048
1237
|
}
|
|
1049
1238
|
|
|
1050
1239
|
// ---------------------------------------------------------------------------
|
|
@@ -1078,6 +1267,7 @@ export function makeExecuteWorkflowTool(
|
|
|
1078
1267
|
}
|
|
1079
1268
|
const activeRuntime =
|
|
1080
1269
|
typeof runtime === "function" ? runtime(ctx) : runtime;
|
|
1270
|
+
const policy = workflowPolicyFromContext(ctx);
|
|
1081
1271
|
|
|
1082
1272
|
switch (action) {
|
|
1083
1273
|
case "get":
|
|
@@ -1096,12 +1286,13 @@ export function makeExecuteWorkflowTool(
|
|
|
1096
1286
|
}
|
|
1097
1287
|
const details = await activeRuntime.runDirect(
|
|
1098
1288
|
withForkParentSession(args, ctx),
|
|
1289
|
+
{ policy },
|
|
1099
1290
|
);
|
|
1100
1291
|
return workflowRunResultFromDetails(details);
|
|
1101
1292
|
}
|
|
1102
1293
|
// Delegate to registry-backed dispatcher.
|
|
1103
1294
|
// Real errors propagate — no broad catch.
|
|
1104
|
-
return activeRuntime.dispatch(args);
|
|
1295
|
+
return activeRuntime.dispatch(args, { policy });
|
|
1105
1296
|
|
|
1106
1297
|
case "status": {
|
|
1107
1298
|
// Detail mode — single-run lookup via id.
|
|
@@ -1123,10 +1314,9 @@ export function makeExecuteWorkflowTool(
|
|
|
1123
1314
|
}
|
|
1124
1315
|
// List mode — emit all retained snapshots; the renderer produces the
|
|
1125
1316
|
// canonical band + card surface.
|
|
1126
|
-
const snapshots = store.runs();
|
|
1127
1317
|
return {
|
|
1128
1318
|
action: "status",
|
|
1129
|
-
snapshots:
|
|
1319
|
+
snapshots: topLevelExpandedSnapshots(),
|
|
1130
1320
|
};
|
|
1131
1321
|
}
|
|
1132
1322
|
|
|
@@ -1200,14 +1390,15 @@ export function makeExecuteWorkflowTool(
|
|
|
1200
1390
|
: stage.message,
|
|
1201
1391
|
};
|
|
1202
1392
|
}
|
|
1203
|
-
const
|
|
1393
|
+
const stageRunId = stage.runId ?? target.runId;
|
|
1394
|
+
const run = store.runs().find((r) => r.id === stageRunId);
|
|
1204
1395
|
const snapshot = run?.stages.find((s) => s.id === stage.stageId);
|
|
1205
1396
|
return snapshot
|
|
1206
|
-
? { action: "stage", runId:
|
|
1397
|
+
? { action: "stage", runId: stageRunId, stage: cloneStage(snapshot) }
|
|
1207
1398
|
: {
|
|
1208
1399
|
action: "stage",
|
|
1209
|
-
runId:
|
|
1210
|
-
error: `Stage not found in run ${
|
|
1400
|
+
runId: stageRunId,
|
|
1401
|
+
error: `Stage not found in run ${stageRunId.slice(0, 8)}: ${stage.stageId}`,
|
|
1211
1402
|
};
|
|
1212
1403
|
}
|
|
1213
1404
|
|
|
@@ -1263,36 +1454,39 @@ export function makeExecuteWorkflowTool(
|
|
|
1263
1454
|
truncated: false,
|
|
1264
1455
|
};
|
|
1265
1456
|
}
|
|
1266
|
-
const
|
|
1457
|
+
const stageRunId = stage.runId ?? target.runId;
|
|
1458
|
+
const run = store.runs().find((r) => r.id === stageRunId);
|
|
1267
1459
|
const snapshot = run?.stages.find((s) => s.id === stage.stageId);
|
|
1268
|
-
const liveHandle = stageControlRegistry.get(
|
|
1460
|
+
const liveHandle = stageControlRegistry.get(stageRunId, stage.stageId);
|
|
1269
1461
|
if (liveHandle !== undefined) {
|
|
1270
|
-
const
|
|
1462
|
+
const sessionFile = liveHandle.sessionFile ?? snapshot?.sessionFile;
|
|
1463
|
+
const sessionId = liveHandle.sessionId ?? snapshot?.sessionId;
|
|
1464
|
+
const limited = selectTranscriptEntries(
|
|
1271
1465
|
liveHandle.messages.map((m) => transcriptEntryFromMessage(m as MessageLike)),
|
|
1272
1466
|
args,
|
|
1273
1467
|
);
|
|
1274
1468
|
return {
|
|
1275
1469
|
action: "transcript",
|
|
1276
|
-
runId:
|
|
1470
|
+
runId: stageRunId,
|
|
1277
1471
|
stageId: stage.stageId,
|
|
1278
1472
|
source: "live",
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1473
|
+
...limited,
|
|
1474
|
+
sessionId,
|
|
1475
|
+
sessionFile,
|
|
1476
|
+
transcriptPath: sessionFile,
|
|
1283
1477
|
};
|
|
1284
1478
|
}
|
|
1285
1479
|
const fallback = snapshotTranscriptEntries(snapshot, args.includeToolOutput === true);
|
|
1286
|
-
const limited =
|
|
1480
|
+
const limited = selectTranscriptEntries(fallback, args);
|
|
1287
1481
|
return {
|
|
1288
1482
|
action: "transcript",
|
|
1289
|
-
runId:
|
|
1483
|
+
runId: stageRunId,
|
|
1290
1484
|
stageId: stage.stageId,
|
|
1291
1485
|
source: "snapshot",
|
|
1292
|
-
|
|
1293
|
-
truncated: limited.truncated,
|
|
1486
|
+
...limited,
|
|
1294
1487
|
sessionId: snapshot?.sessionId,
|
|
1295
1488
|
sessionFile: snapshot?.sessionFile,
|
|
1489
|
+
transcriptPath: snapshot?.sessionFile,
|
|
1296
1490
|
};
|
|
1297
1491
|
}
|
|
1298
1492
|
|
|
@@ -1318,13 +1512,14 @@ export function makeExecuteWorkflowTool(
|
|
|
1318
1512
|
stage.ok ? "Stage id, prefix, or name is required." : stage.message,
|
|
1319
1513
|
);
|
|
1320
1514
|
}
|
|
1321
|
-
const
|
|
1515
|
+
const stageRunId = stage.runId ?? target.runId;
|
|
1516
|
+
const run = store.runs().find((r) => r.id === stageRunId);
|
|
1322
1517
|
const snapshot = run?.stages.find((s) => s.id === stage.stageId);
|
|
1323
1518
|
// Brokered structured prompts (in-stage ask_user_question / readiness
|
|
1324
1519
|
// gate) resolve through StageUiBroker rather than store.pendingPrompt.
|
|
1325
1520
|
// Answer those first when one is pending and the promptId (if any) lines
|
|
1326
1521
|
// up — otherwise fall through to the store-prompt / live-handle paths.
|
|
1327
|
-
const brokerPrompt = stageUiBroker.peekStagePrompt(
|
|
1522
|
+
const brokerPrompt = stageUiBroker.peekStagePrompt(stageRunId, stage.stageId);
|
|
1328
1523
|
const targetsBrokerPrompt =
|
|
1329
1524
|
brokerPrompt !== undefined &&
|
|
1330
1525
|
(args.promptId === undefined || args.promptId === brokerPrompt.id) &&
|
|
@@ -1333,11 +1528,13 @@ export function makeExecuteWorkflowTool(
|
|
|
1333
1528
|
requestedDelivery === "auto");
|
|
1334
1529
|
if (targetsBrokerPrompt && brokerPrompt !== undefined) {
|
|
1335
1530
|
if (!hasPayloadProperty(args)) {
|
|
1336
|
-
return workflowSendResult(
|
|
1531
|
+
return workflowSendResult(stageRunId, stage.stageId, "answer", "noop", "Send requires text, response, or message.");
|
|
1337
1532
|
}
|
|
1338
|
-
const ok = stageUiBroker.answerStagePrompt(
|
|
1533
|
+
const ok = stageUiBroker.answerStagePrompt(stageRunId, stage.stageId, brokerAnswerFromArgs(args), {
|
|
1534
|
+
answerSource: "workflow_tool",
|
|
1535
|
+
});
|
|
1339
1536
|
return workflowSendResult(
|
|
1340
|
-
|
|
1537
|
+
stageRunId,
|
|
1341
1538
|
stage.stageId,
|
|
1342
1539
|
"answer",
|
|
1343
1540
|
ok ? "ok" : "noop",
|
|
@@ -1351,14 +1548,25 @@ export function makeExecuteWorkflowTool(
|
|
|
1351
1548
|
if (targetsPrompt) {
|
|
1352
1549
|
const promptId = args.promptId ?? snapshot?.pendingPrompt?.id;
|
|
1353
1550
|
if (promptId === undefined) {
|
|
1354
|
-
return workflowSendResult(
|
|
1551
|
+
return workflowSendResult(stageRunId, stage.stageId, "answer", "noop", "No pending prompt to answer.");
|
|
1355
1552
|
}
|
|
1356
1553
|
if (!hasPayloadProperty(args)) {
|
|
1357
|
-
return workflowSendResult(
|
|
1554
|
+
return workflowSendResult(stageRunId, stage.stageId, "answer", "noop", "Send requires text, response, or message.");
|
|
1555
|
+
}
|
|
1556
|
+
if (stageUiBroker.wasStagePromptResolved(stageRunId, stage.stageId, promptId)) {
|
|
1557
|
+
return workflowSendResult(
|
|
1558
|
+
stageRunId,
|
|
1559
|
+
stage.stageId,
|
|
1560
|
+
"answer",
|
|
1561
|
+
"ok",
|
|
1562
|
+
`Input request ${promptId} was already answered.`,
|
|
1563
|
+
);
|
|
1358
1564
|
}
|
|
1359
|
-
const ok = store.resolveStagePendingPrompt(
|
|
1565
|
+
const ok = store.resolveStagePendingPrompt(stageRunId, stage.stageId, promptId, promptPayloadFromArgs(args), {
|
|
1566
|
+
answerSource: "workflow_tool",
|
|
1567
|
+
});
|
|
1360
1568
|
return workflowSendResult(
|
|
1361
|
-
|
|
1569
|
+
stageRunId,
|
|
1362
1570
|
stage.stageId,
|
|
1363
1571
|
"answer",
|
|
1364
1572
|
ok ? "ok" : "noop",
|
|
@@ -1367,26 +1575,26 @@ export function makeExecuteWorkflowTool(
|
|
|
1367
1575
|
}
|
|
1368
1576
|
const text = textPayloadFromArgs(args);
|
|
1369
1577
|
if (text === undefined) {
|
|
1370
|
-
return workflowSendResult(
|
|
1578
|
+
return workflowSendResult(stageRunId, stage.stageId, requestedDelivery, "noop", "Send requires text, response, or message.");
|
|
1371
1579
|
}
|
|
1372
|
-
const handle = stageControlRegistry.get(
|
|
1580
|
+
const handle = stageControlRegistry.get(stageRunId, stage.stageId);
|
|
1373
1581
|
if (handle === undefined) {
|
|
1374
|
-
return workflowSendResult(
|
|
1582
|
+
return workflowSendResult(stageRunId, stage.stageId, requestedDelivery, "noop", "No live handle for stage.");
|
|
1375
1583
|
}
|
|
1376
1584
|
if (requestedDelivery === "resume" || (requestedDelivery === "auto" && handle.status === "paused")) {
|
|
1377
1585
|
await handle.resume(text);
|
|
1378
|
-
return workflowSendResult(
|
|
1586
|
+
return workflowSendResult(stageRunId, stage.stageId, "resume", "ok", "Resumed stage with message.");
|
|
1379
1587
|
}
|
|
1380
1588
|
if (requestedDelivery === "steer" || (requestedDelivery === "auto" && handle.isStreaming)) {
|
|
1381
1589
|
await handle.steer(text);
|
|
1382
|
-
return workflowSendResult(
|
|
1590
|
+
return workflowSendResult(stageRunId, stage.stageId, "steer", "ok", "Steered live stage.");
|
|
1383
1591
|
}
|
|
1384
1592
|
if (requestedDelivery === "prompt") {
|
|
1385
1593
|
await handle.prompt(text);
|
|
1386
|
-
return workflowSendResult(
|
|
1594
|
+
return workflowSendResult(stageRunId, stage.stageId, "prompt", "ok", "Prompt sent to stage.");
|
|
1387
1595
|
}
|
|
1388
1596
|
await handle.followUp(text);
|
|
1389
|
-
return workflowSendResult(
|
|
1597
|
+
return workflowSendResult(stageRunId, stage.stageId, "followUp", "ok", "Follow-up queued for stage.");
|
|
1390
1598
|
}
|
|
1391
1599
|
|
|
1392
1600
|
case "pause": {
|
|
@@ -1415,14 +1623,15 @@ export function makeExecuteWorkflowTool(
|
|
|
1415
1623
|
if (target.kind === "not_found") return { action, runId: target.target, status: "noop", message: target.message };
|
|
1416
1624
|
const stage = resolveToolStageTarget(target.runId, args.stageId);
|
|
1417
1625
|
if (!stage.ok) return { action, runId: target.runId, status: "noop", message: stage.message };
|
|
1418
|
-
const
|
|
1626
|
+
const stageRunId = stage.runId ?? target.runId;
|
|
1627
|
+
const result = pauseRun(stageRunId, { stageId: stage.stageId });
|
|
1419
1628
|
return result.ok
|
|
1420
1629
|
? { action, runId: result.runId, status: "paused", message: `Paused ${result.paused.length} stage(s) on run ${result.runId.slice(0, 8)}.` }
|
|
1421
1630
|
: {
|
|
1422
1631
|
action,
|
|
1423
|
-
runId:
|
|
1632
|
+
runId: stageRunId,
|
|
1424
1633
|
status: "noop",
|
|
1425
|
-
message: stageFailureMessage(
|
|
1634
|
+
message: stageFailureMessage(stageRunId, result.reason, "pause"),
|
|
1426
1635
|
};
|
|
1427
1636
|
}
|
|
1428
1637
|
|
|
@@ -1544,7 +1753,8 @@ export function makeExecuteWorkflowTool(
|
|
|
1544
1753
|
if (!stage.ok) {
|
|
1545
1754
|
return { action, runId: target.runId, status: "noop", message: stage.message };
|
|
1546
1755
|
}
|
|
1547
|
-
const
|
|
1756
|
+
const stageRunId = stage.runId ?? target.runId;
|
|
1757
|
+
const result = interruptRun(stageRunId, { stageId: stage.stageId });
|
|
1548
1758
|
if (result.ok) {
|
|
1549
1759
|
return {
|
|
1550
1760
|
action,
|
|
@@ -1557,9 +1767,9 @@ export function makeExecuteWorkflowTool(
|
|
|
1557
1767
|
}
|
|
1558
1768
|
return {
|
|
1559
1769
|
action,
|
|
1560
|
-
runId:
|
|
1770
|
+
runId: stageRunId,
|
|
1561
1771
|
status: "noop",
|
|
1562
|
-
message: stageFailureMessage(
|
|
1772
|
+
message: stageFailureMessage(stageRunId, result.reason, "interrupt"),
|
|
1563
1773
|
};
|
|
1564
1774
|
}
|
|
1565
1775
|
|
|
@@ -1578,20 +1788,21 @@ export function makeExecuteWorkflowTool(
|
|
|
1578
1788
|
if (!stage.ok) {
|
|
1579
1789
|
return { action: "resume", runId: target.runId, status: "noop", message: stage.message };
|
|
1580
1790
|
}
|
|
1581
|
-
const
|
|
1791
|
+
const stageRunId = stage.runId ?? target.runId;
|
|
1792
|
+
const run = store.runs().find((r) => r.id === stageRunId);
|
|
1582
1793
|
const isPaused =
|
|
1583
1794
|
run?.status === "paused" ||
|
|
1584
1795
|
(run?.stages.some((s) => s.status === "paused") ?? false);
|
|
1585
1796
|
if (!isPaused && run?.status === "failed" && run.endedAt !== undefined && run.resumable !== false) {
|
|
1586
|
-
const continuation = activeRuntime.resumeFailedRun(
|
|
1797
|
+
const continuation = activeRuntime.resumeFailedRun(stageRunId, stage.stageId, { policy });
|
|
1587
1798
|
return {
|
|
1588
1799
|
action: "resume",
|
|
1589
|
-
runId: continuation.ok ? continuation.runId :
|
|
1800
|
+
runId: continuation.ok ? continuation.runId : stageRunId,
|
|
1590
1801
|
status: continuation.ok ? "running" : "noop",
|
|
1591
1802
|
message: continuation.message,
|
|
1592
1803
|
};
|
|
1593
1804
|
}
|
|
1594
|
-
const result = resumeRun(
|
|
1805
|
+
const result = resumeRun(stageRunId, { stageId: stage.stageId, message: args.message });
|
|
1595
1806
|
if (result.ok) {
|
|
1596
1807
|
const message = result.message ?? (isPaused
|
|
1597
1808
|
? result.resumed.length === 0
|
|
@@ -1607,9 +1818,9 @@ export function makeExecuteWorkflowTool(
|
|
|
1607
1818
|
}
|
|
1608
1819
|
return {
|
|
1609
1820
|
action: "resume",
|
|
1610
|
-
runId:
|
|
1821
|
+
runId: stageRunId,
|
|
1611
1822
|
status: "noop",
|
|
1612
|
-
message: `Run not found: ${
|
|
1823
|
+
message: `Run not found: ${stageRunId}`,
|
|
1613
1824
|
};
|
|
1614
1825
|
}
|
|
1615
1826
|
|
|
@@ -1638,6 +1849,26 @@ export function makeExecuteWorkflowTool(
|
|
|
1638
1849
|
*/
|
|
1639
1850
|
type WorkflowCommandHandler = PiCommandOptions["handler"];
|
|
1640
1851
|
|
|
1852
|
+
interface ParsedWorkflowSlashCommand {
|
|
1853
|
+
name: string;
|
|
1854
|
+
args: string;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
function parseWorkflowSlashCommand(text: string): ParsedWorkflowSlashCommand | undefined {
|
|
1858
|
+
const trimmed = text.trim();
|
|
1859
|
+
if (!trimmed.startsWith("/")) return undefined;
|
|
1860
|
+
|
|
1861
|
+
// First token (after `/`) is the command name. Whitespace splits
|
|
1862
|
+
// command from args; quote handling lives inside the command
|
|
1863
|
+
// handler itself (`tokenizeWorkflowArgs`).
|
|
1864
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
1865
|
+
const name =
|
|
1866
|
+
firstSpace === -1 ? trimmed.slice(1) : trimmed.slice(1, firstSpace);
|
|
1867
|
+
const args = firstSpace === -1 ? "" : trimmed.slice(firstSpace + 1);
|
|
1868
|
+
|
|
1869
|
+
return { name, args };
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1641
1872
|
/**
|
|
1642
1873
|
* Register a slash command with the host AND remember the handler so
|
|
1643
1874
|
* the input interceptor can dispatch directly.
|
|
@@ -1707,26 +1938,24 @@ function installInputInterceptor(
|
|
|
1707
1938
|
pi.on("input", async (event, ctx) => {
|
|
1708
1939
|
const text = (event as { text?: unknown } | undefined)?.text;
|
|
1709
1940
|
if (typeof text !== "string") return undefined;
|
|
1710
|
-
const
|
|
1711
|
-
if (!
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
// command from args; quote handling lives inside the command
|
|
1715
|
-
// handler itself (`tokenizeWorkflowArgs`).
|
|
1716
|
-
const firstSpace = trimmed.indexOf(" ");
|
|
1717
|
-
const name =
|
|
1718
|
-
firstSpace === -1 ? trimmed.slice(1) : trimmed.slice(1, firstSpace);
|
|
1941
|
+
const parsedCommand = parseWorkflowSlashCommand(text);
|
|
1942
|
+
if (!parsedCommand) return undefined;
|
|
1943
|
+
|
|
1944
|
+
const { name, args } = parsedCommand;
|
|
1719
1945
|
const handler = commands.get(name);
|
|
1720
1946
|
if (!handler) return undefined; // not ours — let host run its normal flow.
|
|
1721
|
-
|
|
1722
|
-
const args = firstSpace === -1 ? "" : trimmed.slice(firstSpace + 1);
|
|
1723
1947
|
const commandCtx = ctx as PiCommandContext;
|
|
1724
1948
|
try {
|
|
1725
1949
|
await handler(args, commandCtx);
|
|
1726
1950
|
} catch (err) {
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1951
|
+
if (commandCtx.hasUI === false) {
|
|
1952
|
+
throw err;
|
|
1953
|
+
}
|
|
1954
|
+
// Match the host command runner for interactive contexts: swallow
|
|
1955
|
+
// handler exceptions so a throw never bubbles out and crashes the
|
|
1956
|
+
// editor submit pipeline. Surface the failure via `ctx.ui.notify` so
|
|
1957
|
+
// the user sees it. Headless contexts rethrow above because notify is
|
|
1958
|
+
// a no-op in print mode and would otherwise hide command failures.
|
|
1730
1959
|
const message = err instanceof Error ? err.message : String(err);
|
|
1731
1960
|
commandCtx.ui.notify(`/${name} failed: ${message}`, "error");
|
|
1732
1961
|
}
|
|
@@ -1805,33 +2034,45 @@ function resolveToolRunTarget(
|
|
|
1805
2034
|
}
|
|
1806
2035
|
|
|
1807
2036
|
type ToolStageTarget =
|
|
1808
|
-
| { ok: true; stageId?: string }
|
|
2037
|
+
| { ok: true; runId?: string; stageId?: string }
|
|
1809
2038
|
| { ok: false; message: string };
|
|
1810
2039
|
|
|
1811
|
-
function stageMatchesIdentifier(stage: { readonly id: string; readonly name: string }, target: string): boolean {
|
|
1812
|
-
return stage.id === target || stage.name === target || stage.id.startsWith(target);
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
function stageMatchLabel(stage: { readonly id: string; readonly name: string }): string {
|
|
1816
|
-
return `${stage.name} (${stage.id.slice(0, 12)})`;
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
2040
|
function resolveStageTarget(runId: string, stageTarget?: string): ToolStageTarget {
|
|
1820
2041
|
const target = stageTarget?.trim();
|
|
1821
|
-
if (!target) return { ok: true };
|
|
2042
|
+
if (!target) return { ok: true, runId };
|
|
1822
2043
|
|
|
1823
|
-
const
|
|
1824
|
-
const exactId =
|
|
1825
|
-
|
|
2044
|
+
const graph = expandWorkflowGraph(store.snapshot(), runId);
|
|
2045
|
+
const exactId = graph.stages.find(
|
|
2046
|
+
(stage) => stage.id === target || stage.workflowGraphTarget.stageId === target,
|
|
2047
|
+
);
|
|
2048
|
+
if (exactId !== undefined) {
|
|
2049
|
+
return {
|
|
2050
|
+
ok: true,
|
|
2051
|
+
runId: exactId.workflowGraphTarget.runId,
|
|
2052
|
+
stageId: exactId.workflowGraphTarget.stageId,
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
1826
2055
|
|
|
1827
|
-
const exactNames =
|
|
1828
|
-
if (exactNames.length === 1)
|
|
1829
|
-
|
|
2056
|
+
const exactNames = graph.stages.filter((stage) => stage.name === target);
|
|
2057
|
+
if (exactNames.length === 1) {
|
|
2058
|
+
const stage = exactNames[0]!;
|
|
2059
|
+
return {
|
|
2060
|
+
ok: true,
|
|
2061
|
+
runId: stage.workflowGraphTarget.runId,
|
|
2062
|
+
stageId: stage.workflowGraphTarget.stageId,
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
if (exactNames.length > 1) return { ok: false, message: `Ambiguous stage identifier "${target}" matches: ${exactNames.map(expandedStageLabel).join(", ")}` };
|
|
1830
2066
|
|
|
1831
|
-
const matches =
|
|
2067
|
+
const matches = graph.stages.filter((stage) => stageMatchesExpandedIdentifier(stage, target));
|
|
1832
2068
|
if (matches.length === 0) return { ok: false, message: `Stage not found in run ${runId.slice(0, 8)}: ${target}` };
|
|
1833
|
-
if (matches.length > 1) return { ok: false, message: `Ambiguous stage identifier "${target}" matches: ${matches.map(
|
|
1834
|
-
|
|
2069
|
+
if (matches.length > 1) return { ok: false, message: `Ambiguous stage identifier "${target}" matches: ${matches.map(expandedStageLabel).join(", ")}` };
|
|
2070
|
+
const stage = matches[0]!;
|
|
2071
|
+
return {
|
|
2072
|
+
ok: true,
|
|
2073
|
+
runId: stage.workflowGraphTarget.runId,
|
|
2074
|
+
stageId: stage.workflowGraphTarget.stageId,
|
|
2075
|
+
};
|
|
1835
2076
|
}
|
|
1836
2077
|
|
|
1837
2078
|
function resolveToolStageTarget(runId: string, stageTarget?: string): ToolStageTarget {
|
|
@@ -1920,8 +2161,8 @@ export function tokenizeWorkflowArgs(args: string): string[] {
|
|
|
1920
2161
|
* Tokens that are standalone valid JSON objects/arrays are merged in.
|
|
1921
2162
|
* All other tokens are ignored (non-kv positional args not supported).
|
|
1922
2163
|
*/
|
|
1923
|
-
export function parseWorkflowArgs(tokens: string[]):
|
|
1924
|
-
const result: Record<string,
|
|
2164
|
+
export function parseWorkflowArgs(tokens: string[]): WorkflowInputValues {
|
|
2165
|
+
const result: Record<string, WorkflowSerializableValue> = {};
|
|
1925
2166
|
for (const token of tokens) {
|
|
1926
2167
|
// Try JSON object/array merge
|
|
1927
2168
|
if (
|
|
@@ -1935,7 +2176,7 @@ export function parseWorkflowArgs(tokens: string[]): Record<string, unknown> {
|
|
|
1935
2176
|
typeof parsed === "object" &&
|
|
1936
2177
|
!Array.isArray(parsed)
|
|
1937
2178
|
) {
|
|
1938
|
-
Object.assign(result, parsed as
|
|
2179
|
+
Object.assign(result, parsed as WorkflowInputValues);
|
|
1939
2180
|
}
|
|
1940
2181
|
continue;
|
|
1941
2182
|
} catch {
|
|
@@ -1948,9 +2189,9 @@ export function parseWorkflowArgs(tokens: string[]): Record<string, unknown> {
|
|
|
1948
2189
|
const key = token.slice(0, eqIdx);
|
|
1949
2190
|
const raw = token.slice(eqIdx + 1);
|
|
1950
2191
|
// Try to parse value as JSON for typed values (numbers, booleans, objects)
|
|
1951
|
-
let value:
|
|
2192
|
+
let value: WorkflowSerializableValue = raw;
|
|
1952
2193
|
try {
|
|
1953
|
-
value = JSON.parse(raw) as
|
|
2194
|
+
value = JSON.parse(raw) as WorkflowSerializableValue;
|
|
1954
2195
|
} catch {
|
|
1955
2196
|
// keep as string
|
|
1956
2197
|
}
|
|
@@ -2101,16 +2342,29 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2101
2342
|
);
|
|
2102
2343
|
let lifecycleNotificationsUnsubscribe: (() => void) | null = null;
|
|
2103
2344
|
let lifecycleNotificationsActive = false;
|
|
2345
|
+
let hilAnswerNotificationsUnsubscribe: (() => void) | null = null;
|
|
2346
|
+
let hilAnswerNotificationsActive = false;
|
|
2104
2347
|
const lifecycleNotificationState = createWorkflowLifecycleNotificationState();
|
|
2348
|
+
const hilAnswerNotificationState = createWorkflowHilAnswerNotificationState();
|
|
2105
2349
|
const lifecycleNotificationConfigRef: { current: WorkflowLifecycleNotificationConfig } = {
|
|
2106
2350
|
current: WORKFLOW_CONFIG_DEFAULTS.workflowNotifications,
|
|
2107
2351
|
};
|
|
2352
|
+
const registerMessageRenderer: ExtensionAPI["registerMessageRenderer"] | undefined =
|
|
2353
|
+
typeof pi.registerMessageRenderer === "function"
|
|
2354
|
+
? (event, renderer) => pi.registerMessageRenderer!(event, renderer)
|
|
2355
|
+
: undefined;
|
|
2108
2356
|
registerLifecycleNoticeRenderer({
|
|
2109
2357
|
rendererHost: pi,
|
|
2110
|
-
registerMessageRenderer
|
|
2111
|
-
? (event, renderer) => pi.registerMessageRenderer?.(event, renderer)
|
|
2112
|
-
: undefined,
|
|
2358
|
+
registerMessageRenderer,
|
|
2113
2359
|
});
|
|
2360
|
+
registerHilAnswerNoticeRenderer({
|
|
2361
|
+
rendererHost: pi,
|
|
2362
|
+
registerMessageRenderer,
|
|
2363
|
+
});
|
|
2364
|
+
const sendWorkflowNotificationMessage: ExtensionAPI["sendMessage"] | undefined =
|
|
2365
|
+
typeof pi.sendMessage === "function"
|
|
2366
|
+
? (message, options) => pi.sendMessage!(message, options)
|
|
2367
|
+
: undefined;
|
|
2114
2368
|
const reinstallLifecycleNotifications = (): void => {
|
|
2115
2369
|
lifecycleNotificationsUnsubscribe?.();
|
|
2116
2370
|
lifecycleNotificationsUnsubscribe = null;
|
|
@@ -2120,11 +2374,33 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2120
2374
|
config: lifecycleNotificationConfigRef.current,
|
|
2121
2375
|
state: lifecycleNotificationState,
|
|
2122
2376
|
seedExisting: true,
|
|
2123
|
-
sendMessage:
|
|
2124
|
-
? (message, options) => pi.sendMessage?.(message, options)
|
|
2125
|
-
: undefined,
|
|
2377
|
+
sendMessage: sendWorkflowNotificationMessage,
|
|
2126
2378
|
});
|
|
2127
2379
|
};
|
|
2380
|
+
const reinstallHilAnswerNotifications = (): void => {
|
|
2381
|
+
hilAnswerNotificationsUnsubscribe?.();
|
|
2382
|
+
hilAnswerNotificationsUnsubscribe = null;
|
|
2383
|
+
if (!hilAnswerNotificationsActive) return;
|
|
2384
|
+
hilAnswerNotificationsUnsubscribe = installWorkflowHilAnswerNotifications({
|
|
2385
|
+
store,
|
|
2386
|
+
stageUiBroker,
|
|
2387
|
+
state: hilAnswerNotificationState,
|
|
2388
|
+
sendMessage: sendWorkflowNotificationMessage,
|
|
2389
|
+
});
|
|
2390
|
+
};
|
|
2391
|
+
|
|
2392
|
+
async function runWithLifecycleSuppressedForPolicy<T>(
|
|
2393
|
+
policy: WorkflowExecutionPolicy,
|
|
2394
|
+
fn: () => Promise<T>,
|
|
2395
|
+
): Promise<T> {
|
|
2396
|
+
if (policy.mode !== "non_interactive" || policy.awaitTerminalRun !== true) {
|
|
2397
|
+
return fn();
|
|
2398
|
+
}
|
|
2399
|
+
return withWorkflowLifecycleNotificationsSuppressedAsync(
|
|
2400
|
+
lifecycleNotificationState,
|
|
2401
|
+
fn,
|
|
2402
|
+
);
|
|
2403
|
+
}
|
|
2128
2404
|
let intercomParentSession: string | null = null;
|
|
2129
2405
|
const intercomPort = {
|
|
2130
2406
|
emit:
|
|
@@ -2135,9 +2411,11 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2135
2411
|
parentSession: () => intercomParentSession ?? undefined,
|
|
2136
2412
|
};
|
|
2137
2413
|
|
|
2414
|
+
const startupDiscovery = discoverStartupWorkflowsSync();
|
|
2138
2415
|
const runtimeRef: { current: ExtensionRuntime } = {
|
|
2139
2416
|
current: createExtensionRuntime({
|
|
2140
|
-
registry:
|
|
2417
|
+
registry: startupDiscovery.registry,
|
|
2418
|
+
cwd: process.cwd(),
|
|
2141
2419
|
adapters,
|
|
2142
2420
|
cancellation: cancellationRegistry,
|
|
2143
2421
|
persistence: persistenceRef.current,
|
|
@@ -2154,14 +2432,14 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2154
2432
|
get registry() {
|
|
2155
2433
|
return runtimeRef.current.registry;
|
|
2156
2434
|
},
|
|
2157
|
-
dispatch(args) {
|
|
2158
|
-
return runtimeRef.current.dispatch(args);
|
|
2435
|
+
dispatch(args, options) {
|
|
2436
|
+
return runtimeRef.current.dispatch(args, options);
|
|
2159
2437
|
},
|
|
2160
|
-
runDirect(args) {
|
|
2161
|
-
return runtimeRef.current.runDirect(args);
|
|
2438
|
+
runDirect(args, options) {
|
|
2439
|
+
return runtimeRef.current.runDirect(args, options);
|
|
2162
2440
|
},
|
|
2163
|
-
resumeFailedRun(sourceRunId, stageId) {
|
|
2164
|
-
return runtimeRef.current.resumeFailedRun(sourceRunId, stageId);
|
|
2441
|
+
resumeFailedRun(sourceRunId, stageId, options) {
|
|
2442
|
+
return runtimeRef.current.resumeFailedRun(sourceRunId, stageId, options);
|
|
2165
2443
|
},
|
|
2166
2444
|
};
|
|
2167
2445
|
|
|
@@ -2203,6 +2481,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2203
2481
|
if (models === undefined) return runtimeProxy;
|
|
2204
2482
|
return createExtensionRuntime({
|
|
2205
2483
|
registry: runtimeRef.current.registry,
|
|
2484
|
+
cwd: process.cwd(),
|
|
2206
2485
|
adapters,
|
|
2207
2486
|
cancellation: cancellationRegistry,
|
|
2208
2487
|
persistence: persistenceRef.current,
|
|
@@ -2230,6 +2509,16 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2230
2509
|
await reload;
|
|
2231
2510
|
}
|
|
2232
2511
|
|
|
2512
|
+
async function loadPackageWorkflowPaths(): Promise<string[]> {
|
|
2513
|
+
const packageResources =
|
|
2514
|
+
(await pi.refreshWorkflowResources?.()) ??
|
|
2515
|
+
pi.getWorkflowResources?.() ??
|
|
2516
|
+
[];
|
|
2517
|
+
return packageResources
|
|
2518
|
+
.filter((resource) => resource.enabled !== false)
|
|
2519
|
+
.map((resource) => resource.path);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2233
2522
|
async function reloadWorkflowResourcesNow(options?: { allowInFlight?: boolean }): Promise<void> {
|
|
2234
2523
|
const activeRuns = inFlightRunCount();
|
|
2235
2524
|
if (options?.allowInFlight !== true) {
|
|
@@ -2260,9 +2549,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2260
2549
|
)
|
|
2261
2550
|
: undefined;
|
|
2262
2551
|
|
|
2263
|
-
const packageWorkflowPaths = (
|
|
2264
|
-
.filter((resource) => resource.enabled !== false)
|
|
2265
|
-
.map((resource) => resource.path);
|
|
2552
|
+
const packageWorkflowPaths = await loadPackageWorkflowPaths();
|
|
2266
2553
|
const result = await discoverWorkflows({ config: discoveryConfig, packageWorkflowPaths });
|
|
2267
2554
|
discoveryRef.current = result;
|
|
2268
2555
|
|
|
@@ -2289,6 +2576,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2289
2576
|
);
|
|
2290
2577
|
runtimeRef.current = createExtensionRuntime({
|
|
2291
2578
|
registry: result.registry,
|
|
2579
|
+
cwd: process.cwd(),
|
|
2292
2580
|
adapters,
|
|
2293
2581
|
cancellation: cancellationRegistry,
|
|
2294
2582
|
persistence: persistenceRef.current,
|
|
@@ -2325,13 +2613,19 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2325
2613
|
pi.registerTool<WorkflowToolArgs, WorkflowToolResult>({
|
|
2326
2614
|
name: "workflow",
|
|
2327
2615
|
label: "workflow",
|
|
2328
|
-
description:
|
|
2616
|
+
description: WORKFLOW_TOOL_DESCRIPTION,
|
|
2329
2617
|
parameters: workflowParameters,
|
|
2330
2618
|
renderShell: "self",
|
|
2331
2619
|
execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
|
|
2332
2620
|
// Overlay is opt-in via F2 / ctrl+h; do not auto-open from a
|
|
2333
|
-
// tool-call dispatch path.
|
|
2334
|
-
|
|
2621
|
+
// tool-call dispatch path. Awaited non-interactive runs suppress
|
|
2622
|
+
// lifecycle steer notices until the terminal tool result is ready.
|
|
2623
|
+
const policy = workflowPolicyFromContext(ctx);
|
|
2624
|
+
const details = (params.action ?? "run") === "run"
|
|
2625
|
+
? await runWithLifecycleSuppressedForPolicy(policy, () =>
|
|
2626
|
+
executeWorkflowTool(params, ctx),
|
|
2627
|
+
)
|
|
2628
|
+
: await executeWorkflowTool(params, ctx);
|
|
2335
2629
|
return {
|
|
2336
2630
|
content: [{ type: "text", text: renderWorkflowToolContent(details, params) }],
|
|
2337
2631
|
details,
|
|
@@ -2376,18 +2670,41 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2376
2670
|
action: "connect" | "interrupt" | "kill" | "attach" | "pause" | "resume",
|
|
2377
2671
|
rest: string[],
|
|
2378
2672
|
ctx: PiCommandContext,
|
|
2673
|
+
reporter: WorkflowCommandReporter = createWorkflowCommandReporter(ctx),
|
|
2379
2674
|
): Promise<boolean> {
|
|
2380
|
-
const
|
|
2675
|
+
const policy = workflowPolicyFromContext(ctx);
|
|
2676
|
+
const print = (msg: string): void => reporter.info(msg);
|
|
2677
|
+
const fail = (msg: string): void => reporter.error(msg);
|
|
2678
|
+
const canOpenPicker = (ui: PiCommandContext["ui"] | undefined): boolean =>
|
|
2679
|
+
policy.allowInputPicker && typeof ui?.custom === "function";
|
|
2680
|
+
const confirmationPrompt = policy.allowHumanInput && typeof ctx.ui?.confirm === "function"
|
|
2681
|
+
? ctx.ui.confirm.bind(ctx.ui)
|
|
2682
|
+
: undefined;
|
|
2381
2683
|
const theme = deriveGraphTheme({});
|
|
2684
|
+
const failHeadlessAttachCommand = (
|
|
2685
|
+
targetAction: "connect" | "attach",
|
|
2686
|
+
runId: string,
|
|
2687
|
+
stageId?: string,
|
|
2688
|
+
): boolean => {
|
|
2689
|
+
if (policy.allowInputPicker) return false;
|
|
2690
|
+
const displayTarget = stageId
|
|
2691
|
+
? `${runId.slice(0, 8)} stage ${stageId.slice(0, 8)}`
|
|
2692
|
+
: runId.slice(0, 8);
|
|
2693
|
+
fail(
|
|
2694
|
+
`/workflow ${targetAction} requires an interactive UI surface and cannot attach in non-interactive mode. ` +
|
|
2695
|
+
`Target: ${displayTarget}. Use /workflow status ${runId.slice(0, 8)} or the workflow tool's status/stages/transcript actions for non-interactive inspection.`,
|
|
2696
|
+
);
|
|
2697
|
+
return true;
|
|
2698
|
+
};
|
|
2382
2699
|
|
|
2383
2700
|
if (action === "connect") {
|
|
2384
2701
|
const target = rest.find((t) => !t.startsWith("--"));
|
|
2385
2702
|
if (!target) {
|
|
2386
2703
|
// Picker mode — mount the overlay and route the resolved action.
|
|
2387
2704
|
const ui = ctx.ui;
|
|
2388
|
-
if (!ui
|
|
2389
|
-
|
|
2390
|
-
`${renderSessionList(store.runs(), { theme, includeAll: true })}\n\nPicker requires
|
|
2705
|
+
if (!canOpenPicker(ui)) {
|
|
2706
|
+
fail(
|
|
2707
|
+
`${renderSessionList(store.runs(), { theme, includeAll: true })}\n\nPicker requires an interactive UI surface. Pass a runId: /workflow connect <id>`,
|
|
2391
2708
|
);
|
|
2392
2709
|
return true;
|
|
2393
2710
|
}
|
|
@@ -2400,7 +2717,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2400
2717
|
if (result.kind === "kill") {
|
|
2401
2718
|
const run = store.runs().find((r) => r.id === result.runId);
|
|
2402
2719
|
if (!run) {
|
|
2403
|
-
|
|
2720
|
+
fail(`Run not found: ${result.runId}`);
|
|
2404
2721
|
return true;
|
|
2405
2722
|
}
|
|
2406
2723
|
if (run.endedAt !== undefined) {
|
|
@@ -2424,34 +2741,37 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2424
2741
|
run,
|
|
2425
2742
|
previousStatus: killed.previousStatus,
|
|
2426
2743
|
});
|
|
2744
|
+
print(`Run ${killed.runId.slice(0, 8)} killed and retained for inspection.`);
|
|
2745
|
+
} else if (killed.reason === "already_ended") {
|
|
2746
|
+
print(formatAlreadyEndedRetainedMessage(killed.runId));
|
|
2747
|
+
} else {
|
|
2748
|
+
fail(`Run not found: ${result.runId.slice(0, 8)}.`);
|
|
2427
2749
|
}
|
|
2428
|
-
print(
|
|
2429
|
-
killed.ok
|
|
2430
|
-
? `Run ${killed.runId.slice(0, 8)} killed and retained for inspection.`
|
|
2431
|
-
: killed.reason === "already_ended"
|
|
2432
|
-
? formatAlreadyEndedRetainedMessage(killed.runId)
|
|
2433
|
-
: `Run not found: ${result.runId.slice(0, 8)}.`,
|
|
2434
|
-
);
|
|
2435
2750
|
return true;
|
|
2436
2751
|
}
|
|
2437
2752
|
return true;
|
|
2438
2753
|
}
|
|
2439
2754
|
const resolved = resolveRunIdPrefix(target);
|
|
2440
2755
|
if (resolved.kind === "not_found") {
|
|
2441
|
-
|
|
2756
|
+
fail(
|
|
2442
2757
|
`Run not found: ${target}\n\n${renderSessionList(store.runs(), { theme, includeAll: true })}`,
|
|
2443
2758
|
);
|
|
2444
2759
|
return true;
|
|
2445
2760
|
}
|
|
2446
2761
|
if (resolved.kind === "ambiguous") {
|
|
2447
|
-
|
|
2762
|
+
fail(
|
|
2448
2763
|
`Ambiguous run prefix "${target}" matches: ${resolved.matches
|
|
2449
2764
|
.map((id) => id.slice(0, 12))
|
|
2450
2765
|
.join(", ")}`,
|
|
2451
2766
|
);
|
|
2452
2767
|
return true;
|
|
2453
2768
|
}
|
|
2454
|
-
|
|
2769
|
+
if (failHeadlessAttachCommand("connect", resolved.runId)) {
|
|
2770
|
+
return true;
|
|
2771
|
+
}
|
|
2772
|
+
if (policy.allowInputPicker) {
|
|
2773
|
+
overlay.open(resolved.runId, overlaySurfaceFromContext(ctx));
|
|
2774
|
+
}
|
|
2455
2775
|
print(
|
|
2456
2776
|
`Attached to ${resolved.runId.slice(0, 8)}. h/ctrl+d hide · q kill · esc close.`,
|
|
2457
2777
|
);
|
|
@@ -2465,18 +2785,18 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2465
2785
|
if (!target && !wantsAll) {
|
|
2466
2786
|
target = store.activeRunId() ?? undefined;
|
|
2467
2787
|
if (!target) {
|
|
2468
|
-
|
|
2788
|
+
fail("No in-flight runs to interrupt.");
|
|
2469
2789
|
return true;
|
|
2470
2790
|
}
|
|
2471
2791
|
}
|
|
2472
2792
|
if (wantsAll) {
|
|
2473
|
-
const inFlight = store.runs().filter((r) => r.endedAt === undefined);
|
|
2793
|
+
const inFlight = topLevelWorkflowRuns(store.runs()).filter((r) => r.endedAt === undefined);
|
|
2474
2794
|
if (inFlight.length === 0) {
|
|
2475
|
-
|
|
2795
|
+
fail("No in-flight runs to interrupt.");
|
|
2476
2796
|
return true;
|
|
2477
2797
|
}
|
|
2478
|
-
if (!yes &&
|
|
2479
|
-
const ok = await
|
|
2798
|
+
if (!yes && confirmationPrompt) {
|
|
2799
|
+
const ok = await confirmationPrompt(
|
|
2480
2800
|
`Interrupt all ${inFlight.length} in-flight workflow runs?`,
|
|
2481
2801
|
`Pauses: ${inFlight.map((r) => `${r.name} (${r.id.slice(0, 8)})`).join(", ")}`,
|
|
2482
2802
|
);
|
|
@@ -2487,20 +2807,20 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2487
2807
|
}
|
|
2488
2808
|
const results = interruptAllRuns();
|
|
2489
2809
|
const interrupted = results.filter((r) => r.ok).length;
|
|
2490
|
-
|
|
2491
|
-
interrupted
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2810
|
+
if (interrupted > 0) {
|
|
2811
|
+
print(`Interrupted ${interrupted} run(s).`);
|
|
2812
|
+
} else {
|
|
2813
|
+
fail("No in-flight runs to interrupt.");
|
|
2814
|
+
}
|
|
2495
2815
|
return true;
|
|
2496
2816
|
}
|
|
2497
2817
|
const resolved = resolveRunIdPrefix(target!);
|
|
2498
2818
|
if (resolved.kind === "not_found") {
|
|
2499
|
-
|
|
2819
|
+
fail(`Run not found: ${target}`);
|
|
2500
2820
|
return true;
|
|
2501
2821
|
}
|
|
2502
2822
|
if (resolved.kind === "ambiguous") {
|
|
2503
|
-
|
|
2823
|
+
fail(
|
|
2504
2824
|
`Ambiguous run prefix "${target}" matches multiple runs: ${resolved.matches
|
|
2505
2825
|
.map((id) => id.slice(0, 12))
|
|
2506
2826
|
.join(", ")}`,
|
|
@@ -2508,8 +2828,8 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2508
2828
|
return true;
|
|
2509
2829
|
}
|
|
2510
2830
|
const run = store.runs().find((r) => r.id === resolved.runId);
|
|
2511
|
-
if (!yes && run && run.endedAt === undefined &&
|
|
2512
|
-
const confirmed = await
|
|
2831
|
+
if (!yes && run && run.endedAt === undefined && confirmationPrompt) {
|
|
2832
|
+
const confirmed = await confirmationPrompt(
|
|
2513
2833
|
`Interrupt workflow run ${run.name} (${run.id.slice(0, 8)})?`,
|
|
2514
2834
|
"Pauses live work so it can be resumed later.",
|
|
2515
2835
|
);
|
|
@@ -2526,7 +2846,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2526
2846
|
`Run ${result.runId.slice(0, 8)} interrupted and can be resumed.`,
|
|
2527
2847
|
);
|
|
2528
2848
|
} else {
|
|
2529
|
-
|
|
2849
|
+
fail(
|
|
2530
2850
|
result.reason === "not_found"
|
|
2531
2851
|
? `Run not found: ${target}`
|
|
2532
2852
|
: result.reason === "already_ended"
|
|
@@ -2546,18 +2866,18 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2546
2866
|
if (!target && !wantsAll) {
|
|
2547
2867
|
target = store.activeRunId() ?? undefined;
|
|
2548
2868
|
if (!target) {
|
|
2549
|
-
|
|
2869
|
+
fail("No in-flight runs to kill.");
|
|
2550
2870
|
return true;
|
|
2551
2871
|
}
|
|
2552
2872
|
}
|
|
2553
2873
|
if (wantsAll) {
|
|
2554
|
-
const inFlight = store.runs().filter((r) => r.endedAt === undefined);
|
|
2874
|
+
const inFlight = topLevelWorkflowRuns(store.runs()).filter((r) => r.endedAt === undefined);
|
|
2555
2875
|
if (inFlight.length === 0) {
|
|
2556
|
-
|
|
2876
|
+
fail("No in-flight runs to kill.");
|
|
2557
2877
|
return true;
|
|
2558
2878
|
}
|
|
2559
|
-
if (!yes &&
|
|
2560
|
-
const ok = await
|
|
2879
|
+
if (!yes && confirmationPrompt) {
|
|
2880
|
+
const ok = await confirmationPrompt(
|
|
2561
2881
|
`Kill ${inFlight.length} in-flight workflow runs? Killed runs are retained for inspection.`,
|
|
2562
2882
|
`Aborts: ${inFlight.map((r) => `${r.name} (${r.id.slice(0, 8)})`).join(", ")}`,
|
|
2563
2883
|
);
|
|
@@ -2571,20 +2891,20 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2571
2891
|
persistence: persistenceRef.current,
|
|
2572
2892
|
});
|
|
2573
2893
|
const killed = results.filter((r) => r.ok).length;
|
|
2574
|
-
|
|
2575
|
-
killed
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2894
|
+
if (killed > 0) {
|
|
2895
|
+
print(`Killed and retained ${killed} run(s) for inspection.`);
|
|
2896
|
+
} else {
|
|
2897
|
+
fail("No in-flight runs to kill.");
|
|
2898
|
+
}
|
|
2579
2899
|
return true;
|
|
2580
2900
|
}
|
|
2581
2901
|
const resolved = resolveRunIdPrefix(target!);
|
|
2582
2902
|
if (resolved.kind === "not_found") {
|
|
2583
|
-
|
|
2903
|
+
fail(`Run not found: ${target}`);
|
|
2584
2904
|
return true;
|
|
2585
2905
|
}
|
|
2586
2906
|
if (resolved.kind === "ambiguous") {
|
|
2587
|
-
|
|
2907
|
+
fail(
|
|
2588
2908
|
`Ambiguous run prefix "${target}" matches multiple runs: ${resolved.matches
|
|
2589
2909
|
.map((id) => id.slice(0, 12))
|
|
2590
2910
|
.join(", ")}`,
|
|
@@ -2596,7 +2916,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2596
2916
|
print(formatAlreadyEndedRetainedMessage(resolved.runId));
|
|
2597
2917
|
return true;
|
|
2598
2918
|
}
|
|
2599
|
-
if (!yes && run &&
|
|
2919
|
+
if (!yes && run && confirmationPrompt) {
|
|
2600
2920
|
const confirmed = await openKillConfirm(ctx.ui, run, theme);
|
|
2601
2921
|
if (!confirmed) {
|
|
2602
2922
|
print(
|
|
@@ -2620,12 +2940,10 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2620
2940
|
print(
|
|
2621
2941
|
`Run ${result.runId.slice(0, 8)} killed and retained for inspection (was ${result.previousStatus}).`,
|
|
2622
2942
|
);
|
|
2943
|
+
} else if (result.reason === "already_ended") {
|
|
2944
|
+
print(formatAlreadyEndedRetainedMessage(result.runId));
|
|
2623
2945
|
} else {
|
|
2624
|
-
|
|
2625
|
-
result.reason === "already_ended"
|
|
2626
|
-
? formatAlreadyEndedRetainedMessage(result.runId)
|
|
2627
|
-
: `Run not found: ${target}`,
|
|
2628
|
-
);
|
|
2946
|
+
fail(`Run not found: ${target}`);
|
|
2629
2947
|
}
|
|
2630
2948
|
return true;
|
|
2631
2949
|
}
|
|
@@ -2636,9 +2954,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2636
2954
|
let runId: string;
|
|
2637
2955
|
if (!target) {
|
|
2638
2956
|
const ui = ctx.ui;
|
|
2639
|
-
if (!ui
|
|
2640
|
-
|
|
2641
|
-
`${renderSessionList(store.runs(), { theme, includeAll: true })}\n\nPicker requires
|
|
2957
|
+
if (!canOpenPicker(ui)) {
|
|
2958
|
+
fail(
|
|
2959
|
+
`${renderSessionList(store.runs(), { theme, includeAll: true })}\n\nPicker requires an interactive UI surface. Pass a runId: /workflow attach <id> [stageId]`,
|
|
2642
2960
|
);
|
|
2643
2961
|
return true;
|
|
2644
2962
|
}
|
|
@@ -2652,6 +2970,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2652
2970
|
"kill",
|
|
2653
2971
|
[picked.runId, "-y"],
|
|
2654
2972
|
ctx,
|
|
2973
|
+
reporter,
|
|
2655
2974
|
);
|
|
2656
2975
|
}
|
|
2657
2976
|
return true;
|
|
@@ -2660,11 +2979,11 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2660
2979
|
} else {
|
|
2661
2980
|
const resolved = resolveRunIdPrefix(target);
|
|
2662
2981
|
if (resolved.kind === "not_found") {
|
|
2663
|
-
|
|
2982
|
+
fail(`Run not found: ${target}`);
|
|
2664
2983
|
return true;
|
|
2665
2984
|
}
|
|
2666
2985
|
if (resolved.kind === "ambiguous") {
|
|
2667
|
-
|
|
2986
|
+
fail(
|
|
2668
2987
|
`Ambiguous run prefix "${target}" matches: ${resolved.matches.map((id) => id.slice(0, 12)).join(", ")}`,
|
|
2669
2988
|
);
|
|
2670
2989
|
return true;
|
|
@@ -2679,12 +2998,17 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2679
2998
|
exact ?? run.stages.find((s) => s.id.startsWith(stageTarget));
|
|
2680
2999
|
const byName = prefix ?? run.stages.find((s) => s.name === stageTarget);
|
|
2681
3000
|
if (!byName) {
|
|
2682
|
-
|
|
3001
|
+
fail(`Stage not found in run ${runId.slice(0, 8)}: ${stageTarget}`);
|
|
2683
3002
|
return true;
|
|
2684
3003
|
}
|
|
2685
3004
|
stageId = byName.id;
|
|
2686
3005
|
}
|
|
2687
|
-
|
|
3006
|
+
if (failHeadlessAttachCommand("attach", runId, stageId)) {
|
|
3007
|
+
return true;
|
|
3008
|
+
}
|
|
3009
|
+
if (policy.allowInputPicker) {
|
|
3010
|
+
overlay.open(runId, overlaySurfaceFromContext(ctx), stageId);
|
|
3011
|
+
}
|
|
2688
3012
|
const attachedStage = stageId ? run?.stages.find((s) => s.id === stageId) : undefined;
|
|
2689
3013
|
print(
|
|
2690
3014
|
stageId
|
|
@@ -2702,14 +3026,14 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2702
3026
|
let runId: string;
|
|
2703
3027
|
if (!target) {
|
|
2704
3028
|
const ui = ctx.ui;
|
|
2705
|
-
if (!ui
|
|
2706
|
-
const active = store.runs().filter((r) => r.endedAt === undefined);
|
|
3029
|
+
if (!canOpenPicker(ui)) {
|
|
3030
|
+
const active = topLevelWorkflowRuns(store.runs()).filter((r) => r.endedAt === undefined);
|
|
2707
3031
|
if (active.length === 0) {
|
|
2708
|
-
|
|
3032
|
+
fail("No active runs to pause.");
|
|
2709
3033
|
return true;
|
|
2710
3034
|
}
|
|
2711
|
-
|
|
2712
|
-
`Picker requires
|
|
3035
|
+
fail(
|
|
3036
|
+
`Picker requires an interactive UI surface. Active runs:\n${active.map((r) => ` ${r.id.slice(0, 8)} ${r.name}`).join("\n")}\n\nUsage: /workflow pause <runId> [stageId]`,
|
|
2713
3037
|
);
|
|
2714
3038
|
return true;
|
|
2715
3039
|
}
|
|
@@ -2719,11 +3043,11 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2719
3043
|
} else {
|
|
2720
3044
|
const resolved = resolveRunIdPrefix(target);
|
|
2721
3045
|
if (resolved.kind === "not_found") {
|
|
2722
|
-
|
|
3046
|
+
fail(`Run not found: ${target}`);
|
|
2723
3047
|
return true;
|
|
2724
3048
|
}
|
|
2725
3049
|
if (resolved.kind === "ambiguous") {
|
|
2726
|
-
|
|
3050
|
+
fail(
|
|
2727
3051
|
`Ambiguous run prefix "${target}" matches: ${resolved.matches.map((id) => id.slice(0, 12)).join(", ")}`,
|
|
2728
3052
|
);
|
|
2729
3053
|
return true;
|
|
@@ -2731,31 +3055,27 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2731
3055
|
runId = resolved.runId;
|
|
2732
3056
|
}
|
|
2733
3057
|
let stageId: string | undefined;
|
|
3058
|
+
let stageRunId = runId;
|
|
2734
3059
|
if (stageTarget) {
|
|
2735
|
-
const
|
|
2736
|
-
|
|
2737
|
-
(
|
|
2738
|
-
s.id === stageTarget ||
|
|
2739
|
-
s.id.startsWith(stageTarget) ||
|
|
2740
|
-
s.name === stageTarget,
|
|
2741
|
-
);
|
|
2742
|
-
if (!stage) {
|
|
2743
|
-
print(`Stage not found in run ${runId.slice(0, 8)}: ${stageTarget}`);
|
|
3060
|
+
const resolvedStage = resolveStageTarget(runId, stageTarget);
|
|
3061
|
+
if (!resolvedStage.ok) {
|
|
3062
|
+
fail(resolvedStage.message);
|
|
2744
3063
|
return true;
|
|
2745
3064
|
}
|
|
2746
|
-
stageId =
|
|
3065
|
+
stageId = resolvedStage.stageId;
|
|
3066
|
+
stageRunId = resolvedStage.runId ?? runId;
|
|
2747
3067
|
}
|
|
2748
|
-
const result = pauseRun(
|
|
3068
|
+
const result = pauseRun(stageRunId, { stageId });
|
|
2749
3069
|
if (!result.ok) {
|
|
2750
3070
|
const why =
|
|
2751
3071
|
result.reason === "not_found"
|
|
2752
|
-
? `Run not found: ${
|
|
3072
|
+
? `Run not found: ${stageRunId.slice(0, 8)}`
|
|
2753
3073
|
: result.reason === "already_ended"
|
|
2754
|
-
? `Run ${
|
|
3074
|
+
? `Run ${stageRunId.slice(0, 8)} already ended.`
|
|
2755
3075
|
: result.reason === "no_active_stages"
|
|
2756
|
-
? `No pausable stages on run ${
|
|
3076
|
+
? `No pausable stages on run ${stageRunId.slice(0, 8)}.`
|
|
2757
3077
|
: `Stage not found: ${stageTarget ?? "(unknown)"}`;
|
|
2758
|
-
|
|
3078
|
+
fail(why);
|
|
2759
3079
|
return true;
|
|
2760
3080
|
}
|
|
2761
3081
|
// Open the orchestrator overlay (graph for run-level pause, stage
|
|
@@ -2763,13 +3083,13 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2763
3083
|
// the full-screen overlay hides Pi's "Working… (esc to interrupt)"
|
|
2764
3084
|
// spinner, which otherwise stays visible because the host session
|
|
2765
3085
|
// is still streaming whatever was happening before the pause hit.
|
|
2766
|
-
if (
|
|
3086
|
+
if (policy.allowInputPicker) {
|
|
2767
3087
|
overlay.open(runId, overlaySurfaceFromContext(ctx), stageId);
|
|
2768
3088
|
}
|
|
2769
3089
|
print(
|
|
2770
3090
|
result.paused.length === 0
|
|
2771
|
-
? `No stages were paused on run ${
|
|
2772
|
-
: `Paused ${result.paused.length} stage(s) on run ${
|
|
3091
|
+
? `No stages were paused on run ${stageRunId.slice(0, 8)}.`
|
|
3092
|
+
: `Paused ${result.paused.length} stage(s) on run ${stageRunId.slice(0, 8)}: ${result.paused.map((s) => s.name).join(", ")}`,
|
|
2773
3093
|
);
|
|
2774
3094
|
return true;
|
|
2775
3095
|
}
|
|
@@ -2781,8 +3101,8 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2781
3101
|
let runId: string;
|
|
2782
3102
|
if (!target) {
|
|
2783
3103
|
const ui = ctx.ui;
|
|
2784
|
-
if (!ui
|
|
2785
|
-
|
|
3104
|
+
if (!canOpenPicker(ui)) {
|
|
3105
|
+
fail(`Usage: /workflow resume <runId> [stageId] [message…]`);
|
|
2786
3106
|
return true;
|
|
2787
3107
|
}
|
|
2788
3108
|
const picked = await openSessionPicker(ui, store, theme, "resume");
|
|
@@ -2791,11 +3111,11 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2791
3111
|
} else {
|
|
2792
3112
|
const resolved = resolveRunIdPrefix(target);
|
|
2793
3113
|
if (resolved.kind === "not_found") {
|
|
2794
|
-
|
|
3114
|
+
fail(`Run not found: ${target}`);
|
|
2795
3115
|
return true;
|
|
2796
3116
|
}
|
|
2797
3117
|
if (resolved.kind === "ambiguous") {
|
|
2798
|
-
|
|
3118
|
+
fail(
|
|
2799
3119
|
`Ambiguous run prefix "${target}" matches: ${resolved.matches.map((id) => id.slice(0, 12)).join(", ")}`,
|
|
2800
3120
|
);
|
|
2801
3121
|
return true;
|
|
@@ -2803,29 +3123,36 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2803
3123
|
runId = resolved.runId;
|
|
2804
3124
|
}
|
|
2805
3125
|
let stageId: string | undefined;
|
|
2806
|
-
const run = store.runs().find((r) => r.id === runId);
|
|
2807
3126
|
const resolvedStage = resolveStageTarget(runId, stageTarget);
|
|
2808
3127
|
if (!resolvedStage.ok) {
|
|
2809
|
-
|
|
3128
|
+
fail(resolvedStage.message);
|
|
2810
3129
|
return true;
|
|
2811
3130
|
}
|
|
2812
3131
|
stageId = resolvedStage.stageId;
|
|
3132
|
+
const stageRunId = resolvedStage.runId ?? runId;
|
|
3133
|
+
const run = store.runs().find((r) => r.id === stageRunId);
|
|
2813
3134
|
const isPaused =
|
|
2814
3135
|
run?.status === "paused" ||
|
|
2815
3136
|
(run?.stages.some((s) => s.status === "paused") ?? false);
|
|
2816
3137
|
if (!isPaused && run?.status === "failed" && run.endedAt !== undefined && run.resumable !== false) {
|
|
2817
|
-
const continuation = runtimeForContext(ctx).resumeFailedRun(
|
|
2818
|
-
|
|
3138
|
+
const continuation = runtimeForContext(ctx).resumeFailedRun(stageRunId, stageId, { policy });
|
|
3139
|
+
if (continuation.ok) {
|
|
3140
|
+
print(continuation.message);
|
|
3141
|
+
} else {
|
|
3142
|
+
fail(continuation.message);
|
|
3143
|
+
}
|
|
2819
3144
|
return true;
|
|
2820
3145
|
}
|
|
2821
|
-
const result = resumeRun(
|
|
3146
|
+
const result = resumeRun(stageRunId, { stageId, message });
|
|
2822
3147
|
if (!result.ok) {
|
|
2823
|
-
|
|
3148
|
+
fail(`Run not found: ${stageRunId.slice(0, 8)}`);
|
|
2824
3149
|
return true;
|
|
2825
3150
|
}
|
|
2826
3151
|
if (!isPaused) {
|
|
2827
|
-
// Non-paused fallback: reopen the orchestrator overlay as before.
|
|
2828
|
-
|
|
3152
|
+
// Non-paused fallback: reopen the orchestrator overlay as before when interactive.
|
|
3153
|
+
if (policy.allowInputPicker) {
|
|
3154
|
+
overlay.open(result.runId, overlaySurfaceFromContext(ctx));
|
|
3155
|
+
}
|
|
2829
3156
|
print(
|
|
2830
3157
|
result.message ?? `Snapshot available: run ${result.runId} (${result.snapshot.name}) \u2014 status: ${result.snapshot.status}, stages: ${result.snapshot.stages.length}`,
|
|
2831
3158
|
);
|
|
@@ -2834,14 +3161,14 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2834
3161
|
// Paused live resume: when no message was provided and the picker
|
|
2835
3162
|
// is available, open the attached chat so the user can talk to
|
|
2836
3163
|
// the freshly-resumed stage.
|
|
2837
|
-
if (!message && stageId &&
|
|
3164
|
+
if (!message && stageId && policy.allowInputPicker) {
|
|
2838
3165
|
overlay.open(runId, overlaySurfaceFromContext(ctx), stageId);
|
|
2839
3166
|
}
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
3167
|
+
if (result.resumed.length === 0) {
|
|
3168
|
+
fail(`No paused stages on run ${stageRunId.slice(0, 8)}.`);
|
|
3169
|
+
} else {
|
|
3170
|
+
print(`Resumed ${result.resumed.length} stage(s) on run ${stageRunId.slice(0, 8)}${message ? ` with message: "${message}"` : ""}.`);
|
|
3171
|
+
}
|
|
2845
3172
|
return true;
|
|
2846
3173
|
}
|
|
2847
3174
|
|
|
@@ -2853,16 +3180,48 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2853
3180
|
"workflow",
|
|
2854
3181
|
{
|
|
2855
3182
|
description:
|
|
2856
|
-
"Run or inspect
|
|
3183
|
+
"Run or inspect Atomic workflows. Usage: /workflow <name> [key=value…] | /workflow [list|status|connect|attach|interrupt|kill|pause|resume|inputs|reload] [args]",
|
|
2857
3184
|
handler: async (args: string, ctx: PiCommandContext) => {
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
3185
|
+
const policy = workflowPolicyFromContext(ctx);
|
|
3186
|
+
const reporter = createWorkflowCommandReporter(ctx, policy, pi);
|
|
3187
|
+
const print = (msg: string): void => reporter.info(msg);
|
|
3188
|
+
const fail = (msg: string): void => reporter.error(msg);
|
|
3189
|
+
const withImplicitYesFlag = (tokens: string[]): string[] =>
|
|
3190
|
+
tokens.some((t) => t === "--yes" || t === "-y") ? tokens : [...tokens, "-y"];
|
|
3191
|
+
const showWorkflowInputs = async (
|
|
3192
|
+
workflowName: string,
|
|
3193
|
+
command: WorkflowCommandOutputDetails["command"] = "inputs",
|
|
3194
|
+
): Promise<void> => {
|
|
3195
|
+
const result = await runtimeForContext(ctx).dispatch({
|
|
3196
|
+
workflow: workflowName,
|
|
3197
|
+
inputs: {},
|
|
3198
|
+
action: "inputs",
|
|
3199
|
+
}, { policy });
|
|
3200
|
+
if (result.action === "inputs" && "inputs" in result) {
|
|
3201
|
+
const r = result as Extract<
|
|
3202
|
+
WorkflowToolResult,
|
|
3203
|
+
{ action: "inputs" }
|
|
3204
|
+
>;
|
|
3205
|
+
if (r.error) {
|
|
3206
|
+
const available = runtimeProxy.registry.names();
|
|
3207
|
+
fail(
|
|
3208
|
+
`${r.error}\nAvailable: ${formatAvailableWorkflowNames(available)}`,
|
|
3209
|
+
);
|
|
3210
|
+
} else {
|
|
3211
|
+
const schemaText = renderInputsSchema(workflowName, r.inputs, {
|
|
3212
|
+
theme: deriveGraphTheme({}),
|
|
3213
|
+
});
|
|
3214
|
+
if (policy.mode === "non_interactive") {
|
|
3215
|
+
emitWorkflowCommandOutput(pi, schemaText, {
|
|
3216
|
+
command,
|
|
3217
|
+
workflowName,
|
|
3218
|
+
});
|
|
3219
|
+
} else {
|
|
3220
|
+
print(schemaText);
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
};
|
|
2866
3225
|
// Quote-aware split so `prompt="map the codebase"` stays a single
|
|
2867
3226
|
// token. Plain `.split(/\s+/)` would mangle quoted multi-word values
|
|
2868
3227
|
// into `prompt="map`, `the`, `codebase"` — the dispatch confirm then
|
|
@@ -2876,15 +3235,15 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2876
3235
|
// pause — pause a run or specific stage.
|
|
2877
3236
|
// -----------------------------------------------------------------------
|
|
2878
3237
|
if (subcommand === "connect") {
|
|
2879
|
-
await handleRunControlCommand("connect", parts.slice(1), ctx);
|
|
3238
|
+
await handleRunControlCommand("connect", parts.slice(1), ctx, reporter);
|
|
2880
3239
|
return;
|
|
2881
3240
|
}
|
|
2882
3241
|
if (subcommand === "attach") {
|
|
2883
|
-
await handleRunControlCommand("attach", parts.slice(1), ctx);
|
|
3242
|
+
await handleRunControlCommand("attach", parts.slice(1), ctx, reporter);
|
|
2884
3243
|
return;
|
|
2885
3244
|
}
|
|
2886
3245
|
if (subcommand === "pause") {
|
|
2887
|
-
await handleRunControlCommand("pause", parts.slice(1), ctx);
|
|
3246
|
+
await handleRunControlCommand("pause", parts.slice(1), ctx, reporter);
|
|
2888
3247
|
return;
|
|
2889
3248
|
}
|
|
2890
3249
|
|
|
@@ -2898,7 +3257,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2898
3257
|
description: def.description,
|
|
2899
3258
|
inputs: Object.entries(def.inputs).map(([iname, schema]) => ({
|
|
2900
3259
|
name: iname,
|
|
2901
|
-
required: schema
|
|
3260
|
+
required: schemaIsRequired(schema),
|
|
2902
3261
|
})),
|
|
2903
3262
|
}));
|
|
2904
3263
|
emitChatSurface(pi, { kind: "list", entries: items });
|
|
@@ -2915,11 +3274,11 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2915
3274
|
if (target && !target.startsWith("--")) {
|
|
2916
3275
|
const resolved = resolveRunIdPrefix(target);
|
|
2917
3276
|
if (resolved.kind === "not_found") {
|
|
2918
|
-
|
|
3277
|
+
fail(`Run not found: ${target}`);
|
|
2919
3278
|
return;
|
|
2920
3279
|
}
|
|
2921
3280
|
if (resolved.kind === "ambiguous") {
|
|
2922
|
-
|
|
3281
|
+
fail(
|
|
2923
3282
|
`Ambiguous run prefix "${target}" matches: ${resolved.matches
|
|
2924
3283
|
.map((id) => id.slice(0, 12))
|
|
2925
3284
|
.join(", ")}`,
|
|
@@ -2928,7 +3287,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2928
3287
|
}
|
|
2929
3288
|
const inspected = inspectRun(resolved.runId);
|
|
2930
3289
|
if (!inspected.ok) {
|
|
2931
|
-
|
|
3290
|
+
fail(`Run not found: ${target}`);
|
|
2932
3291
|
return;
|
|
2933
3292
|
}
|
|
2934
3293
|
emitChatSurface(pi, { kind: "detail", detail: inspected.detail });
|
|
@@ -2954,14 +3313,14 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2954
3313
|
if (subcommand === "reload") {
|
|
2955
3314
|
const activeRuns = inFlightRunCount();
|
|
2956
3315
|
if (activeRuns > 0) {
|
|
2957
|
-
|
|
3316
|
+
fail(reloadBlockedMessage(activeRuns));
|
|
2958
3317
|
return;
|
|
2959
3318
|
}
|
|
2960
3319
|
try {
|
|
2961
3320
|
await reloadWorkflowResources();
|
|
2962
3321
|
print("Reloaded workflow resources.");
|
|
2963
3322
|
} catch (error) {
|
|
2964
|
-
|
|
3323
|
+
fail(reloadFailureMessage(error));
|
|
2965
3324
|
}
|
|
2966
3325
|
return;
|
|
2967
3326
|
}
|
|
@@ -2975,11 +3334,11 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2975
3334
|
// command should pause immediately, even when a confirm surface is
|
|
2976
3335
|
// unavailable or would steal focus from the running workflow.
|
|
2977
3336
|
const interruptArgs = parts.slice(1);
|
|
2978
|
-
const hasYes = interruptArgs.some((t) => t === "--yes" || t === "-y");
|
|
2979
3337
|
await handleRunControlCommand(
|
|
2980
3338
|
"interrupt",
|
|
2981
|
-
|
|
3339
|
+
withImplicitYesFlag(interruptArgs),
|
|
2982
3340
|
ctx,
|
|
3341
|
+
reporter,
|
|
2983
3342
|
);
|
|
2984
3343
|
return;
|
|
2985
3344
|
}
|
|
@@ -2989,11 +3348,11 @@ function factory(pi: ExtensionAPI): void {
|
|
|
2989
3348
|
// -----------------------------------------------------------------------
|
|
2990
3349
|
if (subcommand === "kill") {
|
|
2991
3350
|
const killArgs = parts.slice(1);
|
|
2992
|
-
const hasYes = killArgs.some((t) => t === "--yes" || t === "-y");
|
|
2993
3351
|
await handleRunControlCommand(
|
|
2994
3352
|
"kill",
|
|
2995
|
-
|
|
3353
|
+
withImplicitYesFlag(killArgs),
|
|
2996
3354
|
ctx,
|
|
3355
|
+
reporter,
|
|
2997
3356
|
);
|
|
2998
3357
|
return;
|
|
2999
3358
|
}
|
|
@@ -3003,7 +3362,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3003
3362
|
// behaviour); paused runs resume live work through the registry.
|
|
3004
3363
|
// -----------------------------------------------------------------------
|
|
3005
3364
|
if (subcommand === "resume") {
|
|
3006
|
-
await handleRunControlCommand("resume", parts.slice(1), ctx);
|
|
3365
|
+
await handleRunControlCommand("resume", parts.slice(1), ctx, reporter);
|
|
3007
3366
|
return;
|
|
3008
3367
|
}
|
|
3009
3368
|
|
|
@@ -3013,32 +3372,10 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3013
3372
|
if (subcommand === "inputs") {
|
|
3014
3373
|
const workflowName = parts[1] ?? "";
|
|
3015
3374
|
if (!workflowName) {
|
|
3016
|
-
|
|
3375
|
+
fail("Usage: /workflow inputs <name>");
|
|
3017
3376
|
return;
|
|
3018
3377
|
}
|
|
3019
|
-
|
|
3020
|
-
workflow: workflowName,
|
|
3021
|
-
inputs: {},
|
|
3022
|
-
action: "inputs",
|
|
3023
|
-
});
|
|
3024
|
-
if (result.action === "inputs" && "inputs" in result) {
|
|
3025
|
-
const r = result as Extract<
|
|
3026
|
-
WorkflowToolResult,
|
|
3027
|
-
{ action: "inputs" }
|
|
3028
|
-
>;
|
|
3029
|
-
if (r.error) {
|
|
3030
|
-
const available = runtimeProxy.registry.names();
|
|
3031
|
-
print(
|
|
3032
|
-
`${r.error}\nAvailable: ${available.length > 0 ? available.join(", ") : "(none)"}`,
|
|
3033
|
-
);
|
|
3034
|
-
} else {
|
|
3035
|
-
print(
|
|
3036
|
-
renderInputsSchema(workflowName, r.inputs, {
|
|
3037
|
-
theme: deriveGraphTheme({}),
|
|
3038
|
-
}),
|
|
3039
|
-
);
|
|
3040
|
-
}
|
|
3041
|
-
}
|
|
3378
|
+
await showWorkflowInputs(workflowName);
|
|
3042
3379
|
return;
|
|
3043
3380
|
}
|
|
3044
3381
|
|
|
@@ -3051,29 +3388,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3051
3388
|
const inputTokens = parts.slice(1);
|
|
3052
3389
|
|
|
3053
3390
|
if (inputTokens.includes("--help")) {
|
|
3054
|
-
|
|
3055
|
-
workflow: workflowName,
|
|
3056
|
-
inputs: {},
|
|
3057
|
-
action: "inputs",
|
|
3058
|
-
});
|
|
3059
|
-
if (helpResult.action === "inputs" && "inputs" in helpResult) {
|
|
3060
|
-
const r = helpResult as Extract<
|
|
3061
|
-
WorkflowToolResult,
|
|
3062
|
-
{ action: "inputs" }
|
|
3063
|
-
>;
|
|
3064
|
-
if (r.error) {
|
|
3065
|
-
const available = runtimeProxy.registry.names();
|
|
3066
|
-
print(
|
|
3067
|
-
`${r.error}\nAvailable: ${available.length > 0 ? available.join(", ") : "(none)"}`,
|
|
3068
|
-
);
|
|
3069
|
-
} else {
|
|
3070
|
-
print(
|
|
3071
|
-
renderInputsSchema(workflowName, r.inputs, {
|
|
3072
|
-
theme: deriveGraphTheme({}),
|
|
3073
|
-
}),
|
|
3074
|
-
);
|
|
3075
|
-
}
|
|
3076
|
-
}
|
|
3391
|
+
await showWorkflowInputs(workflowName, "help");
|
|
3077
3392
|
return;
|
|
3078
3393
|
}
|
|
3079
3394
|
|
|
@@ -3107,6 +3422,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3107
3422
|
// back to the supported overlay picker rather than surfacing the host
|
|
3108
3423
|
// exception as a workflow command error.
|
|
3109
3424
|
const canOpenPicker =
|
|
3425
|
+
policy.allowInputPicker &&
|
|
3110
3426
|
!wantsPickerSkip &&
|
|
3111
3427
|
(typeof ctx.ui?.setEditorComponent === "function" ||
|
|
3112
3428
|
typeof ctx.ui?.custom === "function");
|
|
@@ -3115,7 +3431,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3115
3431
|
workflow: workflowName,
|
|
3116
3432
|
inputs: {},
|
|
3117
3433
|
action: "inputs",
|
|
3118
|
-
});
|
|
3434
|
+
}, { policy });
|
|
3119
3435
|
const schema =
|
|
3120
3436
|
schemaResult.action === "inputs" && "inputs" in schemaResult
|
|
3121
3437
|
? (schemaResult as Extract<
|
|
@@ -3165,26 +3481,38 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3165
3481
|
}
|
|
3166
3482
|
}
|
|
3167
3483
|
|
|
3168
|
-
const result = await
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3484
|
+
const result = await runWithLifecycleSuppressedForPolicy(policy, () =>
|
|
3485
|
+
runtimeForContext(ctx).dispatch({
|
|
3486
|
+
workflow: workflowName,
|
|
3487
|
+
inputs: mergedInputs,
|
|
3488
|
+
action: "run",
|
|
3489
|
+
}, { policy }),
|
|
3490
|
+
);
|
|
3173
3491
|
if (result.action === "run" && "runId" in result) {
|
|
3174
3492
|
const r = result as Extract<
|
|
3175
3493
|
WorkflowToolResult,
|
|
3176
3494
|
{ action: "run"; runId: string }
|
|
3177
3495
|
>;
|
|
3178
3496
|
if (r.status === "failed" && r.runId === "") {
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3497
|
+
if (r.error?.toLowerCase().includes("not found")) {
|
|
3498
|
+
const available = runtimeProxy.registry.names();
|
|
3499
|
+
fail(
|
|
3500
|
+
`Workflow not found: ${workflowName}\nAvailable: ${formatAvailableWorkflowNames(available)}`,
|
|
3501
|
+
);
|
|
3502
|
+
} else {
|
|
3503
|
+
fail(
|
|
3504
|
+
`Workflow "${workflowName}" failed: ${r.error ?? "unknown error"}`,
|
|
3505
|
+
);
|
|
3506
|
+
}
|
|
3183
3507
|
} else if (r.status === "failed") {
|
|
3184
|
-
|
|
3508
|
+
fail(
|
|
3185
3509
|
`Workflow "${workflowName}" failed: ${r.error ?? "unknown error"}`,
|
|
3186
3510
|
);
|
|
3187
3511
|
} else {
|
|
3512
|
+
if (policy.mode === "non_interactive") {
|
|
3513
|
+
emitTerminalRunDetailSurface(pi, workflowName, mergedInputs, r);
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3188
3516
|
// Always-background — the run is alive, the chat is free.
|
|
3189
3517
|
// Route via emitChatSurface so the band+card chrome receives the
|
|
3190
3518
|
// real chat content width via pi-tui's Component contract
|
|
@@ -3239,7 +3567,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3239
3567
|
}));
|
|
3240
3568
|
|
|
3241
3569
|
const runIdItems = (): PiArgumentCompletion[] =>
|
|
3242
|
-
store.runs().map((run) => ({
|
|
3570
|
+
topLevelWorkflowRuns(store.runs()).map((run) => ({
|
|
3243
3571
|
value: `${run.id} `,
|
|
3244
3572
|
label: run.id.slice(0, 8),
|
|
3245
3573
|
description: `${run.name} — ${run.status}`,
|
|
@@ -3368,17 +3696,19 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3368
3696
|
if (equalsIndex > 0) {
|
|
3369
3697
|
const inputName = token.slice(0, equalsIndex);
|
|
3370
3698
|
const schema = workflow.inputs[inputName];
|
|
3371
|
-
|
|
3699
|
+
const schemaChoiceValues = schema === undefined ? undefined : schemaChoices(schema);
|
|
3700
|
+
const schemaKind = schema === undefined ? undefined : schemaFieldKind(schema);
|
|
3701
|
+
if (schemaChoiceValues !== undefined) {
|
|
3372
3702
|
return completeToken(
|
|
3373
3703
|
partial,
|
|
3374
|
-
|
|
3704
|
+
schemaChoiceValues.map((choice) => ({
|
|
3375
3705
|
value: `${inputName}=${choice} `,
|
|
3376
3706
|
label: choice,
|
|
3377
3707
|
description: inputName,
|
|
3378
3708
|
})),
|
|
3379
3709
|
);
|
|
3380
3710
|
}
|
|
3381
|
-
if (
|
|
3711
|
+
if (schemaKind === "boolean") {
|
|
3382
3712
|
return completeToken(partial, [
|
|
3383
3713
|
{
|
|
3384
3714
|
value: `${inputName}=true `,
|
|
@@ -3400,7 +3730,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3400
3730
|
).map(([name, schema]) => ({
|
|
3401
3731
|
value: `${name}=`,
|
|
3402
3732
|
label: name,
|
|
3403
|
-
description: schema
|
|
3733
|
+
description: schemaDescription(schema),
|
|
3404
3734
|
}));
|
|
3405
3735
|
return completeToken(partial, [
|
|
3406
3736
|
{
|
|
@@ -3488,11 +3818,12 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3488
3818
|
});
|
|
3489
3819
|
|
|
3490
3820
|
pi.on("session_start", async (_event, ctx) => {
|
|
3491
|
-
// Non-interactive (`-p` / `--mode json`) sessions
|
|
3492
|
-
//
|
|
3493
|
-
//
|
|
3494
|
-
//
|
|
3495
|
-
|
|
3821
|
+
// Non-interactive (`-p` / `--mode json`) sessions keep the workflow tool
|
|
3822
|
+
// available for deterministic automation. Policy gates disable pickers
|
|
3823
|
+
// and make runtime human-input APIs unavailable.
|
|
3824
|
+
// Defense-in-depth for older/nonstandard hosts: remove only the
|
|
3825
|
+
// unavailable human-input tool from the active tool set.
|
|
3826
|
+
deAdvertiseAskUserQuestionWhenHeadless(pi, ctx?.hasUI);
|
|
3496
3827
|
|
|
3497
3828
|
// Workflow lifecycle is scoped to the originating chat session.
|
|
3498
3829
|
// A new session inherits a clean store; any leftover live runs from a
|
|
@@ -3506,6 +3837,7 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3506
3837
|
});
|
|
3507
3838
|
store.clear();
|
|
3508
3839
|
resetWorkflowLifecycleNotificationState(lifecycleNotificationState);
|
|
3840
|
+
resetWorkflowHilAnswerNotificationState(hilAnswerNotificationState);
|
|
3509
3841
|
stageControlRegistry.clear();
|
|
3510
3842
|
|
|
3511
3843
|
// pi-intercom session naming lives here so we don't trip the
|
|
@@ -3517,7 +3849,9 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3517
3849
|
// tunables must be resolved first.
|
|
3518
3850
|
await discoveryPromise;
|
|
3519
3851
|
lifecycleNotificationsActive = true;
|
|
3852
|
+
hilAnswerNotificationsActive = true;
|
|
3520
3853
|
reinstallLifecycleNotifications();
|
|
3854
|
+
reinstallHilAnswerNotifications();
|
|
3521
3855
|
if (ctx?.ui) {
|
|
3522
3856
|
const diagnostics = formatStartupDiagnostics(configLoadRef.current, discoveryRef.current);
|
|
3523
3857
|
if (diagnostics !== null) {
|
|
@@ -3569,12 +3903,16 @@ function factory(pi: ExtensionAPI): void {
|
|
|
3569
3903
|
cancellation: cancellationRegistry,
|
|
3570
3904
|
persistence: persistenceRef.current,
|
|
3571
3905
|
});
|
|
3906
|
+
stageControlRegistry.clear();
|
|
3572
3907
|
}
|
|
3573
3908
|
storeWidgetUnsubscribe?.();
|
|
3574
3909
|
storeWidgetUnsubscribe = null;
|
|
3575
3910
|
lifecycleNotificationsActive = false;
|
|
3911
|
+
hilAnswerNotificationsActive = false;
|
|
3576
3912
|
lifecycleNotificationsUnsubscribe?.();
|
|
3577
3913
|
lifecycleNotificationsUnsubscribe = null;
|
|
3914
|
+
hilAnswerNotificationsUnsubscribe?.();
|
|
3915
|
+
hilAnswerNotificationsUnsubscribe = null;
|
|
3578
3916
|
});
|
|
3579
3917
|
}
|
|
3580
3918
|
|