@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.19.1-fork.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/src/agent-execution.js +114 -87
  3. package/dist/src/background-widget.d.ts +61 -0
  4. package/dist/src/background-widget.js +23 -16
  5. package/dist/src/decoders.js +28 -1
  6. package/dist/src/execution.d.ts +4 -2
  7. package/dist/src/execution.js +43 -2
  8. package/dist/src/host-delivery.js +1 -0
  9. package/dist/src/host-navigator.d.ts +18 -0
  10. package/dist/src/host-navigator.js +71 -13
  11. package/dist/src/host-recovery.d.ts +5 -2
  12. package/dist/src/host-recovery.js +3 -3
  13. package/dist/src/host-view.d.ts +21 -1
  14. package/dist/src/host-view.js +138 -47
  15. package/dist/src/host.d.ts +2 -2
  16. package/dist/src/host.js +246 -60
  17. package/dist/src/index.d.ts +3 -1
  18. package/dist/src/index.js +1346 -622
  19. package/dist/src/index.js.map +3 -3
  20. package/dist/src/paths.d.ts +2 -0
  21. package/dist/src/paths.js +2 -0
  22. package/dist/src/registry.js +3 -2
  23. package/dist/src/roles.d.ts +70 -0
  24. package/dist/src/roles.js +326 -0
  25. package/dist/src/script-tools.d.ts +27 -0
  26. package/dist/src/script-tools.js +87 -0
  27. package/dist/src/settings.d.ts +26 -0
  28. package/dist/src/settings.js +277 -0
  29. package/dist/src/trajectory.d.ts +39 -4
  30. package/dist/src/trajectory.js +82 -5
  31. package/dist/src/types.d.ts +30 -3
  32. package/dist/src/types.js +1 -1
  33. package/dist/src/utils.d.ts +21 -1
  34. package/dist/src/utils.js +40 -0
  35. package/dist/src/validation.d.ts +10 -17
  36. package/dist/src/validation.js +29 -458
  37. package/dist/starter/index.js +0 -2
  38. package/dist/starter/index.js.map +1 -1
  39. package/dist/starter/review-loop.d.ts +0 -13
  40. package/dist/starter/review-loop.js +0 -2
  41. package/dist/subagents/index.js +1344 -798
  42. package/dist/subagents/index.js.map +3 -3
  43. package/dist/subagents/src/contracts.d.ts +49 -29
  44. package/dist/subagents/src/contracts.js +18 -7
  45. package/dist/subagents/src/index.d.ts +1 -1
  46. package/dist/subagents/src/index.js +15 -7
  47. package/dist/subagents/src/manager.js +32 -13
  48. package/dist/subagents/src/navigator.js +396 -247
  49. package/dist/subagents/src/view.d.ts +24 -2
  50. package/dist/subagents/src/view.js +107 -80
  51. package/dist/trajectory/assets/index.html +370 -232
  52. package/dist/trajectory/assets/prism.min.js +13 -0
  53. package/dist/trajectory/index.js +445 -277
  54. package/dist/trajectory/index.js.map +3 -3
  55. package/dist/trajectory/src/assets/index.html +370 -232
  56. package/dist/trajectory/src/assets/prism.min.js +13 -0
  57. package/dist/trajectory/src/export.js +4 -2
  58. package/dist/trajectory/src/index.d.ts +2 -0
  59. package/dist/trajectory/src/index.js +151 -60
  60. package/dist/trajectory/src/server.js +138 -11
  61. package/dist/trajectory/src/server.js.map +2 -2
  62. package/package.json +7 -6
  63. package/skills/pi-extensible-workflows/SKILL.md +2 -0
  64. package/src/agent-execution.ts +110 -80
  65. package/src/background-widget.ts +26 -19
  66. package/src/decoders.ts +22 -1
  67. package/src/execution.ts +41 -4
  68. package/src/host-delivery.ts +1 -0
  69. package/src/host-navigator.ts +58 -14
  70. package/src/host-recovery.ts +8 -7
  71. package/src/host-view.ts +116 -42
  72. package/src/host.ts +223 -55
  73. package/src/index.ts +3 -1
  74. package/src/paths.ts +3 -0
  75. package/src/registry.ts +3 -2
  76. package/src/roles.ts +327 -0
  77. package/src/script-tools.ts +76 -0
  78. package/src/settings.ts +224 -0
  79. package/src/trajectory.ts +97 -9
  80. package/src/types.ts +12 -5
  81. package/src/utils.ts +36 -1
  82. package/src/validation.ts +30 -356
  83. package/starter/review-loop.ts +0 -2
  84. package/subagents/README.md +3 -3
  85. package/subagents/src/contracts.ts +22 -7
  86. package/subagents/src/index.ts +18 -7
  87. package/subagents/src/manager.ts +31 -13
  88. package/subagents/src/navigator.ts +336 -180
  89. package/subagents/src/view.ts +108 -76
  90. package/trajectory/src/assets/index.html +370 -232
  91. package/trajectory/src/assets/prism.min.js +13 -0
  92. package/trajectory/src/export.ts +4 -2
  93. package/trajectory/src/index.ts +124 -47
  94. package/trajectory/src/server.ts +114 -14
