@marcoscale98/pi-extensible-workflows 5.19.1-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 (123) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +2 -2
  3. package/dist/src/agent-execution.d.ts +43 -10
  4. package/dist/src/agent-execution.js +365 -127
  5. package/dist/src/background-widget.d.ts +0 -1
  6. package/dist/src/background-widget.js +4 -9
  7. package/dist/src/decoders.d.ts +2 -3
  8. package/dist/src/decoders.js +33 -37
  9. package/dist/src/execution.d.ts +2 -0
  10. package/dist/src/execution.js +5 -5
  11. package/dist/src/host-delivery.js +4 -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 +6 -6
  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 +1 -1
  18. package/dist/src/host-view.js +17 -15
  19. package/dist/src/host.js +62 -87
  20. package/dist/src/index.d.ts +1 -2
  21. package/dist/src/index.js +1523 -1630
  22. package/dist/src/index.js.map +3 -3
  23. package/dist/src/paths.d.ts +7 -6
  24. package/dist/src/paths.js +24 -15
  25. package/dist/src/persistence.d.ts +1 -0
  26. package/dist/src/persistence.js +1 -0
  27. package/dist/src/pi-runtime-runner.js +4 -1
  28. package/dist/src/registry.d.ts +9 -7
  29. package/dist/src/registry.js +28 -70
  30. package/dist/src/runtime/agent-runner.d.ts +2 -0
  31. package/dist/src/session-lease.d.ts +1 -1
  32. package/dist/src/session-lease.js +45 -11
  33. package/dist/src/settings.d.ts +6 -3
  34. package/dist/src/settings.js +36 -47
  35. package/dist/src/store.d.ts +3 -1
  36. package/dist/src/store.js +17 -1
  37. package/dist/src/trajectory.d.ts +0 -1
  38. package/dist/src/trajectory.js +2 -3
  39. package/dist/src/types.d.ts +100 -44
  40. package/dist/src/types.js +4 -7
  41. package/dist/src/utils.d.ts +3 -2
  42. package/dist/src/utils.js +20 -10
  43. package/dist/src/validation.d.ts +8 -2
  44. package/dist/src/validation.js +122 -86
  45. package/dist/starter/index.js +8 -9
  46. package/dist/starter/index.js.map +1 -1
  47. package/dist/starter/review-loop.js +8 -7
  48. package/dist/subagents/index.js +654 -814
  49. package/dist/subagents/index.js.map +3 -3
  50. package/dist/subagents/src/contracts.d.ts +4 -3
  51. package/dist/subagents/src/contracts.js +2 -2
  52. package/dist/subagents/src/index.d.ts +1 -1
  53. package/dist/subagents/src/index.js +1 -2
  54. package/dist/subagents/src/manager.js +49 -70
  55. package/dist/subagents/src/navigator.js +3 -14
  56. package/dist/subagents/src/view.d.ts +0 -1
  57. package/dist/subagents/src/view.js +2 -16
  58. package/dist/subagents/src/worktree.js +1 -1
  59. package/dist/trajectory/assets/index.html +8 -8
  60. package/dist/trajectory/index.js +487 -891
  61. package/dist/trajectory/index.js.map +3 -3
  62. package/dist/trajectory/src/assets/index.html +8 -8
  63. package/dist/trajectory/src/index.js +126 -86
  64. package/dist/trajectory/src/server.js +264 -25
  65. package/dist/trajectory/src/server.js.map +3 -3
  66. package/dist/trajectory/src/startup-mutex.d.ts +17 -0
  67. package/dist/trajectory/src/startup-mutex.js +172 -0
  68. package/examples/workflow-extension-template/README.md +4 -7
  69. package/examples/workflow-extension-template/extension.test.mjs +4 -8
  70. package/examples/workflow-extension-template/index.js +0 -3
  71. package/package.json +5 -5
  72. package/skills/pi-extensible-workflows/SKILL.md +10 -8
  73. package/src/agent-execution.ts +339 -127
  74. package/src/background-widget.ts +4 -10
  75. package/src/decoders.ts +31 -37
  76. package/src/execution.ts +5 -5
  77. package/src/host-delivery.ts +4 -8
  78. package/src/host-navigator.ts +4 -2
  79. package/src/host-phases.ts +1 -1
  80. package/src/host-recovery.ts +20 -26
  81. package/src/host-runtime.ts +44 -6
  82. package/src/host-view.ts +18 -11
  83. package/src/host.ts +62 -113
  84. package/src/index.ts +1 -2
  85. package/src/paths.ts +18 -12
  86. package/src/persistence.ts +1 -0
  87. package/src/pi-runtime-runner.ts +3 -1
  88. package/src/registry.ts +27 -63
  89. package/src/runtime/agent-runner.ts +2 -0
  90. package/src/session-lease.ts +34 -6
  91. package/src/settings.ts +35 -47
  92. package/src/store.ts +18 -2
  93. package/src/trajectory.ts +2 -4
  94. package/src/types.ts +71 -30
  95. package/src/utils.ts +18 -9
  96. package/src/validation.ts +102 -79
  97. package/starter/index.ts +0 -1
  98. package/starter/review-loop.ts +8 -7
  99. package/starter/roles/developer.md +0 -1
  100. package/starter/roles/oracle.md +0 -1
  101. package/starter/roles/researcher.md +0 -1
  102. package/starter/roles/reviewer.md +0 -1
  103. package/starter/roles/scout.md +0 -1
  104. package/subagents/README.md +8 -14
  105. package/subagents/src/contracts.ts +3 -4
  106. package/subagents/src/index.ts +2 -3
  107. package/subagents/src/manager.ts +45 -125
  108. package/subagents/src/navigator.ts +4 -13
  109. package/subagents/src/view.ts +4 -14
  110. package/subagents/src/worktree.ts +1 -1
  111. package/trajectory/src/assets/index.html +8 -8
  112. package/trajectory/src/index.ts +106 -82
  113. package/trajectory/src/server.ts +44 -8
  114. package/trajectory/src/startup-mutex.ts +127 -0
  115. package/dist/src/roles.d.ts +0 -70
  116. package/dist/src/roles.js +0 -326
  117. package/dist/starter/roles/developer.md +0 -6
  118. package/dist/starter/roles/oracle.md +0 -18
  119. package/dist/starter/roles/researcher.md +0 -20
  120. package/dist/starter/roles/reviewer.md +0 -19
  121. package/dist/starter/roles/scout.md +0 -18
  122. package/examples/workflow-extension-template/roles/reviewer.md +0 -4
  123. package/src/roles.ts +0 -327
