@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
package/src/host-recovery.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
|
-
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { FairAgentScheduler, WorkflowAgentExecutor, type
|
|
3
|
+
import { type ExtensionAPI, type ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { FairAgentScheduler, WorkflowAgentExecutor, type AgentExecutionRoot, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
|
|
5
5
|
import { listRunIds, RunStore, structuralPath as operationPath, type PersistedRun } from "./persistence.js";
|
|
6
6
|
import { budgetUsage, budgetRelaxed, mergeBudget, resumeBudgetAllowed, validateBudget, validateBudgetPatch, WorkflowBudgetRuntime } from "./budget.js";
|
|
7
7
|
import { aliasDrift, createLaunchSnapshot, errorCode, errorText, jsonValue, object } from "./utils.js";
|
|
8
|
-
import { LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError, isExternallyEndedRunState, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowExtensionSettings, type WorkflowMetadata, type WorkflowRetryProvenance, type WorkflowWorktreeReference } from "./types.js";
|
|
8
|
+
import { LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError, isExternallyEndedRunState, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type ToolIdentity, type WorkflowExtensionSettings, type WorkflowMetadata, type WorkflowRetryProvenance, type WorkflowWorktreeReference } from "./types.js";
|
|
9
9
|
import { type WorkflowRegistryApi } from "./registry.js";
|
|
10
10
|
import { RunLifecycle, WorkflowEventPublisher, hostSessionContext, withWorkflowFunctions, withoutActiveShells, workflowRunContext, type WorkflowRunRecord, type WorkflowToolUpdate } from "./host-runtime.js";
|
|
11
11
|
import { runWorkflow } from "./execution.js";
|
|
@@ -13,7 +13,7 @@ import { createWorkflowFailureDiagnostics, formatWorkflowFailureDelivery, format
|
|
|
13
13
|
|
|
14
14
|
/** The subset of Pi's model registry the host relies on; every member is optional because older or headless hosts expose fewer of them. */
|
|
15
15
|
export type ModelRegistryCapability = { getAll?: () => readonly Model<Api>[]; getAvailable?: () => readonly Model<Api>[]; find?: (provider: string, model: string) => Model<Api> | undefined; refresh?: () => Promise<void>; getError?: () => string | undefined };
|
|
16
|
-
export type WorkflowRecoveryContext = { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext; signal?: AbortSignal; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> };
|
|
16
|
+
export type WorkflowRecoveryContext = { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext; toolContext?: ExtensionToolContext | undefined; signal?: AbortSignal; resolvedAliases?: Readonly<Record<string, string>>; resolvedDynamicAliasNames?: readonly string[]; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> };
|
|
17
17
|
export type WorkflowRecoveryDependencies = {
|
|
18
18
|
pi: Pick<ExtensionAPI, "getThinkingLevel">;
|
|
19
19
|
home: string | undefined;
|
|
@@ -28,10 +28,10 @@ export type WorkflowRecoveryDependencies = {
|
|
|
28
28
|
createAgentExecutor: (root: Omit<AgentExecutionRoot, "agentDir" | "agentSetupHooks">) => WorkflowAgentExecutor;
|
|
29
29
|
activeSnapshotTools: (tools: readonly string[], active: ReadonlySet<string> | "session") => Set<string>;
|
|
30
30
|
frozenResourcePolicy: (policy: AgentResourcePolicy) => () => AgentResourcePolicy;
|
|
31
|
-
resolveLaunchPrologue: (input: { snapshot: Readonly<LaunchSnapshot>; cwd: string; trustedProject: boolean; rootModel: ModelSpec; modelRegistry?: WorkflowRecoveryContext["modelRegistry"]; signal: AbortSignal; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>>; withPreflight: boolean }) => Promise<{ active: Set<string>; settingsPath: string; currentPolicy: AgentResourcePolicy; previousAliases: Readonly<Record<string, string>>; knownModels: ReadonlySet<string>; availableModels: ReadonlySet<string>; currentAliases: Readonly<Record<string, string>>; blockedAliases: ReadonlySet<string>; blockedAliasTargets: Readonly<Record<string, string>>; snapshot: Readonly<LaunchSnapshot>; script: string | undefined }>;
|
|
32
|
-
|
|
33
|
-
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>;
|
|
31
|
+
resolveLaunchPrologue: (input: { snapshot: Readonly<LaunchSnapshot>; cwd: string; trustedProject: boolean; rootModel: ModelSpec; modelRegistry?: WorkflowRecoveryContext["modelRegistry"]; signal: AbortSignal; resolvedAliases?: Readonly<Record<string, string>>; resolvedDynamicAliasNames?: readonly string[]; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>>; withPreflight: boolean }) => Promise<{ active: Set<string>; settingsPath: string; currentPolicy: AgentResourcePolicy; previousAliases: Readonly<Record<string, string>>; knownModels: ReadonlySet<string>; availableModels: ReadonlySet<string>; currentAliases: Readonly<Record<string, string>>; dynamicAliasNames: readonly string[]; blockedAliases: ReadonlySet<string>; blockedAliasTargets: Readonly<Record<string, string>>; snapshot: Readonly<LaunchSnapshot>; script: string | undefined }>;
|
|
32
|
+
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>;
|
|
34
33
|
shellForRun: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity) => Promise<ShellResult>;
|
|
34
|
+
toolForRun: (store: RunStore, lifecycle: RunLifecycle, context: ExtensionToolContext | undefined, identifier: string, args: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: ToolIdentity) => Promise<JsonValue>;
|
|
35
35
|
resolveWorktree: (store: RunStore, metadata: WorkflowMetadata, owner: string) => Promise<Readonly<WorkflowWorktreeReference>>;
|
|
36
36
|
checkpointBridge: (runId: string, store: RunStore, metadata: WorkflowMetadata, foreground: boolean, ui?: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, headless?: boolean) => (raw: Readonly<Record<string, JsonValue>>, signal: AbortSignal) => Promise<boolean>;
|
|
37
37
|
phaseBridge: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle) => (phase: string) => Promise<void>;
|
|
@@ -60,13 +60,9 @@ function workflowRecoveryGuidance(action: "resume" | "retry", state: RunState):
|
|
|
60
60
|
if (state === "interrupted") return "Interrupted workflow runs must be resumed from the interactive /workflow picker";
|
|
61
61
|
return `Only failed workflow runs can be retried; source is ${state}`;
|
|
62
62
|
}
|
|
63
|
-
/** A snapshot can only be re-executed when it carries the current identity scheme and
|
|
64
|
-
function assertResumableSnapshot(snapshot: Readonly<LaunchSnapshot
|
|
63
|
+
/** A snapshot can only be re-executed when it carries the current identity scheme and validated concrete agent configurations. */
|
|
64
|
+
function assertResumableSnapshot(snapshot: Readonly<LaunchSnapshot>): void {
|
|
65
65
|
if (snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
|
|
66
|
-
if (snapshot.roles === undefined) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow role definitions are missing from the launch snapshot");
|
|
67
|
-
if ((snapshot.projectRoles?.length ?? 0) > 0 && !trustedProject) throw new WorkflowError("RESUME_INCOMPATIBLE", "Cannot restore project roles in an untrusted project");
|
|
68
|
-
const missingRole = snapshot.agentTypes.find((role) => !snapshot.roles?.[role]);
|
|
69
|
-
if (missingRole) throw new WorkflowError("RESUME_INCOMPATIBLE", `Role definition is missing from the launch snapshot: ${missingRole}`);
|
|
70
66
|
}
|
|
71
67
|
function assertExpectedWorkflowState(expectedState: string | undefined, actualState: RunState): void {
|
|
72
68
|
if (expectedState !== undefined && expectedState !== actualState) throw new WorkflowError("RESUME_INCOMPATIBLE", `Workflow run state changed: expected state ${expectedState}, actual state ${actualState}`);
|
|
@@ -74,7 +70,7 @@ function assertExpectedWorkflowState(expectedState: string | undefined, actualSt
|
|
|
74
70
|
export function persistedFailure(run: PersistedRun, error: WorkflowError): PersistedRun { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
|
|
75
71
|
|
|
76
72
|
export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
77
|
-
const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue,
|
|
73
|
+
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;
|
|
78
74
|
const coordinateRunMutation = deps.coordinateRunMutation ?? (<T>(task: () => Promise<T>): Promise<T> => task());
|
|
79
75
|
type BudgetDecisionResult = { state: "running" | "completed" | "budget_exhausted"; approved: boolean; value?: JsonValue; run?: PersistedRun; completion?: CompletionDeliveryResult };
|
|
80
76
|
const budgetDecisionDelivery = (metadata: WorkflowMetadata, request: BudgetApprovalRequest) => `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}.`;
|
|
@@ -96,11 +92,11 @@ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
|
96
92
|
const loaded = await run.store.load();
|
|
97
93
|
const trustedProject = context?.projectTrusted ?? run.projectTrusted();
|
|
98
94
|
const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
|
|
99
|
-
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 });
|
|
95
|
+
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 });
|
|
100
96
|
await run.store.saveSnapshot(snapshot);
|
|
101
97
|
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
102
98
|
scheduler.setRunExtensionSettings(run.store.runId, snapshot.settings.extensionSettings);
|
|
103
|
-
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,
|
|
99
|
+
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) });
|
|
104
100
|
run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
|
|
105
101
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
106
102
|
if (drift.length) await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
@@ -111,33 +107,32 @@ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
|
111
107
|
return { hasUI: host?.hasUI === true, ui };
|
|
112
108
|
};
|
|
113
109
|
type ColdResumeResult = { value: JsonValue; resultPath: string; resultBytes: number; completion: CompletionDeliveryResult };
|
|
114
|
-
const coldResumeRun = async (run: WorkflowRunRecord, hasUI: boolean, ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, trustedProject: boolean, context?: { model: { provider: string; id: string } | undefined; modelRegistry: WorkflowRecoveryContext["modelRegistry"]; deliveryContext: CompletionDeliveryContext; signal?: AbortSignal | undefined; resolvedAliases?: Readonly<Record<string, string>>; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> }, modeOverride?: boolean, waitForCompletion = true): Promise<ColdResumeResult | undefined> => {
|
|
110
|
+
const coldResumeRun = async (run: WorkflowRunRecord, hasUI: boolean, ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, trustedProject: boolean, context?: { model: { provider: string; id: string } | undefined; modelRegistry: WorkflowRecoveryContext["modelRegistry"]; deliveryContext: CompletionDeliveryContext; toolContext?: ExtensionToolContext | undefined; signal?: AbortSignal | undefined; resolvedAliases?: Readonly<Record<string, string>>; resolvedDynamicAliasNames?: readonly string[]; blockedAliases?: ReadonlySet<string>; blockedAliasTargets?: Readonly<Record<string, string>> }, modeOverride?: boolean, waitForCompletion = true): Promise<ColdResumeResult | undefined> => {
|
|
115
111
|
const loaded = await run.store.load();
|
|
116
112
|
const foreground = modeOverride ?? (loaded.run.delivery?.mode === "foreground" || (loaded.run.delivery?.mode === "background" && loaded.run.delivery.toolCallId !== undefined) || (loaded.run.delivery === undefined && loaded.snapshot.launchMode === "foreground"));
|
|
117
113
|
if (loaded.run.activeShells !== undefined || loaded.run.activeShellStartedAt !== undefined || loaded.run.activeShellsByPhase !== undefined) await persistRunState(run.store, run.metadata, withoutActiveShells);
|
|
118
114
|
await run.store.validateRetrySource();
|
|
119
115
|
await run.store.validateBorrowedWorktrees();
|
|
120
|
-
assertResumableSnapshot(loaded.snapshot
|
|
116
|
+
assertResumableSnapshot(loaded.snapshot);
|
|
121
117
|
const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
|
|
122
118
|
const controller = new AbortController();
|
|
123
119
|
if (context?.signal?.aborted) controller.abort(); else { context?.signal?.addEventListener("abort", () => { controller.abort(); }, { once: true }); }
|
|
124
120
|
run.abortController = controller;
|
|
125
|
-
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 });
|
|
121
|
+
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 });
|
|
126
122
|
if (!script) throw new WorkflowError("INTERNAL_ERROR", "Resume preflight did not produce a launch script");
|
|
127
123
|
const persistedSnapshot = modeOverride === undefined ? snapshot : createLaunchSnapshot({ ...snapshot, launchMode: foreground ? "foreground" : "background" });
|
|
128
124
|
await run.store.saveSnapshot(persistedSnapshot);
|
|
129
125
|
if (modeOverride !== undefined) await persistRunState(run.store, run.metadata, (current) => ({ ...current, delivery: { ...(current.delivery ?? {}), mode: foreground ? "foreground" : "background", state: foreground ? "attached" : "pending" } }));
|
|
130
126
|
scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
|
|
131
127
|
scheduler.setRunExtensionSettings(run.store.runId, snapshot.settings.extensionSettings);
|
|
132
|
-
|
|
133
|
-
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) });
|
|
128
|
+
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) });
|
|
134
129
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
135
130
|
if (drift.length) await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
136
131
|
const runContext = workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, controller.signal);
|
|
137
132
|
run.executor.setRunContext(runContext);
|
|
138
133
|
await scheduler.cancelRun(run.store.runId);
|
|
139
134
|
await run.lifecycle.resume();
|
|
140
|
-
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
|
|
135
|
+
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);
|
|
141
136
|
run.execution = execution;
|
|
142
137
|
const completion = execution.result.then(async (value) => {
|
|
143
138
|
await scheduler.flush(run.store.runId);
|
|
@@ -243,7 +238,7 @@ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
|
243
238
|
return { state: "running" };
|
|
244
239
|
};
|
|
245
240
|
const retryReservations = new Set<string>();
|
|
246
|
-
type PreparedRetryWorkflowRun = { childRun: WorkflowRunRecord; parentRunId: string; lineageRootRunId: string; concurrency: number; extensionSettings?: Readonly<WorkflowExtensionSettings>; hostModel: { provider: string; id: string }; modelRegistry: WorkflowRecoveryContext["modelRegistry"]; currentAliases: Readonly<Record<string, string>>; blockedAliases: ReadonlySet<string>; blockedAliasTargets: Readonly<Record<string, string>> };
|
|
241
|
+
type PreparedRetryWorkflowRun = { childRun: WorkflowRunRecord; parentRunId: string; lineageRootRunId: string; concurrency: number; extensionSettings?: Readonly<WorkflowExtensionSettings>; hostModel: { provider: string; id: string }; modelRegistry: WorkflowRecoveryContext["modelRegistry"]; currentAliases: Readonly<Record<string, string>>; dynamicAliasNames: readonly string[]; blockedAliases: ReadonlySet<string>; blockedAliasTargets: Readonly<Record<string, string>> };
|
|
247
242
|
const retryWorkflowRunUnlocked = async (runId: string, context: unknown, signal?: AbortSignal, expectedState?: string): Promise<PreparedRetryWorkflowRun> => {
|
|
248
243
|
if (typeof runId !== "string" || !runId.trim()) throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
|
|
249
244
|
const host = object(context) ? context : {};
|
|
@@ -276,11 +271,11 @@ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
|
276
271
|
const trustedProject = projectTrusted(context);
|
|
277
272
|
await sourceStore.validateRetrySource();
|
|
278
273
|
await sourceStore.validateBorrowedWorktrees();
|
|
279
|
-
assertResumableSnapshot(loaded.snapshot
|
|
274
|
+
assertResumableSnapshot(loaded.snapshot);
|
|
280
275
|
const modelRegistry = resumeHostContext(context).modelRegistry;
|
|
281
276
|
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: "" };
|
|
282
277
|
const rootModel: ModelSpec = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
|
|
283
|
-
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 });
|
|
278
|
+
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 });
|
|
284
279
|
await sourceStore.validateNamedWorktrees();
|
|
285
280
|
for (const name of loaded.run.retry?.namedWorktrees ?? []) await sourceStore.resolveNamedWorktree(name);
|
|
286
281
|
const completedPaths = (await sourceStore.replayableOperations()).map(({ path }) => path);
|
|
@@ -298,10 +293,10 @@ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
|
298
293
|
const abortController = new AbortController();
|
|
299
294
|
const providerErrorRecovery = createProviderErrorRecovery(context, availableModels, () => { abortController.abort(); });
|
|
300
295
|
const providerPause = async () => { deliver(`Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
301
|
-
const childRun: WorkflowRunRecord = { executor: createAgentExecutor({ cwd, model, tools: activeSnapshotTools(loaded.snapshot.tools, active), resourceSelectors: currentPolicy.effective, extensionSettings: childBaseSnapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath,
|
|
296
|
+
const childRun: WorkflowRunRecord = { 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 } : {}) };
|
|
302
297
|
runs.set(childRunId, childRun);
|
|
303
298
|
childCreated = true;
|
|
304
|
-
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 };
|
|
299
|
+
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 };
|
|
305
300
|
} finally {
|
|
306
301
|
if (!childCreated) retryReservations.delete(lineageRootRunId);
|
|
307
302
|
}
|
|
@@ -314,7 +309,7 @@ export function createWorkflowRecovery(deps: WorkflowRecoveryDependencies) {
|
|
|
314
309
|
await eventPublisher.runStarted(prepared.childRun.store, prepared.childRun.metadata);
|
|
315
310
|
const { hasUI, ui } = recoveryUi(context);
|
|
316
311
|
const recoveryContext = resumeHostContext(context);
|
|
317
|
-
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);
|
|
312
|
+
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);
|
|
318
313
|
const completion = runs.get(prepared.childRun.store.runId)?.completion;
|
|
319
314
|
if (completion) {
|
|
320
315
|
childStarted = true;
|
package/src/host-runtime.ts
CHANGED
|
@@ -2,10 +2,11 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import { type AgentProviderFailure, type AgentProviderRecovery, WorkflowAgentExecutor } from "./agent-execution.js";
|
|
3
3
|
import { type WorkflowBudgetRuntime } from "./budget.js";
|
|
4
4
|
import { type PersistedRun, type RunStore, type WorktreeReference } from "./persistence.js";
|
|
5
|
-
import {
|
|
5
|
+
import { FUNCTION_HANDLE_PREFIX } from "./execution.js";
|
|
6
|
+
import { deepFreeze, errorCode, errorText, fail, isWorkflowErrorCode, jsonDigest, jsonValue, object } from "./utils.js";
|
|
6
7
|
import { validateAgentOptions, validateShellOptions, workflowPrompt } from "./validation.js";
|
|
7
8
|
import { type WorkflowRegistryApi } from "./registry.js";
|
|
8
|
-
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, type AgentOptions, type AgentRecord, type BudgetEvent, type FunctionIdentity, type JsonValue, type ModelSpec, type ParallelResult, type ParallelTasks, type PipelineItems, type PipelineResult, type PipelineStages, type RunState, type WorkflowBridge, type WorkflowCheckpointState, type WorkflowErrorShape, type WorkflowEventBase, type WorkflowExecution, type WorkflowExtensionSettings, type WorkflowFunctionContext, type WorkflowMetadata, type WorkflowRunContext, type WorkflowWorktreeCallback, type WorkflowWorktreeReference } from "./types.js";
|
|
9
|
+
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, type AgentHandle, type AgentOptions, type AgentRecord, type BudgetEvent, type FunctionIdentity, type JsonValue, type ModelSpec, type ParallelResult, type ParallelTasks, type PipelineItems, type PipelineResult, type PipelineStages, type RunState, type WorkflowBridge, type WorkflowCheckpointState, type WorkflowErrorShape, type WorkflowEventBase, type WorkflowExecution, type WorkflowExtensionSettings, type WorkflowFunctionContext, type WorkflowMetadata, type WorkflowRunContext, type WorkflowWorktreeCallback, type WorkflowWorktreeReference } from "./types.js";
|
|
9
10
|
import { structuralPath as operationPath } from "./persistence.js";
|
|
10
11
|
|
|
11
12
|
export type WorkflowEventSink = { emit: (name: string, payload: unknown) => unknown };
|
|
@@ -150,7 +151,7 @@ export class WorkflowEventPublisher {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
async agentState(store: RunStore, metadata: WorkflowMetadata, previous: AgentRecord | undefined, agent: AgentRecord): Promise<void> {
|
|
153
|
-
await this.#publish(store, metadata, WORKFLOW_AGENT_STATE_CHANGED_EVENT, { agentId: agent.id, displayLabel: agent.label ?? agent.name,
|
|
154
|
+
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 });
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
async agentStates(store: RunStore, metadata: WorkflowMetadata, previous: readonly AgentRecord[], current: readonly AgentRecord[]): Promise<void> {
|
|
@@ -288,6 +289,7 @@ function functionBreadcrumb(name: string, occurrence: number): string { return o
|
|
|
288
289
|
|
|
289
290
|
export function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, runContext: Readonly<WorkflowRunContext>, registry: WorkflowRegistryApi, settings: Readonly<WorkflowExtensionSettings> = Object.freeze({})): WorkflowBridge {
|
|
290
291
|
const functionAgentOccurrences = new Map<string, number>();
|
|
292
|
+
const functionHandles = new Set<string>();
|
|
291
293
|
const functionShellOccurrences = new Map<string, number>();
|
|
292
294
|
const functionInvokeOccurrences = new Map<string, number>();
|
|
293
295
|
const functionInvokeBreadcrumbOccurrences = new Map<string, number>();
|
|
@@ -309,6 +311,7 @@ export function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, r
|
|
|
309
311
|
// Own rejection immediately; Promise.all below still propagates it after the function completes.
|
|
310
312
|
void sideEffect.catch(() => undefined);
|
|
311
313
|
};
|
|
314
|
+
let closed = false;
|
|
312
315
|
const parentBreadcrumb = breadcrumb ?? functionBreadcrumb(name, identity.occurrence);
|
|
313
316
|
const context: WorkflowFunctionContext = {
|
|
314
317
|
settings: deepFreeze(structuredClone(settings)),
|
|
@@ -328,7 +331,7 @@ export function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, r
|
|
|
328
331
|
const nestedIdentity: FunctionIdentity = { path: nestedPath, structuralPath: [...inherited], occurrence, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) };
|
|
329
332
|
return invokeFunction(targetName, targetInput, signal, nestedIdentity, `${parentBreadcrumb} > ${functionBreadcrumb(breadcrumbName, breadcrumbOccurrence)}`);
|
|
330
333
|
},
|
|
331
|
-
agent: async (prompt: string, options?: Readonly<AgentOptions>) => {
|
|
334
|
+
agent: Object.assign(async (prompt: string, options?: Readonly<AgentOptions>) => {
|
|
332
335
|
if (!bridge.agent || typeof prompt !== "string") fail("AGENT_FAILED", "No agent bridge is available");
|
|
333
336
|
const validatedOptions = validateAgentOptions(options === undefined ? {} : options);
|
|
334
337
|
const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
|
|
@@ -337,7 +340,42 @@ export function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, r
|
|
|
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: readonly unknown[]): AgentHandle => {
|
|
345
|
+
if (created.length !== 1 || !object(created[0])) fail("INVALID_METADATA", "agent.create requires one options object");
|
|
346
|
+
const { name, ...rest } = created[0];
|
|
347
|
+
if (typeof name !== "string" || !name.trim()) fail("INVALID_METADATA", "agent.create requires a stable explicit name");
|
|
348
|
+
const config = deepFreeze(structuredClone(validateAgentOptions(rest)));
|
|
349
|
+
const scopedStructuralPath = [...(inheritedHostAgentPath.getStore() ?? [])];
|
|
350
|
+
const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
|
|
351
|
+
const scope = JSON.stringify([scopedStructuralPath, scopedWorktreeOwner ?? null]);
|
|
352
|
+
// 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.
|
|
353
|
+
const handle = operationPath(FUNCTION_HANDLE_PREFIX, jsonDigest([path, scopedStructuralPath, scopedWorktreeOwner ?? null]), name);
|
|
354
|
+
if (functionHandles.has(handle)) fail("INVALID_METADATA", `Duplicate agent handle name: ${name}`);
|
|
355
|
+
functionHandles.add(handle);
|
|
356
|
+
let turn = 0;
|
|
357
|
+
let inflight = false;
|
|
358
|
+
// Not a nested promise chain: the caller owns the one returned promise, and inflight clears in finally on every outcome.
|
|
359
|
+
const send = async (...sent: readonly unknown[]): Promise<JsonValue> => {
|
|
360
|
+
if (!bridge.agent) fail("AGENT_FAILED", "No agent bridge is available");
|
|
361
|
+
if (sent.length < 1 || sent.length > 2) fail("INVALID_METADATA", "send requires a prompt and optional options");
|
|
362
|
+
if (closed) fail("INVALID_METADATA", `Agent handle outlived the function invocation that created it: ${name}`);
|
|
363
|
+
if (typeof sent[0] !== "string") fail("INVALID_METADATA", "send prompt must be a string");
|
|
364
|
+
const sendOptions = sent[1] === undefined ? {} : sent[1];
|
|
365
|
+
if (!object(sendOptions) || Object.keys(sendOptions).some((key) => key !== "outputSchema" && key !== "timeoutMs")) fail("INVALID_METADATA", "send options must contain only outputSchema and timeoutMs");
|
|
366
|
+
const sendStructuralPath = [...(inheritedHostAgentPath.getStore() ?? [])];
|
|
367
|
+
if (JSON.stringify([sendStructuralPath, inheritedHostWorktreeOwner.getStore() ?? worktreeOwner ?? null]) !== scope) fail("INVALID_METADATA", `Agent handle sends must run in the scope that created the handle: ${name}`);
|
|
368
|
+
if (inflight) fail("INVALID_METADATA", "Concurrent send(...) calls on one agent handle are unsupported; await each turn");
|
|
369
|
+
const validatedOptions = validateAgentOptions({ ...config, ...sendOptions });
|
|
370
|
+
inflight = true;
|
|
371
|
+
turn += 1;
|
|
372
|
+
try {
|
|
373
|
+
return await bridge.agent(sent[0], validatedOptions, signal, { structuralPath: sendStructuralPath, callSite: `function:${path}:handle:${name}`, occurrence: turn, handle, turn, parentBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
|
|
374
|
+
} finally { inflight = false; }
|
|
375
|
+
};
|
|
376
|
+
return Object.freeze({ name, send }) as AgentHandle;
|
|
377
|
+
},
|
|
378
|
+
}),
|
|
341
379
|
shell: async (...args: readonly unknown[]) => {
|
|
342
380
|
if (!bridge.shell) fail("SHELL_FAILED", "No shell bridge is available");
|
|
343
381
|
if (typeof args[0] !== "string") fail("INVALID_METADATA", "shell command must be a string");
|
|
@@ -360,7 +398,7 @@ export function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, r
|
|
|
360
398
|
phase: (name: string) => { ownSideEffect(() => bridge.phase?.(name)); },
|
|
361
399
|
log: (message: string) => { ownSideEffect(() => bridge.log?.(message)); },
|
|
362
400
|
};
|
|
363
|
-
const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } }));
|
|
401
|
+
const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } })).finally(() => { closed = true; });
|
|
364
402
|
await Promise.all(sideEffects);
|
|
365
403
|
if (!replayed) await store.complete(path, stored ?? result);
|
|
366
404
|
return result;
|
package/src/host-view.ts
CHANGED
|
@@ -90,6 +90,15 @@ function formatShellActivity(activeShells: number | undefined, startedAt: number
|
|
|
90
90
|
const timing = started && startedAt !== undefined && !Number.isNaN(started.getTime()) ? ` ${styles.dim(`started=${started.toISOString()} elapsed=${formatWorkflowRuntime(Math.max(0, now - startedAt))}`)}` : "";
|
|
91
91
|
return `${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(count)} active)`)}${timing}`;
|
|
92
92
|
}
|
|
93
|
+
function formatToolActivity(run: Pick<PersistedRun, "activeTools">, spinner: string, styles: WorkflowProgressStyles, now: number): string | undefined {
|
|
94
|
+
const calls = run.activeTools ?? [];
|
|
95
|
+
if (!calls.length) return undefined;
|
|
96
|
+
const counts = new Map<string, number>();
|
|
97
|
+
for (const { name } of calls) counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
98
|
+
const names = [...counts].map(([name, count]) => count > 1 ? `${name} ×${String(count)}` : name).join(", ");
|
|
99
|
+
const elapsed = formatWorkflowRuntime(Math.max(0, now - Math.min(...calls.map(({ startedAt }) => startedAt))));
|
|
100
|
+
return `${styles.accent(spinner)} tools ${styles.accent("[running]")} ${sanitizeDisplayText(names)} ${styles.dim(`elapsed=${elapsed}`)}`;
|
|
101
|
+
}
|
|
93
102
|
function formatLogTimestamp(timestamp: number | undefined): string {
|
|
94
103
|
if (timestamp === undefined || !Number.isFinite(timestamp)) return "--:--:--";
|
|
95
104
|
const date = new Date(timestamp);
|
|
@@ -161,12 +170,15 @@ function workflowProgressLines(run: PersistedRun, spinner: string, styles: Workf
|
|
|
161
170
|
const scopedShells = (run.activeShellsByPhase?.length ?? 0) > 0;
|
|
162
171
|
const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, spinner, styles, now);
|
|
163
172
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
173
|
+
const toolActivity = formatToolActivity(run, spinner, styles, now);
|
|
174
|
+
if (toolActivity) lines.push(` ${toolActivity}`);
|
|
164
175
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
165
176
|
const position = new Map(run.agents.map((agent, index) => [agent, index]));
|
|
166
177
|
const renderAgents = (agents: readonly AgentRecord[], nested: boolean) => renderGroupedAgents(agents, ({ agent, depth }, grouped) => {
|
|
167
|
-
const icon = agentStateGlyph(agent.state,
|
|
178
|
+
const icon = agentStateGlyph(agent.state, "●");
|
|
168
179
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
169
|
-
const
|
|
180
|
+
const idleSpinner = agent.state === "running" && !agentActivityLabel(agent) ? styles.accent(spinner) : "";
|
|
181
|
+
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : [idleSpinner, formatAgentActivity(agent, spinner, styles, now)].filter(Boolean).join(" ");
|
|
170
182
|
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
171
183
|
const state = progressStyleForState(agent.state, styles);
|
|
172
184
|
const detail = expanded ? formatWorkflowAgentDetail(agent, now) : "";
|
|
@@ -469,7 +481,7 @@ export function workflowProgressBlock(run: PersistedRun, theme: Theme, progress?
|
|
|
469
481
|
invalidate() {
|
|
470
482
|
const displayed = currentRun();
|
|
471
483
|
const now = Date.now();
|
|
472
|
-
if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0)) return;
|
|
484
|
+
if (!progress || !refresh || displayed.state !== "running" || (!displayed.agents.some((agent) => agent.state === "running") && (displayed.activeShells ?? 0) <= 0 && !displayed.activeTools?.length)) return;
|
|
473
485
|
if (progress.refresh || now - progress.lastRefreshAt < WORKFLOW_PROGRESS_REFRESH_MS) return;
|
|
474
486
|
progress.lastRefreshAt = now;
|
|
475
487
|
const inputRun = progress.inputRun;
|
|
@@ -573,7 +585,7 @@ export function formatStalledDuration(durationMs: number): string {
|
|
|
573
585
|
const remainingMinutes = minutes % 60;
|
|
574
586
|
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
575
587
|
}
|
|
576
|
-
function stalledDuration(agent: Pick<AgentDetailPresentation, "state" | "lastEventAt">, now: number): number | undefined {
|
|
588
|
+
export function stalledDuration(agent: Pick<AgentDetailPresentation, "state" | "lastEventAt">, now: number): number | undefined {
|
|
577
589
|
if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt)) return undefined;
|
|
578
590
|
const duration = now - agent.lastEventAt;
|
|
579
591
|
return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
|
|
@@ -583,7 +595,7 @@ function elapsedDurationMs(agent: Pick<AgentDetailPresentation, "durationMs" | "
|
|
|
583
595
|
if (agent.durationMs !== undefined && Number.isFinite(agent.durationMs)) return Math.max(0, agent.durationMs);
|
|
584
596
|
return agent.startedAt === undefined || !Number.isFinite(agent.startedAt) ? undefined : Math.max(0, end - agent.startedAt);
|
|
585
597
|
}
|
|
586
|
-
function agentActivityLabel(agent: { readonly activity?: AgentRecord["activity"]; readonly toolCalls?: AgentRecord["toolCalls"] }): string {
|
|
598
|
+
export function agentActivityLabel(agent: { readonly activity?: AgentRecord["activity"]; readonly toolCalls?: AgentRecord["toolCalls"] }): string {
|
|
587
599
|
const activity = agent.activity;
|
|
588
600
|
if (activity?.kind === "reasoning" || activity?.kind === "text") {
|
|
589
601
|
const name = activity.kind === "reasoning" ? "reasoning" : "responding";
|
|
@@ -627,7 +639,6 @@ export interface AgentDetailPresentation {
|
|
|
627
639
|
readonly lastEventAt?: number;
|
|
628
640
|
readonly structuralPath?: readonly string[];
|
|
629
641
|
readonly model?: AgentRecord["model"];
|
|
630
|
-
readonly role?: string;
|
|
631
642
|
readonly tools?: readonly string[];
|
|
632
643
|
readonly attempts?: number;
|
|
633
644
|
readonly startedAt?: number;
|
|
@@ -652,7 +663,6 @@ export function formatAgentDetail(agent: Readonly<AgentDetailPresentation>, styl
|
|
|
652
663
|
`State: ${state(agent.state)}`,
|
|
653
664
|
...(agent.structuralPath?.length ? [`Structural path: ${agent.structuralPath.join(" > ")}`] : []),
|
|
654
665
|
...(model === undefined ? [] : [`Model: ${model}`]),
|
|
655
|
-
`Role: ${agent.role ?? "(none)"}`,
|
|
656
666
|
`Tools: ${tools}`,
|
|
657
667
|
...(agent.attempts !== undefined && Number.isFinite(agent.attempts) && agent.attempts > 1 ? [`Attempts: ${String(agent.attempts)}`] : []),
|
|
658
668
|
...(duration === undefined ? [] : [`Duration: ${formatWorkflowRuntime(duration)}`]),
|
|
@@ -715,6 +725,8 @@ export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonl
|
|
|
715
725
|
const lines = [header, meta, ...formatCompactBudgetStatus(run)];
|
|
716
726
|
const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
717
727
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
728
|
+
const toolActivity = formatToolActivity(run, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
729
|
+
if (toolActivity) lines.push(` ${toolActivity}`);
|
|
718
730
|
if (run.error) lines.push(`Error: ${run.error.code}: ${run.error.message}`);
|
|
719
731
|
if (run.events?.length) lines.push(...run.events.filter((event) => event.type === "warning").map((event) => `Warning: ${event.message}`));
|
|
720
732
|
lines.push("");
|
|
@@ -752,6 +764,8 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
|
|
|
752
764
|
];
|
|
753
765
|
const shellActivity = formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
754
766
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
767
|
+
const toolActivity = formatToolActivity(run, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now);
|
|
768
|
+
if (toolActivity) lines.push(` ${toolActivity}`);
|
|
755
769
|
if (run.error) lines.push(`Run error: ${run.error.code}: ${run.error.message}`);
|
|
756
770
|
if (run.events?.length) lines.push(...run.events.filter((event) => event.type === "warning").map((event) => `Warning: ${event.message}`));
|
|
757
771
|
const aliases = snapshot.modelAliases ?? snapshot.settings.modelAliases;
|
|
@@ -762,11 +776,10 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
|
|
|
762
776
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
763
777
|
lines.push(...renderGroupedAgents(run.agents, ({ agent, index, depth }, grouped) => {
|
|
764
778
|
const model = `${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`;
|
|
765
|
-
const role = agent.role ? ` role=${agent.role}` : "";
|
|
766
779
|
const tools = ` tools=${agent.tools.join(",") || "(none)"}`;
|
|
767
780
|
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"}` : "";
|
|
768
781
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
769
|
-
const result = [`${indent}#${String(index + 1)} ${grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId)} state=${agent.state} model=${model}${agent.requestedModel ? ` requested=${agent.requestedModel}` : ""}${
|
|
782
|
+
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}`];
|
|
770
783
|
for (const attempt of agent.attemptDetails ?? []) result.push(`${indent} attempt ${String(attempt.attempt)}${attempt.error ? ` error=${attempt.error.code}: ${attempt.error.message}` : ""}`);
|
|
771
784
|
for (const call of agent.toolCalls ?? []) result.push(`${indent} tool ${call.name} state=${call.state}`);
|
|
772
785
|
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
@@ -798,11 +811,17 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
798
811
|
const visit = (id: string): void => {
|
|
799
812
|
const child = tree.byId.get(id);
|
|
800
813
|
if (!child) return;
|
|
801
|
-
if (child.agent) agents.push(child.agent);
|
|
814
|
+
if (child.agent) agents.push(child.agent);
|
|
815
|
+
for (const childId of child.children) visit(childId);
|
|
802
816
|
};
|
|
803
|
-
|
|
817
|
+
visit(node.id);
|
|
804
818
|
return agents;
|
|
805
819
|
};
|
|
820
|
+
const usageLines = (agents: readonly AgentRecord[]): string[] => {
|
|
821
|
+
const spans = agents.flatMap((agent) => agent.startedAt === undefined ? [] : [[agent.startedAt, agent.startedAt + (elapsedDurationMs(agent, now) ?? 0)] as const]);
|
|
822
|
+
const elapsed = spans.length ? formatWorkflowRuntime(Math.max(...spans.map(([, end]) => end)) - Math.min(...spans.map(([start]) => start))) : "";
|
|
823
|
+
return [...formatAgentAccounting(sumAccounting(agents.map((agent) => agent.accounting ?? zeroAccounting()))), ...(elapsed ? [`Elapsed: ${elapsed}`] : [])];
|
|
824
|
+
};
|
|
806
825
|
const nodeStatus = (node: WorkflowPhaseTreeNode): string => phaseStyle(node.state)(node.state);
|
|
807
826
|
const nodeIcon = (node: WorkflowPhaseTreeNode): string => node.children.length ? expanded.has(node.id) ? "▾" : "▸" : node.kind === "agent" ? "•" : node.kind === "shell" ? "◇" : " ";
|
|
808
827
|
const treeLine = (node: WorkflowPhaseTreeNode): string => {
|
|
@@ -822,12 +841,12 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
822
841
|
const selected = node.phase;
|
|
823
842
|
const counts = selected?.counts ?? phaseAgentCounts(agents);
|
|
824
843
|
const hint = node.children.length ? "enter expand/collapse" : "enter phase details";
|
|
825
|
-
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)])];
|
|
844
|
+
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)])];
|
|
826
845
|
}
|
|
827
846
|
if (node.kind === "operation") {
|
|
828
847
|
const states = phaseAgentCounts(agents);
|
|
829
848
|
const hint = node.children.length ? "enter expand/collapse" : "enter phase details";
|
|
830
|
-
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)])];
|
|
849
|
+
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)])];
|
|
831
850
|
}
|
|
832
851
|
if (node.kind === "shell") {
|
|
833
852
|
return [styles.bold("Selected shell"), formatShellActivity(node.shellActivity?.active, node.shellActivity?.startedAt, "⠦", styles, now) ?? "shell [running]"];
|
|
@@ -849,13 +868,16 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
849
868
|
const scopedShells = (run.activeShellsByPhase?.length ?? 0) > 0;
|
|
850
869
|
const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", styles, now);
|
|
851
870
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
871
|
+
const toolActivity = formatToolActivity(run, "⠦", styles, now);
|
|
872
|
+
if (toolActivity) lines.push(` ${toolActivity}`);
|
|
852
873
|
const actionRows = (): string[] => {
|
|
853
874
|
const actions = selection.actions;
|
|
854
875
|
if (!actions) return [];
|
|
855
876
|
return ["", styles.bold(actions.title), ...actions.options.map((option, index) => `${index === actions.index ? "→ " : " "}${index === actions.index ? styles.accent(option) : option}`)];
|
|
856
877
|
};
|
|
857
878
|
const treeRows = [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.map(treeLine) : [styles.muted("(empty)")])];
|
|
858
|
-
|
|
879
|
+
const detailOffset = Math.max(0, (selection.scrollTop ?? 0) - lines.flatMap(wrap).length);
|
|
880
|
+
lines.push(...formatNavigatorColumns(treeRows, [...details(selectedNode), ...actionRows()], safeWidth, { ...selection, detailOffset }));
|
|
859
881
|
if (model.unassignedAgents?.length && !tree.nodes.some((node) => node.phaseId === "unassigned")) lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
|
|
860
882
|
return lines.flatMap((line) => wrap(line));
|
|
861
883
|
}
|
|
@@ -863,12 +885,15 @@ function wrapNavigatorLine(text: string, width: number): string[] {
|
|
|
863
885
|
return truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, Math.max(1, width), 0).visualLines.map((line) => line.trimEnd());
|
|
864
886
|
}
|
|
865
887
|
/** The navigator layout: a sidebar beside details at 80 columns or wider, otherwise one of them (or both stacked). */
|
|
866
|
-
export function formatNavigatorColumns(sidebar: readonly string[], detail: readonly string[], width: number, layout: Readonly<{ treeOnly?: boolean | undefined; detailsOnly?: boolean | undefined }> = {}): string[] {
|
|
888
|
+
export function formatNavigatorColumns(sidebar: readonly string[], detail: readonly string[], width: number, layout: Readonly<{ treeOnly?: boolean | undefined; detailsOnly?: boolean | undefined; detailOffset?: number | undefined }> = {}): string[] {
|
|
867
889
|
const safeWidth = Math.max(1, width);
|
|
868
890
|
if (safeWidth >= 80) {
|
|
869
891
|
const sidebarWidth = Math.min(42, Math.max(24, Math.floor((safeWidth - 3) * 0.38)));
|
|
870
892
|
const left = sidebar.flatMap((line) => wrapNavigatorLine(line, sidebarWidth));
|
|
871
|
-
const
|
|
893
|
+
const wrapped = detail.flatMap((line) => wrapNavigatorLine(line, safeWidth - sidebarWidth - 3));
|
|
894
|
+
// Keep the details in view when the tree is scrolled, without making the block taller than the tree.
|
|
895
|
+
const shift = Math.min(Math.max(0, layout.detailOffset ?? 0), Math.max(0, left.length - wrapped.length));
|
|
896
|
+
const right = [...Array<string>(shift).fill(""), ...wrapped];
|
|
872
897
|
return Array.from({ length: Math.max(left.length, right.length) }, (_, index) => {
|
|
873
898
|
const cell = left[index] ?? "";
|
|
874
899
|
return `${cell}${" ".repeat(Math.max(0, sidebarWidth - visibleWidth(cell)))} | ${right[index] ?? ""}`;
|