@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 6.2.1-fork.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +145 -0
- package/README.md +2 -2
- package/dist/src/agent-execution.d.ts +43 -10
- package/dist/src/agent-execution.js +442 -162
- package/dist/src/background-widget.d.ts +5 -1
- package/dist/src/background-widget.js +5 -10
- package/dist/src/decoders.d.ts +2 -3
- package/dist/src/decoders.js +59 -37
- package/dist/src/execution.d.ts +6 -2
- package/dist/src/execution.js +48 -7
- package/dist/src/host-delivery.js +5 -8
- package/dist/src/host-navigator.js +5 -2
- package/dist/src/host-phases.d.ts +1 -0
- package/dist/src/host-recovery.d.ts +11 -8
- package/dist/src/host-recovery.js +14 -22
- package/dist/src/host-runtime.js +56 -5
- package/dist/src/host-view.d.ts +6 -1
- package/dist/src/host-view.js +46 -20
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +246 -118
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +1985 -1690
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +8 -5
- package/dist/src/paths.js +24 -13
- package/dist/src/persistence.d.ts +1 -0
- package/dist/src/persistence.js +1 -0
- package/dist/src/pi-runtime-runner.js +4 -1
- package/dist/src/registry.d.ts +9 -7
- package/dist/src/registry.js +29 -71
- package/dist/src/runtime/agent-runner.d.ts +2 -0
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/session-lease.d.ts +1 -1
- package/dist/src/session-lease.js +45 -11
- package/dist/src/settings.d.ts +19 -3
- package/dist/src/settings.js +63 -50
- package/dist/src/store.d.ts +3 -1
- package/dist/src/store.js +17 -1
- package/dist/src/trajectory.d.ts +12 -3
- package/dist/src/trajectory.js +32 -11
- package/dist/src/types.d.ts +129 -45
- package/dist/src/types.js +5 -8
- package/dist/src/utils.d.ts +21 -1
- package/dist/src/utils.js +46 -6
- package/dist/src/validation.d.ts +14 -3
- package/dist/src/validation.js +143 -87
- package/dist/starter/index.js +8 -11
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +8 -9
- package/dist/subagents/index.js +904 -956
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +48 -31
- package/dist/subagents/src/contracts.js +20 -9
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +14 -8
- package/dist/subagents/src/manager.js +52 -73
- package/dist/subagents/src/navigator.js +3 -14
- package/dist/subagents/src/view.d.ts +1 -1
- package/dist/subagents/src/view.js +15 -68
- package/dist/subagents/src/worktree.js +1 -1
- package/dist/trajectory/assets/index.html +35 -13
- package/dist/trajectory/index.js +576 -932
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +35 -13
- package/dist/trajectory/src/index.js +134 -104
- package/dist/trajectory/src/server.js +268 -27
- package/dist/trajectory/src/server.js.map +3 -3
- package/dist/trajectory/src/startup-mutex.d.ts +17 -0
- package/dist/trajectory/src/startup-mutex.js +172 -0
- package/examples/workflow-extension-template/README.md +4 -7
- package/examples/workflow-extension-template/extension.test.mjs +4 -8
- package/examples/workflow-extension-template/index.js +0 -3
- package/package.json +10 -10
- package/skills/pi-extensible-workflows/SKILL.md +12 -8
- package/src/agent-execution.ts +412 -164
- package/src/background-widget.ts +5 -11
- package/src/decoders.ts +51 -37
- package/src/execution.ts +46 -9
- package/src/host-delivery.ts +5 -8
- package/src/host-navigator.ts +4 -2
- package/src/host-phases.ts +1 -1
- package/src/host-recovery.ts +23 -28
- package/src/host-runtime.ts +44 -6
- package/src/host-view.ts +41 -16
- package/src/host.ts +229 -142
- package/src/index.ts +1 -2
- package/src/paths.ts +18 -9
- package/src/persistence.ts +1 -0
- package/src/pi-runtime-runner.ts +3 -1
- package/src/registry.ts +28 -64
- package/src/runtime/agent-runner.ts +2 -0
- package/src/script-tools.ts +76 -0
- package/src/session-lease.ts +34 -6
- package/src/settings.ts +56 -50
- package/src/store.ts +18 -2
- package/src/trajectory.ts +32 -15
- package/src/types.ts +82 -34
- package/src/utils.ts +42 -5
- package/src/validation.ts +121 -80
- package/starter/index.ts +0 -1
- package/starter/review-loop.ts +8 -9
- package/starter/roles/developer.md +0 -1
- package/starter/roles/oracle.md +0 -1
- package/starter/roles/researcher.md +0 -1
- package/starter/roles/reviewer.md +0 -1
- package/starter/roles/scout.md +0 -1
- package/subagents/README.md +10 -16
- package/subagents/src/contracts.ts +21 -11
- package/subagents/src/index.ts +17 -8
- package/subagents/src/manager.ts +48 -128
- package/subagents/src/navigator.ts +4 -13
- package/subagents/src/view.ts +16 -63
- package/subagents/src/worktree.ts +1 -1
- package/trajectory/src/assets/index.html +35 -13
- package/trajectory/src/index.ts +112 -91
- package/trajectory/src/server.ts +46 -10
- package/trajectory/src/startup-mutex.ts +127 -0
- package/dist/src/roles.d.ts +0 -70
- package/dist/src/roles.js +0 -325
- package/dist/starter/roles/developer.md +0 -6
- package/dist/starter/roles/oracle.md +0 -18
- package/dist/starter/roles/researcher.md +0 -20
- package/dist/starter/roles/reviewer.md +0 -19
- package/dist/starter/roles/scout.md +0 -18
- package/examples/workflow-extension-template/roles/reviewer.md +0 -4
- package/src/roles.ts +0 -326
|
@@ -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
|
|
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
|
-
|
|
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
|
|
32
|
-
function assertResumableSnapshot(snapshot
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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;
|
package/dist/src/host-runtime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import {
|
|
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,
|
|
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);
|
package/dist/src/host-view.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/host-view.js
CHANGED
|
@@ -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,
|
|
187
|
+
const icon = agentStateGlyph(agent.state, "●");
|
|
174
188
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
175
|
-
const
|
|
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}` : ""}${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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] ?? ""}`;
|
package/dist/src/host.d.ts
CHANGED
|
@@ -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;
|