package/src/host.ts CHANGED
@@ -5,7 +5,7 @@ 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
7
  import { copyToClipboard, getAgentDir, ModelSelectorComponent, type ExtensionAPI, type ExtensionContext, type ExtensionToolContext, type ModelRuntime, type ToolDefinition } from "@earendil-works/pi-coding-agent";
8
- import { FairAgentScheduler, getAgentAttempts, WorkflowAgentExecutor, localAgentTransport, type AgentActivity, type AgentAttempt, type AgentDefinition, type AgentExecutionRoot, type AgentProgress, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
8
+ import { FairAgentScheduler, getAgentAttempts, WorkflowAgentExecutor, localAgentTransport, type AgentActivity, type AgentAttempt, 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";
11
11
  import { registerWorkflowNavigator, uiHostCapabilities } from "./host-navigator.js";
@@ -13,9 +13,8 @@ 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, 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";
16
+ import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue, modelAliasErrorName, object, parseModelReference, positiveInteger, reachableTools, resolveModelReference, VIRTUAL_MODEL_PROVIDER, sanitizeDisplayText, validateModelAliases } from "./utils.js";
17
+ import { loadCodemodeToolsSetting, preflight, resolveAgentResourcePolicy, staticAgentPreparationOptions, resolveWorkflowSettings, validateCheckpoint, validateModelAliasAvailability, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowSettingsPath, workflowToolExposure } from "./validation.js";
19
18
  import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistryIfIdle, retainWorkflowRegistry, type WorkflowRegistryApi } from "./registry.js";
20
19
  import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath, toolIdentityPath } from "./execution.js";
21
20
  import { prepareScriptToolLoadout, scriptTool, scriptToolContext, scriptToolValue, validateScriptToolReferences } from "./script-tools.js";
@@ -24,41 +23,10 @@ import { showChangelogNotice } from "./changelog.js";
24
23
  import { createTrajectoryRunLoader, createTrajectoryRunMetadataLoader, createTrajectorySubagentLoader, createTrajectorySubagentMetadataLoader, createTrajectoryTranscriptLoader, type TrajectoryActionRequest, type TrajectoryActionResult, type TrajectorySubagent } from "./trajectory.js";
25
24
  import { getTrajectoryHost, type TrajectoryPublisherProvider } from "./trajectory-host-handle.js";
26
25
  import { getSubagentManager } from "./subagent-manager-handle.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
+ import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, THINKING_LEVELS, WORKFLOW_BLOCKED_EVENT, WorkflowError, type WorkflowFunction, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState, 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";
28
27
  import type { SubagentManagerContext, SubagentRunRequest, SubagentStatus } from "../subagents/src/contracts.js";
