@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 6.2.1-fork.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +2 -2
  3. package/dist/src/agent-execution.d.ts +43 -10
  4. package/dist/src/agent-execution.js +442 -162
  5. package/dist/src/background-widget.d.ts +5 -1
  6. package/dist/src/background-widget.js +5 -10
  7. package/dist/src/decoders.d.ts +2 -3
  8. package/dist/src/decoders.js +59 -37
  9. package/dist/src/execution.d.ts +6 -2
  10. package/dist/src/execution.js +48 -7
  11. package/dist/src/host-delivery.js +5 -8
  12. package/dist/src/host-navigator.js +5 -2
  13. package/dist/src/host-phases.d.ts +1 -0
  14. package/dist/src/host-recovery.d.ts +11 -8
  15. package/dist/src/host-recovery.js +14 -22
  16. package/dist/src/host-runtime.js +56 -5
  17. package/dist/src/host-view.d.ts +6 -1
  18. package/dist/src/host-view.js +46 -20
  19. package/dist/src/host.d.ts +2 -2
  20. package/dist/src/host.js +246 -118
  21. package/dist/src/index.d.ts +1 -2
  22. package/dist/src/index.js +1985 -1690
  23. package/dist/src/index.js.map +3 -3
  24. package/dist/src/paths.d.ts +8 -5
  25. package/dist/src/paths.js +24 -13
  26. package/dist/src/persistence.d.ts +1 -0
  27. package/dist/src/persistence.js +1 -0
  28. package/dist/src/pi-runtime-runner.js +4 -1
  29. package/dist/src/registry.d.ts +9 -7
  30. package/dist/src/registry.js +29 -71
  31. package/dist/src/runtime/agent-runner.d.ts +2 -0
  32. package/dist/src/script-tools.d.ts +27 -0
  33. package/dist/src/script-tools.js +87 -0
  34. package/dist/src/session-lease.d.ts +1 -1
  35. package/dist/src/session-lease.js +45 -11
  36. package/dist/src/settings.d.ts +19 -3
  37. package/dist/src/settings.js +63 -50
  38. package/dist/src/store.d.ts +3 -1
  39. package/dist/src/store.js +17 -1
  40. package/dist/src/trajectory.d.ts +12 -3
  41. package/dist/src/trajectory.js +32 -11
  42. package/dist/src/types.d.ts +129 -45
  43. package/dist/src/types.js +5 -8
  44. package/dist/src/utils.d.ts +21 -1
  45. package/dist/src/utils.js +46 -6
  46. package/dist/src/validation.d.ts +14 -3
  47. package/dist/src/validation.js +143 -87
  48. package/dist/starter/index.js +8 -11
  49. package/dist/starter/index.js.map +1 -1
  50. package/dist/starter/review-loop.d.ts +0 -13
  51. package/dist/starter/review-loop.js +8 -9
  52. package/dist/subagents/index.js +904 -956
  53. package/dist/subagents/index.js.map +3 -3
  54. package/dist/subagents/src/contracts.d.ts +48 -31
  55. package/dist/subagents/src/contracts.js +20 -9
  56. package/dist/subagents/src/index.d.ts +1 -1
  57. package/dist/subagents/src/index.js +14 -8
  58. package/dist/subagents/src/manager.js +52 -73
  59. package/dist/subagents/src/navigator.js +3 -14
  60. package/dist/subagents/src/view.d.ts +1 -1
  61. package/dist/subagents/src/view.js +15 -68
  62. package/dist/subagents/src/worktree.js +1 -1
  63. package/dist/trajectory/assets/index.html +35 -13
  64. package/dist/trajectory/index.js +576 -932
  65. package/dist/trajectory/index.js.map +3 -3
  66. package/dist/trajectory/src/assets/index.html +35 -13
  67. package/dist/trajectory/src/index.js +134 -104
  68. package/dist/trajectory/src/server.js +268 -27
  69. package/dist/trajectory/src/server.js.map +3 -3
  70. package/dist/trajectory/src/startup-mutex.d.ts +17 -0
  71. package/dist/trajectory/src/startup-mutex.js +172 -0
  72. package/examples/workflow-extension-template/README.md +4 -7
  73. package/examples/workflow-extension-template/extension.test.mjs +4 -8
  74. package/examples/workflow-extension-template/index.js +0 -3
  75. package/package.json +10 -10
  76. package/skills/pi-extensible-workflows/SKILL.md +12 -8
  77. package/src/agent-execution.ts +412 -164
  78. package/src/background-widget.ts +5 -11
  79. package/src/decoders.ts +51 -37
  80. package/src/execution.ts +46 -9
  81. package/src/host-delivery.ts +5 -8
  82. package/src/host-navigator.ts +4 -2
  83. package/src/host-phases.ts +1 -1
  84. package/src/host-recovery.ts +23 -28
  85. package/src/host-runtime.ts +44 -6
  86. package/src/host-view.ts +41 -16
  87. package/src/host.ts +229 -142
  88. package/src/index.ts +1 -2
  89. package/src/paths.ts +18 -9
  90. package/src/persistence.ts +1 -0
  91. package/src/pi-runtime-runner.ts +3 -1
  92. package/src/registry.ts +28 -64
  93. package/src/runtime/agent-runner.ts +2 -0
  94. package/src/script-tools.ts +76 -0
  95. package/src/session-lease.ts +34 -6
  96. package/src/settings.ts +56 -50
  97. package/src/store.ts +18 -2
  98. package/src/trajectory.ts +32 -15
  99. package/src/types.ts +82 -34
  100. package/src/utils.ts +42 -5
  101. package/src/validation.ts +121 -80
  102. package/starter/index.ts +0 -1
  103. package/starter/review-loop.ts +8 -9
  104. package/starter/roles/developer.md +0 -1
  105. package/starter/roles/oracle.md +0 -1
  106. package/starter/roles/researcher.md +0 -1
  107. package/starter/roles/reviewer.md +0 -1
  108. package/starter/roles/scout.md +0 -1
  109. package/subagents/README.md +10 -16
  110. package/subagents/src/contracts.ts +21 -11
  111. package/subagents/src/index.ts +17 -8
  112. package/subagents/src/manager.ts +48 -128
  113. package/subagents/src/navigator.ts +4 -13
  114. package/subagents/src/view.ts +16 -63
  115. package/subagents/src/worktree.ts +1 -1
  116. package/trajectory/src/assets/index.html +35 -13
  117. package/trajectory/src/index.ts +112 -91
  118. package/trajectory/src/server.ts +46 -10
  119. package/trajectory/src/startup-mutex.ts +127 -0
  120. package/dist/src/roles.d.ts +0 -70
  121. package/dist/src/roles.js +0 -325
  122. package/dist/starter/roles/developer.md +0 -6
  123. package/dist/starter/roles/oracle.md +0 -18
  124. package/dist/starter/roles/researcher.md +0 -20
  125. package/dist/starter/roles/reviewer.md +0 -19
  126. package/dist/starter/roles/scout.md +0 -18
  127. package/examples/workflow-extension-template/roles/reviewer.md +0 -4
  128. package/src/roles.ts +0 -326