package/src/host.ts CHANGED
@@ -4,7 +4,7 @@ import { copyFile, mkdir } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { Type, type Api, type Model, type Static, type TSchema } from "@earendil-works/pi-ai";
7
- import { copyToClipboard, getAgentDir, ModelSelectorComponent, type ExtensionAPI, type ExtensionContext, type ModelRuntime, type ToolDefinition } from "@earendil-works/pi-coding-agent";
7
+ import { copyToClipboard, getAgentDir, ModelSelectorComponent, type ExtensionAPI, type ExtensionContext, type ExtensionToolContext, type ModelRuntime, type ToolDefinition } from "@earendil-works/pi-coding-agent";
8
8
  import { FairAgentScheduler, getAgentAttempts, WorkflowAgentExecutor, localAgentTransport, type AgentActivity, type AgentAttempt, type AgentDefinition, type AgentExecutionRoot, type AgentProgress, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
9
9
  import { RunLifecycle, WorkflowEventPublisher, hostSessionContext, nextNamedOccurrence, withWorkflowFunctions, withoutActiveShells, workflowRunContext, type WorkflowEventSink, type WorkflowRunRecord, type WorkflowToolUpdate } from "./host-runtime.js";
10
10
  import { createWorkflowRecovery, persistedFailure, type ModelRegistryCapability } from "./host-recovery.js";
@@ -13,16 +13,18 @@ import { acquireSessionLease, isPersistedRun, listPersistedSessionIds, listRunId
13
13
  import { retainTerminalRuns } from "./retention.js";
14
14
  import type { PersistedRun, WorktreeReference } from "./persistence.js";
15
15
  import { validateBudget, WorkflowBudgetRuntime } from "./budget.js";
16
- import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue, mergeWorkflowExtensionSettings, modelAliasErrorName, modelCapability, object, parseModelReference, positiveInteger, sanitizeDisplayText, validateModelAliases } from "./utils.js";
17
- import { loadAgentDefinitions, loadProjectAgentDefinitions, loadSettings, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, validateCheckpoint, validateModelAliasAvailability, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowSettingsPath } from "./validation.js";
16
+ import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue, mergeWorkflowExtensionSettings, modelAliasErrorName, modelCapability, object, parseModelReference, positiveInteger, reachableTools, resolveModelReference, VIRTUAL_MODEL_PROVIDER, sanitizeDisplayText, validateModelAliases } from "./utils.js";
17
+ import { loadCodemodeToolsSetting, loadSettings, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, validateCheckpoint, validateModelAliasAvailability, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowSettingsPath, workflowToolExposure } from "./validation.js";
18
+ import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
18
19
  import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistryIfIdle, retainWorkflowRegistry, type WorkflowRegistryApi } from "./registry.js";
19
- import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
20
+ import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath, toolIdentityPath } from "./execution.js";
21
+ import { prepareScriptToolLoadout, scriptTool, scriptToolContext, scriptToolValue, validateScriptToolReferences } from "./script-tools.js";
20
22
  import backgroundWidget, { type BackgroundWidgetAPI } from "./background-widget.js";
21
23
  import { showChangelogNotice } from "./changelog.js";
22
24
  import { createTrajectoryRunLoader, createTrajectoryRunMetadataLoader, createTrajectorySubagentLoader, createTrajectorySubagentMetadataLoader, createTrajectoryTranscriptLoader, type TrajectoryActionRequest, type TrajectoryActionResult, type TrajectorySubagent } from "./trajectory.js";
23
25
  import { getTrajectoryHost, type TrajectoryPublisherProvider } from "./trajectory-host-handle.js";
24
26
  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, type AgentAccounting, type AgentIdentity, type AgentRecord, type AgentResourcePolicy, type AgentTransport, type JsonValue, type LaunchSnapshot, type LiveSessionHandoff, type HardTerminalRunState, type ModelSpec, type PreparedAgentSession, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowAgentSession, type WorkflowErrorCode, type WorkflowMetadata, type WorkflowModelAliasResolverContext, type WorkflowSettings, type WorkflowSettingsResolution, type WorkflowWorktreeReference } from "./types.js";
27
+ import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, THINKING_LEVELS, WORKFLOW_BLOCKED_EVENT, WorkflowError, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState, roleNameOf, type AgentAccounting, type AgentIdentity, type AgentRecord, type AgentResourcePolicy, type AgentTransport, type JsonValue, type LaunchSnapshot, type LiveSessionHandoff, type HardTerminalRunState, type ModelSpec, type PreparedAgentSession, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type ToolIdentity, type WorkflowAgentSession, type WorkflowErrorCode, type WorkflowMetadata, type WorkflowModelAliasResolverContext, type WorkflowSettings, type WorkflowScriptCall, type WorkflowSettingsResolution, type WorkflowWorktreeReference } from "./types.js";
26
28
  import type { SubagentManagerContext, SubagentRunRequest, SubagentStatus } from "../subagents/src/contracts.js";
27
29
  import {
28
30
  SETTLED_AGENT_STATES,
@@ -34,7 +36,6 @@ import {
34
36
  workflowControlCall,
35
37
  workflowControlResult,
36
38
  workflowProgressBlock,
37
- formatWorkflowProgress,
38
39
  type WorkflowProgressRenderState,
39
40
  } from "./host-view.js";
40
41
  import {
@@ -59,7 +60,7 @@ import {
59
60
  type WorkflowLogEntry,
60
61
  } from "./host-delivery.js";
61
62
 
62
- export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
63
+ export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Partial<Pick<ExtensionAPI, "getAllTools">> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
63
64
 
64
65
  export {
65
66
  agentBreadcrumb,
@@ -117,6 +118,13 @@ function mainAgentError(error: unknown): WorkflowError {
117
118
  Object.assign(presented, typed);
118
119
  return presented;
119
120
  }
121
+ /** The structured counterpart of {@link completionControlContent}: the result without the persisted run, with the full value. */
122
+ function completionControlValue(result: Readonly<Record<string, unknown>>, controlRunId?: string): Record<string, unknown> {
123
+ const record: Record<string, unknown> = { ...(controlRunId === undefined ? {} : { runId: controlRunId }), ...result };
124
+ delete record.run;
125
+ delete record.completion;
126
+ return record;
127
+ }
120
128
  function completionControlContent(result: unknown, controlRunId?: string): string {
121
129
  const record = object(result) ? { ...result } : undefined;
122
130
  if (record && controlRunId !== undefined && record.runId === undefined) record.runId = controlRunId;
@@ -159,7 +167,7 @@ export function formatWorkflowPreview(args: { script?: unknown; scriptPath?: unk
159
167
  return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
160
168
  }
161
169
  export const WORKFLOW_TOOL_LABEL = "Workflow";
162
- export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default"
170
+ 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"
163
171
  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."
164
172
  export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
165
173
  name: Type.String({ description: "Required non-empty workflow name" }),
@@ -174,9 +182,26 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
174
182
  }, { additionalProperties: false });
175
183
  export const WORKFLOW_STATUS_PARAMETERS = Type.Object({ runId: Type.String({ description: "Workflow run ID visible in the current project" }) }, { additionalProperties: false });
176
184
  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" })) });
185
+ // Structured results codemode scripts receive instead of the JSON text; nested records stay loose so they cannot drift from persistence.
186
+ const WORKFLOW_ERROR_OUTPUT = Type.Object({ code: Type.String(), message: Type.String() });
187
+ const WORKFLOW_LAUNCH_OUTPUT = Type.Object({ runId: Type.String(), state: Type.String(), detached: Type.Optional(Type.Boolean()), value: Type.Optional(Type.Unknown()) });
188
+ 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()) })) });
189
+ const WORKFLOW_STOP_OUTPUT = Type.Object({ runId: Type.String(), state: Type.String(), stopped: Type.Boolean(), reason: Type.Optional(Type.String()) });
190
+ 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()) });
191
+ 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()) });
192
+ 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()) });
193
+ // The index without a name; with one, the function, the model alias, or { error }.
194
+ const WORKFLOW_CATALOG_OUTPUT = Type.Union([
195
+ 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()) }),
196
+ Type.Object({ name: Type.String(), version: Type.String(), headline: Type.String(), description: Type.String(), input: Type.Unknown(), output: Type.Unknown() }),
197
+ WORKFLOW_CATALOG_ALIAS_OUTPUT,
198
+ Type.Object({ error: Type.Object({ code: Type.String(), name: Type.String(), message: Type.String() }) }),
199
+ ]);
177
200
 