29
- import {
30
- SETTLED_AGENT_STATES,
31
- catalogResultValue,
32
- formatWorkflowCatalog,
33
- styledTextBlock,
34
- textBlock,
35
- workflowCatalogBlock,
36
- workflowControlCall,
37
- workflowControlResult,
38
- workflowProgressBlock,
39
- type WorkflowProgressRenderState,
40
- } from "./host-view.js";
41
- import {
42
- DELIVERY_LIMIT_BYTES,
43
- ForegroundDeliveryController,
44
- markWorkflowFailureDiagnostics,
45
- WORKFLOW_LOG_ENTRY,
46
- completionDescriptor,
47
- completionDeliveryFromStore,
48
- createWorkflowFailureDiagnostics,
49
- failureDiagnosticsFrom,
50
- formatWorkflowFailure,
51
- formatWorkflowFailureDelivery,
52
- formatWorkflowFailureDeliveryFallback,
53
- formatWorkflowFailureDiagnostics,
54
- isWorkflowFailureDiagnostics,
55
- serializeWorkflowFailureDiagnostics,
56
- utf8Prefix,
57
- type CompletionDeliveryContext,
58
- type ForegroundDelivery,
59
- type ForegroundDetachResult,
60
- type WorkflowLogEntry,
61
- } from "./host-delivery.js";
28
+ import { SETTLED_AGENT_STATES, catalogResultValue, formatWorkflowCatalog, styledTextBlock, textBlock, workflowCatalogBlock, workflowControlCall, workflowControlResult, workflowProgressBlock, type WorkflowProgressRenderState } from "./host-view.js";
29
+ import { DELIVERY_LIMIT_BYTES, ForegroundDeliveryController, markWorkflowFailureDiagnostics, WORKFLOW_LOG_ENTRY, completionDescriptor, completionDeliveryFromStore, createWorkflowFailureDiagnostics, failureDiagnosticsFrom, formatWorkflowFailure, formatWorkflowFailureDelivery, formatWorkflowFailureDeliveryFallback, formatWorkflowFailureDiagnostics, isWorkflowFailureDiagnostics, serializeWorkflowFailureDiagnostics, utf8Prefix, type CompletionDeliveryContext, type ForegroundDelivery, type ForegroundDetachResult, type WorkflowLogEntry } from "./host-delivery.js";
62
30
 
63
31
  export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Partial<Pick<ExtensionAPI, "getAllTools">> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
64
32
 
@@ -141,26 +109,6 @@ function completionControlContent(result: unknown, controlRunId?: string): strin
141
109
  delete record.run;
142
110
  return JSON.stringify({ ...record, value });
143
111
  }
144
- type RoleCapture = { readonly snapshot: Readonly<LaunchSnapshot>; capture(role: string, model: ModelSpec): Promise<void> };
145
- /** 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. */
146
- function roleCapture(store: RunStore, initial: Readonly<LaunchSnapshot>, definitions: Readonly<Record<string, AgentDefinition>>, projectDefinitions: Readonly<Record<string, AgentDefinition>>): RoleCapture {
147
- let persisted = initial;
148
- return {
149
- get snapshot() { return persisted; },
150
- async capture(role, model) {
151
- const definition = definitions[role];
152
- if (!definition) return;
153
- const modelName = `${model.provider}/${model.model}`;
154
- const hasProjectRole = projectDefinitions[role] !== undefined;
155
- if (persisted.roles?.[role] !== undefined && (!hasProjectRole || persisted.projectRoles?.includes(role)) && persisted.models.includes(modelName)) return;
156
- const roles = { ...(persisted.roles ?? {}), [role]: definition };
157
- const projectRoles = hasProjectRole ? [...new Set([...(persisted.projectRoles ?? []), role])] : persisted.projectRoles ?? [];
158
- const models = [...new Set([...persisted.models, modelName])];
159
- persisted = createLaunchSnapshot({ ...persisted, models, roles, projectRoles });
160
- await store.saveSnapshot(persisted);
161
- },
162
- };
163
- }
164
112
  export function formatWorkflowPreview(args: { script?: unknown; scriptPath?: unknown; name?: unknown; description?: unknown }): string {
165
113
  const name = typeof args.name === "string" && args.name.trim() ? args.name.trim() : "workflow";
166
114
  if (typeof args.script !== "string" || !args.script.trim()) return `workflow ${name}`;
@@ -336,6 +284,20 @@ async function resolveLaunchAliases(registry: WorkflowRegistryApi, staticAliases
336
284
  }
337
285
  }
338
286
 
287
+ /**
288
+ * Prepares each completely static agent configuration in inspection mode, so preparation plugins resolve their own aliases
289
+ * and options without persisting a configuration. Any preparation failure rejects launch before effects; cancellation stops
290
+ * the remaining inspections.
291
+ */
292
+ async function inspectStaticAgentConfigurations(executor: WorkflowAgentExecutor, script: string, workflowName: string, signal: AbortSignal, functions: Readonly<Record<string, WorkflowFunction>>): Promise<void> {
293
+ // NOTE: inspection uses the launch cwd; agents inside withWorktree(...) prepare against their checkout at runtime.
294
+ for (const agentOptions of staticAgentPreparationOptions(script, functions)) {
295
+ if (signal.aborted) throw new WorkflowError("CANCELLED", "Workflow launch cancelled");
296
+ await executor.prepare({ label: "agent", workflowName, agentOptions }, undefined, signal, "inspection");
297
+ }
298
+ if (signal.aborted) throw new WorkflowError("CANCELLED", "Workflow launch cancelled");
299
+ }
300
+
339
301
  export default function workflowExtension(pi: WorkflowExtensionAPI, home?: string, clipboard = copyToClipboard, transport: AgentTransport = localAgentTransport, agentDir?: string, additionalSkillPaths: readonly string[] = []) {
340
302
  beginWorkflowExtensionLoading();
341
303
  const registry = loadingRegistry();
@@ -351,7 +313,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
351
313
  });
352
314
  let backgroundWidgetEnabled = true;