@@ -1,8 +1,8 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
- import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { FairAgentScheduler, WorkflowAgentExecutor, type AgentDefinition, type AgentExecutionRoot, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
2
+ import { type ExtensionAPI, type ExtensionToolContext } from "@earendil-works/pi-coding-agent";
3
+ import { FairAgentScheduler, WorkflowAgentExecutor, type AgentExecutionRoot, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
4
4
  import { RunStore, type PersistedRun } from "./persistence.js";
5
- import { WorkflowError, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowMetadata, type WorkflowWorktreeReference } from "./types.js";
5
+ import { WorkflowError, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type ToolIdentity, type WorkflowMetadata, type WorkflowWorktreeReference } from "./types.js";
6
6
  import { type WorkflowRegistryApi } from "./registry.js";
7
7
  import { RunLifecycle, WorkflowEventPublisher, type WorkflowRunRecord, type WorkflowToolUpdate } from "./host-runtime.js";
8
8
  import { type CompletionDeliveryContext, type CompletionDeliveryResult } from "./host-delivery.js";
@@ -21,8 +21,10 @@ export type WorkflowRecoveryContext = {
21
21
  } | undefined;
22
22
  modelRegistry: ModelRegistryCapability | undefined;
23
23
  deliveryContext: CompletionDeliveryContext;
24
+ toolContext?: ExtensionToolContext | undefined;
24
25
  signal?: AbortSignal;
25
26
  resolvedAliases?: Readonly<Record<string, string>>;
27
+ resolvedDynamicAliasNames?: readonly string[];
26
28
  blockedAliases?: ReadonlySet<string>;
27
29
  blockedAliasTargets?: Readonly<Record<string, string>>;
28
30
  };
@@ -48,6 +50,7 @@ export type WorkflowRecoveryDependencies = {
48
50
  modelRegistry?: WorkflowRecoveryContext["modelRegistry"];
49
51
  signal: AbortSignal;
50
52
  resolvedAliases?: Readonly<Record<string, string>>;
53
+ resolvedDynamicAliasNames?: readonly string[];
51
54
  blockedAliases?: ReadonlySet<string>;
52
55
  blockedAliasTargets?: Readonly<Record<string, string>>;
53
56
  withPreflight: boolean;
@@ -59,17 +62,15 @@ export type WorkflowRecoveryDependencies = {
59
62
  knownModels: ReadonlySet<string>;
60
63
  availableModels: ReadonlySet<string>;
61
64
  currentAliases: Readonly<Record<string, string>>;
65
+ dynamicAliasNames: readonly string[];
62
66
  blockedAliases: ReadonlySet<string>;
63
67
  blockedAliasTargets: Readonly<Record<string, string>>;
64
68
  snapshot: Readonly<LaunchSnapshot>;
65
69
  script: string | undefined;
66
70
  }>;
67
- resumeRoles: (store: RunStore, snapshot: Readonly<LaunchSnapshot>, cwd: string, trustedProject: boolean) => {
68
- definitions: Readonly<Record<string, AgentDefinition>>;
69
- capture: (role: string, model: ModelSpec) => Promise<void>;
70
- };
71
- workflowAgentHandler: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string, captureRole?: (role: string, model: ModelSpec) => Promise<void>) => (prompt: string, options: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: AgentIdentity) => Promise<JsonValue>;
71
+ workflowAgentHandler: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string) => (prompt: string, options: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: AgentIdentity) => Promise<JsonValue>;
72
72
  shellForRun: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity) => Promise<ShellResult>;
73
+ toolForRun: (store: RunStore, lifecycle: RunLifecycle, context: ExtensionToolContext | undefined, identifier: string, args: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: ToolIdentity) => Promise<JsonValue>;
73
74
  resolveWorktree: (store: RunStore, metadata: WorkflowMetadata, owner: string) => Promise<Readonly<WorkflowWorktreeReference>>;
74
75
  checkpointBridge: (runId: string, store: RunStore, metadata: WorkflowMetadata, foreground: boolean, ui?: {
75
76
  select?: (prompt: string, options: string[]) => Promise<string | undefined>;
@@ -104,8 +105,10 @@ export declare function createWorkflowRecovery(deps: WorkflowRecoveryDependencie
104
105
  } | undefined;
105
106
  modelRegistry: WorkflowRecoveryContext["modelRegistry"];
106
107
  deliveryContext: CompletionDeliveryContext;
108
+ toolContext?: ExtensionToolContext | undefined;
107
109
  signal?: AbortSignal | undefined;
108
110
  resolvedAliases?: Readonly<Record<string, string>>;
111
+ resolvedDynamicAliasNames?: readonly string[];
109
112
  blockedAliases?: ReadonlySet<string>;
110
113
  blockedAliasTargets?: Readonly<Record<string, string>>;
111
114
  }, modeOverride?: boolean, waitForCompletion?: boolean) => Promise<{
@@ -28,17 +28,10 @@ function workflowRecoveryGuidance(action, state) {
28
28
  return "Interrupted workflow runs must be resumed from the interactive /workflow picker";
29
29
  return `Only failed workflow runs can be retried; source is ${state}`;
30
30
  }
31
- /** A snapshot can only be re-executed when it carries the current identity scheme and every role its script references. */
32
- function assertResumableSnapshot(snapshot, trustedProject) {
31
+ /** A snapshot can only be re-executed when it carries the current identity scheme and validated concrete agent configurations. */
32
+ function assertResumableSnapshot(snapshot) {
33
33
  if (snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION)
34
34
  throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
35
- if (snapshot.roles === undefined)
36
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow role definitions are missing from the launch snapshot");
37
- if ((snapshot.projectRoles?.length ?? 0) > 0 && !trustedProject)
38
- throw new WorkflowError("RESUME_INCOMPATIBLE", "Cannot restore project roles in an untrusted project");
39
- const missingRole = snapshot.agentTypes.find((role) => !snapshot.roles?.[role]);
40
- if (missingRole)
41
- throw new WorkflowError("RESUME_INCOMPATIBLE", `Role definition is missing from the launch snapshot: ${missingRole}`);
42
35
  }
43
36
  function assertExpectedWorkflowState(expectedState, actualState) {
44
37
  if (expectedState !== undefined && expectedState !== actualState)
@@ -46,7 +39,7 @@ function assertExpectedWorkflowState(expectedState, actualState) {
46
39
  }
47
40
  export function persistedFailure(run, error) { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
48
41
  export function createWorkflowRecovery(deps) {
49
- const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver, deliverTerminal, workflowToolUpdate, registry, modelSpec } = deps;
42
+ const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue, workflowAgentHandler, shellForRun, toolForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver, deliverTerminal, workflowToolUpdate, registry, modelSpec } = deps;
50
43
  const coordinateRunMutation = deps.coordinateRunMutation ?? ((task) => task());
51
44
  const budgetDecisionDelivery = (metadata, request) => `Workflow ${metadata.name} budget adjustment ${request.proposalId} for run ${request.runId} requires approval. Consumed usage: ${JSON.stringify(request.consumed)}. Previous limits: ${JSON.stringify(request.previous)}. Proposed limits: ${JSON.stringify(request.proposed)}. Respond with workflow_respond using proposalId ${request.proposalId}.`;
52
45
  const appendBudgetDecisionEvent = async (run, request, type) => {
@@ -70,11 +63,11 @@ export function createWorkflowRecovery(deps) {
70
63
  const loaded = await run.store.load();
71
64
  const trustedProject = context?.projectTrusted ?? run.projectTrusted();
72
65
  const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
73
- const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot } = await resolveLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: run.abortController.signal, withPreflight: false });
66
+ const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, dynamicAliasNames, blockedAliases, blockedAliasTargets, snapshot } = await resolveLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: run.abortController.signal, withPreflight: false });
74
67
  await run.store.saveSnapshot(snapshot);
