@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 6.2.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 +145 -0
- package/README.md +2 -2
- package/dist/src/agent-execution.d.ts +43 -10
- package/dist/src/agent-execution.js +442 -162
- package/dist/src/background-widget.d.ts +5 -1
- package/dist/src/background-widget.js +5 -10
- package/dist/src/decoders.d.ts +2 -3
- package/dist/src/decoders.js +59 -37
- package/dist/src/execution.d.ts +6 -2
- package/dist/src/execution.js +48 -7
- package/dist/src/host-delivery.js +5 -8
- package/dist/src/host-navigator.js +5 -2
- package/dist/src/host-phases.d.ts +1 -0
- package/dist/src/host-recovery.d.ts +11 -8
- package/dist/src/host-recovery.js +14 -22
- package/dist/src/host-runtime.js +56 -5
- package/dist/src/host-view.d.ts +6 -1
- package/dist/src/host-view.js +46 -20
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +246 -118
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +1985 -1690
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +8 -5
- package/dist/src/paths.js +24 -13
- package/dist/src/persistence.d.ts +1 -0
- package/dist/src/persistence.js +1 -0
- package/dist/src/pi-runtime-runner.js +4 -1
- package/dist/src/registry.d.ts +9 -7
- package/dist/src/registry.js +29 -71
- package/dist/src/runtime/agent-runner.d.ts +2 -0
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/session-lease.d.ts +1 -1
- package/dist/src/session-lease.js +45 -11
- package/dist/src/settings.d.ts +19 -3
- package/dist/src/settings.js +63 -50
- package/dist/src/store.d.ts +3 -1
- package/dist/src/store.js +17 -1
- package/dist/src/trajectory.d.ts +12 -3
- package/dist/src/trajectory.js +32 -11
- package/dist/src/types.d.ts +129 -45
- package/dist/src/types.js +5 -8
- package/dist/src/utils.d.ts +21 -1
- package/dist/src/utils.js +46 -6
- package/dist/src/validation.d.ts +14 -3
- package/dist/src/validation.js +143 -87
- package/dist/starter/index.js +8 -11
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +8 -9
- package/dist/subagents/index.js +904 -956
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +48 -31
- package/dist/subagents/src/contracts.js +20 -9
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +14 -8
- package/dist/subagents/src/manager.js +52 -73
- package/dist/subagents/src/navigator.js +3 -14
- package/dist/subagents/src/view.d.ts +1 -1
- package/dist/subagents/src/view.js +15 -68
- package/dist/subagents/src/worktree.js +1 -1
- package/dist/trajectory/assets/index.html +35 -13
- package/dist/trajectory/index.js +576 -932
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +35 -13
- package/dist/trajectory/src/index.js +134 -104
- package/dist/trajectory/src/server.js +268 -27
- package/dist/trajectory/src/server.js.map +3 -3
- package/dist/trajectory/src/startup-mutex.d.ts +17 -0
- package/dist/trajectory/src/startup-mutex.js +172 -0
- package/examples/workflow-extension-template/README.md +4 -7
- package/examples/workflow-extension-template/extension.test.mjs +4 -8
- package/examples/workflow-extension-template/index.js +0 -3
- package/package.json +10 -10
- package/skills/pi-extensible-workflows/SKILL.md +12 -8
- package/src/agent-execution.ts +412 -164
- package/src/background-widget.ts +5 -11
- package/src/decoders.ts +51 -37
- package/src/execution.ts +46 -9
- package/src/host-delivery.ts +5 -8
- package/src/host-navigator.ts +4 -2
- package/src/host-phases.ts +1 -1
- package/src/host-recovery.ts +23 -28
- package/src/host-runtime.ts +44 -6
- package/src/host-view.ts +41 -16
- package/src/host.ts +229 -142
- package/src/index.ts +1 -2
- package/src/paths.ts +18 -9
- package/src/persistence.ts +1 -0
- package/src/pi-runtime-runner.ts +3 -1
- package/src/registry.ts +28 -64
- package/src/runtime/agent-runner.ts +2 -0
- package/src/script-tools.ts +76 -0
- package/src/session-lease.ts +34 -6
- package/src/settings.ts +56 -50
- package/src/store.ts +18 -2
- package/src/trajectory.ts +32 -15
- package/src/types.ts +82 -34
- package/src/utils.ts +42 -5
- package/src/validation.ts +121 -80
- package/starter/index.ts +0 -1
- package/starter/review-loop.ts +8 -9
- package/starter/roles/developer.md +0 -1
- package/starter/roles/oracle.md +0 -1
- package/starter/roles/researcher.md +0 -1
- package/starter/roles/reviewer.md +0 -1
- package/starter/roles/scout.md +0 -1
- package/subagents/README.md +10 -16
- package/subagents/src/contracts.ts +21 -11
- package/subagents/src/index.ts +17 -8
- package/subagents/src/manager.ts +48 -128
- package/subagents/src/navigator.ts +4 -13
- package/subagents/src/view.ts +16 -63
- package/subagents/src/worktree.ts +1 -1
- package/trajectory/src/assets/index.html +35 -13
- package/trajectory/src/index.ts +112 -91
- package/trajectory/src/server.ts +46 -10
- package/trajectory/src/startup-mutex.ts +127 -0
- package/dist/src/roles.d.ts +0 -70
- package/dist/src/roles.js +0 -325
- package/dist/starter/roles/developer.md +0 -6
- package/dist/starter/roles/oracle.md +0 -18
- package/dist/starter/roles/researcher.md +0 -20
- package/dist/starter/roles/reviewer.md +0 -19
- package/dist/starter/roles/scout.md +0 -18
- package/examples/workflow-extension-template/roles/reviewer.md +0 -4
- package/src/roles.ts +0 -326
package/dist/src/host.js
CHANGED
|
@@ -12,19 +12,19 @@ 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,
|
|
16
|
-
import {
|
|
17
|
-
import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
|
|
15
|
+
import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue, modelAliasErrorName, object, parseModelReference, positiveInteger, reachableTools, resolveModelReference, VIRTUAL_MODEL_PROVIDER, sanitizeDisplayText, validateModelAliases } from "./utils.js";
|
|
16
|
+
import { loadCodemodeToolsSetting, preflight, resolveAgentResourcePolicy, staticAgentPreparationOptions, resolveWorkflowSettings, validateCheckpoint, validateModelAliasAvailability, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowSettingsPath, workflowToolExposure } from "./validation.js";
|
|
18
17
|
import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistryIfIdle, retainWorkflowRegistry } from "./registry.js";
|
|
19
|
-
import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
|
|
18
|
+
import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath, toolIdentityPath } from "./execution.js";
|
|
19
|
+
import { prepareScriptToolLoadout, scriptTool, scriptToolContext, scriptToolValue, validateScriptToolReferences } from "./script-tools.js";
|
|
20
20
|
import backgroundWidget from "./background-widget.js";
|
|
21
21
|
import { showChangelogNotice } from "./changelog.js";
|
|
22
22
|
import { createTrajectoryRunLoader, createTrajectoryRunMetadataLoader, createTrajectorySubagentLoader, createTrajectorySubagentMetadataLoader, createTrajectoryTranscriptLoader } from "./trajectory.js";
|
|
23
23
|
import { getTrajectoryHost } from "./trajectory-host-handle.js";
|
|
24
24
|
import { getSubagentManager } from "./subagent-manager-handle.js";
|
|
25
|
-
import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_BLOCKED_EVENT, WorkflowError, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState
|
|
26
|
-
import { SETTLED_AGENT_STATES, catalogResultValue, formatWorkflowCatalog, styledTextBlock, textBlock, workflowCatalogBlock, workflowControlCall, workflowControlResult, workflowProgressBlock
|
|
27
|
-
import { DELIVERY_LIMIT_BYTES, ForegroundDeliveryController, markWorkflowFailureDiagnostics, WORKFLOW_LOG_ENTRY, completionDescriptor, completionDeliveryFromStore, createWorkflowFailureDiagnostics, failureDiagnosticsFrom, formatWorkflowFailure, formatWorkflowFailureDelivery, formatWorkflowFailureDeliveryFallback, formatWorkflowFailureDiagnostics, isWorkflowFailureDiagnostics, serializeWorkflowFailureDiagnostics, utf8Prefix
|
|
25
|
+
import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, THINKING_LEVELS, WORKFLOW_BLOCKED_EVENT, WorkflowError, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState } from "./types.js";
|
|
26
|
+
import { SETTLED_AGENT_STATES, catalogResultValue, formatWorkflowCatalog, styledTextBlock, textBlock, workflowCatalogBlock, workflowControlCall, workflowControlResult, workflowProgressBlock } from "./host-view.js";
|
|
27
|
+
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
28
|
export { agentBreadcrumb, agentBreadcrumbParts, formatBudgetStatus, formatNavigatorDashboard, formatNavigatorRun, formatStalledDuration, formatWorkflowPhaseDashboard, formatWorkflowProgress, navigatorAttentionSort, truncateWorkflowProgress, } from "./host-view.js";
|
|
29
29
|
export { buildWorkflowPhaseModel, buildWorkflowPhaseTree, navigateWorkflowPhaseTree, preserveWorkflowPhaseSelection, preserveWorkflowPhaseTreeSelection, workflowPhaseTreeInitialExpanded, workflowPhaseTreeVisibleNodes } from "./host-phases.js";
|
|
30
30
|
export { formatWorkflowFailure, formatWorkflowFailureDelivery, formatWorkflowFailureDiagnostics } from "./host-delivery.js";
|
|
@@ -55,6 +55,13 @@ function mainAgentError(error) {
|
|
|
55
55
|
Object.assign(presented, typed);
|
|
56
56
|
return presented;
|
|
57
57
|
}
|
|
58
|
+
/** The structured counterpart of {@link completionControlContent}: the result without the persisted run, with the full value. */
|
|
59
|
+
function completionControlValue(result, controlRunId) {
|
|
60
|
+
const record = { ...(controlRunId === undefined ? {} : { runId: controlRunId }), ...result };
|
|
61
|
+
delete record.run;
|
|
62
|
+
delete record.completion;
|
|
63
|
+
return record;
|
|
64
|
+
}
|
|
58
65
|
function completionControlContent(result, controlRunId) {
|
|
59
66
|
const record = object(result) ? { ...result } : undefined;
|
|
60
67
|
if (record && controlRunId !== undefined && record.runId === undefined)
|
|
@@ -76,27 +83,6 @@ function completionControlContent(result, controlRunId) {
|
|
|
76
83
|
delete record.run;
|
|
77
84
|
return JSON.stringify({ ...record, value });
|
|
78
85
|
}
|
|
79
|
-
/** Persists a role definition (and its model) into the launch snapshot the first time an agent uses it, so later replays and retries see the definition the run actually ran with. */
|
|
80
|
-
function roleCapture(store, initial, definitions, projectDefinitions) {
|
|
81
|
-
let persisted = initial;
|
|
82
|
-
return {
|
|
83
|
-
get snapshot() { return persisted; },
|
|
84
|
-
async capture(role, model) {
|
|
85
|
-
const definition = definitions[role];
|
|
86
|
-
if (!definition)
|
|
87
|
-
return;
|
|
88
|
-
const modelName = `${model.provider}/${model.model}`;
|
|
89
|
-
const hasProjectRole = projectDefinitions[role] !== undefined;
|
|
90
|
-
if (persisted.roles?.[role] !== undefined && (!hasProjectRole || persisted.projectRoles?.includes(role)) && persisted.models.includes(modelName))
|
|
91
|
-
return;
|
|
92
|
-
const roles = { ...(persisted.roles ?? {}), [role]: definition };
|
|
93
|
-
const projectRoles = hasProjectRole ? [...new Set([...(persisted.projectRoles ?? []), role])] : persisted.projectRoles ?? [];
|
|
94
|
-
const models = [...new Set([...persisted.models, modelName])];
|
|
95
|
-
persisted = createLaunchSnapshot({ ...persisted, models, roles, projectRoles });
|
|
96
|
-
await store.saveSnapshot(persisted);
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
86
|
export function formatWorkflowPreview(args) {
|
|
101
87
|
const name = typeof args.name === "string" && args.name.trim() ? args.name.trim() : "workflow";
|
|
102
88
|
if (typeof args.script !== "string" || !args.script.trim())
|
|
@@ -104,7 +90,7 @@ export function formatWorkflowPreview(args) {
|
|
|
104
90
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
105
91
|
}
|
|
106
92
|
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";
|
|
93
|
+
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
94
|
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
95
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
110
96
|
name: Type.String({ description: "Required non-empty workflow name" }),
|
|
@@ -119,6 +105,21 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
|
119
105
|
}, { additionalProperties: false });
|
|
120
106
|
export const WORKFLOW_STATUS_PARAMETERS = Type.Object({ runId: Type.String({ description: "Workflow run ID visible in the current project" }) }, { additionalProperties: false });
|
|
121
107
|
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" })) });
|
|
108
|
+
// Structured results codemode scripts receive instead of the JSON text; nested records stay loose so they cannot drift from persistence.
|
|
109
|
+
const WORKFLOW_ERROR_OUTPUT = Type.Object({ code: Type.String(), message: Type.String() });
|
|
110
|
+
const WORKFLOW_LAUNCH_OUTPUT = Type.Object({ runId: Type.String(), state: Type.String(), detached: Type.Optional(Type.Boolean()), value: Type.Optional(Type.Unknown()) });
|
|
111
|
+
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()) })) });
|
|
112
|
+
const WORKFLOW_STOP_OUTPUT = Type.Object({ runId: Type.String(), state: Type.String(), stopped: Type.Boolean(), reason: Type.Optional(Type.String()) });
|
|
113
|
+
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()) });
|
|
114
|
+
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()) });
|
|
115
|
+
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()) });
|
|
116
|
+
// The index without a name; with one, the function, the model alias, or { error }.
|
|
117
|
+
const WORKFLOW_CATALOG_OUTPUT = Type.Union([
|
|
118
|
+
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()) }),
|
|
119
|
+
Type.Object({ name: Type.String(), version: Type.String(), headline: Type.String(), description: Type.String(), input: Type.Unknown(), output: Type.Unknown() }),
|
|
120
|
+
WORKFLOW_CATALOG_ALIAS_OUTPUT,
|
|
121
|
+
Type.Object({ error: Type.Object({ code: Type.String(), name: Type.String(), message: Type.String() }) }),
|
|
122
|
+
]);
|
|
122
123
|
function workflowToolUpdate(run) {
|
|
123
124
|
//NOTE: renderers read details.run; the partial text stays O(1) so high-frequency updates never format the whole tree.
|
|
124
125
|
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
@@ -204,7 +205,45 @@ function modelInventory(root, registry) {
|
|
|
204
205
|
}
|
|
205
206
|
function resumeHostContext(ctx) {
|
|
206
207
|
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) };
|
|
208
|
+
return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry, deliveryContext: completionContext(ctx), toolContext: scriptToolContext(ctx) };
|
|
209
|
+
}
|
|
210
|
+
// State is rewritten on every update, so large shell inputs and errors are kept as prefixes.
|
|
211
|
+
const SCRIPT_CALL_TEXT_LIMIT_BYTES = 16 * 1024;
|
|
212
|
+
function scriptCallInput(input) {
|
|
213
|
+
const text = JSON.stringify(input);
|
|
214
|
+
const bytes = Buffer.byteLength(text);
|
|
215
|
+
return bytes <= SCRIPT_CALL_TEXT_LIMIT_BYTES ? { input } : { input: utf8Prefix(text, SCRIPT_CALL_TEXT_LIMIT_BYTES), inputBytes: bytes };
|
|
216
|
+
}
|
|
217
|
+
/** Records a script call as started, replacing the record of an attempt that an interruption left unfinished. */
|
|
218
|
+
function startScriptCall(run, call) {
|
|
219
|
+
const recorded = { ...call, ...(run.phase === undefined ? {} : { phase: run.phase }) };
|
|
220
|
+
const calls = run.scriptCalls ?? [];
|
|
221
|
+
return { ...run, scriptCalls: calls.some(({ path }) => path === call.path) ? calls.map((current) => current.path === call.path ? recorded : current) : [...calls, recorded] };
|
|
222
|
+
}
|
|
223
|
+
function finishScriptCall(run, path, error) {
|
|
224
|
+
const finishedAt = Date.now();
|
|
225
|
+
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) };
|
|
226
|
+
}
|
|
227
|
+
/** Lists workflow model aliases in `/model` as virtual models; needs a Pi host with `registerVirtualModel`. */
|
|
228
|
+
function registerAliasVirtualModels(pi, names, settingsPath) {
|
|
229
|
+
const register = object(pi) ? asFn(pi.registerVirtualModel) : undefined;
|
|
230
|
+
if (!register)
|
|
231
|
+
return;
|
|
232
|
+
for (const name of names) {
|
|
233
|
+
const definition = {
|
|
234
|
+
provider: VIRTUAL_MODEL_PROVIDER, id: name, name: `${name} (workflow alias)`, thinkingLevels: THINKING_LEVELS,
|
|
235
|
+
route: (request, ctx) => {
|
|
236
|
+
// Settings are read per request so alias edits apply without a reload.
|
|
237
|
+
const aliases = resolveWorkflowSettings(ctx.cwd, projectTrusted(ctx), settingsPath).effective.modelAliases ?? {};
|
|
238
|
+
const target = resolveModelReference(name, aliases, new Set(ctx.modelRegistry.getAll().map((model) => `${model.provider}/${model.id}`)), settingsPath);
|
|
239
|
+
const model = ctx.modelRegistry.find(target.provider, target.model);
|
|
240
|
+
if (!model)
|
|
241
|
+
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${name} resolved to ${target.provider}/${target.model}`);
|
|
242
|
+
return { model, thinkingLevel: request.thinkingLevel };
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
Reflect.apply(register, pi, [definition]);
|
|
246
|
+
}
|
|
208
247
|
}
|
|
209
248
|
async function resolveLaunchAliases(registry, staticAliases, context, availableModels, knownModels, settingsPath) {
|
|
210
249
|
const dynamic = typeof registry.resolveModelAliases === "function" ? await registry.resolveModelAliases(context, new Set(Object.keys(staticAliases))) : {};
|
|
@@ -222,6 +261,21 @@ async function resolveLaunchAliases(registry, staticAliases, context, availableM
|
|
|
222
261
|
throw error;
|
|
223
262
|
}
|
|
224
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Prepares each completely static agent configuration in inspection mode, so preparation plugins resolve their own aliases
|
|
266
|
+
* and options without persisting a configuration. Any preparation failure rejects launch before effects; cancellation stops
|
|
267
|
+
* the remaining inspections.
|
|
268
|
+
*/
|
|
269
|
+
async function inspectStaticAgentConfigurations(executor, script, workflowName, signal, functions) {
|
|
270
|
+
// NOTE: inspection uses the launch cwd; agents inside withWorktree(...) prepare against their checkout at runtime.
|
|
271
|
+
for (const agentOptions of staticAgentPreparationOptions(script, functions)) {
|
|
272
|
+
if (signal.aborted)
|
|
273
|
+
throw new WorkflowError("CANCELLED", "Workflow launch cancelled");
|
|
274
|
+
await executor.prepare({ label: "agent", workflowName, agentOptions }, undefined, signal, "inspection");
|
|
275
|
+
}
|
|
276
|
+
if (signal.aborted)
|
|
277
|
+
throw new WorkflowError("CANCELLED", "Workflow launch cancelled");
|
|
278
|
+
}
|
|
225
279
|
export default function workflowExtension(pi, home, clipboard = copyToClipboard, transport = localAgentTransport, agentDir, additionalSkillPaths = []) {
|
|
226
280
|
beginWorkflowExtensionLoading();
|
|
227
281
|
const registry = loadingRegistry();
|
|
@@ -237,10 +291,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
237
291
|
});
|
|
238
292
|
let backgroundWidgetEnabled = true;
|
|
239
293
|
try {
|
|
240
|
-
|
|
294
|
+
const globalSettings = resolveWorkflowSettings(process.cwd(), false, workflowSettingsPath(extensionAgentDir)).effective;
|
|
295
|
+
backgroundWidgetEnabled = globalSettings.backgroundWidget ?? true;
|
|
296
|
+
// Registered at load, before Pi resolves --model and restores the session model. Project aliases need project trust, which is unknown here.
|
|
297
|
+
registerAliasVirtualModels(pi, Object.keys(globalSettings.modelAliases ?? {}), workflowSettingsPath(extensionAgentDir));
|
|
241
298
|
}
|
|
242
299
|
catch { /* Keep the optional UI enabled; the launch path reports settings errors. */ }
|
|
243
300
|
const backgroundWidgetController = backgroundWidget(pi, backgroundWidgetEnabled);
|
|
301
|
+
const codemodeTools = loadCodemodeToolsSetting(extensionAgentDir);
|
|
244
302
|
const logBridge = (store, lifecycle, workflowName) => async (message) => {
|
|
245
303
|
const timestamp = Date.now();
|
|
246
304
|
const bounded = utf8Prefix(message, DELIVERY_LIMIT_BYTES);
|
|
@@ -273,6 +331,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
273
331
|
return skillPath ? { skillPaths: [skillPath] } : undefined;
|
|
274
332
|
});
|
|
275
333
|
const runs = new Map();
|
|
334
|
+
// Extension load failures a headless launcher reported for this host. They hold for the whole process, so every executor,
|
|
335
|
+
// including those that paused resume, cold resume and retry create, keeps failing ownerless options closed.
|
|
336
|
+
let extensionLoad;
|
|
276
337
|
const deliveryController = new ForegroundDeliveryController({ runs, ...(typeof pi.sendMessage === "function" ? { deliver: (content) => { deliver(pi, content); } } : {}) });
|
|
277
338
|
let releaseWorkflowRegistry;
|
|
278
339
|
const providerRecoveryLane = new SerialLane();
|
|
@@ -552,7 +613,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
552
613
|
return readShellResult(replayed.value);
|
|
553
614
|
const shellStartedAt = Date.now();
|
|
554
615
|
let shellPhaseIndex = -1;
|
|
555
|
-
|
|
616
|
+
// Env values stay out of the record: they may carry secrets, and a shared trajectory publishes run state.
|
|
617
|
+
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 };
|
|
618
|
+
let failure;
|
|
619
|
+
const started = await persistRunState(store, metadata, (run) => {
|
|
620
|
+
const current = startScriptCall(run, call);
|
|
556
621
|
const history = current.phaseHistory ?? [];
|
|
557
622
|
if (current.phase !== undefined)
|
|
558
623
|
shellPhaseIndex = current.phaseHistoryIndex ?? (history.length ? history.length - 1 : 0);
|
|
@@ -573,8 +638,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
573
638
|
await store.complete(path, result);
|
|
574
639
|
return result;
|
|
575
640
|
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
failure = asWorkflowError(error);
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
576
645
|
finally {
|
|
577
|
-
const stopped = await persistRunState(store, metadata, (
|
|
646
|
+
const stopped = await persistRunState(store, metadata, (run) => {
|
|
647
|
+
const current = finishScriptCall(run, path, failure);
|
|
578
648
|
const phaseActivities = [...(current.activeShellsByPhase ?? [])];
|
|
579
649
|
const phaseActivityIndex = phaseActivities.findIndex(({ phaseIndex }) => phaseIndex === shellPhaseIndex);
|
|
580
650
|
const phaseActivity = phaseActivities[phaseActivityIndex];
|
|
@@ -599,6 +669,64 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
599
669
|
await lifecycle.leave();
|
|
600
670
|
}
|
|
601
671
|
};
|
|
672
|
+
/** A script `tools.<id>(args)` call: replayed from the journal, otherwise run through the session's tool pipeline and journaled. */
|
|
673
|
+
const toolForRun = async (store, lifecycle, context, identifier, args, signal, identity) => {
|
|
674
|
+
await lifecycle.enter();
|
|
675
|
+
try {
|
|
676
|
+
const path = toolIdentityPath(identifier, args, identity);
|
|
677
|
+
const replayed = await store.replay(path);
|
|
678
|
+
if (replayed)
|
|
679
|
+
return replayed.value;
|
|
680
|
+
if (!context)
|
|
681
|
+
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`);
|
|
682
|
+
// Rendering is best-effort: it must never lose or mask a completed call.
|
|
683
|
+
const render = async () => { try {
|
|
684
|
+
const update = runs.get(store.runId)?.update;
|
|
685
|
+
if (update)
|
|
686
|
+
update(workflowToolUpdate(withLiveActivities((await store.load()).run)));
|
|
687
|
+
}
|
|
688
|
+
catch { /* best-effort progress */ } };
|
|
689
|
+
// Inspection writes must not prevent execution, mask errors, or reject a journaled success.
|
|
690
|
+
const record = async (update) => { try {
|
|
691
|
+
await store.updateState(update);
|
|
692
|
+
}
|
|
693
|
+
catch { /* best-effort call metadata */ } };
|
|
694
|
+
let value;
|
|
695
|
+
try {
|
|
696
|
+
const tool = scriptTool(context, identifier);
|
|
697
|
+
if (!tool)
|
|
698
|
+
fail("UNKNOWN_TOOL", `tools.${identifier} is not a tool this session can call`);
|
|
699
|
+
const cancelled = () => signal.aborted;
|
|
700
|
+
if (cancelled())
|
|
701
|
+
fail("CANCELLED", "Workflow cancelled");
|
|
702
|
+
await record((run) => startScriptCall(run, { kind: "tool", name: tool.name, path, startedAt: Date.now() }));
|
|
703
|
+
const finished = liveAgents.toolStarted(store.runId, tool.name);
|
|
704
|
+
try {
|
|
705
|
+
await render();
|
|
706
|
+
const outcome = await context.executeTool(tool.name, args, { signal });
|
|
707
|
+
if (cancelled())
|
|
708
|
+
fail("CANCELLED", "Workflow cancelled");
|
|
709
|
+
value = scriptToolValue(tool, outcome);
|
|
710
|
+
}
|
|
711
|
+
finally {
|
|
712
|
+
finished();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
// A stale context throws plain errors from ctx.tools; report them as tool failures.
|
|
717
|
+
const failure = error instanceof WorkflowError ? error : new WorkflowError("TOOL_FAILED", `tools.${identifier}: ${errorText(error)}`);
|
|
718
|
+
await record((run) => finishScriptCall(run, path, failure));
|
|
719
|
+
throw failure;
|
|
720
|
+
}
|
|
721
|
+
await store.complete(path, value);
|
|
722
|
+
await record((run) => finishScriptCall(run, path));
|
|
723
|
+
await render();
|
|
724
|
+
return value;
|
|
725
|
+
}
|
|
726
|
+
finally {
|
|
727
|
+
await lifecycle.leave();
|
|
728
|
+
}
|
|
729
|
+
};
|
|
602
730
|
const lifecycleFor = (store, state, metadata) => new RunLifecycle(state, async (next, previous, reason) => {
|
|
603
731
|
const run = runs.get(store.runId);
|
|
604
732
|
if (!run)
|
|
@@ -685,7 +813,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
685
813
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, lastEventAt } : agent) }));
|
|
686
814
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
687
815
|
};
|
|
688
|
-
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, tuiIndex, tuiLabel: options.requestedLabel ?? options.label, agentNodeId: id, ...(options.extensionSettings === undefined ? {} : { inheritedExtensionSettings: options.extensionSettings }), onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.
|
|
816
|
+
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, tuiIndex, tuiLabel: options.requestedLabel ?? options.label, agentNodeId: id, ...(options.extensionSettings === undefined ? {} : { inheritedExtensionSettings: options.extensionSettings }), onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.contextFiles ? { contextFiles: options.contextFiles } : {}), tools: options.tools, ...(options.skills ? { skills: options.skills } : {}), ...(options.extensions ? { extensions: options.extensions } : {}), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.sessionPath ? { sessionPath: options.sessionPath } : {}), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}), ...(options.configuration ? { configuration: options.configuration } : {}), ...(options.capabilities ? { capabilities: options.capabilities } : {}), ...(options.projectTrusted === undefined ? {} : { projectTrusted: options.projectTrusted }), ...(options.inheritedExcludeTools ? { inheritedExcludeTools: options.inheritedExcludeTools } : {}), onConfiguration: (configuration) => scheduler.setConfiguration(id, configuration) }, signal, scheduler.toolsFor(id), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
|
|
689
817
|
cancelDeferredProgress();
|
|
690
818
|
const before = (await run.store.load()).run;
|
|
691
819
|
await persistAgentAttempts(run.store, id, result.attempts);
|
|
@@ -723,7 +851,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
723
851
|
const existing = new Map(current.agents.map((agent) => [agent.id, agent]));
|
|
724
852
|
const agents = ownership.map((node) => {
|
|
725
853
|
const previous = existing.get(node.id);
|
|
726
|
-
const requested = { label: node.options.label, workflowName: run.metadata.name, ...(node.options.skills ? { skills: node.options.skills } : {}), ...(node.options.extensions ? { extensions: node.options.extensions } : {}), ...(node.options.model ? { model: node.options.model } : {}), ...(node.options.
|
|
854
|
+
const requested = { label: node.options.label, workflowName: run.metadata.name, ...(node.options.skills ? { skills: node.options.skills } : {}), ...(node.options.extensions ? { extensions: node.options.extensions } : {}), ...(node.options.model ? { model: node.options.model } : {}), ...(node.options.contextFiles ? { contextFiles: node.options.contextFiles } : {}), ...(node.options.configuration ? { configuration: node.options.configuration } : {}) };
|
|
727
855
|
let effective;
|
|
728
856
|
try {
|
|
729
857
|
effective = { ...run.executor.resolve({ ...requested, effectiveTools: node.options.tools }), tools: node.options.tools };
|
|
@@ -732,12 +860,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
732
860
|
effective = previous ? { model: previous.model, ...(previous.requestedModel ? { requestedModel: previous.requestedModel } : {}), tools: previous.tools } : { model: node.options.model ? modelSpec(node.options.model, run.model) : run.model, ...(node.options.model ? { requestedModel: node.options.model } : {}), tools: node.options.tools };
|
|
733
861
|
}
|
|
734
862
|
const resultPath = !node.parentId && node.options.agentIdentity ? agentIdentityPath(node.options.agentIdentity) : undefined;
|
|
735
|
-
const nodeRole = roleNameOf(node.options.role);
|
|
736
863
|
const now = Date.now();
|
|
737
864
|
const lastEventAt = node.state === "running" ? previous?.state === "running" && previous.lastEventAt !== undefined ? previous.lastEventAt : now : previous?.lastEventAt;
|
|
738
865
|
const startedAt = previous?.startedAt ?? (node.state === "running" ? now : undefined);
|
|
739
866
|
const durationMs = previous?.durationMs ?? (SETTLED_AGENT_STATES.has(node.state) && startedAt !== undefined ? Math.max(0, now - startedAt) : undefined);
|
|
740
|
-
return { ...(previous?.systemPrompt === undefined ? {} : { systemPrompt: previous.systemPrompt }), ...(node.prompt !== undefined ? { prompt: node.prompt } : previous?.prompt !== undefined ? { prompt: previous.prompt } : {}), id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.agentIdentity?.handle === undefined ? {} : { handle: node.options.agentIdentity.handle, ...(node.options.agentIdentity.turn === undefined ? {} : { turn: node.options.agentIdentity.turn }), ...(node.options.continuity ? { continuity: node.options.continuity } : {}) }), ...(
|
|
867
|
+
return { ...(previous?.systemPrompt === undefined ? {} : { systemPrompt: previous.systemPrompt }), ...(node.prompt !== undefined ? { prompt: node.prompt } : previous?.prompt !== undefined ? { prompt: previous.prompt } : {}), id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.agentIdentity?.handle === undefined ? {} : { handle: node.options.agentIdentity.handle, ...(node.options.agentIdentity.turn === undefined ? {} : { turn: node.options.agentIdentity.turn }), ...(node.options.continuity ? { continuity: node.options.continuity } : {}) }), ...(effective.requestedModel ? { requestedModel: effective.requestedModel } : {}), model: effective.model, tools: effective.tools, attempts: previous?.attempts ?? 0, ...(startedAt === undefined ? {} : { startedAt }), ...(durationMs === undefined ? {} : { durationMs }), ...(previous?.attemptDetails ? { attemptDetails: previous.attemptDetails } : {}), ...(previous?.accounting ? { accounting: previous.accounting } : {}), ...(previous?.toolCalls ? { toolCalls: previous.toolCalls } : {}), ...(previous?.activity ? { activity: previous.activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
|
|
741
868
|
});
|
|
742
869
|
return { ...current, agents };
|
|
743
870
|
});
|
|
@@ -884,16 +1011,18 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
884
1011
|
return decision;
|
|
885
1012
|
};
|
|
886
1013
|
};
|
|
887
|
-
const registerControlTool = (name, label, description, parameters, run) => {
|
|
1014
|
+
const registerControlTool = (name, label, description, parameters, outputSchema, run) => {
|
|
888
1015
|
pi.registerTool({
|
|
889
1016
|
name,
|
|
890
1017
|
label,
|
|
891
1018
|
description,
|
|
892
1019
|
parameters,
|
|
1020
|
+
outputSchema,
|
|
1021
|
+
...workflowToolExposure(name, codemodeTools),
|
|
893
1022
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
894
1023
|
try {
|
|
895
1024
|
const result = await run(params, signal, ctx);
|
|
896
|
-
return { content: [{ type: "text", text: result.text }], details: result.details };
|
|
1025
|
+
return { content: [{ type: "text", text: result.text }], details: result.details, structuredContent: result.structured };
|
|
897
1026
|
}
|
|
898
1027
|
catch (error) {
|
|
899
1028
|
throw mainAgentError(error);
|
|
@@ -903,27 +1032,29 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
903
1032
|
renderResult(result, options, theme, context) { return workflowCatalogBlock(workflowControlResult(name, context.args, result, options.expanded, theme, context.isError), options.expanded); },
|
|
904
1033
|
});
|
|
905
1034
|
};
|
|
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) => {
|
|
1035
|
+
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
1036
|
if (params.proposalId) {
|
|
908
1037
|
const result = await recovery.answerBudgetDecision(params.runId, params.proposalId, params.approved, false, ctx, signal);
|
|
909
1038
|
if (!result) {
|
|
910
1039
|
const denied = { state: "budget_exhausted", approved: false, reason: "proposal_not_pending" };
|
|
911
|
-
return { text: JSON.stringify(denied), details: denied };
|
|
1040
|
+
return { text: JSON.stringify(denied), details: denied, structured: denied };
|
|
912
1041
|
}
|
|
913
|
-
|
|
1042
|
+
const reason = params.approved ? "approved" : "rejected";
|
|
1043
|
+
return { text: completionControlContent(result, params.runId), details: { ...result, reason }, structured: { ...completionControlValue(result, params.runId), reason } };
|
|
914
1044
|
}
|
|
915
1045
|
if (!params.name)
|
|
916
1046
|
throw new WorkflowError("INVALID_METADATA", "workflow_respond requires name or proposalId");
|
|
917
1047
|
const accepted = await answerCheckpoint(params.runId, params.name, params.approved);
|
|
918
|
-
|
|
1048
|
+
const details = { accepted, state: accepted ? "checkpoint_answered" : "not_pending", approved: params.approved, reason: "checkpoint" };
|
|
1049
|
+
return { text: accepted ? "Checkpoint response accepted." : "Checkpoint is not awaiting a response.", details, structured: details };
|
|
919
1050
|
});
|
|
920
|
-
registerControlTool("workflow_stop", "Workflow Stop", "Stop an active workflow run by ID", Type.Object({ runId: Type.String() }, { additionalProperties: false }), async (params) => {
|
|
1051
|
+
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
1052
|
const result = await stopWorkflowRun(params.runId);
|
|
922
|
-
return { text: JSON.stringify(result), details: result };
|
|
1053
|
+
return { text: JSON.stringify(result), details: result, structured: result };
|
|
923
1054
|
});
|
|
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) => {
|
|
1055
|
+
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
1056
|
const result = await workflowStatusRun(params.runId, ctx);
|
|
926
|
-
return { text: JSON.stringify(result), details: result };
|
|
1057
|
+
return { text: JSON.stringify(result), details: result, structured: result };
|
|
927
1058
|
});
|
|
928
1059
|
let catalogRegistered = false;
|
|
929
1060
|
let sessionStarted = false;
|
|
@@ -937,13 +1068,15 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
937
1068
|
return;
|
|
938
1069
|
pi.registerTool({
|
|
939
1070
|
name: "workflow_catalog",
|
|
1071
|
+
...workflowToolExposure("workflow_catalog", codemodeTools),
|
|
1072
|
+
outputSchema: WORKFLOW_CATALOG_OUTPUT,
|
|
940
1073
|
label: "Workflow Catalog",
|
|
941
1074
|
description: "List reusable workflow functions and model aliases; pass `name` to load one entry in full",
|
|
942
1075
|
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or model alias name for full detail" })) }, { additionalProperties: false }),
|
|
943
1076
|
async execute(_id, params = {}) {
|
|
944
1077
|
const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
|
|
945
1078
|
const result = params.name === undefined ? registry.catalogIndex(context) : registry.catalogDetail(params.name, context);
|
|
946
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
1079
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result, structuredContent: result };
|
|
947
1080
|
},
|
|
948
1081
|
renderCall(args, theme) {
|
|
949
1082
|
const title = theme.fg("toolTitle", theme.bold("workflow_catalog"));
|
|
@@ -955,26 +1088,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
955
1088
|
});
|
|
956
1089
|
catalogRegistered = true;
|
|
957
1090
|
};
|
|
958
|
-
const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks(), validateExtensionSettings: registry.validateExtensionSettings, onResourceWarning: (message) => { deliverWarning(pi, message); }, onAgentSettings: (agentId, settings) => { scheduler.setExtensionSettings(agentId, settings); } }, transport);
|
|
959
|
-
// Resumed runs keep the role definitions frozen in their snapshot and fall back to the current definitions for roles a
|
|
960
|
-
// registered function reaches for the first time after the restart; those are captured into the snapshot like at launch.
|
|
961
|
-
const resumeRoles = (store, snapshot, cwd, trustedProject) => {
|
|
962
|
-
let current = {};
|
|
963
|
-
let project = {};
|
|
964
|
-
try {
|
|
965
|
-
current = loadAgentDefinitions(cwd, extensionAgentDir, trustedProject, registry.roleDirectoryRegistrations());
|
|
966
|
-
project = trustedProject ? loadProjectAgentDefinitions(cwd) : {};
|
|
967
|
-
}
|
|
968
|
-
catch { /* Unreadable current roles leave the resume with the snapshot roles, as before. */ }
|
|
969
|
-
const definitions = { ...current, ...(snapshot.roles ?? {}) };
|
|
970
|
-
const captured = roleCapture(store, snapshot, definitions, project);
|
|
971
|
-
return { definitions, capture: (role, model) => captured.capture(role, model) };
|
|
972
|
-
};
|
|
1091
|
+
const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, ...(extensionLoad ? { extensionLoad } : {}), agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentPreparationHooks: registry.agentPreparationHooks(), agentSetupHooks: registry.agentSetupHooks(), validateExtensionSettings: registry.validateExtensionSettings, onResourceWarning: root.onResourceWarning ?? ((message) => { deliverWarning(pi, message); }), onAgentSettings: (agentId, settings) => { scheduler.setExtensionSettings(agentId, settings); }, onAgentCapabilities: (agentId, capabilities, projectTrusted) => { scheduler.setAttemptCapabilities(agentId, capabilities, projectTrusted); } }, transport);
|
|
973
1092
|
const activeSnapshotTools = (tools, active) => active === "session"
|
|
974
|
-
? new Set(tools.filter((tool) => pi
|
|
1093
|
+
? new Set(tools.filter((tool) => reachableTools(pi).includes(tool) && tool !== "workflow_catalog"))
|
|
975
1094
|
: new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
|
|
976
1095
|
const resumeLaunchPrologue = async (input) => {
|
|
977
|
-
const active = new Set(pi
|
|
1096
|
+
const active = new Set(reachableTools(pi).filter((tool) => !INTERNAL_WORKFLOW_TOOLS.includes(tool)));
|
|
978
1097
|
const missing = input.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
979
1098
|
if (missing)
|
|
980
1099
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
@@ -983,27 +1102,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
983
1102
|
const extensionSettings = input.snapshot.settings.extensionSettings;
|
|
984
1103
|
const extensionSettingsPath = input.snapshot.settingsSources?.extensionSettings ?? input.snapshot.settingsPath ?? settingsPath;
|
|
985
1104
|
registry.validateExtensionSettings(extensionSettings, { source: "effective", cwd: input.cwd, projectTrusted: input.trustedProject, settingsPath: extensionSettingsPath });
|
|
986
|
-
for (const [role, definition] of Object.entries(input.snapshot.roles ?? {}))
|
|
987
|
-
registry.validateExtensionSettings(mergeWorkflowExtensionSettings(extensionSettings, definition.extensionSettings), { source: "role", cwd: input.cwd, projectTrusted: input.trustedProject, settingsPath: extensionSettingsPath, role });
|
|
988
1105
|
const currentPolicy = resolveAgentResourcePolicy(input.cwd, input.trustedProject, settingsPath);
|
|
989
1106
|
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
990
1107
|
const previousAliases = input.snapshot.modelAliases ?? input.snapshot.settings.modelAliases ?? {};
|
|
991
1108
|
const inventory = modelInventory(input.rootModel, input.modelRegistry);
|
|
992
1109
|
const knownModels = input.modelRegistry ? inventory.knownModels : new Set([...input.snapshot.models, ...inventory.knownModels]);
|
|
993
1110
|
const availableModels = input.modelRegistry ? inventory.availableModels : new Set([...input.snapshot.models, ...inventory.availableModels]);
|
|
994
|
-
const
|
|
1111
|
+
const resolved = input.resolvedAliases ? { aliases: input.resolvedAliases, dynamicNames: input.resolvedDynamicAliasNames ?? [] } : await resolveLaunchAliases(registry, staticAliases, { cwd: input.cwd, projectTrusted: input.trustedProject, rootModel: input.rootModel, knownModels, availableModels, signal: input.signal }, availableModels, knownModels, settingsPath);
|
|
1112
|
+
const currentAliases = resolved.aliases;
|
|
1113
|
+
const dynamicAliasNames = resolved.dynamicNames;
|
|
995
1114
|
const blockedAliases = input.blockedAliases ?? new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
996
1115
|
const blockedAliasTargets = input.blockedAliasTargets ?? Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
997
1116
|
const script = input.withPreflight ? input.snapshot.script : undefined;
|
|
998
1117
|
if (script !== undefined) {
|
|
999
1118
|
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
1000
|
-
preflight(script, { models: availableModels, tools: active,
|
|
1119
|
+
preflight(script, { models: availableModels, tools: active, modelAliases: resumeAliases, knownModels, settingsPath, skipModelAvailability: true }, input.snapshot.schemas, input.snapshot.metadata, true);
|
|
1001
1120
|
}
|
|
1002
1121
|
const refreshed = resumedSnapshotSettings(input.snapshot, resolution, currentAliases);
|
|
1003
1122
|
const snapshot = createLaunchSnapshot({ ...input.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
1004
|
-
return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script };
|
|
1123
|
+
return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, dynamicAliasNames, blockedAliases, blockedAliasTargets, snapshot, script };
|
|
1005
1124
|
};
|
|
1006
|
-
const workflowAgentHandler = (store, metadata, lifecycle, executor, cwd, runId
|
|
1125
|
+
const workflowAgentHandler = (store, metadata, lifecycle, executor, cwd, runId) => {
|
|
1007
1126
|
// Replay lookup and spawn are serialised per run so concurrent agent(...) calls reach the scheduler in call order.
|
|
1008
1127
|
const admission = new SerialLane();
|
|
1009
1128
|
return async (prompt, options, agentSignal, identity) => {
|
|
@@ -1016,21 +1135,21 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1016
1135
|
return { replayed };
|
|
1017
1136
|
const worktree = agentWorktree(identity);
|
|
1018
1137
|
const agentCwd = worktree.worktreeOwner ? (await persistWorktree(store, metadata, worktree.worktreeOwner)).cwd : cwd;
|
|
1019
|
-
const role = typeof options.role === "string" ? options.role : undefined;
|
|
1020
1138
|
const model = typeof options.model === "string" ? options.model : undefined;
|
|
1021
1139
|
const requestedLabel = typeof options.label === "string" ? options.label : undefined;
|
|
1022
1140
|
const skills = Array.isArray(options.skills) ? options.skills : undefined;
|
|
1023
1141
|
const extensions = Array.isArray(options.extensions) ? options.extensions : undefined;
|
|
1024
1142
|
const contextFiles = Array.isArray(options.contextFiles) && options.contextFiles.every(isContextFileScope) ? options.contextFiles : undefined;
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
const
|
|
1143
|
+
// A paused resume replaces the run executor with refreshed aliases; new identities must prepare against it.
|
|
1144
|
+
const current = runs.get(runId)?.executor ?? executor;
|
|
1145
|
+
const configuration = await current.prepare({ label: requestedLabel ?? "agent", workflowName: metadata.name, agentOptions: options, agentIdentity: identity, ...(worktree.worktreeOwner ? { worktreeOwner: worktree.worktreeOwner } : {}) }, agentCwd, agentSignal);
|
|
1146
|
+
const resolved = configuration;
|
|
1147
|
+
const label = requestedLabel ?? configuration.label ?? resolved.model.model;
|
|
1029
1148
|
const tools = resolved.tools;
|
|
1030
1149
|
const schema = object(options.outputSchema) ? options.outputSchema : undefined;
|
|
1031
1150
|
const sessionPath = identity.handle !== undefined && identity.turn !== undefined ? await handleTurnInput(store, identity.handle, identity.turn) : undefined;
|
|
1032
1151
|
const continuity = identity.handle === undefined ? undefined : sessionPath ? "continued" : "fresh";
|
|
1033
|
-
return { spawned: scheduler.spawn(runId, prompt, { label, ...(requestedLabel ? { requestedLabel } : {}), ...(identity.parentBreadcrumb ? { parentBreadcrumb: identity.parentBreadcrumb } : {}), cwd: agentCwd, tools, ...(skills ? { skills } : {}), ...(extensions ? { extensions } : {}), ...worktree, ...(model ? { model } : {}), ...(
|
|
1152
|
+
return { spawned: scheduler.spawn(runId, prompt, { label, ...(requestedLabel ? { requestedLabel } : {}), ...(identity.parentBreadcrumb ? { parentBreadcrumb: identity.parentBreadcrumb } : {}), cwd: agentCwd, tools, ...(skills ? { skills } : {}), ...(extensions ? { extensions } : {}), ...worktree, ...(model ? { model } : {}), ...(contextFiles ? { contextFiles } : {}), ...(schema ? { schema } : {}), ...(typeof options.retries === "number" ? { retries: options.retries } : {}), ...(positiveInteger(options.timeoutMs) || options.timeoutMs === null ? { timeoutMs: options.timeoutMs } : {}), ...(sessionPath ? { sessionPath } : {}), ...(continuity ? { continuity } : {}), agentOptions: options, agentIdentity: identity, configuration }) };
|
|
1034
1153
|
});
|
|
1035
1154
|
if ("replayed" in admitted)
|
|
1036
1155
|
return admitted.replayed.value;
|
|
@@ -1053,7 +1172,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1053
1172
|
};
|
|
1054
1173
|
};
|
|
1055
1174
|
const recovery = createWorkflowRecovery({
|
|
1056
|
-
pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, coordinateRunMutation, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue: resumeLaunchPrologue,
|
|
1175
|
+
pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, coordinateRunMutation, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue: resumeLaunchPrologue, workflowAgentHandler, shellForRun, toolForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver: (content) => { deliver(pi, content); }, deliverTerminal: deliveryController.deliverTerminal, workflowToolUpdate, registry, modelSpec,
|
|
1057
1176
|
});
|
|
1058
1177
|
const resumeSelectedWorkflow = async (runId, foreground, context, budgetPatch) => {
|
|
1059
1178
|
const run = runs.get(runId);
|
|
@@ -1074,9 +1193,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1074
1193
|
await deliveryController.moveForegroundToBackground(runId);
|
|
1075
1194
|
if (foreground && !wasAttached) {
|
|
1076
1195
|
run.foreground = true;
|
|
1077
|
-
const loaded = await run.store.load();
|
|
1078
1196
|
await persistRunState(run.store, run.metadata, (current) => ({ ...current, delivery: { ...(current.delivery ?? {}), mode: "foreground", state: "attached" } }));
|
|
1079
|
-
await run.store.
|
|
1197
|
+
await run.store.setLaunchMode("foreground");
|
|
1080
1198
|
}
|
|
1081
1199
|
else if (!foreground)
|
|
1082
1200
|
run.foreground = false;
|
|
@@ -1120,13 +1238,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1120
1238
|
const completed = await recovery.coldResumeRun(run, hasUI, ui, projectTrusted(context), recoveryContext, foreground, foreground);
|
|
1121
1239
|
return completed ? { workflowName: run.metadata.name, state: "completed", attached: false, value: completed.value } : { workflowName: run.metadata.name, state: "running", attached: false };
|
|
1122
1240
|
};
|
|
1123
|
-
registerControlTool("workflow_retry", "Workflow Retry", "Retry a failed workflow run by replaying its completed structural operations", WORKFLOW_RETRY_PARAMETERS, async (params, signal, ctx) => {
|
|
1241
|
+
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
1242
|
const result = await recovery.retryWorkflowRun(params.runId, ctx, signal, params.foreground, params.expectedState);
|
|
1125
|
-
return { text: completionControlContent(result), details: result };
|
|
1243
|
+
return { text: completionControlContent(result), details: result, structured: completionControlValue(result) };
|
|
1126
1244
|
});
|
|
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) => {
|
|
1245
|
+
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
1246
|
const result = await recovery.resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground, true, params.expectedState);
|
|
1129
|
-
return { text: completionControlContent(result), details: result };
|
|
1247
|
+
return { text: completionControlContent(result), details: result, structured: completionControlValue(result) };
|
|
1130
1248
|
});
|
|
1131
1249
|
const deliverStaleTerminal = async (store, run) => {
|
|
1132
1250
|
if (!((run.delivery?.mode === "foreground" && run.delivery.state === "attached") || (run.delivery?.mode === "background" && run.delivery.state === "pending")))
|
|
@@ -1211,10 +1329,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1211
1329
|
const budgetRuntime = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents, { active: loaded.run.state === "running" });
|
|
1212
1330
|
const lifecycle = lifecycleFor(store, loaded.run.state, loaded.snapshot.metadata);
|
|
1213
1331
|
const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
1214
|
-
const roleDefinitions = loaded.snapshot.roles ?? {};
|
|
1215
1332
|
const abortController = new AbortController();
|
|
1216
1333
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, new Set(loaded.snapshot.models), () => { abortController.abort(); });
|
|
1217
|
-
runs.set(runId, { executor: createAgentExecutor({ cwd: ctx.cwd, model, tools: activeSnapshotTools(loaded.snapshot.tools, "session"), resourceSelectors: snapshotResourcePolicy(loaded.snapshot, store.cwd, projectTrusted(ctx), workflowSettingsPath(extensionAgentDir)).effective, extensionSettings: loaded.snapshot.settings.extensionSettings, availableModels: new Set(loaded.snapshot.models), knownModels: new Set(loaded.snapshot.models), ...(loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ? { modelAliases: loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases } : {}), ...(loaded.snapshot.settingsSources?.modelAliases ? { settingsPath: loaded.snapshot.settingsSources.modelAliases } : loaded.snapshot.settingsPath ? { settingsPath: loaded.snapshot.settingsPath } : {}),
|
|
1334
|
+
runs.set(runId, { executor: createAgentExecutor({ cwd: ctx.cwd, projectTrusted: projectTrusted(ctx), model, tools: activeSnapshotTools(loaded.snapshot.tools, "session"), resourceSelectors: snapshotResourcePolicy(loaded.snapshot, store.cwd, projectTrusted(ctx), workflowSettingsPath(extensionAgentDir)).effective, extensionSettings: loaded.snapshot.settings.extensionSettings, availableModels: new Set(loaded.snapshot.models), knownModels: new Set(loaded.snapshot.models), ...(loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ? { modelAliases: loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases } : {}), ...(loaded.snapshot.settingsSources?.modelAliases ? { settingsPath: loaded.snapshot.settingsSources.modelAliases } : loaded.snapshot.settingsPath ? { settingsPath: loaded.snapshot.settingsPath } : {}), runStore: store, providerPause, agentResourcePolicy: frozenResourcePolicy(snapshotResourcePolicy(loaded.snapshot, store.cwd, projectTrusted(ctx), workflowSettingsPath(extensionAgentDir))) }), store, metadata: loaded.snapshot.metadata, model, lifecycle, budget: budgetRuntime, abortController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) });
|
|
1218
1335
|
for (const checkpoint of await store.awaitingCheckpoints())
|
|
1219
1336
|
deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
|
|
1220
1337
|
for (const decision of await store.pendingWorkflowDecisions())
|
|
@@ -1253,21 +1370,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1253
1370
|
throw error;
|
|
1254
1371
|
}
|
|
1255
1372
|
});
|
|
1256
|
-
pi.on("before_agent_start", (event, ctx) => {
|
|
1257
|
-
if (!pi.getActiveTools().includes("workflow"))
|
|
1258
|
-
return;
|
|
1259
|
-
const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx), typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : undefined)).filter(([, definition]) => definition.description);
|
|
1260
|
-
if (!roles.length)
|
|
1261
|
-
return;
|
|
1262
|
-
const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
|
|
1263
|
-
return { systemPrompt: `${event.systemPrompt}\n\n${content}` };
|
|
1264
|
-
});
|
|
1265
1373
|
const workflowTool = {
|
|
1266
1374
|
name: "workflow",
|
|
1267
1375
|
label: WORKFLOW_TOOL_LABEL,
|
|
1268
1376
|
description: WORKFLOW_TOOL_DESCRIPTION,
|
|
1269
1377
|
promptSnippet: WORKFLOW_TOOL_PROMPT_SNIPPET,
|
|
1270
1378
|
parameters: WORKFLOW_TOOL_PARAMETERS,
|
|
1379
|
+
outputSchema: WORKFLOW_LAUNCH_OUTPUT,
|
|
1380
|
+
prepareLoadout: prepareScriptToolLoadout,
|
|
1271
1381
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1272
1382
|
let resolveDetached;
|
|
1273
1383
|
let foregroundStore;
|
|
@@ -1275,7 +1385,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1275
1385
|
const detachedResult = params.foreground ? new Promise((resolve) => { resolveDetached = resolve; }) : undefined;
|
|
1276
1386
|
const detachedToolResult = (run) => {
|
|
1277
1387
|
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.` } };
|
|
1388
|
+
return { content: [{ type: "text", text: JSON.stringify(detached) }], details: { ...detached, run, preview: `Moved workflow ${run.id} to background.` }, structuredContent: detached };
|
|
1279
1389
|
};
|
|
1280
1390
|
try {
|
|
1281
1391
|
const headless = object(ctx) && ctx.headless === true;
|
|
@@ -1289,7 +1399,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1289
1399
|
const inventory = modelInventory(rootModel, modelRegistry);
|
|
1290
1400
|
const knownModels = inventory.knownModels;
|
|
1291
1401
|
const availableModels = inventory.availableModels;
|
|
1292
|
-
const rootTools = pi
|
|
1402
|
+
const rootTools = reachableTools(pi).filter((name) => !INTERNAL_WORKFLOW_TOOLS.includes(name));
|
|
1293
1403
|
const trustedProject = projectTrusted(ctx);
|
|
1294
1404
|
const launchCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
1295
1405
|
const launch = workflowLaunchSettings(launchCwd, trustedProject, settingsPath, params.concurrency);
|
|
@@ -1308,23 +1418,33 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1308
1418
|
if (trustedProject && launch.resolution.project.extensionSettings !== undefined)
|
|
1309
1419
|
registry.validateExtensionSettings(launch.resolution.project.extensionSettings, { source: "project", cwd: ctx.cwd, projectTrusted: true, settingsPath: launch.resolution.projectSettingsPath });
|
|
1310
1420
|
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
|
-
const { script, checked
|
|
1421
|
+
const { script, checked } = validated;
|
|
1422
|
+
const toolContext = scriptToolContext(ctx);
|
|
1423
|
+
validateScriptToolReferences(script, toolContext);
|
|
1424
|
+
// Headless hosts that tolerated extension load failures report them with the extensions they loaded.
|
|
1425
|
+
const strings = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1426
|
+
const loadErrors = object(ctx) ? strings(ctx.extensionLoadErrors) : [];
|
|
1427
|
+
if (loadErrors.length)
|
|
1428
|
+
extensionLoad = Object.freeze({ errors: loadErrors, loaded: object(ctx) ? strings(ctx.loadedExtensionPaths) : [] });
|
|
1429
|
+
const executorRoot = { cwd: ctx.cwd, projectTrusted: launch.resourcePolicy.projectTrusted, model: rootModel, tools: new Set(rootTools), resourceSelectors: launch.resourcePolicy.effective, extensionSettings: settings.extensionSettings, availableModels, knownModels, modelAliases, dynamicModelAliasNames: resolvedAliases.dynamicNames, settingsPath, agentResourcePolicy: frozenResourcePolicy(launch.resourcePolicy) };
|
|
1430
|
+
// Runtime emits the same resource warnings, so inspection stays silent.
|
|
1431
|
+
await inspectStaticAgentConfigurations(createAgentExecutor({ ...executorRoot, onResourceWarning: () => undefined }), script, checked.metadata.name, runController.signal, registry.functions());
|
|
1312
1432
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
1433
|
+
// Cancellation can arrive while the lease or a queued parent validation is pending; no run may be created after it.
|
|
1434
|
+
const assertLaunchActive = () => { if (runController.signal.aborted)
|
|
1435
|
+
throw new WorkflowError("CANCELLED", "Workflow launch cancelled"); };
|
|
1436
|
+
assertLaunchActive();
|
|
1313
1437
|
const runId = randomUUID();
|
|
1314
1438
|
const args = params.args ?? null;
|
|
1315
1439
|
encoded(args);
|
|
1316
1440
|
const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
|
|
1317
1441
|
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
1318
1442
|
const parentRunId = params.parentRunId;
|
|
1319
|
-
const
|
|
1320
|
-
const
|
|
1321
|
-
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
|
|
1322
|
-
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
1323
|
-
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, launchMode: params.foreground ? "foreground" : "background", settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
|
|
1324
|
-
const capturedRoles = roleCapture(store, snapshot, agentDefinitions, projectAgentDefinitions);
|
|
1443
|
+
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models])];
|
|
1444
|
+
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, launchMode: params.foreground ? "foreground" : "background", settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentConfigurations: {}, schemas: checked.schemas });
|
|
1325
1445
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
1326
1446
|
const initialBudget = budgetRuntime.snapshot();
|
|
1327
|
-
const createRun = () => store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], agentSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
1447
|
+
const createRun = () => { assertLaunchActive(); return store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], agentSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot); };
|
|
1328
1448
|
// Parent validation and persistence share the mutation lane so a manual deletion cannot remove the parent between the two.
|
|
1329
1449
|
if (parentRunId !== undefined)
|
|
1330
1450
|
await coordinateRunMutation(async () => { await store.validateParentRun(parentRunId); await createRun(); });
|
|
@@ -1352,7 +1472,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1352
1472
|
activeRun.foreground = false;
|
|
1353
1473
|
delete activeRun.update;
|
|
1354
1474
|
}
|
|
1355
|
-
await store.
|
|
1475
|
+
await store.setLaunchMode("background");
|
|
1356
1476
|
for (const checkpoint of await store.awaitingCheckpoints())
|
|
1357
1477
|
deliverBackgroundCheckpoint(checked.metadata.name, runId, checkpoint);
|
|
1358
1478
|
signal?.removeEventListener("abort", onForegroundAbort);
|
|
@@ -1369,13 +1489,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1369
1489
|
const providerPause = async () => { if (!foregroundAttached)
|
|
1370
1490
|
deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
1371
1491
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, availableModels, () => { runController.abort(); });
|
|
1372
|
-
const executor = createAgentExecutor({
|
|
1492
|
+
const executor = createAgentExecutor({ ...executorRoot, runStore: store, providerPause, runContext });
|
|
1373
1493
|
const runRecord = { executor, store, metadata: checked.metadata, model: rootModel, lifecycle, budget: budgetRuntime, abortController: runController, foreground: foregroundAttached, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}), ...(params.foreground && onUpdate ? { update: onUpdate } : {}) };
|
|
1374
1494
|
runs.set(runId, runRecord);
|
|
1375
1495
|
if (params.foreground && onUpdate)
|
|
1376
1496
|
onUpdate(workflowToolUpdate((await store.load()).run));
|
|
1377
1497
|
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
|
|
1498
|
+
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), 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
1499
|
runRecord.execution = execution;
|
|
1380
1500
|
await eventPublisher.runStarted(store, checked.metadata);
|
|
1381
1501
|
const finish = execution.result.then(async (value) => {
|
|
@@ -1416,7 +1536,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1416
1536
|
}, async (error) => {
|
|
1417
1537
|
await deliveryController.deliverTerminal(store, deliverFailureContent(error), true);
|
|
1418
1538
|
});
|
|
1419
|
-
return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
1539
|
+
return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` }, structuredContent: { runId, state: "running" } };
|
|
1420
1540
|
}
|
|
1421
1541
|
void completion.then(async (result) => {
|
|
1422
1542
|
await deliveryController.deliverDetachedTerminal(toolCallId, completionContent("background", result));
|
|
@@ -1442,7 +1562,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1442
1562
|
}
|
|
1443
1563
|
deliveryController.foregroundDeliveries.delete(toolCallId);
|
|
1444
1564
|
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 } };
|
|
1565
|
+
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
1566
|
}
|
|
1447
1567
|
catch (error) {
|
|
1448
1568
|
if (params.foreground && foregroundStore && completionInstalled) {
|
|
@@ -1529,7 +1649,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1529
1649
|
return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
|
|
1530
1650
|
},
|
|
1531
1651
|
};
|
|
1532
|
-
pi.registerTool(workflowTool);
|
|
1652
|
+
pi.registerTool({ ...workflowTool, ...workflowToolExposure(workflowTool.name, codemodeTools) });
|
|
1533
1653
|
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
1654
|
backgroundWidgetController.suspend();
|
|
1535
1655
|
else
|
|
@@ -1561,6 +1681,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1561
1681
|
}
|
|
1562
1682
|
class LiveAgentRegistry {
|
|
1563
1683
|
#byRun = new Map();
|
|
1684
|
+
#tools = new Map();
|
|
1564
1685
|
get(runId, agentId) { return this.#byRun.get(runId)?.get(agentId); }
|
|
1565
1686
|
setSession(runId, agentId, session) {
|
|
1566
1687
|
if (session)
|
|
@@ -1587,19 +1708,29 @@ class LiveAgentRegistry {
|
|
|
1587
1708
|
if (timestamp !== undefined)
|
|
1588
1709
|
this.#state(runId, agentId).lastEventAt = timestamp;
|
|
1589
1710
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1711
|
+
/** Marks a script tool call in flight until the returned callback runs. */
|
|
1712
|
+
toolStarted(runId, name) {
|
|
1713
|
+
const call = { name, startedAt: Date.now() };
|
|
1714
|
+
const calls = this.#tools.get(runId) ?? new Set();
|
|
1715
|
+
this.#tools.set(runId, calls.add(call));
|
|
1716
|
+
return () => { calls.delete(call); if (!calls.size && this.#tools.get(runId) === calls)
|
|
1717
|
+
this.#tools.delete(runId); };
|
|
1718
|
+
}
|
|
1719
|
+
deleteRun(runId) { this.#byRun.delete(runId); this.#tools.delete(runId); }
|
|
1720
|
+
/** Overlays live activity, freshness, and script tool calls onto a persisted run; returns the input when nothing is live. */
|
|
1592
1721
|
overlay(run) {
|
|
1722
|
+
const tools = this.#tools.get(run.id);
|
|
1723
|
+
const withTools = tools?.size ? { ...run, activeTools: [...tools].map((call) => ({ ...call })) } : run;
|
|
1593
1724
|
const agents = this.#byRun.get(run.id);
|
|
1594
1725
|
if (!agents?.size)
|
|
1595
|
-
return
|
|
1726
|
+
return withTools;
|
|
1596
1727
|
const next = run.agents.map((agent) => {
|
|
1597
1728
|
const live = agents.get(agent.id);
|
|
1598
1729
|
if (!live || (live.activity === undefined && live.lastEventAt === undefined))
|
|
1599
1730
|
return agent;
|
|
1600
1731
|
return { ...agent, ...(live.activity === undefined ? {} : { activity: live.activity }), ...(live.lastEventAt === undefined ? {} : { lastEventAt: live.lastEventAt }) };
|
|
1601
1732
|
});
|
|
1602
|
-
return next.some((agent, index) => agent !== run.agents[index]) ? { ...
|
|
1733
|
+
return next.some((agent, index) => agent !== run.agents[index]) ? { ...withTools, agents: next } : withTools;
|
|
1603
1734
|
}
|
|
1604
1735
|
#state(runId, agentId) {
|
|
1605
1736
|
const agents = this.#byRun.get(runId) ?? new Map();
|
|
@@ -1660,9 +1791,6 @@ async function handleTurnInput(store, handle, turn) {
|
|
|
1660
1791
|
}
|
|
1661
1792
|
return target;
|
|
1662
1793
|
}
|
|
1663
|
-
function displayAgentName(label, role, model) {
|
|
1664
|
-
return label ?? role ?? model.model;
|
|
1665
|
-
}
|
|
1666
1794
|
function modelSpec(value, fallback) {
|
|
1667
1795
|
try {
|
|
1668
1796
|
const parsed = parseModelReference(value);
|