353
315
  try {
354
- const globalSettings = loadSettings(workflowSettingsPath(extensionAgentDir));
316
+ const globalSettings = resolveWorkflowSettings(process.cwd(), false, workflowSettingsPath(extensionAgentDir)).effective;
355
317
  backgroundWidgetEnabled = globalSettings.backgroundWidget ?? true;
356
318
  // Registered at load, before Pi resolves --model and restores the session model. Project aliases need project trust, which is unknown here.
357
319
  registerAliasVirtualModels(pi, Object.keys(globalSettings.modelAliases ?? {}), workflowSettingsPath(extensionAgentDir));
@@ -380,6 +342,9 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
380
342
  return skillPath ? { skillPaths: [skillPath] } : undefined;
381
343
  });
382
344
  const runs = new Map<string, WorkflowRunRecord>();
345
+ // Extension load failures a headless launcher reported for this host. They hold for the whole process, so every executor,
346
+ // including those that paused resume, cold resume and retry create, keeps failing ownerless options closed.
347
+ let extensionLoad: AgentExecutionRoot["extensionLoad"];
383
348
  const deliveryController = new ForegroundDeliveryController({ runs, ...(typeof pi.sendMessage === "function" ? { deliver: (content: string) => { deliver(pi, content); } } : {}) });
384
349
  let releaseWorkflowRegistry: (() => void) | undefined;
385
350
  const providerRecoveryLane = new SerialLane();
@@ -758,7 +723,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
758
723
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, lastEventAt } : agent) }));
759
724
  run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
760
725
  };
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); });
726
+ 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); });
762
727
  cancelDeferredProgress();
763
728
  const before = (await run.store.load()).run;
764
729
  await persistAgentAttempts(run.store, id, result.attempts);
@@ -794,17 +759,16 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
794
759
  const existing = new Map(current.agents.map((agent) => [agent.id, agent]));
795
760
  const agents = ownership.map((node) => {
796
761
  const previous = existing.get(node.id);
797
- 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 } : {}) };
762
+ 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 } : {}) };
798
763
  let effective: { model: ModelSpec; requestedModel?: string; tools: readonly string[] };
799
764
  try { effective = { ...run.executor.resolve({ ...requested, effectiveTools: node.options.tools }), tools: node.options.tools }; }
800
765
  catch { 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 }; }
801
766
  const resultPath = !node.parentId && node.options.agentIdentity ? agentIdentityPath(node.options.agentIdentity) : undefined;
802
- const nodeRole = roleNameOf(node.options.role);
803
767
  const now = Date.now();
804
768
  const lastEventAt = node.state === "running" ? previous?.state === "running" && previous.lastEventAt !== undefined ? previous.lastEventAt : now : previous?.lastEventAt;
805
769
  const startedAt = previous?.startedAt ?? (node.state === "running" ? now : undefined);
806
770
  const durationMs = previous?.durationMs ?? (SETTLED_AGENT_STATES.has(node.state) && startedAt !== undefined ? Math.max(0, now - startedAt) : undefined);
807
- 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 }) };
771
+ 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 }) };
808
772
  });
809
773
  return { ...current, agents };
810
774
  });
@@ -1028,20 +992,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1028
992
  });
1029
993
  catalogRegistered = true;
1030
994
  };
1031
- const createAgentExecutor = (root: Omit<AgentExecutionRoot, "agentDir" | "agentSetupHooks">) => 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);
1032
- // Resumed runs keep the role definitions frozen in their snapshot and fall back to the current definitions for roles a
1033
- // registered function reaches for the first time after the restart; those are captured into the snapshot like at launch.
1034
- const resumeRoles = (store: RunStore, snapshot: Readonly<LaunchSnapshot>, cwd: string, trustedProject: boolean) => {
1035
- let current: Readonly<Record<string, AgentDefinition>> = {};
1036
- let project: Readonly<Record<string, AgentDefinition>> = {};
1037
- try {
1038
- current = loadAgentDefinitions(cwd, extensionAgentDir, trustedProject, registry.roleDirectoryRegistrations());
1039
- project = trustedProject ? loadProjectAgentDefinitions(cwd) : {};
1040
- } catch { /* Unreadable current roles leave the resume with the snapshot roles, as before. */ }
1041
- const definitions = { ...current, ...(snapshot.roles ?? {}) };
1042
- const captured = roleCapture(store, snapshot, definitions, project);
1043
- return { definitions, capture: (role: string, model: ModelSpec) => captured.capture(role, model) };
1044
- };
995
+ const createAgentExecutor = (root: Omit<AgentExecutionRoot, "agentDir" | "agentSetupHooks">) => 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);
1045
996
  const activeSnapshotTools = (tools: readonly string[], active: ReadonlySet<string> | "session") => active === "session"
1046
997
  ? new Set(tools.filter((tool) => reachableTools(pi).includes(tool) && tool !== "workflow_catalog"))
1047
998
  : new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
@@ -1053,6 +1004,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1053
1004
  modelRegistry?: ModelRegistryCapability | undefined;
1054
1005
  signal: AbortSignal;
1055
1006
  resolvedAliases?: Readonly<Record<string, string>>;
1007
+ resolvedDynamicAliasNames?: readonly string[];
1056
1008
  blockedAliases?: ReadonlySet<string>;
