@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 5.19.1-fork.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/dist/src/agent-execution.js +81 -39
- package/dist/src/background-widget.d.ts +5 -0
- package/dist/src/background-widget.js +1 -1
- package/dist/src/decoders.js +26 -0
- package/dist/src/execution.d.ts +4 -2
- package/dist/src/execution.js +43 -2
- package/dist/src/host-delivery.js +1 -0
- package/dist/src/host-recovery.d.ts +5 -2
- package/dist/src/host-recovery.js +3 -3
- package/dist/src/host-view.d.ts +5 -0
- package/dist/src/host-view.js +29 -5
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +190 -37
- package/dist/src/index.js +495 -93
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/registry.js +1 -1
- package/dist/src/roles.js +4 -3
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/settings.d.ts +13 -0
- package/dist/src/settings.js +27 -3
- package/dist/src/trajectory.d.ts +12 -2
- package/dist/src/trajectory.js +30 -8
- package/dist/src/types.d.ts +30 -2
- package/dist/src/types.js +1 -1
- package/dist/src/utils.d.ts +19 -0
- package/dist/src/utils.js +30 -0
- package/dist/src/validation.d.ts +6 -1
- package/dist/src/validation.js +21 -1
- package/dist/starter/index.js +0 -2
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +0 -2
- package/dist/subagents/index.js +267 -159
- package/dist/subagents/index.js.map +2 -2
- package/dist/subagents/src/contracts.d.ts +45 -29
- package/dist/subagents/src/contracts.js +18 -7
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +13 -6
- package/dist/subagents/src/manager.js +3 -3
- package/dist/subagents/src/view.d.ts +1 -0
- package/dist/subagents/src/view.js +13 -52
- package/dist/trajectory/assets/index.html +27 -5
- package/dist/trajectory/index.js +76 -28
- package/dist/trajectory/index.js.map +2 -2
- package/dist/trajectory/src/assets/index.html +27 -5
- package/dist/trajectory/src/index.js +8 -18
- package/dist/trajectory/src/server.js +4 -2
- package/dist/trajectory/src/server.js.map +2 -2
- package/package.json +6 -6
- package/skills/pi-extensible-workflows/SKILL.md +2 -0
- package/src/agent-execution.ts +77 -41
- package/src/background-widget.ts +1 -1
- package/src/decoders.ts +20 -0
- package/src/execution.ts +41 -4
- package/src/host-delivery.ts +1 -0
- package/src/host-recovery.ts +8 -7
- package/src/host-view.ts +23 -5
- package/src/host.ts +173 -35
- package/src/paths.ts +3 -0
- package/src/registry.ts +1 -1
- package/src/roles.ts +4 -3
- package/src/script-tools.ts +76 -0
- package/src/settings.ts +22 -4
- package/src/trajectory.ts +30 -11
- package/src/types.ts +11 -4
- package/src/utils.ts +28 -0
- package/src/validation.ts +19 -1
- package/starter/review-loop.ts +0 -2
- package/subagents/README.md +2 -2
- package/subagents/src/contracts.ts +18 -7
- package/subagents/src/index.ts +16 -6
- package/subagents/src/manager.ts +3 -3
- package/subagents/src/view.ts +13 -50
- package/trajectory/src/assets/index.html +27 -5
- package/trajectory/src/index.ts +6 -9
- package/trajectory/src/server.ts +2 -2
package/dist/src/host-view.js
CHANGED
|
@@ -74,6 +74,17 @@ function formatShellActivity(activeShells, startedAt, spinner, styles, now) {
|
|
|
74
74
|
const timing = started && startedAt !== undefined && !Number.isNaN(started.getTime()) ? ` ${styles.dim(`started=${started.toISOString()} elapsed=${formatWorkflowRuntime(Math.max(0, now - startedAt))}`)}` : "";
|
|
75
75
|
return `${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(count)} active)`)}${timing}`;
|
|
76
76
|
}
|
|
77
|
+
function formatToolActivity(run, spinner, styles, now) {
|
|
78
|
+
const calls = run.activeTools ?? [];
|
|
79
|
+
if (!calls.length)
|
|
80
|
+
return undefined;
|
|
81
|
+
const counts = new Map();
|
|
82
|
+
for (const { name } of calls)
|
|
83
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
84
|
+
const names = [...counts].map(([name, count]) => count > 1 ? `${name} ×${String(count)}` : name).join(", ");
|
|
85
|
+
const elapsed = formatWorkflowRuntime(Math.max(0, now - Math.min(...calls.map(({ startedAt }) => startedAt))));
|
|
86
|
+
return `${styles.accent(spinner)} tools ${styles.accent("[running]")} ${sanitizeDisplayText(names)} ${styles.dim(`elapsed=${elapsed}`)}`;
|
|
87
|
+
}
|
|
77
88
|
function formatLogTimestamp(timestamp) {
|
|
78
89
|
if (timestamp === undefined || !Number.isFinite(timestamp))
|
|
79
90
|
return "--:--:--";
|
|
@@ -167,12 +178,16 @@ function workflowProgressLines(run, spinner, styles, now, expanded, width, showH
|
|
|
167
178
|
const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, spinner, styles, now);
|
|
168
179
|
if (shellActivity)
|
|
169
180
|
lines.push(` ${shellActivity}`);
|
|
181
|
+
const toolActivity = formatToolActivity(run, spinner, styles, now);
|
|
182
|
+
if (toolActivity)
|
|
183
|
+
lines.push(` ${toolActivity}`);
|
|
170
184
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
171
185
|
const position = new Map(run.agents.map((agent, index) => [agent, index]));
|
|
172
186
|
const renderAgents = (agents, nested) => renderGroupedAgents(agents, ({ agent, depth }, grouped) => {
|
|
173
|
-
const icon = agentStateGlyph(agent.state,
|
|
187
|
+
const icon = agentStateGlyph(agent.state, "●");
|
|
174
188
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
175
|
-
const
|
|
189
|
+
const idleSpinner = agent.state === "running" && !agentActivityLabel(agent) ? styles.accent(spinner) : "";
|
|
190
|
+
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : [idleSpinner, formatAgentActivity(agent, spinner, styles, now)].filter(Boolean).join(" ");
|
|
176
191
|
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
177
192
|
const state = progressStyleForState(agent.state, styles);
|
|
178
193
|
const detail = expanded ? formatWorkflowAgentDetail(agent, now) : "";
|
|
@@ -493,7 +508,7 @@ export function workflowProgressBlock(run, theme, progress, refresh, invalidate,
|
|
|
493
508
|
invalidate() {
|
|
494
509
|
const displayed = currentRun();
|
|
495
510
|
const now = Date.now();
|
|
496
|
-
if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0))
|
|
511
|
+
if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0 && !displayed.activeTools?.length))
|
|
497
512
|
return;
|
|
498
513
|
if (progress.refresh || now - progress.lastRefreshAt < WORKFLOW_PROGRESS_REFRESH_MS)
|
|
499
514
|
return;
|
|
@@ -606,7 +621,7 @@ export function formatStalledDuration(durationMs) {
|
|
|
606
621
|
const remainingMinutes = minutes % 60;
|
|
607
622
|
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
608
623
|
}
|
|
609
|
-
function stalledDuration(agent, now) {
|
|
624
|
+
export function stalledDuration(agent, now) {
|
|
610
625
|
if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt))
|
|
611
626
|
return undefined;
|
|
612
627
|
const duration = now - agent.lastEventAt;
|
|
@@ -618,7 +633,7 @@ function elapsedDurationMs(agent, end) {
|
|
|
618
633
|
return Math.max(0, agent.durationMs);
|
|
619
634
|
return agent.startedAt === undefined || !Number.isFinite(agent.startedAt) ? undefined : Math.max(0, end - agent.startedAt);
|
|
620
635
|
}
|
|
621
|
-
function agentActivityLabel(agent) {
|
|
636
|
+
export function agentActivityLabel(agent) {
|
|
622
637
|
const activity = agent.activity;
|
|
623
638
|
if (activity?.kind === "reasoning" || activity?.kind === "text") {
|
|
624
639
|
const name = activity.kind === "reasoning" ? "reasoning" : "responding";
|
|
@@ -731,6 +746,9 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees, now = Date
|
|
|
731
746
|
const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
732
747
|
if (shellActivity)
|
|
733
748
|
lines.push(` ${shellActivity}`);
|
|
749
|
+
const toolActivity = formatToolActivity(run, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
750
|
+
if (toolActivity)
|
|
751
|
+
lines.push(` ${toolActivity}`);
|
|
734
752
|
if (run.error)
|
|
735
753
|
lines.push(`Error: ${run.error.code}: ${run.error.message}`);
|
|
736
754
|
if (run.events?.length)
|
|
@@ -776,6 +794,9 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees, now = Date.no
|
|
|
776
794
|
const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
777
795
|
if (shellActivity)
|
|
778
796
|
lines.push(` ${shellActivity}`);
|
|
797
|
+
const toolActivity = formatToolActivity(run, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
798
|
+
if (toolActivity)
|
|
799
|
+
lines.push(` ${toolActivity}`);
|
|
779
800
|
if (run.error)
|
|
780
801
|
lines.push(`Run error: ${run.error.code}: ${run.error.message}`);
|
|
781
802
|
if (run.events?.length)
|
|
@@ -897,6 +918,9 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
|
|
|
897
918
|
const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", styles, now);
|
|
898
919
|
if (shellActivity)
|
|
899
920
|
lines.push(` ${shellActivity}`);
|
|
921
|
+
const toolActivity = formatToolActivity(run, "⠦", styles, now);
|
|
922
|
+
if (toolActivity)
|
|
923
|
+
lines.push(` ${toolActivity}`);
|
|
900
924
|
const actionRows = () => {
|
|
901
925
|
const actions = selection.actions;
|
|
902
926
|
if (!actions)
|
package/dist/src/host.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Type } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import { copyToClipboard, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { type BackgroundWidgetAPI } from "./background-widget.js";
|
|
4
4
|
import { type AgentTransport } from "./types.js";
|
|
5
|
-
export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
|
|
5
|
+
export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Partial<Pick<ExtensionAPI, "getAllTools">> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
|
|
6
6
|
export { agentBreadcrumb, agentBreadcrumbParts, formatBudgetStatus, formatNavigatorDashboard, formatNavigatorRun, formatStalledDuration, formatWorkflowPhaseDashboard, formatWorkflowProgress, navigatorAttentionSort, truncateWorkflowProgress, } from "./host-view.js";
|
|
7
7
|
export { buildWorkflowPhaseModel, buildWorkflowPhaseTree, navigateWorkflowPhaseTree, preserveWorkflowPhaseSelection, preserveWorkflowPhaseTreeSelection, workflowPhaseTreeInitialExpanded, workflowPhaseTreeVisibleNodes } from "./host-phases.js";
|
|
8
8
|
export type { WorkflowPhaseAgentCounts, WorkflowPhaseModel, WorkflowPhaseSelection, WorkflowPhaseState, WorkflowPhaseTree, WorkflowPhaseTreeDirection, WorkflowPhaseTreeNode, WorkflowPhaseTreeNodeKind, WorkflowPhaseTreeSelection, WorkflowPhaseView, } from "./host-phases.js";
|
|
@@ -16,7 +16,7 @@ export declare function formatWorkflowPreview(args: {
|
|
|
16
16
|
description?: unknown;
|
|
17
17
|
}): string;
|
|
18
18
|
export declare const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
19
|
-
export declare const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default";
|
|
19
|
+
export declare const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default. Scripts call this session's tools as `await tools.<name>(args)`, outside withWorktree: a tool resolves to its text unless its description gives a `tools.<name>(args)` result shape, and failures reject";
|
|
20
20
|
export declare const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Provide exactly one of script or scriptPath and a non-empty name. Registered catalog functions are available as globals inside the script; call them there, for example return await someFunction(args). Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. Manage runs from the interactive /workflow picker; use workflow_status, workflow_resume, workflow_retry, workflow_stop, and workflow_respond for explicit tool controls. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately and delivers completion or failure as a follow-up. After failure follow-ups, especially CANCELLED or interrupted runs, call workflow_status({ runId }) before recovery or replacement work, then pass its state as expectedState to workflow_retry/workflow_resume so recovery cannot act on a state that changed. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, expectedState?, foreground? }) replays a failed run into a child; workflow_resume({ runId, expectedState?, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
|
|
21
21
|
export declare const WORKFLOW_TOOL_PARAMETERS: Type.TObject<{
|
|
22
22
|
name: Type.TString;
|
package/dist/src/host.js
CHANGED
|
@@ -12,17 +12,18 @@ import { registerWorkflowNavigator, uiHostCapabilities } from "./host-navigator.
|
|
|
12
12
|
import { acquireSessionLease, isPersistedRun, listPersistedSessionIds, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
|
|
13
13
|
import { retainTerminalRuns } from "./retention.js";
|
|
14
14
|
import { validateBudget, WorkflowBudgetRuntime } from "./budget.js";
|
|
15
|
-
import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue, mergeWorkflowExtensionSettings, modelAliasErrorName, modelCapability, object, parseModelReference, positiveInteger, sanitizeDisplayText, validateModelAliases } from "./utils.js";
|
|
16
|
-
import { loadSettings, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, validateCheckpoint, validateModelAliasAvailability, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowSettingsPath } from "./validation.js";
|
|
15
|
+
import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue, mergeWorkflowExtensionSettings, modelAliasErrorName, modelCapability, object, parseModelReference, positiveInteger, reachableTools, resolveModelReference, VIRTUAL_MODEL_PROVIDER, sanitizeDisplayText, validateModelAliases } from "./utils.js";
|
|
16
|
+
import { loadCodemodeToolsSetting, loadSettings, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, validateCheckpoint, validateModelAliasAvailability, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowSettingsPath, workflowToolExposure } from "./validation.js";
|
|
17
17
|
import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
|
|
18
18
|
import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistryIfIdle, retainWorkflowRegistry } from "./registry.js";
|
|
19
|
-
import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
|
|
19
|
+
import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath, toolIdentityPath } from "./execution.js";
|
|
20
|
+
import { prepareScriptToolLoadout, scriptTool, scriptToolContext, scriptToolValue, validateScriptToolReferences } from "./script-tools.js";
|
|
20
21
|
import backgroundWidget from "./background-widget.js";
|
|
21
22
|
import { showChangelogNotice } from "./changelog.js";
|
|
22
23
|
import { createTrajectoryRunLoader, createTrajectoryRunMetadataLoader, createTrajectorySubagentLoader, createTrajectorySubagentMetadataLoader, createTrajectoryTranscriptLoader } from "./trajectory.js";
|
|
23
24
|
import { getTrajectoryHost } from "./trajectory-host-handle.js";
|
|
24
25
|
import { getSubagentManager } from "./subagent-manager-handle.js";
|
|
25
|
-
import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_BLOCKED_EVENT, WorkflowError, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState, roleNameOf } from "./types.js";
|
|
26
|
+
import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, THINKING_LEVELS, WORKFLOW_BLOCKED_EVENT, WorkflowError, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState, roleNameOf } from "./types.js";
|
|
26
27
|
import { SETTLED_AGENT_STATES, catalogResultValue, formatWorkflowCatalog, styledTextBlock, textBlock, workflowCatalogBlock, workflowControlCall, workflowControlResult, workflowProgressBlock, } from "./host-view.js";
|
|
27
28
|
import { DELIVERY_LIMIT_BYTES, ForegroundDeliveryController, markWorkflowFailureDiagnostics, WORKFLOW_LOG_ENTRY, completionDescriptor, completionDeliveryFromStore, createWorkflowFailureDiagnostics, failureDiagnosticsFrom, formatWorkflowFailure, formatWorkflowFailureDelivery, formatWorkflowFailureDeliveryFallback, formatWorkflowFailureDiagnostics, isWorkflowFailureDiagnostics, serializeWorkflowFailureDiagnostics, utf8Prefix, } from "./host-delivery.js";
|
|
28
29
|
export { agentBreadcrumb, agentBreadcrumbParts, formatBudgetStatus, formatNavigatorDashboard, formatNavigatorRun, formatStalledDuration, formatWorkflowPhaseDashboard, formatWorkflowProgress, navigatorAttentionSort, truncateWorkflowProgress, } from "./host-view.js";
|
|
@@ -55,6 +56,13 @@ function mainAgentError(error) {
|
|
|
55
56
|
Object.assign(presented, typed);
|
|
56
57
|
return presented;
|
|
57
58
|
}
|
|
59
|
+
/** The structured counterpart of {@link completionControlContent}: the result without the persisted run, with the full value. */
|
|
60
|
+
function completionControlValue(result, controlRunId) {
|
|
61
|
+
const record = { ...(controlRunId === undefined ? {} : { runId: controlRunId }), ...result };
|
|
62
|
+
delete record.run;
|
|
63
|
+
delete record.completion;
|
|
64
|
+
return record;
|
|
65
|
+
}
|
|
58
66
|
function completionControlContent(result, controlRunId) {
|
|
59
67
|
const record = object(result) ? { ...result } : undefined;
|
|
60
68
|
if (record && controlRunId !== undefined && record.runId === undefined)
|
|
@@ -104,7 +112,7 @@ export function formatWorkflowPreview(args) {
|
|
|
104
112
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
105
113
|
}
|
|
106
114
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
107
|
-
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default";
|
|
115
|
+
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default. Scripts call this session's tools as `await tools.<name>(args)`, outside withWorktree: a tool resolves to its text unless its description gives a `tools.<name>(args)` result shape, and failures reject";
|
|
108
116
|
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Provide exactly one of script or scriptPath and a non-empty name. Registered catalog functions are available as globals inside the script; call them there, for example return await someFunction(args). Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. Manage runs from the interactive /workflow picker; use workflow_status, workflow_resume, workflow_retry, workflow_stop, and workflow_respond for explicit tool controls. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately and delivers completion or failure as a follow-up. After failure follow-ups, especially CANCELLED or interrupted runs, call workflow_status({ runId }) before recovery or replacement work, then pass its state as expectedState to workflow_retry/workflow_resume so recovery cannot act on a state that changed. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, expectedState?, foreground? }) replays a failed run into a child; workflow_resume({ runId, expectedState?, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
|
|
109
117
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
110
118
|
name: Type.String({ description: "Required non-empty workflow name" }),
|
|
@@ -119,6 +127,21 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
|
119
127
|
}, { additionalProperties: false });
|
|
120
128
|
export const WORKFLOW_STATUS_PARAMETERS = Type.Object({ runId: Type.String({ description: "Workflow run ID visible in the current project" }) }, { additionalProperties: false });
|
|
121
129
|
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }), expectedState: Type.Optional(Type.String({ description: "Persisted source state observed before recovery" })), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) });
|
|
130
|
+
// Structured results codemode scripts receive instead of the JSON text; nested records stay loose so they cannot drift from persistence.
|
|
131
|
+
const WORKFLOW_ERROR_OUTPUT = Type.Object({ code: Type.String(), message: Type.String() });
|
|
132
|
+
const WORKFLOW_LAUNCH_OUTPUT = Type.Object({ runId: Type.String(), state: Type.String(), detached: Type.Optional(Type.Boolean()), value: Type.Optional(Type.Unknown()) });
|
|
133
|
+
const WORKFLOW_STATUS_OUTPUT = Type.Object({ runId: Type.String(), workflowName: Type.String(), state: Type.String(), error: Type.Optional(WORKFLOW_ERROR_OUTPUT), failedAt: Type.Optional(Type.String()), budget: Type.Optional(Type.Unknown()), usage: Type.Optional(Type.Unknown()), phase: Type.Optional(Type.String()), delivery: Type.Optional(Type.Object({ mode: Type.String(), state: Type.String() })), agents: Type.Array(Type.Object({ id: Type.String(), label: Type.Optional(Type.String()), path: Type.String(), state: Type.String(), lastEventAt: Type.Optional(Type.Number()), accounting: Type.Optional(Type.Unknown()) })) });
|
|
134
|
+
const WORKFLOW_STOP_OUTPUT = Type.Object({ runId: Type.String(), state: Type.String(), stopped: Type.Boolean(), reason: Type.Optional(Type.String()) });
|
|
135
|
+
const WORKFLOW_RECOVERY_OUTPUT = Type.Object({ state: Type.String(), runId: Type.Optional(Type.String()), parentRunId: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), value: Type.Optional(Type.Unknown()) });
|
|
136
|
+
const WORKFLOW_RESPOND_OUTPUT = Type.Object({ state: Type.String(), approved: Type.Boolean(), reason: Type.String(), accepted: Type.Optional(Type.Boolean()), runId: Type.Optional(Type.String()), value: Type.Optional(Type.Unknown()) });
|
|
137
|
+
const WORKFLOW_CATALOG_ALIAS_OUTPUT = Type.Object({ name: Type.String(), kind: Type.String(), provenance: Type.String(), version: Type.Optional(Type.String()), headline: Type.Optional(Type.String()) });
|
|
138
|
+
// The index without a name; with one, the function, the model alias, or { error }.
|
|
139
|
+
const WORKFLOW_CATALOG_OUTPUT = Type.Union([
|
|
140
|
+
Type.Object({ functions: Type.Array(Type.Object({ name: Type.String(), description: Type.String(), input: Type.Unknown() })), modelAliasEntries: Type.Optional(Type.Array(WORKFLOW_CATALOG_ALIAS_OUTPUT)), settings: Type.Optional(Type.Unknown()) }),
|
|
141
|
+
Type.Object({ name: Type.String(), version: Type.String(), headline: Type.String(), description: Type.String(), input: Type.Unknown(), output: Type.Unknown() }),
|
|
142
|
+
WORKFLOW_CATALOG_ALIAS_OUTPUT,
|
|
143
|
+
Type.Object({ error: Type.Object({ code: Type.String(), name: Type.String(), message: Type.String() }) }),
|
|
144
|
+
]);
|
|
122
145
|
function workflowToolUpdate(run) {
|
|
123
146
|
//NOTE: renderers read details.run; the partial text stays O(1) so high-frequency updates never format the whole tree.
|
|
124
147
|
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
@@ -204,7 +227,45 @@ function modelInventory(root, registry) {
|
|
|
204
227
|
}
|
|
205
228
|
function resumeHostContext(ctx) {
|
|
206
229
|
const model = object(ctx) && object(ctx.model) && typeof ctx.model.provider === "string" && typeof ctx.model.id === "string" ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
|
|
207
|
-
return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry, deliveryContext: completionContext(ctx) };
|
|
230
|
+
return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry, deliveryContext: completionContext(ctx), toolContext: scriptToolContext(ctx) };
|
|
231
|
+
}
|
|
232
|
+
// State is rewritten on every update, so large shell inputs and errors are kept as prefixes.
|
|
233
|
+
const SCRIPT_CALL_TEXT_LIMIT_BYTES = 16 * 1024;
|
|
234
|
+
function scriptCallInput(input) {
|
|
235
|
+
const text = JSON.stringify(input);
|
|
236
|
+
const bytes = Buffer.byteLength(text);
|
|
237
|
+
return bytes <= SCRIPT_CALL_TEXT_LIMIT_BYTES ? { input } : { input: utf8Prefix(text, SCRIPT_CALL_TEXT_LIMIT_BYTES), inputBytes: bytes };
|
|
238
|
+
}
|
|
239
|
+
/** Records a script call as started, replacing the record of an attempt that an interruption left unfinished. */
|
|
240
|
+
function startScriptCall(run, call) {
|
|
241
|
+
const recorded = { ...call, ...(run.phase === undefined ? {} : { phase: run.phase }) };
|
|
242
|
+
const calls = run.scriptCalls ?? [];
|
|
243
|
+
return { ...run, scriptCalls: calls.some(({ path }) => path === call.path) ? calls.map((current) => current.path === call.path ? recorded : current) : [...calls, recorded] };
|
|
244
|
+
}
|
|
245
|
+
function finishScriptCall(run, path, error) {
|
|
246
|
+
const finishedAt = Date.now();
|
|
247
|
+
return { ...run, scriptCalls: (run.scriptCalls ?? []).map((call) => call.path === path ? { ...call, finishedAt, ...(error ? { error: { code: error.code, ...(call.kind === "shell" ? { message: utf8Prefix(error.message, SCRIPT_CALL_TEXT_LIMIT_BYTES) } : {}) } } : {}) } : call) };
|
|
248
|
+
}
|
|
249
|
+
/** Lists workflow model aliases in `/model` as virtual models; needs a Pi host with `registerVirtualModel`. */
|
|
250
|
+
function registerAliasVirtualModels(pi, names, settingsPath) {
|
|
251
|
+
const register = object(pi) ? asFn(pi.registerVirtualModel) : undefined;
|
|
252
|
+
if (!register)
|
|
253
|
+
return;
|
|
254
|
+
for (const name of names) {
|
|
255
|
+
const definition = {
|
|
256
|
+
provider: VIRTUAL_MODEL_PROVIDER, id: name, name: `${name} (workflow alias)`, thinkingLevels: THINKING_LEVELS,
|
|
257
|
+
route: (request, ctx) => {
|
|
258
|
+
// Settings are read per request so alias edits apply without a reload.
|
|
259
|
+
const aliases = resolveWorkflowSettings(ctx.cwd, projectTrusted(ctx), settingsPath).effective.modelAliases ?? {};
|
|
260
|
+
const target = resolveModelReference(name, aliases, new Set(ctx.modelRegistry.getAll().map((model) => `${model.provider}/${model.id}`)), settingsPath);
|
|
261
|
+
const model = ctx.modelRegistry.find(target.provider, target.model);
|
|
262
|
+
if (!model)
|
|
263
|
+
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${name} resolved to ${target.provider}/${target.model}`);
|
|
264
|
+
return { model, thinkingLevel: request.thinkingLevel };
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
Reflect.apply(register, pi, [definition]);
|
|
268
|
+
}
|
|
208
269
|
}
|
|
209
270
|
async function resolveLaunchAliases(registry, staticAliases, context, availableModels, knownModels, settingsPath) {
|
|
210
271
|
const dynamic = typeof registry.resolveModelAliases === "function" ? await registry.resolveModelAliases(context, new Set(Object.keys(staticAliases))) : {};
|
|
@@ -237,10 +298,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
237
298
|
});
|
|
238
299
|
let backgroundWidgetEnabled = true;
|
|
239
300
|
try {
|
|
240
|
-
|
|
301
|
+
const globalSettings = loadSettings(workflowSettingsPath(extensionAgentDir));
|
|
302
|
+
backgroundWidgetEnabled = globalSettings.backgroundWidget ?? true;
|
|
303
|
+
// Registered at load, before Pi resolves --model and restores the session model. Project aliases need project trust, which is unknown here.
|
|
304
|
+
registerAliasVirtualModels(pi, Object.keys(globalSettings.modelAliases ?? {}), workflowSettingsPath(extensionAgentDir));
|
|
241
305
|
}
|
|
242
306
|
catch { /* Keep the optional UI enabled; the launch path reports settings errors. */ }
|
|
243
307
|
const backgroundWidgetController = backgroundWidget(pi, backgroundWidgetEnabled);
|
|
308
|
+
const codemodeTools = loadCodemodeToolsSetting(extensionAgentDir);
|
|
244
309
|
const logBridge = (store, lifecycle, workflowName) => async (message) => {
|
|
245
310
|
const timestamp = Date.now();
|
|
246
311
|
const bounded = utf8Prefix(message, DELIVERY_LIMIT_BYTES);
|
|
@@ -552,7 +617,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
552
617
|
return readShellResult(replayed.value);
|
|
553
618
|
const shellStartedAt = Date.now();
|
|
554
619
|
let shellPhaseIndex = -1;
|
|
555
|
-
|
|
620
|
+
// Env values stay out of the record: they may carry secrets, and a shared trajectory publishes run state.
|
|
621
|
+
const call = { kind: "shell", name: "shell", path, ...scriptCallInput({ command, ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.env === undefined ? {} : { env: Object.keys(options.env) }), ...(identity.worktreeOwner ? { worktree: identity.worktreeOwner } : {}) }), startedAt: shellStartedAt };
|
|
622
|
+
let failure;
|
|
623
|
+
const started = await persistRunState(store, metadata, (run) => {
|
|
624
|
+
const current = startScriptCall(run, call);
|
|
556
625
|
const history = current.phaseHistory ?? [];
|
|
557
626
|
if (current.phase !== undefined)
|
|
558
627
|
shellPhaseIndex = current.phaseHistoryIndex ?? (history.length ? history.length - 1 : 0);
|
|
@@ -573,8 +642,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
573
642
|
await store.complete(path, result);
|
|
574
643
|
return result;
|
|
575
644
|
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
failure = asWorkflowError(error);
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
576
649
|
finally {
|
|
577
|
-
const stopped = await persistRunState(store, metadata, (
|
|
650
|
+
const stopped = await persistRunState(store, metadata, (run) => {
|
|
651
|
+
const current = finishScriptCall(run, path, failure);
|
|
578
652
|
const phaseActivities = [...(current.activeShellsByPhase ?? [])];
|
|
579
653
|
const phaseActivityIndex = phaseActivities.findIndex(({ phaseIndex }) => phaseIndex === shellPhaseIndex);
|
|
580
654
|
const phaseActivity = phaseActivities[phaseActivityIndex];
|
|
@@ -599,6 +673,64 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
599
673
|
await lifecycle.leave();
|
|
600
674
|
}
|
|
601
675
|
};
|
|
676
|
+
/** A script `tools.<id>(args)` call: replayed from the journal, otherwise run through the session's tool pipeline and journaled. */
|
|
677
|
+
const toolForRun = async (store, lifecycle, context, identifier, args, signal, identity) => {
|
|
678
|
+
await lifecycle.enter();
|
|
679
|
+
try {
|
|
680
|
+
const path = toolIdentityPath(identifier, args, identity);
|
|
681
|
+
const replayed = await store.replay(path);
|
|
682
|
+
if (replayed)
|
|
683
|
+
return replayed.value;
|
|
684
|
+
if (!context)
|
|
685
|
+
fail("UNKNOWN_TOOL", `tools.${identifier} needs a run launched or recovered by a Pi tool call; runs resumed from /workflow or at session start, and headless launches, only replay journaled tool calls`);
|
|
686
|
+
// Rendering is best-effort: it must never lose or mask a completed call.
|
|
687
|
+
const render = async () => { try {
|
|
688
|
+
const update = runs.get(store.runId)?.update;
|
|
689
|
+
if (update)
|
|
690
|
+
update(workflowToolUpdate(withLiveActivities((await store.load()).run)));
|
|
691
|
+
}
|
|
692
|
+
catch { /* best-effort progress */ } };
|
|
693
|
+
// Inspection writes must not prevent execution, mask errors, or reject a journaled success.
|
|
694
|
+
const record = async (update) => { try {
|
|
695
|
+
await store.updateState(update);
|
|
696
|
+
}
|
|
697
|
+
catch { /* best-effort call metadata */ } };
|
|
698
|
+
let value;
|
|
699
|
+
try {
|
|
700
|
+
const tool = scriptTool(context, identifier);
|
|
701
|
+
if (!tool)
|
|
702
|
+
fail("UNKNOWN_TOOL", `tools.${identifier} is not a tool this session can call`);
|
|
703
|
+
const cancelled = () => signal.aborted;
|
|
704
|
+
if (cancelled())
|
|
705
|
+
fail("CANCELLED", "Workflow cancelled");
|
|
706
|
+
await record((run) => startScriptCall(run, { kind: "tool", name: tool.name, path, startedAt: Date.now() }));
|
|
707
|
+
const finished = liveAgents.toolStarted(store.runId, tool.name);
|
|
708
|
+
try {
|
|
709
|
+
await render();
|
|
710
|
+
const outcome = await context.executeTool(tool.name, args, { signal });
|
|
711
|
+
if (cancelled())
|
|
712
|
+
fail("CANCELLED", "Workflow cancelled");
|
|
713
|
+
value = scriptToolValue(tool, outcome);
|
|
714
|
+
}
|
|
715
|
+
finally {
|
|
716
|
+
finished();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
// A stale context throws plain errors from ctx.tools; report them as tool failures.
|
|
721
|
+
const failure = error instanceof WorkflowError ? error : new WorkflowError("TOOL_FAILED", `tools.${identifier}: ${errorText(error)}`);
|
|
722
|
+
await record((run) => finishScriptCall(run, path, failure));
|
|
723
|
+
throw failure;
|
|
724
|
+
}
|
|
725
|
+
await store.complete(path, value);
|
|
726
|
+
await record((run) => finishScriptCall(run, path));
|
|
727
|
+
await render();
|
|
728
|
+
return value;
|
|
729
|
+
}
|
|
730
|
+
finally {
|
|
731
|
+
await lifecycle.leave();
|
|
732
|
+
}
|
|
733
|
+
};
|
|
602
734
|
const lifecycleFor = (store, state, metadata) => new RunLifecycle(state, async (next, previous, reason) => {
|
|
603
735
|
const run = runs.get(store.runId);
|
|
604
736
|
if (!run)
|
|
@@ -884,16 +1016,18 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
884
1016
|
return decision;
|
|
885
1017
|
};
|
|
886
1018
|
};
|
|
887
|
-
const registerControlTool = (name, label, description, parameters, run) => {
|
|
1019
|
+
const registerControlTool = (name, label, description, parameters, outputSchema, run) => {
|
|
888
1020
|
pi.registerTool({
|
|
889
1021
|
name,
|
|
890
1022
|
label,
|
|
891
1023
|
description,
|
|
892
1024
|
parameters,
|
|
1025
|
+
outputSchema,
|
|
1026
|
+
...workflowToolExposure(name, codemodeTools),
|
|
893
1027
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
894
1028
|
try {
|
|
895
1029
|
const result = await run(params, signal, ctx);
|
|
896
|
-
return { content: [{ type: "text", text: result.text }], details: result.details };
|
|
1030
|
+
return { content: [{ type: "text", text: result.text }], details: result.details, structuredContent: result.structured };
|
|
897
1031
|
}
|
|
898
1032
|
catch (error) {
|
|
899
1033
|
throw mainAgentError(error);
|
|
@@ -903,27 +1037,29 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
903
1037
|
renderResult(result, options, theme, context) { return workflowCatalogBlock(workflowControlResult(name, context.args, result, options.expanded, theme, context.isError), options.expanded); },
|
|
904
1038
|
});
|
|
905
1039
|
};
|
|
906
|
-
registerControlTool("workflow_respond", "Workflow Respond", "Approve or reject one pending workflow checkpoint or budget decision", Type.Object({ runId: Type.String(), name: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), approved: Type.Boolean() }, { additionalProperties: false }), async (params, signal, ctx) => {
|
|
1040
|
+
registerControlTool("workflow_respond", "Workflow Respond", "Approve or reject one pending workflow checkpoint or budget decision", Type.Object({ runId: Type.String(), name: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), approved: Type.Boolean() }, { additionalProperties: false }), WORKFLOW_RESPOND_OUTPUT, async (params, signal, ctx) => {
|
|
907
1041
|
if (params.proposalId) {
|
|
908
1042
|
const result = await recovery.answerBudgetDecision(params.runId, params.proposalId, params.approved, false, ctx, signal);
|
|
909
1043
|
if (!result) {
|
|
910
1044
|
const denied = { state: "budget_exhausted", approved: false, reason: "proposal_not_pending" };
|
|
911
|
-
return { text: JSON.stringify(denied), details: denied };
|
|
1045
|
+
return { text: JSON.stringify(denied), details: denied, structured: denied };
|
|
912
1046
|
}
|
|
913
|
-
|
|
1047
|
+
const reason = params.approved ? "approved" : "rejected";
|
|
1048
|
+
return { text: completionControlContent(result, params.runId), details: { ...result, reason }, structured: { ...completionControlValue(result, params.runId), reason } };
|
|
914
1049
|
}
|
|
915
1050
|
if (!params.name)
|
|
916
1051
|
throw new WorkflowError("INVALID_METADATA", "workflow_respond requires name or proposalId");
|
|
917
1052
|
const accepted = await answerCheckpoint(params.runId, params.name, params.approved);
|
|
918
|
-
|
|
1053
|
+
const details = { accepted, state: accepted ? "checkpoint_answered" : "not_pending", approved: params.approved, reason: "checkpoint" };
|
|
1054
|
+
return { text: accepted ? "Checkpoint response accepted." : "Checkpoint is not awaiting a response.", details, structured: details };
|
|
919
1055
|
});
|
|
920
|
-
registerControlTool("workflow_stop", "Workflow Stop", "Stop an active workflow run by ID", Type.Object({ runId: Type.String() }, { additionalProperties: false }), async (params) => {
|
|
1056
|
+
registerControlTool("workflow_stop", "Workflow Stop", "Stop an active workflow run by ID", Type.Object({ runId: Type.String() }, { additionalProperties: false }), WORKFLOW_STOP_OUTPUT, async (params) => {
|
|
921
1057
|
const result = await stopWorkflowRun(params.runId);
|
|
922
|
-
return { text: JSON.stringify(result), details: result };
|
|
1058
|
+
return { text: JSON.stringify(result), details: result, structured: result };
|
|
923
1059
|
});
|
|
924
|
-
registerControlTool("workflow_status", "Workflow Status", "Read a compact summary of a workflow run in the current project", WORKFLOW_STATUS_PARAMETERS, async (params, _signal, ctx) => {
|
|
1060
|
+
registerControlTool("workflow_status", "Workflow Status", "Read a compact summary of a workflow run in the current project", WORKFLOW_STATUS_PARAMETERS, WORKFLOW_STATUS_OUTPUT, async (params, _signal, ctx) => {
|
|
925
1061
|
const result = await workflowStatusRun(params.runId, ctx);
|
|
926
|
-
return { text: JSON.stringify(result), details: result };
|
|
1062
|
+
return { text: JSON.stringify(result), details: result, structured: result };
|
|
927
1063
|
});
|
|
928
1064
|
let catalogRegistered = false;
|
|
929
1065
|
let sessionStarted = false;
|
|
@@ -937,13 +1073,15 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
937
1073
|
return;
|
|
938
1074
|
pi.registerTool({
|
|
939
1075
|
name: "workflow_catalog",
|
|
1076
|
+
...workflowToolExposure("workflow_catalog", codemodeTools),
|
|
1077
|
+
outputSchema: WORKFLOW_CATALOG_OUTPUT,
|
|
940
1078
|
label: "Workflow Catalog",
|
|
941
1079
|
description: "List reusable workflow functions and model aliases; pass `name` to load one entry in full",
|
|
942
1080
|
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or model alias name for full detail" })) }, { additionalProperties: false }),
|
|
943
1081
|
async execute(_id, params = {}) {
|
|
944
1082
|
const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
|
|
945
1083
|
const result = params.name === undefined ? registry.catalogIndex(context) : registry.catalogDetail(params.name, context);
|
|
946
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
1084
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result, structuredContent: result };
|
|
947
1085
|
},
|
|
948
1086
|
renderCall(args, theme) {
|
|
949
1087
|
const title = theme.fg("toolTitle", theme.bold("workflow_catalog"));
|
|
@@ -971,10 +1109,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
971
1109
|
return { definitions, capture: (role, model) => captured.capture(role, model) };
|
|
972
1110
|
};
|
|
973
1111
|
const activeSnapshotTools = (tools, active) => active === "session"
|
|
974
|
-
? new Set(tools.filter((tool) => pi
|
|
1112
|
+
? new Set(tools.filter((tool) => reachableTools(pi).includes(tool) && tool !== "workflow_catalog"))
|
|
975
1113
|
: new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
|
|
976
1114
|
const resumeLaunchPrologue = async (input) => {
|
|
977
|
-
const active = new Set(pi
|
|
1115
|
+
const active = new Set(reachableTools(pi).filter((tool) => !INTERNAL_WORKFLOW_TOOLS.includes(tool)));
|
|
978
1116
|
const missing = input.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
979
1117
|
if (missing)
|
|
980
1118
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
@@ -1053,7 +1191,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1053
1191
|
};
|
|
1054
1192
|
};
|
|
1055
1193
|
const recovery = createWorkflowRecovery({
|
|
1056
|
-
pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, coordinateRunMutation, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue: resumeLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver: (content) => { deliver(pi, content); }, deliverTerminal: deliveryController.deliverTerminal, workflowToolUpdate, registry, modelSpec,
|
|
1194
|
+
pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, coordinateRunMutation, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue: resumeLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, toolForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver: (content) => { deliver(pi, content); }, deliverTerminal: deliveryController.deliverTerminal, workflowToolUpdate, registry, modelSpec,
|
|
1057
1195
|
});
|
|
1058
1196
|
const resumeSelectedWorkflow = async (runId, foreground, context, budgetPatch) => {
|
|
1059
1197
|
const run = runs.get(runId);
|
|
@@ -1120,13 +1258,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1120
1258
|
const completed = await recovery.coldResumeRun(run, hasUI, ui, projectTrusted(context), recoveryContext, foreground, foreground);
|
|
1121
1259
|
return completed ? { workflowName: run.metadata.name, state: "completed", attached: false, value: completed.value } : { workflowName: run.metadata.name, state: "running", attached: false };
|
|
1122
1260
|
};
|
|
1123
|
-
registerControlTool("workflow_retry", "Workflow Retry", "Retry a failed workflow run by replaying its completed structural operations", WORKFLOW_RETRY_PARAMETERS, async (params, signal, ctx) => {
|
|
1261
|
+
registerControlTool("workflow_retry", "Workflow Retry", "Retry a failed workflow run by replaying its completed structural operations", WORKFLOW_RETRY_PARAMETERS, WORKFLOW_RECOVERY_OUTPUT, async (params, signal, ctx) => {
|
|
1124
1262
|
const result = await recovery.retryWorkflowRun(params.runId, ctx, signal, params.foreground, params.expectedState);
|
|
1125
|
-
return { text: completionControlContent(result), details: result };
|
|
1263
|
+
return { text: completionControlContent(result), details: result, structured: completionControlValue(result) };
|
|
1126
1264
|
});
|
|
1127
|
-
registerControlTool("workflow_resume", "Workflow Resume", "Resume an exhausted workflow with unchanged or patched aggregate budgets", Type.Object({ runId: Type.String(), expectedState: Type.Optional(Type.String({ description: "Persisted source state observed before recovery" })), budget: Type.Optional(Type.Unknown()), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) }, { additionalProperties: false }), async (params, signal, ctx) => {
|
|
1265
|
+
registerControlTool("workflow_resume", "Workflow Resume", "Resume an exhausted workflow with unchanged or patched aggregate budgets", Type.Object({ runId: Type.String(), expectedState: Type.Optional(Type.String({ description: "Persisted source state observed before recovery" })), budget: Type.Optional(Type.Unknown()), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) }, { additionalProperties: false }), WORKFLOW_RECOVERY_OUTPUT, async (params, signal, ctx) => {
|
|
1128
1266
|
const result = await recovery.resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground, true, params.expectedState);
|
|
1129
|
-
return { text: completionControlContent(result), details: result };
|
|
1267
|
+
return { text: completionControlContent(result), details: result, structured: completionControlValue(result) };
|
|
1130
1268
|
});
|
|
1131
1269
|
const deliverStaleTerminal = async (store, run) => {
|
|
1132
1270
|
if (!((run.delivery?.mode === "foreground" && run.delivery.state === "attached") || (run.delivery?.mode === "background" && run.delivery.state === "pending")))
|
|
@@ -1268,6 +1406,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1268
1406
|
description: WORKFLOW_TOOL_DESCRIPTION,
|
|
1269
1407
|
promptSnippet: WORKFLOW_TOOL_PROMPT_SNIPPET,
|
|
1270
1408
|
parameters: WORKFLOW_TOOL_PARAMETERS,
|
|
1409
|
+
outputSchema: WORKFLOW_LAUNCH_OUTPUT,
|
|
1410
|
+
prepareLoadout: prepareScriptToolLoadout,
|
|
1271
1411
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1272
1412
|
let resolveDetached;
|
|
1273
1413
|
let foregroundStore;
|
|
@@ -1275,7 +1415,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1275
1415
|
const detachedResult = params.foreground ? new Promise((resolve) => { resolveDetached = resolve; }) : undefined;
|
|
1276
1416
|
const detachedToolResult = (run) => {
|
|
1277
1417
|
const detached = { runId: run.id, state: "running", detached: true };
|
|
1278
|
-
return { content: [{ type: "text", text: JSON.stringify(detached) }], details: { ...detached, run, preview: `Moved workflow ${run.id} to background.` } };
|
|
1418
|
+
return { content: [{ type: "text", text: JSON.stringify(detached) }], details: { ...detached, run, preview: `Moved workflow ${run.id} to background.` }, structuredContent: detached };
|
|
1279
1419
|
};
|
|
1280
1420
|
try {
|
|
1281
1421
|
const headless = object(ctx) && ctx.headless === true;
|
|
@@ -1289,7 +1429,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1289
1429
|
const inventory = modelInventory(rootModel, modelRegistry);
|
|
1290
1430
|
const knownModels = inventory.knownModels;
|
|
1291
1431
|
const availableModels = inventory.availableModels;
|
|
1292
|
-
const rootTools = pi
|
|
1432
|
+
const rootTools = reachableTools(pi).filter((name) => !INTERNAL_WORKFLOW_TOOLS.includes(name));
|
|
1293
1433
|
const trustedProject = projectTrusted(ctx);
|
|
1294
1434
|
const launchCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
1295
1435
|
const launch = workflowLaunchSettings(launchCwd, trustedProject, settingsPath, params.concurrency);
|
|
@@ -1309,6 +1449,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1309
1449
|
registry.validateExtensionSettings(launch.resolution.project.extensionSettings, { source: "project", cwd: ctx.cwd, projectTrusted: true, settingsPath: launch.resolution.projectSettingsPath });
|
|
1310
1450
|
const validated = validateWorkflowLaunchWithRegistry(params, { cwd: ctx.cwd, agentDir: extensionAgentDir, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases, knownModels, settingsPath, ...(settings.extensionSettings === undefined ? {} : { extensionSettings: settings.extensionSettings }) }, registry);
|
|
1311
1451
|
const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames } = validated;
|
|
1452
|
+
const toolContext = scriptToolContext(ctx);
|
|
1453
|
+
validateScriptToolReferences(script, toolContext);
|
|
1312
1454
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
1313
1455
|
const runId = randomUUID();
|
|
1314
1456
|
const args = params.args ?? null;
|
|
@@ -1375,7 +1517,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1375
1517
|
if (params.foreground && onUpdate)
|
|
1376
1518
|
onUpdate(workflowToolUpdate((await store.load()).run));
|
|
1377
1519
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch(), settings.extensionSettings);
|
|
1378
|
-
const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId, (role, model) => capturedRoles.capture(role, model)), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, () => runs.get(runId)?.foreground ?? foregroundAttached, ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(store, lifecycle, checked.metadata.name) }, store, runContext, registry, settings.extensionSettings), runController.signal);
|
|
1520
|
+
const execution = runWorkflow(script, args, withWorkflowFunctions({ tool: (identifier, toolArgs, signal, identity) => toolForRun(store, lifecycle, toolContext, identifier, toolArgs, signal, identity), shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId, (role, model) => capturedRoles.capture(role, model)), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, () => runs.get(runId)?.foreground ?? foregroundAttached, ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(store, lifecycle, checked.metadata.name) }, store, runContext, registry, settings.extensionSettings), runController.signal);
|
|
1379
1521
|
runRecord.execution = execution;
|
|
1380
1522
|
await eventPublisher.runStarted(store, checked.metadata);
|
|
1381
1523
|
const finish = execution.result.then(async (value) => {
|
|
@@ -1416,7 +1558,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1416
1558
|
}, async (error) => {
|
|
1417
1559
|
await deliveryController.deliverTerminal(store, deliverFailureContent(error), true);
|
|
1418
1560
|
});
|
|
1419
|
-
return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
1561
|
+
return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` }, structuredContent: { runId, state: "running" } };
|
|
1420
1562
|
}
|
|
1421
1563
|
void completion.then(async (result) => {
|
|
1422
1564
|
await deliveryController.deliverDetachedTerminal(toolCallId, completionContent("background", result));
|
|
@@ -1442,7 +1584,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1442
1584
|
}
|
|
1443
1585
|
deliveryController.foregroundDeliveries.delete(toolCallId);
|
|
1444
1586
|
const run = (await store.load()).run;
|
|
1445
|
-
return { content: [{ type: "text", text: foregroundDelivery.content }, ...(foregroundDelivery.inlined ? [{ type: "text", text: `Workflow run ID: ${runId}` }] : [])], details: { runId, value, run } };
|
|
1587
|
+
return { content: [{ type: "text", text: foregroundDelivery.content }, ...(foregroundDelivery.inlined ? [{ type: "text", text: `Workflow run ID: ${runId}` }] : [])], details: { runId, value, run }, structuredContent: { runId, state: "completed", value } };
|
|
1446
1588
|
}
|
|
1447
1589
|
catch (error) {
|
|
1448
1590
|
if (params.foreground && foregroundStore && completionInstalled) {
|
|
@@ -1529,7 +1671,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1529
1671
|
return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
|
|
1530
1672
|
},
|
|
1531
1673
|
};
|
|
1532
|
-
pi.registerTool(workflowTool);
|
|
1674
|
+
pi.registerTool({ ...workflowTool, ...workflowToolExposure(workflowTool.name, codemodeTools) });
|
|
1533
1675
|
registerWorkflowNavigator({ pi, home, clipboard, extensionAgentDir, runs, terminalRunStates, hardTerminalRunStates: HARD_TERMINAL_RUN_STATES, ensureSessionLease, coordinateRunMutation, answerCheckpoint, recovery, stopWorkflowRun, moveForegroundToBackground: deliveryController.moveForegroundToBackground, isForegroundAttached: deliveryController.isForegroundAttached, liveAgents, registry, projectTrusted, resumeHostContext, resumeSelectedWorkflow, reportBlocked: reportWorkflowBlocked, trajectoryProvider, setNavigatorOpen: (open) => { if (open)
|
|
1534
1676
|
backgroundWidgetController.suspend();
|
|
1535
1677
|
else
|
|
@@ -1561,6 +1703,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1561
1703
|
}
|
|
1562
1704
|
class LiveAgentRegistry {
|
|
1563
1705
|
#byRun = new Map();
|
|
1706
|
+
#tools = new Map();
|
|
1564
1707
|
get(runId, agentId) { return this.#byRun.get(runId)?.get(agentId); }
|
|
1565
1708
|
setSession(runId, agentId, session) {
|
|
1566
1709
|
if (session)
|
|
@@ -1587,19 +1730,29 @@ class LiveAgentRegistry {
|
|
|
1587
1730
|
if (timestamp !== undefined)
|
|
1588
1731
|
this.#state(runId, agentId).lastEventAt = timestamp;
|
|
1589
1732
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1733
|
+
/** Marks a script tool call in flight until the returned callback runs. */
|
|
1734
|
+
toolStarted(runId, name) {
|
|
1735
|
+
const call = { name, startedAt: Date.now() };
|
|
1736
|
+
const calls = this.#tools.get(runId) ?? new Set();
|
|
1737
|
+
this.#tools.set(runId, calls.add(call));
|
|
1738
|
+
return () => { calls.delete(call); if (!calls.size && this.#tools.get(runId) === calls)
|
|
1739
|
+
this.#tools.delete(runId); };
|
|
1740
|
+
}
|
|
1741
|
+
deleteRun(runId) { this.#byRun.delete(runId); this.#tools.delete(runId); }
|
|
1742
|
+
/** Overlays live activity, freshness, and script tool calls onto a persisted run; returns the input when nothing is live. */
|
|
1592
1743
|
overlay(run) {
|
|
1744
|
+
const tools = this.#tools.get(run.id);
|
|
1745
|
+
const withTools = tools?.size ? { ...run, activeTools: [...tools].map((call) => ({ ...call })) } : run;
|
|
1593
1746
|
const agents = this.#byRun.get(run.id);
|
|
1594
1747
|
if (!agents?.size)
|
|
1595
|
-
return
|
|
1748
|
+
return withTools;
|
|
1596
1749
|
const next = run.agents.map((agent) => {
|
|
1597
1750
|
const live = agents.get(agent.id);
|
|
1598
1751
|
if (!live || (live.activity === undefined && live.lastEventAt === undefined))
|
|
1599
1752
|
return agent;
|
|
1600
1753
|
return { ...agent, ...(live.activity === undefined ? {} : { activity: live.activity }), ...(live.lastEventAt === undefined ? {} : { lastEventAt: live.lastEventAt }) };
|
|
1601
1754
|
});
|
|
1602
|
-
return next.some((agent, index) => agent !== run.agents[index]) ? { ...
|
|
1755
|
+
return next.some((agent, index) => agent !== run.agents[index]) ? { ...withTools, agents: next } : withTools;
|
|
1603
1756
|
}
|
|
1604
1757
|
#state(runId, agentId) {
|
|
1605
1758
|
const agents = this.#byRun.get(runId) ?? new Map();
|