178
201
  function workflowToolUpdate(run: PersistedRun): WorkflowToolUpdate {
179
- return { content: [{ type: "text", text: formatWorkflowProgress(run) }], details: { runId: run.id, run } };
202
+ //NOTE: renderers read details.run; the partial text stays O(1) so high-frequency updates never format the whole tree.
203
+ const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
204
+ return { content: [{ type: "text", text: `Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done) [${run.state}]` }], details: { runId: run.id, run } };
180
205
  }
181
206
  function agentWithProgress(agent: AgentRecord, progress: AgentProgress): AgentRecord {
182
207
  const next = { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls };
@@ -200,6 +225,7 @@ function deliver(pi: WorkflowExtensionAPI, content: string): void {
200
225
  pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
201
226
  }
202
227
  const WORKFLOW_WARNING_ENTRY = "workflow-warning";
228
+ const PROGRESS_COALESCE_MS = 100;
203
229
  interface WorkflowWarningEntry { message: string }
204
230
  function deliverWarning(pi: WorkflowExtensionAPI, content: string): void {
205
231
  pi.appendEntry<WorkflowWarningEntry>(WORKFLOW_WARNING_ENTRY, { message: content });
@@ -254,9 +280,46 @@ function modelInventory(root: ModelSpec | undefined, registry: ModelRegistryCapa
254
280
  if (rootName) { knownModels.add(rootName); availableModels.add(rootName); }
255
281
  return { knownModels, availableModels };
256
282
  }
257
- function resumeHostContext(ctx: unknown): { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext } {
283
+ function resumeHostContext(ctx: unknown): { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext; toolContext: ExtensionToolContext | undefined } {
258
284
  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;
259
- return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry, deliveryContext: completionContext(ctx) };
285
+ return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry, deliveryContext: completionContext(ctx), toolContext: scriptToolContext(ctx) };
286
+ }
287
+ // State is rewritten on every update, so large shell inputs and errors are kept as prefixes.
288
+ const SCRIPT_CALL_TEXT_LIMIT_BYTES = 16 * 1024;
289
+ function scriptCallInput(input: JsonValue): Pick<WorkflowScriptCall, "input" | "inputBytes"> {
290
+ const text = JSON.stringify(input);
291
+ const bytes = Buffer.byteLength(text);
292
+ return bytes <= SCRIPT_CALL_TEXT_LIMIT_BYTES ? { input } : { input: utf8Prefix(text, SCRIPT_CALL_TEXT_LIMIT_BYTES), inputBytes: bytes };
293
+ }
294
+ /** Records a script call as started, replacing the record of an attempt that an interruption left unfinished. */
295
+ function startScriptCall(run: PersistedRun, call: WorkflowScriptCall): PersistedRun {
296
+ const recorded = { ...call, ...(run.phase === undefined ? {} : { phase: run.phase }) };
297
+ const calls = run.scriptCalls ?? [];
298
+ return { ...run, scriptCalls: calls.some(({ path }) => path === call.path) ? calls.map((current) => current.path === call.path ? recorded : current) : [...calls, recorded] };
299
+ }
300
+ function finishScriptCall(run: PersistedRun, path: string, error?: WorkflowError): PersistedRun {
301
+ const finishedAt = Date.now();
302
+ 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) };
303
+ }
304
+ type AliasVirtualModel = { provider: string; id: string; name: string; thinkingLevels: readonly string[]; route: (request: { thinkingLevel: string }, ctx: ExtensionContext) => { model: Model<Api>; thinkingLevel: string } };
305
+ /** Lists workflow model aliases in `/model` as virtual models; needs a Pi host with `registerVirtualModel`. */
306
+ function registerAliasVirtualModels(pi: unknown, names: readonly string[], settingsPath: string): void {
307
+ const register = object(pi) ? asFn(pi.registerVirtualModel) : undefined;
308
+ if (!register) return;
309
+ for (const name of names) {
310
+ const definition: AliasVirtualModel = {
311
+ provider: VIRTUAL_MODEL_PROVIDER, id: name, name: `${name} (workflow alias)`, thinkingLevels: THINKING_LEVELS,
312
+ route: (request, ctx) => {
313
+ // Settings are read per request so alias edits apply without a reload.
314
+ const aliases = resolveWorkflowSettings(ctx.cwd, projectTrusted(ctx), settingsPath).effective.modelAliases ?? {};
315
+ const target = resolveModelReference(name, aliases, new Set(ctx.modelRegistry.getAll().map((model) => `${model.provider}/${model.id}`)), settingsPath);
316
+ const model = ctx.modelRegistry.find(target.provider, target.model);
317
+ if (!model) throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${name} resolved to ${target.provider}/${target.model}`);
318
+ return { model, thinkingLevel: request.thinkingLevel };
319
+ },
320
+ };
321
+ Reflect.apply(register, pi, [definition]);
322
+ }
260
323
  }
261
324
  async function resolveLaunchAliases(registry: WorkflowRegistryApi, staticAliases: Readonly<Record<string, string>>, context: Readonly<WorkflowModelAliasResolverContext>, availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath: string): Promise<{ aliases: Readonly<Record<string, string>>; dynamicNames: readonly string[] }> {
262
325
  const dynamic = typeof registry.resolveModelAliases === "function" ? await registry.resolveModelAliases(context, new Set(Object.keys(staticAliases))) : {};
@@ -287,8 +350,14 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
287
350
  return textBlock(data ? `Warning: ${data.message}` : "");
288
351
  });
289
352
  let backgroundWidgetEnabled = true;
290
- try { backgroundWidgetEnabled = loadSettings(workflowSettingsPath(extensionAgentDir)).backgroundWidget ?? true; } catch { /* Keep the optional UI enabled; the launch path reports settings errors. */ }
353
+ try {
354
+ const globalSettings = loadSettings(workflowSettingsPath(extensionAgentDir));
355
+ backgroundWidgetEnabled = globalSettings.backgroundWidget ?? true;
356
+ // Registered at load, before Pi resolves --model and restores the session model. Project aliases need project trust, which is unknown here.
357
+ registerAliasVirtualModels(pi, Object.keys(globalSettings.modelAliases ?? {}), workflowSettingsPath(extensionAgentDir));
358
+ } catch { /* Keep the optional UI enabled; the launch path reports settings errors. */ }
291
359
  const backgroundWidgetController = backgroundWidget(pi, backgroundWidgetEnabled);
360
+ const codemodeTools = loadCodemodeToolsSetting(extensionAgentDir);
292
361
  const logBridge = (store: RunStore, lifecycle: RunLifecycle, workflowName: string) => async (message: string) => {
293
362
  const timestamp = Date.now();
294
363
  const bounded = utf8Prefix(message, DELIVERY_LIMIT_BYTES);
@@ -415,7 +484,6 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
415
484
  const trusted = projectTrusted(context);
416
485
  const settings = resolveWorkflowSettings(cwd, trusted, workflowSettingsPath(extensionAgentDir)).effective.extensionSettings?.trajectory;
417
486
  const port = settings?.port;
418
- const themes = settings?.themes ?? false;
419
487
  const loadRuns = createTrajectoryRunLoader(cwd, sessionId, home, overlayLiveRun);
420
488
  const loadSubagents = createTrajectorySubagentLoader(cwd, sessionId, extensionAgentDir, overlayLiveSubagent);
421
489
  const loadRunsMetadata = createTrajectoryRunMetadataLoader(cwd, sessionId, home, overlayLiveRun);
@@ -424,7 +492,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
424
492
  const live = liveSubagents.get(subagentId);
425
493
  return live?.status.sessionId === sessionId ? live.status.attemptDetails?.at(-1)?.session : undefined;
426
494
  });
427
- return { cwd, sessionId, ...(port === undefined ? {} : { port }), themes, loadRuns, loadSubagents, loadMetadata: async () => ({ runs: await loadRunsMetadata(), subagents: await loadSubagentsMetadata() }), loadTranscript, handleAction: (request: Readonly<TrajectoryActionRequest>) => trajectoryAction(request, context) };
495
+ return { cwd, sessionId, ...(port === undefined ? {} : { port }), loadRuns, loadSubagents, loadMetadata: async () => ({ runs: await loadRunsMetadata(), subagents: await loadSubagentsMetadata() }), loadTranscript, handleAction: (request: Readonly<TrajectoryActionRequest>) => trajectoryAction(request, context) };
428
496
  };
429
497
  const withLiveActivities = (run: PersistedRun): PersistedRun => liveAgents.overlay(run);
430
498
  /** Overlays the in-memory lifecycle state and budget usage of an active run onto its persisted record. */
@@ -532,7 +600,11 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
532
600
  if (replayed) return readShellResult(replayed.value);
533
601
  const shellStartedAt = Date.now();
534
602
  let shellPhaseIndex = -1;
535
- const started = await persistRunState(store, metadata, (current) => {
603
+ // Env values stay out of the record: they may carry secrets, and a shared trajectory publishes run state.
604
+ const call: WorkflowScriptCall = { 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 };
605
+ let failure: WorkflowError | undefined;
606
+ const started = await persistRunState(store, metadata, (run) => {
607
+ const current = startScriptCall(run, call);
536
608
  const history = current.phaseHistory ?? [];
537
609
  if (current.phase !== undefined) shellPhaseIndex = current.phaseHistoryIndex ?? (history.length ? history.length - 1 : 0);
538
610
  const phaseActivities = [...(current.activeShellsByPhase ?? [])];
@@ -550,8 +622,12 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
550
622
  if (!jsonValue(result)) fail("SHELL_FAILED", "Shell result is not JSON-compatible");
551
623
  await store.complete(path, result);
552
624
  return result;
625
+ } catch (error) {
626
+ failure = asWorkflowError(error);
627
+ throw error;
553
628
  } finally {
554
- const stopped = await persistRunState(store, metadata, (current) => {
629
+ const stopped = await persistRunState(store, metadata, (run) => {
630
+ const current = finishScriptCall(run, path, failure);
555
631
  const phaseActivities = [...(current.activeShellsByPhase ?? [])];
556
632
  const phaseActivityIndex = phaseActivities.findIndex(({ phaseIndex }) => phaseIndex === shellPhaseIndex);
557
633
  const phaseActivity = phaseActivities[phaseActivityIndex];
@@ -570,6 +646,44 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
570
646
  }
571
647
  } finally { await lifecycle.leave(); }
572
648
  };
649
+ /** A script `tools.<id>(args)` call: replayed from the journal, otherwise run through the session's tool pipeline and journaled. */
650
+ const toolForRun = async (store: RunStore, lifecycle: RunLifecycle, context: ExtensionToolContext | undefined, identifier: string, args: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: ToolIdentity): Promise<JsonValue> => {
651
+ await lifecycle.enter();
652
+ try {
653
+ const path = toolIdentityPath(identifier, args, identity);
654
+ const replayed = await store.replay(path);
655
+ if (replayed) return replayed.value;
656
+ if (!context) 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`);
657
+ // Rendering is best-effort: it must never lose or mask a completed call.
658
+ const render = async () => { try { const update = runs.get(store.runId)?.update; if (update) update(workflowToolUpdate(withLiveActivities((await store.load()).run))); } catch { /* best-effort progress */ } };
659
+ // Inspection writes must not prevent execution, mask errors, or reject a journaled success.
660
+ const record = async (update: (run: PersistedRun) => PersistedRun) => { try { await store.updateState(update); } catch { /* best-effort call metadata */ } };
661
+ let value: JsonValue;
662
+ try {
663
+ const tool = scriptTool(context, identifier);
664
+ if (!tool) fail("UNKNOWN_TOOL", `tools.${identifier} is not a tool this session can call`);
665
+ const cancelled = (): boolean => signal.aborted;
666
+ if (cancelled()) fail("CANCELLED", "Workflow cancelled");
667
+ await record((run) => startScriptCall(run, { kind: "tool", name: tool.name, path, startedAt: Date.now() }));
668
+ const finished = liveAgents.toolStarted(store.runId, tool.name);
669
+ try {
670
+ await render();
671
+ const outcome = await context.executeTool(tool.name, args, { signal });
672
+ if (cancelled()) fail("CANCELLED", "Workflow cancelled");
673
+ value = scriptToolValue(tool, outcome);
674
+ } finally { finished(); }
675
+ } catch (error) {
676
+ // A stale context throws plain errors from ctx.tools; report them as tool failures.
677
+ const failure = error instanceof WorkflowError ? error : new WorkflowError("TOOL_FAILED", `tools.${identifier}: ${errorText(error)}`);
678
+ await record((run) => finishScriptCall(run, path, failure));
679
+ throw failure;
680
+ }
681
+ await store.complete(path, value);
682
+ await record((run) => finishScriptCall(run, path));
683
+ await render();
684
+ return value;
685
+ } finally { await lifecycle.leave(); }
686
+ };
573
687
  const lifecycleFor = (store: RunStore, state: RunState, metadata: WorkflowMetadata) => new RunLifecycle(state, async (next, previous, reason) => {
574
688
  const run = runs.get(store.runId);
575
689
  if (!run) throw new WorkflowError("INTERNAL_ERROR", `Unknown production run: ${store.runId}`);
@@ -586,22 +700,49 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
586
700
  const scheduler = new FairAgentScheduler(async ({ id, runId, tuiIndex, parentId, prompt, options, signal, setSteer }) => {
587
701
  const run = runs.get(runId);
588
702
  if (!run) throw new WorkflowError("INTERNAL_ERROR", `Unknown production run: ${runId}`);
703
+ const applyProgress = async (progress: AgentProgress) => {
704
+ let runState: PersistedRun;
705
+ if (progress.persist) {
706
+ runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) } : current);
707
+ } else {
708
+ const loaded = await run.store.loadStatus();
709
+ if (!loaded.agents.some((agent) => agent.id === id)) return;
710
+ runState = { ...loaded, ...run.budget.snapshot(), agents: loaded.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) };
711
+ }
712
+ if (!runState.agents.some((agent) => agent.id === id)) return;
713
+ liveAgents.setActivity(runId, id, progress.activity);
714
+ liveAgents.setEventTime(runId, id, progress.lastEventAt);
715
+ run.update?.(workflowToolUpdate(withLiveActivities(runState)));
716
+ };
717
+ // Display-only progress bursts coalesce to one trailing apply per window; persisted progress always applies immediately.
718
+ let deferred: AgentProgress | undefined;
719
+ let deferredTimer: NodeJS.Timeout | undefined;
720
+ let appliedAt = 0;
721
+ const cancelDeferredProgress = () => {
722
+ if (deferredTimer) clearTimeout(deferredTimer);
723
+ deferredTimer = undefined;
724
+ deferred = undefined;
725
+ };
726
+ const onProgress = async (progress: AgentProgress) => {
727
+ const now = Date.now();
728
+ if (progress.persist || (!deferredTimer && now - appliedAt >= PROGRESS_COALESCE_MS)) {
729
+ cancelDeferredProgress();
730
+ appliedAt = now;
731
+ await applyProgress(progress);
732
+ return;
733
+ }
734
+ deferred = progress;
735
+ deferredTimer ??= setTimeout(() => {
736
+ deferredTimer = undefined;
737
+ const next = deferred;
738
+ deferred = undefined;
739
+ if (!next) return;
740
+ appliedAt = Date.now();
741
+ void applyProgress(next).catch(() => undefined);
742
+ }, Math.max(0, PROGRESS_COALESCE_MS - (now - appliedAt))).unref();
743
+ };
589
744
  try {
590
745
  const budget = run.budget.forAgent(id);
591
- const onProgress = async (progress: AgentProgress) => {
592
- let runState: PersistedRun;
593
- if (progress.persist) {
594
- runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) } : current);
595
- } else {
596
- const loaded = await run.store.load();
597
- if (!loaded.run.agents.some((agent) => agent.id === id)) return;
598
- runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) };
599
- }
600
- if (!runState.agents.some((agent) => agent.id === id)) return;
601
- liveAgents.setActivity(runId, id, progress.activity);
602
- liveAgents.setEventTime(runId, id, progress.lastEventAt);
603
- run.update?.(workflowToolUpdate(withLiveActivities(runState)));
604
- };
605
746
  const onAttempt = async (attempt: AgentAttempt) => {
606
747
  liveAgents.setSession(runId, id, attempt.liveSession);
607
748
  liveAgents.setHandoff(runId, id, attempt);
@@ -618,6 +759,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
618
759
  run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
619
760
  };