1057
1009
  blockedAliasTargets?: Readonly<Record<string, string>>;
1058
1010
  withPreflight: boolean;
@@ -1065,26 +1017,27 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1065
1017
  const extensionSettings = input.snapshot.settings.extensionSettings;
1066
1018
  const extensionSettingsPath = input.snapshot.settingsSources?.extensionSettings ?? input.snapshot.settingsPath ?? settingsPath;
1067
1019
  registry.validateExtensionSettings(extensionSettings, { source: "effective", cwd: input.cwd, projectTrusted: input.trustedProject, settingsPath: extensionSettingsPath });
1068
- for (const [role, definition] of Object.entries(input.snapshot.roles ?? {})) registry.validateExtensionSettings(mergeWorkflowExtensionSettings(extensionSettings, definition.extensionSettings), { source: "role", cwd: input.cwd, projectTrusted: input.trustedProject, settingsPath: extensionSettingsPath, role });
1069
1020
  const currentPolicy = resolveAgentResourcePolicy(input.cwd, input.trustedProject, settingsPath);
1070
1021
  const staticAliases = resolution.effective.modelAliases ?? {};
1071
1022
  const previousAliases = input.snapshot.modelAliases ?? input.snapshot.settings.modelAliases ?? {};
1072
1023
  const inventory = modelInventory(input.rootModel, input.modelRegistry);
1073
1024
  const knownModels = input.modelRegistry ? inventory.knownModels : new Set([...input.snapshot.models, ...inventory.knownModels]);
1074
1025
  const availableModels = input.modelRegistry ? inventory.availableModels : new Set([...input.snapshot.models, ...inventory.availableModels]);
1075
- 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;
1026
+ 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);
1027
+ const currentAliases = resolved.aliases;
1028
+ const dynamicAliasNames = resolved.dynamicNames;
1076
1029
  const blockedAliases = input.blockedAliases ?? new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
1077
1030
  const blockedAliasTargets = input.blockedAliasTargets ?? Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
1078
1031
  const script = input.withPreflight ? input.snapshot.script : undefined;
1079
1032
  if (script !== undefined) {
1080
1033
  const resumeAliases = { ...previousAliases, ...currentAliases };
1081
- 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);
1034
+ preflight(script, { models: availableModels, tools: active, modelAliases: resumeAliases, knownModels, settingsPath, skipModelAvailability: true }, input.snapshot.schemas, input.snapshot.metadata, true);
1082
1035
  }
1083
1036
  const refreshed = resumedSnapshotSettings(input.snapshot, resolution, currentAliases);
1084
1037
  const snapshot = createLaunchSnapshot({ ...input.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
1085
- return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script };
1038
+ return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, dynamicAliasNames, blockedAliases, blockedAliasTargets, snapshot, script };
1086
1039
  };
1087
- const workflowAgentHandler = (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string, captureRole?: (role: string, model: ModelSpec) => Promise<void>) => {
1040
+ const workflowAgentHandler = (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string) => {
1088
1041
  // Replay lookup and spawn are serialised per run so concurrent agent(...) calls reach the scheduler in call order.
1089
1042
  const admission = new SerialLane();
1090
1043
  return async (prompt: string, options: Readonly<Record<string, JsonValue>>, agentSignal: AbortSignal, identity: AgentIdentity) => {
@@ -1096,20 +1049,21 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1096
1049
  if (replayed) return { replayed };
1097
1050
  const worktree = agentWorktree(identity);
1098
1051
  const agentCwd = worktree.worktreeOwner ? (await persistWorktree(store, metadata, worktree.worktreeOwner)).cwd : cwd;
1099
- const role = typeof options.role === "string" ? options.role : undefined;
1100
1052
  const model = typeof options.model === "string" ? options.model : undefined;
1101
1053
  const requestedLabel = typeof options.label === "string" ? options.label : undefined;
1102
1054
  const skills = Array.isArray(options.skills) ? options.skills as string[] : undefined;
1103
1055
  const extensions = Array.isArray(options.extensions) ? options.extensions as string[] : undefined;
1104
1056
  const contextFiles = Array.isArray(options.contextFiles) && options.contextFiles.every(isContextFileScope) ? options.contextFiles : undefined;
1105
- const resolved = executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: metadata.name, ...(model ? { model } : {}), ...(role ? { role } : {}), ...(contextFiles ? { contextFiles } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools as string[] } : {}), ...(skills ? { skills } : {}), ...(extensions ? { extensions } : {}) });
1106
- if (role) await captureRole?.(role, resolved.model);
1107
- const label = displayAgentName(requestedLabel, role, resolved.model);
1057
+ // A paused resume replaces the run executor with refreshed aliases; new identities must prepare against it.
1058
+ const current = runs.get(runId)?.executor ?? executor;
1059
+ const configuration = await current.prepare({ label: requestedLabel ?? "agent", workflowName: metadata.name, agentOptions: options, agentIdentity: identity, ...(worktree.worktreeOwner ? { worktreeOwner: worktree.worktreeOwner } : {}) }, agentCwd, agentSignal);
1060
+ const resolved = configuration;
1061
+ const label = requestedLabel ?? configuration.label ?? resolved.model.model;
1108
1062
  const tools = resolved.tools;