75
68
  scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
76
69
  scheduler.setRunExtensionSettings(run.store.runId, snapshot.settings.extensionSettings);
77
- run.executor = createAgentExecutor({ cwd: run.store.cwd, model: run.model, tools: activeSnapshotTools(snapshot.tools, "session"), resourceSelectors: currentPolicy.effective, extensionSettings: snapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: resumeRoles(run.store, snapshot, run.store.cwd, trustedProject).definitions, runStore: run.store, providerPause: async () => { deliver(`Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
70
+ run.executor = createAgentExecutor({ cwd: run.store.cwd, projectTrusted: currentPolicy.projectTrusted, model: run.model, tools: activeSnapshotTools(snapshot.tools, "session"), resourceSelectors: currentPolicy.effective, extensionSettings: snapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, dynamicModelAliasNames: dynamicAliasNames, blockedAliases, blockedAliasTargets, settingsPath, runStore: run.store, providerPause: async () => { deliver(`Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
78
71
  run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
79
72
  const drift = aliasDrift(previousAliases, currentAliases);
80
73
  if (drift.length)
@@ -92,7 +85,7 @@ export function createWorkflowRecovery(deps) {
92
85
  await persistRunState(run.store, run.metadata, withoutActiveShells);
93
86
  await run.store.validateRetrySource();
94
87
  await run.store.validateBorrowedWorktrees();
95
- assertResumableSnapshot(loaded.snapshot, trustedProject);
88
+ assertResumableSnapshot(loaded.snapshot);
96
89
  const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
97
90
  const controller = new AbortController();
98
91
  if (context?.signal?.aborted)
@@ -101,7 +94,7 @@ export function createWorkflowRecovery(deps) {
101
94
  context?.signal?.addEventListener("abort", () => { controller.abort(); }, { once: true });
102
95
  }
103
96
  run.abortController = controller;
104
- const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script } = await resolveLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: controller.signal, ...(context?.resolvedAliases ? { resolvedAliases: context.resolvedAliases } : {}), ...(context?.blockedAliases ? { blockedAliases: context.blockedAliases } : {}), ...(context?.blockedAliasTargets ? { blockedAliasTargets: context.blockedAliasTargets } : {}), withPreflight: true });
97
+ const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, dynamicAliasNames, blockedAliases, blockedAliasTargets, snapshot, script } = await resolveLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: controller.signal, ...(context?.resolvedAliases ? { resolvedAliases: context.resolvedAliases } : {}), ...(context?.resolvedDynamicAliasNames ? { resolvedDynamicAliasNames: context.resolvedDynamicAliasNames } : {}), ...(context?.blockedAliases ? { blockedAliases: context.blockedAliases } : {}), ...(context?.blockedAliasTargets ? { blockedAliasTargets: context.blockedAliasTargets } : {}), withPreflight: true });
105
98
  if (!script)
106
99
  throw new WorkflowError("INTERNAL_ERROR", "Resume preflight did not produce a launch script");
107
100
  const persistedSnapshot = modeOverride === undefined ? snapshot : createLaunchSnapshot({ ...snapshot, launchMode: foreground ? "foreground" : "background" });
@@ -110,8 +103,7 @@ export function createWorkflowRecovery(deps) {
110
103
  await persistRunState(run.store, run.metadata, (current) => ({ ...current, delivery: { ...(current.delivery ?? {}), mode: foreground ? "foreground" : "background", state: foreground ? "attached" : "pending" } }));
111
104
  scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
112
105
  scheduler.setRunExtensionSettings(run.store.runId, snapshot.settings.extensionSettings);
113
- const roles = resumeRoles(run.store, persistedSnapshot, run.store.cwd, trustedProject);
114
- run.executor = createAgentExecutor({ cwd: run.store.cwd, model: rootModel, tools: activeSnapshotTools(snapshot.tools, "session"), resourceSelectors: currentPolicy.effective, extensionSettings: snapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: roles.definitions, runStore: run.store, providerPause: async () => { deliver(`Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
106
+ run.executor = createAgentExecutor({ cwd: run.store.cwd, projectTrusted: currentPolicy.projectTrusted, model: rootModel, tools: activeSnapshotTools(snapshot.tools, "session"), resourceSelectors: currentPolicy.effective, extensionSettings: snapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, dynamicModelAliasNames: dynamicAliasNames, blockedAliases, blockedAliasTargets, settingsPath, runStore: run.store, providerPause: async () => { deliver(`Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
115
107
  const drift = aliasDrift(previousAliases, currentAliases);
116
108
  if (drift.length)
117
109
  await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
@@ -119,7 +111,7 @@ export function createWorkflowRecovery(deps) {
119
111
  run.executor.setRunContext(runContext);
120
112
  await scheduler.cancelRun(run.store.runId);
121
113
  await run.lifecycle.resume();
122
- const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId, roles.capture), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.store, run.lifecycle, run.metadata.name) }, run.store, runContext, registry, snapshot.settings.extensionSettings), controller.signal);
114
+ const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ tool: (identifier, args, signal, identity) => toolForRun(run.store, run.lifecycle, context?.toolContext, identifier, args, signal, identity), shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.store, run.lifecycle, run.metadata.name) }, run.store, runContext, registry, snapshot.settings.extensionSettings), controller.signal);
123
115
  run.execution = execution;