620
761
  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); });
762
+ cancelDeferredProgress();
621
763
  const before = (await run.store.load()).run;
622
764
  await persistAgentAttempts(run.store, id, result.attempts);
623
765
  const completed = (await run.store.load()).run;
@@ -628,6 +770,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
628
770
  run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
629
771
  return result.value;
630
772
  } catch (error) {
773
+ cancelDeferredProgress();
631
774
  liveAgents.setSession(runId, id);
632
775
  const attempts = getAgentAttempts(error);
633
776
  if (attempts?.length) {
@@ -791,17 +934,20 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
791
934
  label: string,
792
935
  description: string,
793
936
  parameters: P,
794
- run: (params: Static<P>, signal: AbortSignal, ctx: unknown) => Promise<{ text: string; details: unknown }>,
937
+ outputSchema: TSchema,
938
+ run: (params: Static<P>, signal: AbortSignal, ctx: unknown) => Promise<{ text: string; details: unknown; structured: unknown }>,
795
939
  ) => {
796
940
  pi.registerTool({
797
941
  name,
798
942
  label,
799
943
  description,
800
944
  parameters,
945
+ outputSchema,
946
+ ...workflowToolExposure(name, codemodeTools),
801
947
  async execute(_id, params, signal: AbortSignal, _onUpdate, ctx) {
802
948
  try {
803
949
  const result = await run(params, signal, ctx);
804
- return { content: [{ type: "text" as const, text: result.text }], details: result.details };
950
+ return { content: [{ type: "text" as const, text: result.text }], details: result.details, structuredContent: result.structured as JsonValue };
805
951
  } catch (error) {
806
952
  throw mainAgentError(error);
807
953
  }
@@ -816,15 +962,18 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
816
962
  "Workflow Respond",
817
963
  "Approve or reject one pending workflow checkpoint or budget decision",
818
964
  Type.Object({ runId: Type.String(), name: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), approved: Type.Boolean() }, { additionalProperties: false }),
965
+ WORKFLOW_RESPOND_OUTPUT,
819
966
  async (params, signal, ctx) => {
820
967
  if (params.proposalId) {
821
968
  const result = await recovery.answerBudgetDecision(params.runId, params.proposalId, params.approved, false, ctx, signal);
822
- if (!result) { const denied = { state: "budget_exhausted" as const, approved: false, reason: "proposal_not_pending" }; return { text: JSON.stringify(denied), details: denied }; }
823
- return { text: completionControlContent(result, params.runId), details: { ...result, reason: params.approved ? "approved" : "rejected" } };
969
+ if (!result) { const denied = { state: "budget_exhausted" as const, approved: false, reason: "proposal_not_pending" }; return { text: JSON.stringify(denied), details: denied, structured: denied }; }
970
+ const reason = params.approved ? "approved" : "rejected";
971
+ return { text: completionControlContent(result, params.runId), details: { ...result, reason }, structured: { ...completionControlValue(result, params.runId), reason } };
824
972
  }
825
973
  if (!params.name) throw new WorkflowError("INVALID_METADATA", "workflow_respond requires name or proposalId");
826
974
  const accepted = await answerCheckpoint(params.runId, params.name, params.approved);
827
- 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" } };
975
+ const details = { accepted, state: accepted ? "checkpoint_answered" : "not_pending", approved: params.approved, reason: "checkpoint" };
976
+ return { text: accepted ? "Checkpoint response accepted." : "Checkpoint is not awaiting a response.", details, structured: details };
828
977
  },
829
978
  );
830
979
  registerControlTool(
@@ -832,9 +981,10 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
832
981
  "Workflow Stop",
833
982
  "Stop an active workflow run by ID",
834
983
  Type.Object({ runId: Type.String() }, { additionalProperties: false }),
984
+ WORKFLOW_STOP_OUTPUT,
835
985
  async (params) => {
836
986
  const result = await stopWorkflowRun(params.runId);
837
- return { text: JSON.stringify(result), details: result };
987
+ return { text: JSON.stringify(result), details: result, structured: result };
838
988
  },
839
989
  );
840
990
  registerControlTool(
@@ -842,9 +992,10 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
842
992
  "Workflow Status",
843
993
  "Read a compact summary of a workflow run in the current project",
844
994
  WORKFLOW_STATUS_PARAMETERS,
995
+ WORKFLOW_STATUS_OUTPUT,
845
996
  async (params, _signal, ctx) => {
846
997
  const result = await workflowStatusRun(params.runId, ctx);
847
- return { text: JSON.stringify(result), details: result };
998
+ return { text: JSON.stringify(result), details: result, structured: result };
848
999
  },
849
1000
  );
850
1001
  let catalogRegistered = false;
@@ -857,13 +1008,15 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
857
1008
  if (!catalog.functions.length && !hasAliases && !hasSettings) return;
858
1009
  pi.registerTool({
859
1010
  name: "workflow_catalog",
1011
+ ...workflowToolExposure("workflow_catalog", codemodeTools),
1012
+ outputSchema: WORKFLOW_CATALOG_OUTPUT,
860
1013
  label: "Workflow Catalog",
861
1014
  description: "List reusable workflow functions and model aliases; pass `name` to load one entry in full",
862
1015
  parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or model alias name for full detail" })) }, { additionalProperties: false }),
863
1016
  async execute(_id, params = {}) {
864
1017
  const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
865
1018
  const result = params.name === undefined ? registry.catalogIndex(context) : registry.catalogDetail(params.name, context);
866
- return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
1019
+ return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result, structuredContent: result as unknown as JsonValue };
867
1020
  },
868
1021
  renderCall(args, theme) {
869
1022
  const title = theme.fg("toolTitle", theme.bold("workflow_catalog"));
@@ -890,7 +1043,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
890
1043
  return { definitions, capture: (role: string, model: ModelSpec) => captured.capture(role, model) };
891
1044
  };
892
1045
  const activeSnapshotTools = (tools: readonly string[], active: ReadonlySet<string> | "session") => active === "session"
893
- ? new Set(tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog"))
1046
+ ? new Set(tools.filter((tool) => reachableTools(pi).includes(tool) && tool !== "workflow_catalog"))
894
1047
  : new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
895
1048
  const resumeLaunchPrologue = async (input: {
896
1049
  snapshot: Readonly<LaunchSnapshot>;
@@ -904,7 +1057,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
904
1057
  blockedAliasTargets?: Readonly<Record<string, string>>;
905
1058
  withPreflight: boolean;
906
1059
  }) => {
907
- const active = new Set(pi.getActiveTools().filter((tool) => !INTERNAL_WORKFLOW_TOOLS.includes(tool)));
1060
+ const active = new Set(reachableTools(pi).filter((tool) => !INTERNAL_WORKFLOW_TOOLS.includes(tool)));
908
1061
  const missing = input.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
909
1062
  if (missing) throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
910
1063
  const settingsPath = workflowSettingsPath(extensionAgentDir);
@@ -971,7 +1124,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
971
1124
  };
972
1125
  };
973
1126
  const recovery = createWorkflowRecovery({
974
- 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,
1127
+ pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, coordinateRunMutation, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue: resumeLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, toolForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver: (content) => { deliver(pi, content); }, deliverTerminal: deliveryController.deliverTerminal, workflowToolUpdate, registry, modelSpec,
975
1128
  });
976
1129
  const resumeSelectedWorkflow = async (runId: string, foreground: boolean, context: unknown, budgetPatch?: unknown): Promise<{ workflowName: string; state: "running" | "completed" | "awaiting_approval"; attached: boolean; value?: JsonValue }> => {
977
1130
  const run = runs.get(runId);
@@ -1027,9 +1180,10 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1027
1180
  "Workflow Retry",
1028
1181
  "Retry a failed workflow run by replaying its completed structural operations",
1029
1182
  WORKFLOW_RETRY_PARAMETERS,
1183
+ WORKFLOW_RECOVERY_OUTPUT,
1030
1184
  async (params, signal, ctx) => {
1031
1185
  const result = await recovery.retryWorkflowRun(params.runId, ctx, signal, params.foreground, params.expectedState);
1032
- return { text: completionControlContent(result), details: result };
1186
+ return { text: completionControlContent(result), details: result, structured: completionControlValue(result) };
1033
1187
  },
1034
1188
  );
1035
1189
  registerControlTool(
@@ -1037,9 +1191,10 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1037
1191
  "Workflow Resume",
1038
1192
  "Resume an exhausted workflow with unchanged or patched aggregate budgets",
1039
1193
  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 }),
1194
+ WORKFLOW_RECOVERY_OUTPUT,
1040
1195
  async (params, signal, ctx) => {
1041
1196
  const result = await recovery.resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground, true, params.expectedState);
1042
- return { text: completionControlContent(result), details: result };
1197
+ return { text: completionControlContent(result), details: result, structured: completionControlValue(result) };
1043
1198
  },
1044
1199
  );
1045
1200
  const deliverStaleTerminal = async (store: RunStore, run: PersistedRun): Promise<void> => {
@@ -1141,6 +1296,8 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1141
1296
  description: WORKFLOW_TOOL_DESCRIPTION,
1142
1297
  promptSnippet: WORKFLOW_TOOL_PROMPT_SNIPPET,
1143
1298
  parameters: WORKFLOW_TOOL_PARAMETERS,
1299
+ outputSchema: WORKFLOW_LAUNCH_OUTPUT,
1300
+ prepareLoadout: prepareScriptToolLoadout,
1144
1301
  async execute(toolCallId, params, signal, onUpdate, ctx) {
1145
1302
  let resolveDetached: ((result: ForegroundDetachResult) => void) | undefined;
1146
1303
  let foregroundStore: RunStore | undefined;
@@ -1148,7 +1305,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1148
1305
  const detachedResult = params.foreground ? new Promise<ForegroundDetachResult>((resolve) => { resolveDetached = resolve; }) : undefined;
1149
1306
  const detachedToolResult = (run: PersistedRun) => {
1150
1307
  const detached = { runId: run.id, state: "running" as const, detached: true as const };
1151
- return { content: [{ type: "text" as const, text: JSON.stringify(detached) }], details: { ...detached, run, preview: `Moved workflow ${run.id} to background.` } };
1308
+ return { content: [{ type: "text" as const, text: JSON.stringify(detached) }], details: { ...detached, run, preview: `Moved workflow ${run.id} to background.` }, structuredContent: detached };
1152
1309
  };
1153
1310
  try {
1154
1311
  const headless = object(ctx) && ctx.headless === true;
@@ -1161,7 +1318,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1161
1318
  const inventory = modelInventory(rootModel, modelRegistry);
1162
1319
  const knownModels = inventory.knownModels;
1163
1320
  const availableModels = inventory.availableModels;
1164
- const rootTools = pi.getActiveTools().filter((name) => !INTERNAL_WORKFLOW_TOOLS.includes(name));
1321
+ const rootTools = reachableTools(pi).filter((name) => !INTERNAL_WORKFLOW_TOOLS.includes(name));
1165
1322
  const trustedProject = projectTrusted(ctx);
1166
1323
  const launchCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
1167
1324
  const launch = workflowLaunchSettings(launchCwd, trustedProject, settingsPath, params.concurrency);
@@ -1176,6 +1333,8 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1176
1333
  if (trustedProject && launch.resolution.project.extensionSettings !== undefined) registry.validateExtensionSettings(launch.resolution.project.extensionSettings, { source: "project", cwd: ctx.cwd, projectTrusted: true, settingsPath: launch.resolution.projectSettingsPath });
1177
1334
  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);
1178
1335
  const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames } = validated;
1336
+ const toolContext = scriptToolContext(ctx);
1337
+ validateScriptToolReferences(script, toolContext);
1179
1338
  await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
1180
1339
  const runId = randomUUID();
1181
1340
  const args = params.args ?? null;
@@ -1232,7 +1391,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1232
1391
  runs.set(runId, runRecord);
1233
1392
  if (params.foreground && onUpdate) onUpdate(workflowToolUpdate((await store.load()).run));
1234
1393
  scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch(), settings.extensionSettings);
1235
- 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);
1394
+ const execution = runWorkflow(script, args, withWorkflowFunctions({ tool: (identifier, toolArgs, signal, identity) => toolForRun(store, lifecycle, toolContext, identifier, toolArgs, signal, identity), shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId, (role, model) => capturedRoles.capture(role, model)), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, () => runs.get(runId)?.foreground ?? foregroundAttached, ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(store, lifecycle, checked.metadata.name) }, store, runContext, registry, settings.extensionSettings), runController.signal);
1236
1395
  runRecord.execution = execution;