1109
1063
  const schema = object(options.outputSchema) ? options.outputSchema : undefined;
1110
1064
  const sessionPath = identity.handle !== undefined && identity.turn !== undefined ? await handleTurnInput(store, identity.handle, identity.turn) : undefined;
1111
1065
  const continuity = identity.handle === undefined ? undefined : sessionPath ? "continued" as const : "fresh" as const;
1112
- 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 }) };
1066
+ 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 }) };
1113
1067
  });
1114
1068
  if ("replayed" in admitted) return admitted.replayed.value;
1115
1069
  const { spawned } = admitted;
@@ -1124,7 +1078,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1124
1078
  };
1125
1079
  };
1126
1080
  const recovery = createWorkflowRecovery({
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,
1081
+ 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,
1128
1082
  });
1129
1083
  const resumeSelectedWorkflow = async (runId: string, foreground: boolean, context: unknown, budgetPatch?: unknown): Promise<{ workflowName: string; state: "running" | "completed" | "awaiting_approval"; attached: boolean; value?: JsonValue }> => {
1130
1084
  const run = runs.get(runId);
@@ -1143,9 +1097,8 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1143
1097
  if (!foreground && wasAttached) await deliveryController.moveForegroundToBackground(runId);
1144
1098
  if (foreground && !wasAttached) {
1145
1099
  run.foreground = true;
1146
- const loaded = await run.store.load();
1147
1100
  await persistRunState(run.store, run.metadata, (current) => ({ ...current, delivery: { ...(current.delivery ?? {}), mode: "foreground", state: "attached" } }));
1148
- await run.store.saveSnapshot(createLaunchSnapshot({ ...loaded.snapshot, launchMode: "foreground" }));
1101
+ await run.store.setLaunchMode("foreground");
1149
1102
  } else if (!foreground) run.foreground = false;
1150
1103
  else run.foreground = true;
1151
1104
  await recovery.refreshPausedRunAliases(run, { ...recoveryContext, projectTrusted: projectTrusted(context) });
@@ -1251,10 +1204,9 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1251
1204
  const budgetRuntime = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents, { active: loaded.run.state === "running" });
1252
1205
  const lifecycle = lifecycleFor(store, loaded.run.state, loaded.snapshot.metadata);
1253
1206
  const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
1254
- const roleDefinitions = loaded.snapshot.roles ?? {};
1255
1207
  const abortController = new AbortController();
1256
1208
  const providerErrorRecovery = createProviderErrorRecovery(ctx, new Set(loaded.snapshot.models), () => { abortController.abort(); });
1257
- 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 } : {}) });
1209
+ 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 } : {}) });
1258
1210
  for (const checkpoint of await store.awaitingCheckpoints()) deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
1259
1211
  for (const decision of await store.pendingWorkflowDecisions()) deliver(pi, recovery.budgetDecisionDelivery(loaded.snapshot.metadata, decision));
1260
1212
  scheduler.restoreRun(runId, loaded.snapshot.settings.concurrency, loaded.snapshot.identityVersion === LAUNCH_SNAPSHOT_IDENTITY_VERSION ? await store.loadOwnership() : [], () => runs.get(runId)?.budget.checkAgentLaunch(), loaded.snapshot.settings.extensionSettings);
@@ -1283,13 +1235,6 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1283
1235
  throw error;
1284
1236
  }
1285
1237
  });
1286
- pi.on("before_agent_start", (event, ctx) => {
1287
- if (!pi.getActiveTools().includes("workflow")) return;
1288
- const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx), typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : undefined)).filter(([, definition]) => definition.description);
1289
- if (!roles.length) return;
1290
- const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
1291
- return { systemPrompt: `${event.systemPrompt}\n\n${content}` };
1292
- });
1293
1238
  const workflowTool: ToolDefinition<typeof WORKFLOW_TOOL_PARAMETERS, WorkflowToolResult, WorkflowProgressRenderState> = {
1294
1239
  name: "workflow",
1295
1240
  label: WORKFLOW_TOOL_LABEL,
@@ -1332,25 +1277,31 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1332
1277
  if (launch.resolution.global.extensionSettings !== undefined) registry.validateExtensionSettings(launch.resolution.global.extensionSettings, { source: "global", cwd: ctx.cwd, projectTrusted: trustedProject, settingsPath: launch.resolution.globalSettingsPath });
1333
1278
  if (trustedProject && launch.resolution.project.extensionSettings !== undefined) registry.validateExtensionSettings(launch.resolution.project.extensionSettings, { source: "project", cwd: ctx.cwd, projectTrusted: true, settingsPath: launch.resolution.projectSettingsPath });
1334
1279
  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);
1335
- const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames } = validated;
1280
+ const { script, checked } = validated;
1336
1281
  const toolContext = scriptToolContext(ctx);
1337
1282
  validateScriptToolReferences(script, toolContext);