124
116
  const completion = execution.result.then(async (value) => {
125
117
  await scheduler.flush(run.store.runId);
@@ -292,11 +284,11 @@ export function createWorkflowRecovery(deps) {
292
284
  const trustedProject = projectTrusted(context);
293
285
  await sourceStore.validateRetrySource();
294
286
  await sourceStore.validateBorrowedWorktrees();
295
- assertResumableSnapshot(loaded.snapshot, trustedProject);
287
+ assertResumableSnapshot(loaded.snapshot);
296
288
  const modelRegistry = resumeHostContext(context).modelRegistry;
297
289
  const hostModel = object(host.model) && typeof host.model.provider === "string" && typeof host.model.id === "string" ? { provider: host.model.provider, id: host.model.id } : { provider: "", id: "" };
298
290
  const rootModel = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
299
- const { active, settingsPath, currentPolicy, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot: childBaseSnapshot } = await resolveLaunchPrologue({ snapshot: loaded.snapshot, cwd, trustedProject, rootModel, ...(modelRegistry ? { modelRegistry } : {}), signal: signal ?? new AbortController().signal, withPreflight: true });
291
+ const { active, settingsPath, currentPolicy, knownModels, availableModels, currentAliases, dynamicAliasNames, blockedAliases, blockedAliasTargets, snapshot: childBaseSnapshot } = await resolveLaunchPrologue({ snapshot: loaded.snapshot, cwd, trustedProject, rootModel, ...(modelRegistry ? { modelRegistry } : {}), signal: signal ?? new AbortController().signal, withPreflight: true });
300
292
  await sourceStore.validateNamedWorktrees();
301
293
  for (const name of loaded.run.retry?.namedWorktrees ?? [])
302
294
  await sourceStore.resolveNamedWorktree(name);
@@ -315,10 +307,10 @@ export function createWorkflowRecovery(deps) {
315
307
  const abortController = new AbortController();
316
308
  const providerErrorRecovery = createProviderErrorRecovery(context, availableModels, () => { abortController.abort(); });
317
309
  const providerPause = async () => { deliver(`Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
318
- const childRun = { executor: createAgentExecutor({ cwd, model, tools: activeSnapshotTools(loaded.snapshot.tools, active), resourceSelectors: currentPolicy.effective, extensionSettings: childBaseSnapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: loaded.snapshot.roles ?? {}, runStore: childStore, providerPause, agentResourcePolicy: frozenResourcePolicy(currentPolicy) }), store: childStore, metadata: loaded.snapshot.metadata, model, lifecycle, budget: childBudget, abortController, projectTrusted: () => projectTrusted(context), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) };
310
+ const childRun = { executor: createAgentExecutor({ cwd, projectTrusted: currentPolicy.projectTrusted, model, tools: activeSnapshotTools(loaded.snapshot.tools, active), resourceSelectors: currentPolicy.effective, extensionSettings: childBaseSnapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, dynamicModelAliasNames: dynamicAliasNames, blockedAliases, blockedAliasTargets, settingsPath, runStore: childStore, providerPause, agentResourcePolicy: frozenResourcePolicy(currentPolicy) }), store: childStore, metadata: loaded.snapshot.metadata, model, lifecycle, budget: childBudget, abortController, projectTrusted: () => projectTrusted(context), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) };
319
311
  runs.set(childRunId, childRun);
320
312
  childCreated = true;
321
- return { childRun, parentRunId: loaded.run.id, lineageRootRunId, concurrency: loaded.snapshot.settings.concurrency, ...(childBaseSnapshot.settings.extensionSettings === undefined ? {} : { extensionSettings: childBaseSnapshot.settings.extensionSettings }), hostModel, modelRegistry, currentAliases, blockedAliases, blockedAliasTargets };
313
+ return { childRun, parentRunId: loaded.run.id, lineageRootRunId, concurrency: loaded.snapshot.settings.concurrency, ...(childBaseSnapshot.settings.extensionSettings === undefined ? {} : { extensionSettings: childBaseSnapshot.settings.extensionSettings }), hostModel, modelRegistry, currentAliases, dynamicAliasNames, blockedAliases, blockedAliasTargets };
322
314
  }
323
315
  finally {
324
316
  if (!childCreated)
@@ -333,7 +325,7 @@ export function createWorkflowRecovery(deps) {
333
325
  await eventPublisher.runStarted(prepared.childRun.store, prepared.childRun.metadata);
334
326
  const { hasUI, ui } = recoveryUi(context);
335
327
  const recoveryContext = resumeHostContext(context);
336
- const completed = await coldResumeRun(prepared.childRun, hasUI, ui, projectTrusted(context), { model: prepared.hostModel, modelRegistry: prepared.modelRegistry, deliveryContext: recoveryContext.deliveryContext, resolvedAliases: prepared.currentAliases, blockedAliases: prepared.blockedAliases, blockedAliasTargets: prepared.blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
328
+ const completed = await coldResumeRun(prepared.childRun, hasUI, ui, projectTrusted(context), { model: prepared.hostModel, modelRegistry: prepared.modelRegistry, deliveryContext: recoveryContext.deliveryContext, toolContext: recoveryContext.toolContext, resolvedAliases: prepared.currentAliases, resolvedDynamicAliasNames: prepared.dynamicAliasNames, blockedAliases: prepared.blockedAliases, blockedAliasTargets: prepared.blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
337
329
  const completion = runs.get(prepared.childRun.store.runId)?.completion;
338
330
  if (completion) {
339
331
  childStarted = true;
@@ -1,5 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import { deepFreeze, errorCode, errorText, fail, isWorkflowErrorCode, jsonValue, object } from "./utils.js";
2
+ import { FUNCTION_HANDLE_PREFIX } from "./execution.js";
3
+ import { deepFreeze, errorCode, errorText, fail, isWorkflowErrorCode, jsonDigest, jsonValue, object } from "./utils.js";
3
4
  import { validateAgentOptions, validateShellOptions, workflowPrompt } from "./validation.js";
4
5
  import { HARD_TERMINAL_RUN_STATES, WORKFLOW_AGENT_STATE_CHANGED_EVENT, WORKFLOW_BUDGET_EVENT, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, WORKFLOW_PHASE_CHANGED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, WORKFLOW_WORKTREE_CREATED_EVENT, WorkflowError } from "./types.js";
5
6
  import { structuralPath as operationPath } from "./persistence.js";
@@ -130,7 +131,7 @@ export class WorkflowEventPublisher {
130
131
  await this.#publish(store, metadata, WORKFLOW_RUN_FAILED_EVENT, { error: safeEventError(error) });
131
132
  }
132
133
  async agentState(store, metadata, previous, agent) {
133
- await this.#publish(store, metadata, WORKFLOW_AGENT_STATE_CHANGED_EVENT, { agentId: agent.id, displayLabel: agent.label ?? agent.name, ...(agent.role ? { role: agent.role } : {}), structuralPath: [...(agent.structuralPath ?? [])], ...(agent.parentId ? { parentId: agent.parentId } : {}), ...(agent.parentBreadcrumb ? { parentBreadcrumb: agent.parentBreadcrumb } : {}), ...(agent.worktreeOwner ? { worktreeOwner: agent.worktreeOwner } : {}), ...(previous ? { previousState: previous.state } : {}), state: agent.state, attempt: agent.attempts });
134
+ await this.#publish(store, metadata, WORKFLOW_AGENT_STATE_CHANGED_EVENT, { agentId: agent.id, displayLabel: agent.label ?? agent.name, structuralPath: [...(agent.structuralPath ?? [])], ...(agent.parentId ? { parentId: agent.parentId } : {}), ...(agent.parentBreadcrumb ? { parentBreadcrumb: agent.parentBreadcrumb } : {}), ...(agent.worktreeOwner ? { worktreeOwner: agent.worktreeOwner } : {}), ...(previous ? { previousState: previous.state } : {}), state: agent.state, attempt: agent.attempts });
134
135
  }
135
136
  async agentStates(store, metadata, previous, current) {
136
137
  const previousById = new Map(previous.map((agent) => [agent.id, agent]));
@@ -285,6 +286,7 @@ export function nextNamedOccurrence(counters, label) {
285
286
  function functionBreadcrumb(name, occurrence) { return occurrence === 1 ? name : `${name} #${String(occurrence)}`; }
286
287
  export function withWorkflowFunctions(bridge, store, runContext, registry, settings = Object.freeze({})) {
287
288
  const functionAgentOccurrences = new Map();
289
+ const functionHandles = new Set();
288
290
  const functionShellOccurrences = new Map();
289
291
  const functionInvokeOccurrences = new Map();
290
292
  const functionInvokeBreadcrumbOccurrences = new Map();
@@ -307,6 +309,7 @@ export function withWorkflowFunctions(bridge, store, runContext, registry, setti
307
309
  // Own rejection immediately; Promise.all below still propagates it after the function completes.
308
310
  void sideEffect.catch(() => undefined);
309
311
  };
312
+ let closed = false;
310
313
  const parentBreadcrumb = breadcrumb ?? functionBreadcrumb(name, identity.occurrence);
311
314
  const context = {
312
315
  settings: deepFreeze(structuredClone(settings)),
@@ -327,7 +330,7 @@ export function withWorkflowFunctions(bridge, store, runContext, registry, setti
327
330
  const nestedIdentity = { path: nestedPath, structuralPath: [...inherited], occurrence, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) };
328
331
  return invokeFunction(targetName, targetInput, signal, nestedIdentity, `${parentBreadcrumb} > ${functionBreadcrumb(breadcrumbName, breadcrumbOccurrence)}`);
329
332
  },
330
- agent: async (prompt, options) => {
333
+ agent: Object.assign(async (prompt, options) => {
331
334
  if (!bridge.agent || typeof prompt !== "string")
332
335
  fail("AGENT_FAILED", "No agent bridge is available");
333
336
  const validatedOptions = validateAgentOptions(options === undefined ? {} : options);
@@ -337,7 +340,55 @@ export function withWorkflowFunctions(bridge, store, runContext, registry, setti
337
340
  const occurrence = (functionAgentOccurrences.get(key) ?? 0) + 1;
338
341
  functionAgentOccurrences.set(key, occurrence);
339
342
  return bridge.agent(prompt, validatedOptions, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
340
- },
343
+ }, {
344
+ create: (...created) => {
345
+ if (created.length !== 1 || !object(created[0]))
346
+ fail("INVALID_METADATA", "agent.create requires one options object");
347
+ const { name, ...rest } = created[0];
348
+ if (typeof name !== "string" || !name.trim())
349
+ fail("INVALID_METADATA", "agent.create requires a stable explicit name");
350
+ const config = deepFreeze(structuredClone(validateAgentOptions(rest)));
351
+ const scopedStructuralPath = [...(inheritedHostAgentPath.getStore() ?? [])];
352
+ const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
353
+ const scope = JSON.stringify([scopedStructuralPath, scopedWorktreeOwner ?? null]);
354
+ // The registered-function path makes repeated and nested invocations distinct; scope separates branches and worktrees. The digest bounds the journal and turn-copy file names.
355
+ const handle = operationPath(FUNCTION_HANDLE_PREFIX, jsonDigest([path, scopedStructuralPath, scopedWorktreeOwner ?? null]), name);
356
+ if (functionHandles.has(handle))
357
+ fail("INVALID_METADATA", `Duplicate agent handle name: ${name}`);
358
+ functionHandles.add(handle);
359
+ let turn = 0;
360
+ let inflight = false;
361
+ // Not a nested promise chain: the caller owns the one returned promise, and inflight clears in finally on every outcome.
362
+ const send = async (...sent) => {
363
+ if (!bridge.agent)
364
+ fail("AGENT_FAILED", "No agent bridge is available");
365
+ if (sent.length < 1 || sent.length > 2)
366
+ fail("INVALID_METADATA", "send requires a prompt and optional options");
367
+ if (closed)
368
+ fail("INVALID_METADATA", `Agent handle outlived the function invocation that created it: ${name}`);
369
+ if (typeof sent[0] !== "string")
370
+ fail("INVALID_METADATA", "send prompt must be a string");
371
+ const sendOptions = sent[1] === undefined ? {} : sent[1];
372
+ if (!object(sendOptions) || Object.keys(sendOptions).some((key) => key !== "outputSchema" && key !== "timeoutMs"))
373
+ fail("INVALID_METADATA", "send options must contain only outputSchema and timeoutMs");
374
+ const sendStructuralPath = [...(inheritedHostAgentPath.getStore() ?? [])];
375
+ if (JSON.stringify([sendStructuralPath, inheritedHostWorktreeOwner.getStore() ?? worktreeOwner ?? null]) !== scope)
376
+ fail("INVALID_METADATA", `Agent handle sends must run in the scope that created the handle: ${name}`);
377
+ if (inflight)
378
+ fail("INVALID_METADATA", "Concurrent send(...) calls on one agent handle are unsupported; await each turn");
379
+ const validatedOptions = validateAgentOptions({ ...config, ...sendOptions });
380
+ inflight = true;
381
+ turn += 1;
382
+ try {
383
+ return await bridge.agent(sent[0], validatedOptions, signal, { structuralPath: sendStructuralPath, callSite: `function:${path}:handle:${name}`, occurrence: turn, handle, turn, parentBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
384
+ }
385
+ finally {
386
+ inflight = false;
387
+ }
388
+ };
389
+ return Object.freeze({ name, send });
390
+ },
391
+ }),
341
392
  shell: async (...args) => {
342
393
  if (!bridge.shell)
343
394
  fail("SHELL_FAILED", "No shell bridge is available");
@@ -363,7 +414,7 @@ export function withWorkflowFunctions(bridge, store, runContext, registry, setti
363
414
  phase: (name) => { ownSideEffect(() => bridge.phase?.(name)); },
364
415
  log: (message) => { ownSideEffect(() => bridge.log?.(message)); },
365
416
  };
366
- const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } }));
417
+ const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } })).finally(() => { closed = true; });
367
418
  await Promise.all(sideEffects);
368
419
  if (!replayed)
369
420
  await store.complete(path, stored ?? result);
@@ -84,13 +84,17 @@ export declare function navigatorRunLabels(entries: readonly {
84
84
  export declare function agentBreadcrumbParts(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath?: boolean): string[];
85
85
  export declare function agentBreadcrumb(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath?: boolean): string;
86
86
  export declare function formatStalledDuration(durationMs: number): string;
87
+ export declare function stalledDuration(agent: Pick<AgentDetailPresentation, "state" | "lastEventAt">, now: number): number | undefined;
88
+ export declare function agentActivityLabel(agent: {
89
+ readonly activity?: AgentRecord["activity"];
90
+ readonly toolCalls?: AgentRecord["toolCalls"];
91
+ }): string;
87
92
  export interface AgentDetailPresentation {
88
93
  readonly state: string;
89
94
  readonly activity?: AgentRecord["activity"];
90
95
  readonly lastEventAt?: number;
91
96
  readonly structuralPath?: readonly string[];
92
97
  readonly model?: AgentRecord["model"];
93
- readonly role?: string;
94
98
  readonly tools?: readonly string[];
95
99
  readonly attempts?: number;
96
100
  readonly startedAt?: number;
@@ -127,5 +131,6 @@ export declare function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot
127
131
  export declare function formatNavigatorColumns(sidebar: readonly string[], detail: readonly string[], width: number, layout?: Readonly<{
128
132
  treeOnly?: boolean | undefined;
129
133
  detailsOnly?: boolean | undefined;
134
+ detailOffset?: number | undefined;
130
135
  }>): string[];
131
136
  export declare function formatCheckpointReview(checkpoint: AwaitingCheckpoint): string;
@@ -74,6 +74,17 @@ function formatShellActivity(activeShells, startedAt, spinner, styles, now) {
74
74
  const timing = started && startedAt !== undefined && !Number.isNaN(started.getTime()) ? ` ${styles.dim(`started=${started.toISOString()} elapsed=${formatWorkflowRuntime(Math.max(0, now - startedAt))}`)}` : "";
75
75
  return `${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(count)} active)`)}${timing}`;
76
76
  }
77
+ function formatToolActivity(run, spinner, styles, now) {
78
+ const calls = run.activeTools ?? [];
79
+ if (!calls.length)
80
+ return undefined;
81
+ const counts = new Map();
82
+ for (const { name } of calls)
83
+ counts.set(name, (counts.get(name) ?? 0) + 1);
84
+ const names = [...counts].map(([name, count]) => count > 1 ? `${name} ×${String(count)}` : name).join(", ");
85
+ const elapsed = formatWorkflowRuntime(Math.max(0, now - Math.min(...calls.map(({ startedAt }) => startedAt))));
86
+ return `${styles.accent(spinner)} tools ${styles.accent("[running]")} ${sanitizeDisplayText(names)} ${styles.dim(`elapsed=${elapsed}`)}`;
87
+ }
77
88
  function formatLogTimestamp(timestamp) {
78
89
  if (timestamp === undefined || !Number.isFinite(timestamp))
79
90
  return "--:--:--";
@@ -167,12 +178,16 @@ function workflowProgressLines(run, spinner, styles, now, expanded, width, showH
167
178
  const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, spinner, styles, now);
168
179
  if (shellActivity)
169
180
  lines.push(` ${shellActivity}`);
181
+ const toolActivity = formatToolActivity(run, spinner, styles, now);
182
+ if (toolActivity)
183
+ lines.push(` ${toolActivity}`);
170
184
  const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
171
185
  const position = new Map(run.agents.map((agent, index) => [agent, index]));
172
186
  const renderAgents = (agents, nested) => renderGroupedAgents(agents, ({ agent, depth }, grouped) => {
173
- const icon = agentStateGlyph(agent.state, spinner);
187
+ const icon = agentStateGlyph(agent.state, "●");
174
188
  const indent = " ".repeat((grouped ? 2 : 1) + depth);
175
- const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles, now);
189
+ const idleSpinner = agent.state === "running" && !agentActivityLabel(agent) ? styles.accent(spinner) : "";
190
+ const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : [idleSpinner, formatAgentActivity(agent, spinner, styles, now)].filter(Boolean).join(" ");
176
191
  const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
177
192
  const state = progressStyleForState(agent.state, styles);
178
193
  const detail = expanded ? formatWorkflowAgentDetail(agent, now) : "";
@@ -493,7 +508,7 @@ export function workflowProgressBlock(run, theme, progress, refresh, invalidate,
493
508
  invalidate() {
494
509
  const displayed = currentRun();
495
510
  const now = Date.now();
496
- if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0))
511
+ if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0 && !displayed.activeTools?.length))
497
512
  return;
498
513
  if (progress.refresh || now - progress.lastRefreshAt < WORKFLOW_PROGRESS_REFRESH_MS)
499
514
  return;
@@ -606,7 +621,7 @@ export function formatStalledDuration(durationMs) {
606
621
  const remainingMinutes = minutes % 60;
607
622
  return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
608
623
  }
609
- function stalledDuration(agent, now) {
624
+ export function stalledDuration(agent, now) {
610
625
  if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt))
611
626
  return undefined;
612
627
  const duration = now - agent.lastEventAt;
@@ -618,7 +633,7 @@ function elapsedDurationMs(agent, end) {
618
633
  return Math.max(0, agent.durationMs);
619
634
  return agent.startedAt === undefined || !Number.isFinite(agent.startedAt) ? undefined : Math.max(0, end - agent.startedAt);
620
635
  }
621
- function agentActivityLabel(agent) {
636
+ export function agentActivityLabel(agent) {
622
637
  const activity = agent.activity;
623
638
  if (activity?.kind === "reasoning" || activity?.kind === "text") {
624
639
  const name = activity.kind === "reasoning" ? "reasoning" : "responding";
@@ -671,7 +686,6 @@ export function formatAgentDetail(agent, styles = PLAIN_WORKFLOW_PROGRESS_STYLES
671
686
  `State: ${state(agent.state)}`,
672
687
  ...(agent.structuralPath?.length ? [`Structural path: ${agent.structuralPath.join(" > ")}`] : []),
673
688
  ...(model === undefined ? [] : [`Model: ${model}`]),
674
- `Role: ${agent.role ?? "(none)"}`,
675
689
  `Tools: ${tools}`,
676
690
  ...(agent.attempts !== undefined && Number.isFinite(agent.attempts) && agent.attempts > 1 ? [`Attempts: ${String(agent.attempts)}`] : []),
677
691
  ...(duration === undefined ? [] : [`Duration: ${formatWorkflowRuntime(duration)}`]),
@@ -731,6 +745,9 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees, now = Date
731
745
  const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
732
746
  if (shellActivity)
733
747
  lines.push(` ${shellActivity}`);
748
+ const toolActivity = formatToolActivity(run, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
749
+ if (toolActivity)
750
+ lines.push(` ${toolActivity}`);
734
751
  if (run.error)
735
752
  lines.push(`Error: ${run.error.code}: ${run.error.message}`);
736
753
  if (run.events?.length)
@@ -776,6 +793,9 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees, now = Date.no
776
793
  const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
777
794
  if (shellActivity)
778
795
  lines.push(` ${shellActivity}`);
796
+ const toolActivity = formatToolActivity(run, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
797
+ if (toolActivity)
798
+ lines.push(` ${toolActivity}`);
779
799
  if (run.error)
780
800
  lines.push(`Run error: ${run.error.code}: ${run.error.message}`);
781
801
  if (run.events?.length)
@@ -791,11 +811,10 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees, now = Date.no
791
811
  const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
792
812
  lines.push(...renderGroupedAgents(run.agents, ({ agent, index, depth }, grouped) => {
793
813
  const model = `${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`;
794
- const role = agent.role ? ` role=${agent.role}` : "";
795
814
  const tools = ` tools=${agent.tools.join(",") || "(none)"}`;
796
815
  const accounting = agent.accounting ? ` input=${String(agent.accounting.input)} output=${String(agent.accounting.output)} cache-read=${String(agent.accounting.cacheRead)} cache-write=${String(agent.accounting.cacheWrite)} cost=${formatCost(agent.accounting.cost) || "$0.00"}` : "";
797
816
  const indent = " ".repeat((grouped ? 2 : 1) + depth);
798
- const result = [`${indent}#${String(index + 1)} ${grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId)} state=${agent.state} model=${model}${agent.requestedModel ? ` requested=${agent.requestedModel}` : ""}${role}${tools} attempts=${String(agent.attempts)} retries=${String(Math.max(0, agent.attempts - 1))}${accounting}`];
817
+ const result = [`${indent}#${String(index + 1)} ${grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId)} state=${agent.state} model=${model}${agent.requestedModel ? ` requested=${agent.requestedModel}` : ""}${tools} attempts=${String(agent.attempts)} retries=${String(Math.max(0, agent.attempts - 1))}${accounting}`];
799
818
  for (const attempt of agent.attemptDetails ?? [])
800
819
  result.push(`${indent} attempt ${String(attempt.attempt)}${attempt.error ? ` error=${attempt.error.code}: ${attempt.error.message}` : ""}`);
801
820
  for (const call of agent.toolCalls ?? [])
@@ -835,17 +854,17 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
835
854
  return;
836
855
  if (child.agent)
837
856
  agents.push(child.agent);
838
- else
839
- for (const childId of child.children)
840
- visit(childId);
841
- };
842
- if (node.agent)
843
- agents.push(node.agent);
844
- else
845
- for (const childId of node.children)
857
+ for (const childId of child.children)
846
858
  visit(childId);
859
+ };
860
+ visit(node.id);
847
861
  return agents;
848
862
  };
863
+ const usageLines = (agents) => {
864
+ const spans = agents.flatMap((agent) => agent.startedAt === undefined ? [] : [[agent.startedAt, agent.startedAt + (elapsedDurationMs(agent, now) ?? 0)]]);
865
+ const elapsed = spans.length ? formatWorkflowRuntime(Math.max(...spans.map(([, end]) => end)) - Math.min(...spans.map(([start]) => start))) : "";
866
+ return [...formatAgentAccounting(sumAccounting(agents.map((agent) => agent.accounting ?? zeroAccounting()))), ...(elapsed ? [`Elapsed: ${elapsed}`] : [])];
867
+ };
849
868
  const nodeStatus = (node) => phaseStyle(node.state)(node.state);
850
869
  const nodeIcon = (node) => node.children.length ? expanded.has(node.id) ? "▾" : "▸" : node.kind === "agent" ? "•" : node.kind === "shell" ? "◇" : " ";
851
870
  const treeLine = (node) => {
@@ -866,12 +885,12 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
866
885
  const selected = node.phase;
867
886
  const counts = selected?.counts ?? phaseAgentCounts(agents);
868
887
  const hint = node.children.length ? "enter expand/collapse" : "enter phase details";
869
- return [styles.bold(`Selected phase: ${node.label}`), `Status: ${nodeStatus(node)}`, `agents completed=${String(counts.completed)} running=${String(counts.running)} failed=${String(counts.failed)} cancelled=${String(counts.cancelled)} pending=${String(counts.pending)}`, `Agents: ${String(agents.length)}`, ...(selection.actions ? [] : [styles.muted(hint)])];
888
+ return [styles.bold(`Selected phase: ${node.label}`), `Status: ${nodeStatus(node)}`, `agents completed=${String(counts.completed)} running=${String(counts.running)} failed=${String(counts.failed)} cancelled=${String(counts.cancelled)} pending=${String(counts.pending)}`, `Agents: ${String(agents.length)}`, ...usageLines(agents), ...(selection.actions ? [] : [styles.muted(hint)])];
870
889
  }
871
890
  if (node.kind === "operation") {
872
891
  const states = phaseAgentCounts(agents);
873
892
  const hint = node.children.length ? "enter expand/collapse" : "enter phase details";
874
- return [styles.bold(`Selected operation: ${node.operationPath.join(" > ")}`), `Phase: ${node.phase?.name ?? node.phaseId}`, `Status: ${nodeStatus(node)}`, `agents completed=${String(states.completed)} running=${String(states.running)} failed=${String(states.failed)} cancelled=${String(states.cancelled)} pending=${String(states.pending)}`, `Agents: ${String(agents.length)}`, ...(selection.actions ? [] : [styles.muted(hint)])];
893
+ return [styles.bold(`Selected operation: ${node.operationPath.join(" > ")}`), `Phase: ${node.phase?.name ?? node.phaseId}`, `Status: ${nodeStatus(node)}`, `agents completed=${String(states.completed)} running=${String(states.running)} failed=${String(states.failed)} cancelled=${String(states.cancelled)} pending=${String(states.pending)}`, `Agents: ${String(agents.length)}`, ...usageLines(agents), ...(selection.actions ? [] : [styles.muted(hint)])];
875
894
  }
876
895
  if (node.kind === "shell") {
877
896
  return [styles.bold("Selected shell"), formatShellActivity(node.shellActivity?.active, node.shellActivity?.startedAt, "⠦", styles, now) ?? "shell [running]"];
@@ -897,6 +916,9 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
897
916
  const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", styles, now);
898
917
  if (shellActivity)
899
918
  lines.push(` ${shellActivity}`);
919
+ const toolActivity = formatToolActivity(run, "⠦", styles, now);
920
+ if (toolActivity)
921
+ lines.push(` ${toolActivity}`);
900
922
  const actionRows = () => {
901
923
  const actions = selection.actions;
902
924
  if (!actions)
@@ -904,7 +926,8 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
904
926
  return ["", styles.bold(actions.title), ...actions.options.map((option, index) => `${index === actions.index ? "→ " : " "}${index === actions.index ? styles.accent(option) : option}`)];
905
927
  };
906
928
  const treeRows = [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.map(treeLine) : [styles.muted("(empty)")])];
907
- lines.push(...formatNavigatorColumns(treeRows, [...details(selectedNode), ...actionRows()], safeWidth, selection));
929
+ const detailOffset = Math.max(0, (selection.scrollTop ?? 0) - lines.flatMap(wrap).length);
930
+ lines.push(...formatNavigatorColumns(treeRows, [...details(selectedNode), ...actionRows()], safeWidth, { ...selection, detailOffset }));
908
931
  if (model.unassignedAgents?.length && !tree.nodes.some((node) => node.phaseId === "unassigned"))
909
932
  lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
910
933
  return lines.flatMap((line) => wrap(line));
@@ -918,7 +941,10 @@ export function formatNavigatorColumns(sidebar, detail, width, layout = {}) {
918
941
  if (safeWidth >= 80) {
919
942
  const sidebarWidth = Math.min(42, Math.max(24, Math.floor((safeWidth - 3) * 0.38)));
920
943
  const left = sidebar.flatMap((line) => wrapNavigatorLine(line, sidebarWidth));
921
- const right = detail.flatMap((line) => wrapNavigatorLine(line, safeWidth - sidebarWidth - 3));
944
+ const wrapped = detail.flatMap((line) => wrapNavigatorLine(line, safeWidth - sidebarWidth - 3));
945
+ // Keep the details in view when the tree is scrolled, without making the block taller than the tree.
946
+ const shift = Math.min(Math.max(0, layout.detailOffset ?? 0), Math.max(0, left.length - wrapped.length));
947
+ const right = [...Array(shift).fill(""), ...wrapped];
922
948
  return Array.from({ length: Math.max(left.length, right.length) }, (_, index) => {
923
949
  const cell = left[index] ?? "";
924
950
  return `${cell}${" ".repeat(Math.max(0, sidebarWidth - visibleWidth(cell)))} | ${right[index] ?? ""}`;
@@ -2,7 +2,7 @@ import { Type } from "@earendil-works/pi-ai";
2
2
  import { copyToClipboard, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { type BackgroundWidgetAPI } from "./background-widget.js";
4
4
  import { type AgentTransport } from "./types.js";
5
- export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
5
+ export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Partial<Pick<ExtensionAPI, "getAllTools">> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
6
6
  export { agentBreadcrumb, agentBreadcrumbParts, formatBudgetStatus, formatNavigatorDashboard, formatNavigatorRun, formatStalledDuration, formatWorkflowPhaseDashboard, formatWorkflowProgress, navigatorAttentionSort, truncateWorkflowProgress, } from "./host-view.js";
7
7
  export { buildWorkflowPhaseModel, buildWorkflowPhaseTree, navigateWorkflowPhaseTree, preserveWorkflowPhaseSelection, preserveWorkflowPhaseTreeSelection, workflowPhaseTreeInitialExpanded, workflowPhaseTreeVisibleNodes } from "./host-phases.js";
8
8
  export type { WorkflowPhaseAgentCounts, WorkflowPhaseModel, WorkflowPhaseSelection, WorkflowPhaseState, WorkflowPhaseTree, WorkflowPhaseTreeDirection, WorkflowPhaseTreeNode, WorkflowPhaseTreeNodeKind, WorkflowPhaseTreeSelection, WorkflowPhaseView, } from "./host-phases.js";
@@ -16,7 +16,7 @@ export declare function formatWorkflowPreview(args: {
16
16
  description?: unknown;
17
17
  }): string;
18
18
  export declare const WORKFLOW_TOOL_LABEL = "Workflow";
19
- export declare const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default";
19
+ export declare const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default. Scripts call this session's tools as `await tools.<name>(args)`, outside withWorktree: a tool resolves to its text unless its description gives a `tools.<name>(args)` result shape, and failures reject";
20
20
  export declare const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Provide exactly one of script or scriptPath and a non-empty name. Registered catalog functions are available as globals inside the script; call them there, for example return await someFunction(args). Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. Manage runs from the interactive /workflow picker; use workflow_status, workflow_resume, workflow_retry, workflow_stop, and workflow_respond for explicit tool controls. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately and delivers completion or failure as a follow-up. After failure follow-ups, especially CANCELLED or interrupted runs, call workflow_status({ runId }) before recovery or replacement work, then pass its state as expectedState to workflow_retry/workflow_resume so recovery cannot act on a state that changed. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, expectedState?, foreground? }) replays a failed run into a child; workflow_resume({ runId, expectedState?, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
21
21
  export declare const WORKFLOW_TOOL_PARAMETERS: Type.TObject<{
22
22
  name: Type.TString;