1237
1396
  await eventPublisher.runStarted(store, checked.metadata);
1238
1397
  const finish = execution.result.then(async (value) => {
@@ -1271,7 +1430,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1271
1430
  }, async (error: unknown) => {
1272
1431
  await deliveryController.deliverTerminal(store, deliverFailureContent(error), true);
1273
1432
  });
1274
- return { content: [{ type: "text" as const, text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
1433
+ return { content: [{ type: "text" as const, text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` }, structuredContent: { runId, state: "running" } };
1275
1434
  }
1276
1435
  void completion.then(async (result) => {
1277
1436
  await deliveryController.deliverDetachedTerminal(toolCallId, completionContent("background", result));
@@ -1296,7 +1455,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1296
1455
  }
1297
1456
  deliveryController.foregroundDeliveries.delete(toolCallId);
1298
1457
  const run = (await store.load()).run;
1299
- return { content: [{ type: "text" as const, text: foregroundDelivery.content }, ...(foregroundDelivery.inlined ? [{ type: "text" as const, text: `Workflow run ID: ${runId}` }] : [])], details: { runId, value, run } };
1458
+ return { content: [{ type: "text" as const, text: foregroundDelivery.content }, ...(foregroundDelivery.inlined ? [{ type: "text" as const, text: `Workflow run ID: ${runId}` }] : [])], details: { runId, value, run }, structuredContent: { runId, state: "completed", value } };
1300
1459
  } catch (error) {
1301
1460
  if (params.foreground && foregroundStore && completionInstalled) {
1302
1461
  const claim = await deliveryController.claimForegroundDelivery(foregroundStore, toolCallId);
@@ -1365,8 +1524,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1365
1524
  state.workflowProgressComponent = workflowProgressBlock(currentProgress?.run ?? incoming, theme, currentProgress, async () => {
1366
1525
  const active = runs.get(incoming.id);
1367
1526
  const store = active?.store ?? new RunStore(incoming.cwd, incoming.sessionId, incoming.id, home);
1368
- const loaded = await store.load();
1369
- return withLiveActivities(loaded.run);
1527
+ return withLiveActivities(await store.loadStatus());
1370
1528
  }, () => { if (state.workflowProgress === currentProgress) requestRender(); }, undefined, state.workflowProgressFrozenAt);
1371
1529
  }
1372
1530
  state.workflowProgressComponent.setExpanded(expanded);
@@ -1376,7 +1534,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1376
1534
  return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
1377
1535
  },
1378
1536
  };
1379
- pi.registerTool(workflowTool);
1537
+ pi.registerTool({ ...workflowTool, ...workflowToolExposure(workflowTool.name, codemodeTools) });
1380
1538
  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) backgroundWidgetController.suspend(); else backgroundWidgetController.resume(); } });
1381
1539
  pi.on("session_shutdown", async () => {
1382
1540
  try {
@@ -1406,6 +1564,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1406
1564
  type LiveAgentState = { session?: WorkflowAgentSession; prepared?: Readonly<PreparedAgentSession>; handoff?: LiveSessionHandoff; activity?: AgentActivity; lastEventAt?: number };
1407
1565
  class LiveAgentRegistry {
1408
1566
  readonly #byRun = new Map<string, Map<string, LiveAgentState>>();
1567
+ readonly #tools = new Map<string, Set<{ name: string; startedAt: number }>>();
1409
1568
  get(runId: string, agentId: string): Readonly<LiveAgentState> | undefined { return this.#byRun.get(runId)?.get(agentId); }
1410
1569
  setSession(runId: string, agentId: string, session?: WorkflowAgentSession): void {
1411
1570
  if (session) this.#state(runId, agentId).session = session;
@@ -1422,17 +1581,26 @@ class LiveAgentRegistry {
1422
1581
  setEventTime(runId: string, agentId: string, timestamp?: number): void {
1423
1582
  if (timestamp !== undefined) this.#state(runId, agentId).lastEventAt = timestamp;
1424
1583
  }
1425
- deleteRun(runId: string): void { this.#byRun.delete(runId); }
1426
- /** Overlays live activity and freshness onto a persisted run; returns the input when nothing is live. */
1584
+ /** Marks a script tool call in flight until the returned callback runs. */
1585
+ toolStarted(runId: string, name: string): () => void {
1586
+ const call = { name, startedAt: Date.now() };
1587
+ const calls = this.#tools.get(runId) ?? new Set<{ name: string; startedAt: number }>();
1588
+ this.#tools.set(runId, calls.add(call));
1589
+ return () => { calls.delete(call); if (!calls.size && this.#tools.get(runId) === calls) this.#tools.delete(runId); };
1590
+ }
1591
+ deleteRun(runId: string): void { this.#byRun.delete(runId); this.#tools.delete(runId); }
1592
+ /** Overlays live activity, freshness, and script tool calls onto a persisted run; returns the input when nothing is live. */
1427
1593
  overlay(run: PersistedRun): PersistedRun {
1594
+ const tools = this.#tools.get(run.id);
1595
+ const withTools = tools?.size ? { ...run, activeTools: [...tools].map((call) => ({ ...call })) } : run;
1428
1596
  const agents = this.#byRun.get(run.id);
1429
- if (!agents?.size) return run;
1597
+ if (!agents?.size) return withTools;
1430
1598
  const next = run.agents.map((agent) => {
1431
1599
  const live = agents.get(agent.id);
1432
1600
  if (!live || (live.activity === undefined && live.lastEventAt === undefined)) return agent;
1433
1601
  return { ...agent, ...(live.activity === undefined ? {} : { activity: live.activity }), ...(live.lastEventAt === undefined ? {} : { lastEventAt: live.lastEventAt }) };
1434
1602
  });
1435
- return next.some((agent, index) => agent !== run.agents[index]) ? { ...run, agents: next } : run;
1603
+ return next.some((agent, index) => agent !== run.agents[index]) ? { ...withTools, agents: next } : withTools;
1436
1604
  }
1437
1605
  #state(runId: string, agentId: string): LiveAgentState {
1438
1606
  const agents = this.#byRun.get(runId) ?? new Map<string, LiveAgentState>();