1283
+ // Headless hosts that tolerated extension load failures report them with the extensions they loaded.
1284
+ const strings = (value: unknown) => Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : [];
1285
+ const loadErrors = object(ctx) ? strings(ctx.extensionLoadErrors) : [];
1286
+ if (loadErrors.length) extensionLoad = Object.freeze({ errors: loadErrors, loaded: object(ctx) ? strings(ctx.loadedExtensionPaths) : [] });
1287
+ 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) };
1288
+ // Runtime emits the same resource warnings, so inspection stays silent.
1289
+ await inspectStaticAgentConfigurations(createAgentExecutor({ ...executorRoot, onResourceWarning: () => undefined }), script, checked.metadata.name, runController.signal, registry.functions());
1338
1290
  await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
1291
+ // Cancellation can arrive while the lease or a queued parent validation is pending; no run may be created after it.
1292
+ const assertLaunchActive = () => { if (runController.signal.aborted) throw new WorkflowError("CANCELLED", "Workflow launch cancelled"); };
1293
+ assertLaunchActive();
1339
1294
  const runId = randomUUID();
1340
1295
  const args = params.args ?? null;
1341
1296
  encoded(args);
1342
1297
  const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
1343
1298
  const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
1344
1299
  const parentRunId = params.parentRunId;
1345
- const roles = Object.fromEntries(roleNames.map((role) => [role, agentDefinitions[role]])) as Record<string, AgentDefinition>;
1346
- const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
1347
- const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
1348
- const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
1349
- 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 });
1350
- const capturedRoles = roleCapture(store, snapshot, agentDefinitions, projectAgentDefinitions);
1300
+ const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models])];
1301
+ 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 });
1351
1302
  const budgetRuntime = new WorkflowBudgetRuntime(budget);
1352
1303
  const initialBudget = budgetRuntime.snapshot();
1353
- 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);
1304
+ 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); };
1354
1305
  // Parent validation and persistence share the mutation lane so a manual deletion cannot remove the parent between the two.
1355
1306
  if (parentRunId !== undefined) await coordinateRunMutation(async () => { await store.validateParentRun(parentRunId); await createRun(); });
1356
1307
  else await createRun();
@@ -1371,7 +1322,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1371
1322
  delivery.detached = true;
1372
1323
  const activeRun = runs.get(runId);
1373
1324
  if (activeRun) { activeRun.foreground = false; delete activeRun.update; }
1374
- await store.saveSnapshot(createLaunchSnapshot({ ...capturedRoles.snapshot, launchMode: "background" }));
1325
+ await store.setLaunchMode("background");
1375
1326
  for (const checkpoint of await store.awaitingCheckpoints()) deliverBackgroundCheckpoint(checked.metadata.name, runId, checkpoint);
1376
1327
  signal?.removeEventListener("abort", onForegroundAbort);
1377
1328
  const run = (await store.load()).run;
@@ -1386,12 +1337,12 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
1386
1337
  const backgroundLaunch = !params.foreground;
1387
1338
  const providerPause = async () => { if (!foregroundAttached) deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
1388
1339
  const providerErrorRecovery = createProviderErrorRecovery(ctx, availableModels, () => { runController.abort(); });
1389
- 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 });
1340
+ const executor = createAgentExecutor({ ...executorRoot, runStore: store, providerPause, runContext });
1390
1341
  const runRecord: WorkflowRunRecord = { 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 } : {}) };
1391
1342
  runs.set(runId, runRecord);
1392
1343
  if (params.foreground && onUpdate) onUpdate(workflowToolUpdate((await store.load()).run));
1393
1344
  scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch(), settings.extensionSettings);
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);
1345
+ 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);
1395
1346
  runRecord.execution = execution;
1396
1347
  await eventPublisher.runStarted(store, checked.metadata);
