@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.
Files changed (128) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +2 -2
  3. package/dist/src/agent-execution.d.ts +43 -10
  4. package/dist/src/agent-execution.js +442 -162
  5. package/dist/src/background-widget.d.ts +5 -1
  6. package/dist/src/background-widget.js +5 -10
  7. package/dist/src/decoders.d.ts +2 -3
  8. package/dist/src/decoders.js +59 -37
  9. package/dist/src/execution.d.ts +6 -2
  10. package/dist/src/execution.js +48 -7
  11. package/dist/src/host-delivery.js +5 -8
  12. package/dist/src/host-navigator.js +5 -2
  13. package/dist/src/host-phases.d.ts +1 -0
  14. package/dist/src/host-recovery.d.ts +11 -8
  15. package/dist/src/host-recovery.js +14 -22
  16. package/dist/src/host-runtime.js +56 -5
  17. package/dist/src/host-view.d.ts +6 -1
  18. package/dist/src/host-view.js +46 -20
  19. package/dist/src/host.d.ts +2 -2
  20. package/dist/src/host.js +246 -118
  21. package/dist/src/index.d.ts +1 -2
  22. package/dist/src/index.js +1985 -1690
  23. package/dist/src/index.js.map +3 -3
  24. package/dist/src/paths.d.ts +8 -5
  25. package/dist/src/paths.js +24 -13
  26. package/dist/src/persistence.d.ts +1 -0
  27. package/dist/src/persistence.js +1 -0
  28. package/dist/src/pi-runtime-runner.js +4 -1
  29. package/dist/src/registry.d.ts +9 -7
  30. package/dist/src/registry.js +29 -71
  31. package/dist/src/runtime/agent-runner.d.ts +2 -0
  32. package/dist/src/script-tools.d.ts +27 -0
  33. package/dist/src/script-tools.js +87 -0
  34. package/dist/src/session-lease.d.ts +1 -1
  35. package/dist/src/session-lease.js +45 -11
  36. package/dist/src/settings.d.ts +19 -3
  37. package/dist/src/settings.js +63 -50
  38. package/dist/src/store.d.ts +3 -1
  39. package/dist/src/store.js +17 -1
  40. package/dist/src/trajectory.d.ts +12 -3
  41. package/dist/src/trajectory.js +32 -11
  42. package/dist/src/types.d.ts +129 -45
  43. package/dist/src/types.js +5 -8
  44. package/dist/src/utils.d.ts +21 -1
  45. package/dist/src/utils.js +46 -6
  46. package/dist/src/validation.d.ts +14 -3
  47. package/dist/src/validation.js +143 -87
  48. package/dist/starter/index.js +8 -11
  49. package/dist/starter/index.js.map +1 -1
  50. package/dist/starter/review-loop.d.ts +0 -13
  51. package/dist/starter/review-loop.js +8 -9
  52. package/dist/subagents/index.js +904 -956
  53. package/dist/subagents/index.js.map +3 -3
  54. package/dist/subagents/src/contracts.d.ts +48 -31
  55. package/dist/subagents/src/contracts.js +20 -9
  56. package/dist/subagents/src/index.d.ts +1 -1
  57. package/dist/subagents/src/index.js +14 -8
  58. package/dist/subagents/src/manager.js +52 -73
  59. package/dist/subagents/src/navigator.js +3 -14
  60. package/dist/subagents/src/view.d.ts +1 -1
  61. package/dist/subagents/src/view.js +15 -68
  62. package/dist/subagents/src/worktree.js +1 -1
  63. package/dist/trajectory/assets/index.html +35 -13
  64. package/dist/trajectory/index.js +576 -932
  65. package/dist/trajectory/index.js.map +3 -3
  66. package/dist/trajectory/src/assets/index.html +35 -13
  67. package/dist/trajectory/src/index.js +134 -104
  68. package/dist/trajectory/src/server.js +268 -27
  69. package/dist/trajectory/src/server.js.map +3 -3
  70. package/dist/trajectory/src/startup-mutex.d.ts +17 -0
  71. package/dist/trajectory/src/startup-mutex.js +172 -0
  72. package/examples/workflow-extension-template/README.md +4 -7
  73. package/examples/workflow-extension-template/extension.test.mjs +4 -8
  74. package/examples/workflow-extension-template/index.js +0 -3
  75. package/package.json +10 -10
  76. package/skills/pi-extensible-workflows/SKILL.md +12 -8
  77. package/src/agent-execution.ts +412 -164
  78. package/src/background-widget.ts +5 -11
  79. package/src/decoders.ts +51 -37
  80. package/src/execution.ts +46 -9
  81. package/src/host-delivery.ts +5 -8
  82. package/src/host-navigator.ts +4 -2
  83. package/src/host-phases.ts +1 -1
  84. package/src/host-recovery.ts +23 -28
  85. package/src/host-runtime.ts +44 -6
  86. package/src/host-view.ts +41 -16
  87. package/src/host.ts +229 -142
  88. package/src/index.ts +1 -2
  89. package/src/paths.ts +18 -9
  90. package/src/persistence.ts +1 -0
  91. package/src/pi-runtime-runner.ts +3 -1
  92. package/src/registry.ts +28 -64
  93. package/src/runtime/agent-runner.ts +2 -0
  94. package/src/script-tools.ts +76 -0
  95. package/src/session-lease.ts +34 -6
  96. package/src/settings.ts +56 -50
  97. package/src/store.ts +18 -2
  98. package/src/trajectory.ts +32 -15
  99. package/src/types.ts +82 -34
  100. package/src/utils.ts +42 -5
  101. package/src/validation.ts +121 -80
  102. package/starter/index.ts +0 -1
  103. package/starter/review-loop.ts +8 -9
  104. package/starter/roles/developer.md +0 -1
  105. package/starter/roles/oracle.md +0 -1
  106. package/starter/roles/researcher.md +0 -1
  107. package/starter/roles/reviewer.md +0 -1
  108. package/starter/roles/scout.md +0 -1
  109. package/subagents/README.md +10 -16
  110. package/subagents/src/contracts.ts +21 -11
  111. package/subagents/src/index.ts +17 -8
  112. package/subagents/src/manager.ts +48 -128
  113. package/subagents/src/navigator.ts +4 -13
  114. package/subagents/src/view.ts +16 -63
  115. package/subagents/src/worktree.ts +1 -1
  116. package/trajectory/src/assets/index.html +35 -13
  117. package/trajectory/src/index.ts +112 -91
  118. package/trajectory/src/server.ts +46 -10
  119. package/trajectory/src/startup-mutex.ts +127 -0
  120. package/dist/src/roles.d.ts +0 -70
  121. package/dist/src/roles.js +0 -325
  122. package/dist/starter/roles/developer.md +0 -6
  123. package/dist/starter/roles/oracle.md +0 -18
  124. package/dist/starter/roles/researcher.md +0 -20
  125. package/dist/starter/roles/reviewer.md +0 -19
  126. package/dist/starter/roles/scout.md +0 -18
  127. package/examples/workflow-extension-template/roles/reviewer.md +0 -4
  128. 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, 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";
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, roleNameOf } 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";
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
- backgroundWidgetEnabled = loadSettings(workflowSettingsPath(extensionAgentDir)).backgroundWidget ?? true;
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
- const started = await persistRunState(store, metadata, (current) => {
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, (current) => {
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.role ? { role: options.role } : {}), ...(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 } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, skills, extensions) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}), ...(skills !== undefined ? { skills } : {}), ...(extensions !== undefined ? { extensions } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
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.role ? { role: node.options.role } : {}), ...(node.options.contextFiles ? { contextFiles: node.options.contextFiles } : {}) };
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 } : {}) }), ...(nodeRole ? { role: nodeRole } : {}), ...(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 }) };
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
- return { text: completionControlContent(result, params.runId), details: { ...result, reason: params.approved ? "approved" : "rejected" } };
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
- return { text: accepted ? "Checkpoint response accepted." : "Checkpoint is not awaiting a response.", details: { accepted, state: accepted ? "checkpoint_answered" : "not_pending", approved: params.approved, reason: "checkpoint" } };
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.getActiveTools().includes(tool) && tool !== "workflow_catalog"))
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.getActiveTools().filter((tool) => !INTERNAL_WORKFLOW_TOOLS.includes(tool)));
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 currentAliases = input.resolvedAliases ?? (await resolveLaunchAliases(registry, staticAliases, { cwd: input.cwd, projectTrusted: input.trustedProject, rootModel: input.rootModel, knownModels, availableModels, signal: input.signal }, availableModels, knownModels, settingsPath)).aliases;
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, agentTypes: new Set(input.snapshot.agentTypes), modelAliases: resumeAliases, knownModels, settingsPath, skipModelAvailability: true }, input.snapshot.schemas, input.snapshot.metadata, true);
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, captureRole) => {
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
- const resolved = executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: metadata.name, ...(model ? { model } : {}), ...(role ? { role } : {}), ...(contextFiles ? { contextFiles } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools } : {}), ...(skills ? { skills } : {}), ...(extensions ? { extensions } : {}) });
1026
- if (role)
1027
- await captureRole?.(role, resolved.model);
1028
- const label = displayAgentName(requestedLabel, role, resolved.model);
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 } : {}), ...(role ? { role } : {}), ...(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 }) };
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, resumeRoles, workflowAgentHandler, shellForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver: (content) => { deliver(pi, content); }, deliverTerminal: deliveryController.deliverTerminal, workflowToolUpdate, registry, modelSpec,
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.saveSnapshot(createLaunchSnapshot({ ...loaded.snapshot, launchMode: "foreground" }));
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 } : {}), agentDefinitions: roleDefinitions, 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 } : {}) });
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.getActiveTools().filter((name) => !INTERNAL_WORKFLOW_TOOLS.includes(name));
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, agentDefinitions, projectAgentDefinitions, roleNames } = validated;
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 roles = Object.fromEntries(roleNames.map((role) => [role, agentDefinitions[role]]));
1320
- const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
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.saveSnapshot(createLaunchSnapshot({ ...capturedRoles.snapshot, launchMode: "background" }));
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({ cwd: ctx.cwd, model: rootModel, tools: new Set(rootTools), resourceSelectors: launch.resourcePolicy.effective, extensionSettings: settings.extensionSettings, availableModels, knownModels, modelAliases, settingsPath, agentDefinitions, runStore: store, providerPause, agentResourcePolicy: frozenResourcePolicy(launch.resourcePolicy), runContext });
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, (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);
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
- deleteRun(runId) { this.#byRun.delete(runId); }
1591
- /** Overlays live activity and freshness onto a persisted run; returns the input when nothing is live. */
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 run;
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]) ? { ...run, agents: next } : run;
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);