1397
1348
  const finish = execution.result.then(async (value) => {
@@ -1653,9 +1604,7 @@ async function handleTurnInput(store: RunStore, handle: string, turn: number): P
1653
1604
  return target;
1654
1605
  }
1655
1606
 
1656
- function displayAgentName(label: string | undefined, role: string | undefined, model: ModelSpec): string {
1657
- return label ?? role ?? model.model;
1658
- }
1607
+
1659
1608
 
1660
1609
  function modelSpec(value: string, fallback: ModelSpec): ModelSpec {
1661
1610
  try {
package/src/index.ts CHANGED
@@ -3,10 +3,9 @@ export * from "./package-identity.js";
3
3
  export { createToolTimingExtension, TOOL_TIMING_ENTRY_TYPE } from "./tool-timing.js";
4
4
  export type { ToolTimingEntry } from "./tool-timing.js";
5
5
  export * from "./utils.js";
6
- export { canonicalPath, sameFilesystemPath } from "./paths.js";
6
+ export { canonicalPath, extensionIdentity, sameFilesystemPath } from "./paths.js";
7
7
  export * from "./budget.js";
8
8
  export * from "./validation.js";
9
- export * from "./roles.js";
10
9
  export * from "./registry.js";
11
10
  export * from "./execution.js";
12
11
  export * from "./host.js";
package/src/paths.ts CHANGED
@@ -1,27 +1,33 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { realpathSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { readdir } from "node:fs/promises";
4
5
  import { homedir } from "node:os";
5
- import { basename, join, resolve } from "node:path";
6
+ import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
6
7
  import { WorkflowError } from "./types.js";
7
8
  import { isNodeError } from "./utils.js";
8
9
 
9
10
  export function safePart(value: string): string { return value.replace(/[^a-zA-Z0-9._-]/g, "_"); }
10
11
 
12
+ export function canonicalPath(path: string): string { const absolute = resolve(path); try { return realpathSync(absolute); } catch { return absolute; } }
13
+ export function extensionIdentity(path: string): string { return path.startsWith("builtin:") ? path : canonicalPath(path.startsWith("file:") ? fileURLToPath(path) : path); }
14
+ export function sameFilesystemPath(left: string, right: string): boolean { return canonicalPath(left) === canonicalPath(right); }
11
15
  /**
12
- * Resolves a path to its filesystem spelling, including symlink and platform aliases.
13
- * A missing path safely falls back to its absolute lexical spelling rather than
14
- * guessing at the identity of an object that does not exist.
16
+ * The path Pi's resource loader uses for a resource path an extension returns: trimmed, `~`, `~/` and `file://` expanded,
17
+ * relative to the loader cwd. Synthetic `<...>` and `builtin:` paths stay as they are.
15
18
  */
16
- export function canonicalPath(path: string): string {
17
- const absolute = resolve(path);
18
- try { return realpathSync(absolute); } catch { return absolute; }
19
+ export function piResourcePath(path: string, cwd: string): string {
20
+ //NOTE: Pi also rewrites Windows shell paths and `~\` on win32; those forms are not mirrored here and fail closed.
21
+ if (path.startsWith("<") || path.startsWith("builtin:")) return path;
22
+ const trimmed = path.trim();
23
+ const expanded = trimmed === "~" ? homedir() : trimmed.startsWith("~/") ? join(homedir(), trimmed.slice(2)) : /^file:\/\//.test(trimmed) ? fileURLToPath(trimmed) : trimmed;
24
+ return isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded);
25
+ }
26
+ /** Pi attributes a loaded resource to a contributed path by lexical containment of resolved paths, without following symlinks. */
27
+ export function piResourceContains(root: string, path: string): boolean {
28
+ const relativePath = relative(resolve(root), resolve(path));
29
+ return relativePath === "" || (relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));
19
30
  }
20
-
21
- /** Pi names built-in extensions `builtin:<name>` instead of by a path; other extensions are files. */
22
- export function extensionIdentity(path: string): string { return path.startsWith("builtin:") ? path : canonicalPath(path); }
23
-
24
- export function sameFilesystemPath(left: string, right: string): boolean { return canonicalPath(left) === canonicalPath(right); }
25
31
  export function workflowProjectSettingsPath(cwd: string): string { return join(cwd, ".pi", "pi-extensible-workflows", "settings.json"); }
26
32
 
27
33
  export function projectStorageKey(cwd: string): string {
@@ -4,6 +4,7 @@ export {
4
4
  } from "./paths.js";
5
5
  export { hasLiveSessionLease, SessionLease, acquireSessionLease, listRunIds, processAlive } from "./session-lease.js";
6
6
  export { RunStore } from "./store.js";
7
+ export { decodePreparedAgentConfiguration } from "./decoders.js";
7
8
  export {
8
9
  EffectiveSystemPrompt, PersistedRun, RunSummaryAgent, RunSummaryArtifacts, RunSummary,
9
10
  CompletedOperation, AwaitingCheckpoint, PendingWorkflowDecision, PersistedOwnershipNode,
@@ -172,7 +172,7 @@ export class PiRuntimeAgentRunner implements RuntimeAgentRunner {
172
172
  let structuredResult: RuntimeJsonValue | undefined;
173
173
  const resultTool = prepared.resultTool;
174
174
  if (!resultTool) throw new WorkflowError("INTERNAL_ERROR", "Workflow agent session is missing its workflow_result tool");
175
- const preparedForSession = preparedForRuntimeRequest(prepared, request.customTools, request.signal, (value) => { structuredResult = value; }, resultTool);
175
+ const preparedForSession = Object.freeze({ ...preparedForRuntimeRequest(prepared, request.customTools, request.signal, (value) => { structuredResult = value; }, resultTool), ...(request.turnControl ? { turnControl: request.turnControl } : {}) });
176
176
  const callbacks = this.#options.callbacks;
177
177
  const handoff = this.#options.handoff;
178
178
  let adapter: PiRuntimeSessionAdapter | undefined;
@@ -364,6 +364,8 @@ export class PiRuntimeAgentRunner implements RuntimeAgentRunner {
364
364
  throwIfProgressFailed();
365
365
  beginTurn(false);
366
366
  throwIfTurnPolicyFailed();
367
+ // NOTE: A transport that already sent turnControl (Herdr, in its pane launch prompt) reports turnControlDelivered; steering its suspended local session would fail.
368
+ if (request.turnControl && !session.turnControlDelivered) await activeSession.steer(request.turnControl);
367
369
  await promptAndRecover(request.task, true);
368
370
  completeTurn(hasResult() || !hasToolCall(lastAssistant), false, true);
369
371
  if (turnPolicyFailure) throw turnPolicyFailure;