@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.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { copyFile, mkdir } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { Type, type Api, type Model, type Static, type TSchema } from "@earendil-works/pi-ai";
|
|
7
|
-
import { copyToClipboard, getAgentDir, ModelSelectorComponent, type ExtensionAPI, type ExtensionContext, type ModelRuntime, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { FairAgentScheduler, getAgentAttempts, WorkflowAgentExecutor, localAgentTransport, type AgentActivity, type AgentAttempt, type
|
|
7
|
+
import { copyToClipboard, getAgentDir, ModelSelectorComponent, type ExtensionAPI, type ExtensionContext, type ExtensionToolContext, type ModelRuntime, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { FairAgentScheduler, getAgentAttempts, WorkflowAgentExecutor, localAgentTransport, type AgentActivity, type AgentAttempt, type AgentExecutionRoot, type AgentProgress, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
|
|
9
9
|
import { RunLifecycle, WorkflowEventPublisher, hostSessionContext, nextNamedOccurrence, withWorkflowFunctions, withoutActiveShells, workflowRunContext, type WorkflowEventSink, type WorkflowRunRecord, type WorkflowToolUpdate } from "./host-runtime.js";
|
|
10
10
|
import { createWorkflowRecovery, persistedFailure, type ModelRegistryCapability } from "./host-recovery.js";
|
|
11
11
|
import { registerWorkflowNavigator, uiHostCapabilities } from "./host-navigator.js";
|
|
@@ -13,53 +13,22 @@ import { acquireSessionLease, isPersistedRun, listPersistedSessionIds, listRunId
|
|
|
13
13
|
import { retainTerminalRuns } from "./retention.js";
|
|
14
14
|
import type { PersistedRun, WorktreeReference } from "./persistence.js";
|
|
15
15
|
import { validateBudget, WorkflowBudgetRuntime } from "./budget.js";
|
|
16
|
-
import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue,
|
|
17
|
-
import {
|
|
18
|
-
import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
|
|
16
|
+
import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue, modelAliasErrorName, object, parseModelReference, positiveInteger, reachableTools, resolveModelReference, VIRTUAL_MODEL_PROVIDER, sanitizeDisplayText, validateModelAliases } from "./utils.js";
|
|
17
|
+
import { loadCodemodeToolsSetting, preflight, resolveAgentResourcePolicy, staticAgentPreparationOptions, resolveWorkflowSettings, validateCheckpoint, validateModelAliasAvailability, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowSettingsPath, workflowToolExposure } from "./validation.js";
|
|
19
18
|
import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistryIfIdle, retainWorkflowRegistry, type WorkflowRegistryApi } from "./registry.js";
|
|
20
|
-
import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
|
|
19
|
+
import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath, toolIdentityPath } from "./execution.js";
|
|
20
|
+
import { prepareScriptToolLoadout, scriptTool, scriptToolContext, scriptToolValue, validateScriptToolReferences } from "./script-tools.js";
|
|
21
21
|
import backgroundWidget, { type BackgroundWidgetAPI } from "./background-widget.js";
|
|
22
22
|
import { showChangelogNotice } from "./changelog.js";
|
|
23
23
|
import { createTrajectoryRunLoader, createTrajectoryRunMetadataLoader, createTrajectorySubagentLoader, createTrajectorySubagentMetadataLoader, createTrajectoryTranscriptLoader, type TrajectoryActionRequest, type TrajectoryActionResult, type TrajectorySubagent } from "./trajectory.js";
|
|
24
24
|
import { getTrajectoryHost, type TrajectoryPublisherProvider } from "./trajectory-host-handle.js";
|
|
25
25
|
import { getSubagentManager } from "./subagent-manager-handle.js";
|
|
26
|
-
import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_BLOCKED_EVENT, WorkflowError, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState,
|
|
26
|
+
import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, THINKING_LEVELS, WORKFLOW_BLOCKED_EVENT, WorkflowError, type WorkflowFunction, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState, type AgentAccounting, type AgentIdentity, type AgentRecord, type AgentResourcePolicy, type AgentTransport, type JsonValue, type LaunchSnapshot, type LiveSessionHandoff, type HardTerminalRunState, type ModelSpec, type PreparedAgentSession, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type ToolIdentity, type WorkflowAgentSession, type WorkflowErrorCode, type WorkflowMetadata, type WorkflowModelAliasResolverContext, type WorkflowSettings, type WorkflowScriptCall, type WorkflowSettingsResolution, type WorkflowWorktreeReference } from "./types.js";
|
|
27
27
|
import type { SubagentManagerContext, SubagentRunRequest, SubagentStatus } from "../subagents/src/contracts.js";
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
catalogResultValue,
|
|
31
|
-
formatWorkflowCatalog,
|
|
32
|
-
styledTextBlock,
|
|
33
|
-
textBlock,
|
|
34
|
-
workflowCatalogBlock,
|
|
35
|
-
workflowControlCall,
|
|
36
|
-
workflowControlResult,
|
|
37
|
-
workflowProgressBlock,
|
|
38
|
-
type WorkflowProgressRenderState,
|
|
39
|
-
} from "./host-view.js";
|
|
40
|
-
import {
|
|
41
|
-
DELIVERY_LIMIT_BYTES,
|
|
42
|
-
ForegroundDeliveryController,
|
|
43
|
-
markWorkflowFailureDiagnostics,
|
|
44
|
-
WORKFLOW_LOG_ENTRY,
|
|
45
|
-
completionDescriptor,
|
|
46
|
-
completionDeliveryFromStore,
|
|
47
|
-
createWorkflowFailureDiagnostics,
|
|
48
|
-
failureDiagnosticsFrom,
|
|
49
|
-
formatWorkflowFailure,
|
|
50
|
-
formatWorkflowFailureDelivery,
|
|
51
|
-
formatWorkflowFailureDeliveryFallback,
|
|
52
|
-
formatWorkflowFailureDiagnostics,
|
|
53
|
-
isWorkflowFailureDiagnostics,
|
|
54
|
-
serializeWorkflowFailureDiagnostics,
|
|
55
|
-
utf8Prefix,
|
|
56
|
-
type CompletionDeliveryContext,
|
|
57
|
-
type ForegroundDelivery,
|
|
58
|
-
type ForegroundDetachResult,
|
|
59
|
-
type WorkflowLogEntry,
|
|
60
|
-
} from "./host-delivery.js";
|
|
28
|
+
import { SETTLED_AGENT_STATES, catalogResultValue, formatWorkflowCatalog, styledTextBlock, textBlock, workflowCatalogBlock, workflowControlCall, workflowControlResult, workflowProgressBlock, type WorkflowProgressRenderState } from "./host-view.js";
|
|
29
|
+
import { DELIVERY_LIMIT_BYTES, ForegroundDeliveryController, markWorkflowFailureDiagnostics, WORKFLOW_LOG_ENTRY, completionDescriptor, completionDeliveryFromStore, createWorkflowFailureDiagnostics, failureDiagnosticsFrom, formatWorkflowFailure, formatWorkflowFailureDelivery, formatWorkflowFailureDeliveryFallback, formatWorkflowFailureDiagnostics, isWorkflowFailureDiagnostics, serializeWorkflowFailureDiagnostics, utf8Prefix, type CompletionDeliveryContext, type ForegroundDelivery, type ForegroundDetachResult, type WorkflowLogEntry } from "./host-delivery.js";
|
|
61
30
|
|
|
62
|
-
export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
|
|
31
|
+
export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Partial<Pick<ExtensionAPI, "getAllTools">> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
|
|
63
32
|
|
|
64
33
|
export {
|
|
65
34
|
agentBreadcrumb,
|
|
@@ -117,6 +86,13 @@ function mainAgentError(error: unknown): WorkflowError {
|
|
|
117
86
|
Object.assign(presented, typed);
|
|
118
87
|
return presented;
|
|
119
88
|
}
|
|
89
|
+
/** The structured counterpart of {@link completionControlContent}: the result without the persisted run, with the full value. */
|
|
90
|
+
function completionControlValue(result: Readonly<Record<string, unknown>>, controlRunId?: string): Record<string, unknown> {
|
|
91
|
+
const record: Record<string, unknown> = { ...(controlRunId === undefined ? {} : { runId: controlRunId }), ...result };
|
|
92
|
+
delete record.run;
|
|
93
|
+
delete record.completion;
|
|
94
|
+
return record;
|
|
95
|
+
}
|
|
120
96
|
function completionControlContent(result: unknown, controlRunId?: string): string {
|
|
121
97
|
const record = object(result) ? { ...result } : undefined;
|
|
122
98
|
if (record && controlRunId !== undefined && record.runId === undefined) record.runId = controlRunId;
|
|
@@ -133,33 +109,13 @@ function completionControlContent(result: unknown, controlRunId?: string): strin
|
|
|
133
109
|
delete record.run;
|
|
134
110
|
return JSON.stringify({ ...record, value });
|
|
135
111
|
}
|
|
136
|
-
type RoleCapture = { readonly snapshot: Readonly<LaunchSnapshot>; capture(role: string, model: ModelSpec): Promise<void> };
|
|
137
|
-
/** Persists a role definition (and its model) into the launch snapshot the first time an agent uses it, so later replays and retries see the definition the run actually ran with. */
|
|
138
|
-
function roleCapture(store: RunStore, initial: Readonly<LaunchSnapshot>, definitions: Readonly<Record<string, AgentDefinition>>, projectDefinitions: Readonly<Record<string, AgentDefinition>>): RoleCapture {
|
|
139
|
-
let persisted = initial;
|
|
140
|
-
return {
|
|
141
|
-
get snapshot() { return persisted; },
|
|
142
|
-
async capture(role, model) {
|
|
143
|
-
const definition = definitions[role];
|
|
144
|
-
if (!definition) return;
|
|
145
|
-
const modelName = `${model.provider}/${model.model}`;
|
|
146
|
-
const hasProjectRole = projectDefinitions[role] !== undefined;
|
|
147
|
-
if (persisted.roles?.[role] !== undefined && (!hasProjectRole || persisted.projectRoles?.includes(role)) && persisted.models.includes(modelName)) return;
|
|
148
|
-
const roles = { ...(persisted.roles ?? {}), [role]: definition };
|
|
149
|
-
const projectRoles = hasProjectRole ? [...new Set([...(persisted.projectRoles ?? []), role])] : persisted.projectRoles ?? [];
|
|
150
|
-
const models = [...new Set([...persisted.models, modelName])];
|
|
151
|
-
persisted = createLaunchSnapshot({ ...persisted, models, roles, projectRoles });
|
|
152
|
-
await store.saveSnapshot(persisted);
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
112
|
export function formatWorkflowPreview(args: { script?: unknown; scriptPath?: unknown; name?: unknown; description?: unknown }): string {
|
|
157
113
|
const name = typeof args.name === "string" && args.name.trim() ? args.name.trim() : "workflow";
|
|
158
114
|
if (typeof args.script !== "string" || !args.script.trim()) return `workflow ${name}`;
|
|
159
115
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
160
116
|
}
|
|
161
117
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
162
|
-
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default"
|
|
118
|
+
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default. Scripts call this session's tools as `await tools.<name>(args)`, outside withWorktree: a tool resolves to its text unless its description gives a `tools.<name>(args)` result shape, and failures reject"
|
|
163
119
|
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Provide exactly one of script or scriptPath and a non-empty name. Registered catalog functions are available as globals inside the script; call them there, for example return await someFunction(args). Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. Manage runs from the interactive /workflow picker; use workflow_status, workflow_resume, workflow_retry, workflow_stop, and workflow_respond for explicit tool controls. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately and delivers completion or failure as a follow-up. After failure follow-ups, especially CANCELLED or interrupted runs, call workflow_status({ runId }) before recovery or replacement work, then pass its state as expectedState to workflow_retry/workflow_resume so recovery cannot act on a state that changed. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, expectedState?, foreground? }) replays a failed run into a child; workflow_resume({ runId, expectedState?, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes."
|
|
164
120
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
165
121
|
name: Type.String({ description: "Required non-empty workflow name" }),
|
|
@@ -174,6 +130,21 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
|
174
130
|
}, { additionalProperties: false });
|
|
175
131
|
export const WORKFLOW_STATUS_PARAMETERS = Type.Object({ runId: Type.String({ description: "Workflow run ID visible in the current project" }) }, { additionalProperties: false });
|
|
176
132
|
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }), expectedState: Type.Optional(Type.String({ description: "Persisted source state observed before recovery" })), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) });
|
|
133
|
+
// Structured results codemode scripts receive instead of the JSON text; nested records stay loose so they cannot drift from persistence.
|
|
134
|
+
const WORKFLOW_ERROR_OUTPUT = Type.Object({ code: Type.String(), message: Type.String() });
|
|
135
|
+
const WORKFLOW_LAUNCH_OUTPUT = Type.Object({ runId: Type.String(), state: Type.String(), detached: Type.Optional(Type.Boolean()), value: Type.Optional(Type.Unknown()) });
|
|
136
|
+
const WORKFLOW_STATUS_OUTPUT = Type.Object({ runId: Type.String(), workflowName: Type.String(), state: Type.String(), error: Type.Optional(WORKFLOW_ERROR_OUTPUT), failedAt: Type.Optional(Type.String()), budget: Type.Optional(Type.Unknown()), usage: Type.Optional(Type.Unknown()), phase: Type.Optional(Type.String()), delivery: Type.Optional(Type.Object({ mode: Type.String(), state: Type.String() })), agents: Type.Array(Type.Object({ id: Type.String(), label: Type.Optional(Type.String()), path: Type.String(), state: Type.String(), lastEventAt: Type.Optional(Type.Number()), accounting: Type.Optional(Type.Unknown()) })) });
|
|
137
|
+
const WORKFLOW_STOP_OUTPUT = Type.Object({ runId: Type.String(), state: Type.String(), stopped: Type.Boolean(), reason: Type.Optional(Type.String()) });
|
|
138
|
+
const WORKFLOW_RECOVERY_OUTPUT = Type.Object({ state: Type.String(), runId: Type.Optional(Type.String()), parentRunId: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), value: Type.Optional(Type.Unknown()) });
|
|
139
|
+
const WORKFLOW_RESPOND_OUTPUT = Type.Object({ state: Type.String(), approved: Type.Boolean(), reason: Type.String(), accepted: Type.Optional(Type.Boolean()), runId: Type.Optional(Type.String()), value: Type.Optional(Type.Unknown()) });
|
|
140
|
+
const WORKFLOW_CATALOG_ALIAS_OUTPUT = Type.Object({ name: Type.String(), kind: Type.String(), provenance: Type.String(), version: Type.Optional(Type.String()), headline: Type.Optional(Type.String()) });
|
|
141
|
+
// The index without a name; with one, the function, the model alias, or { error }.
|
|
142
|
+
const WORKFLOW_CATALOG_OUTPUT = Type.Union([
|
|
143
|
+
Type.Object({ functions: Type.Array(Type.Object({ name: Type.String(), description: Type.String(), input: Type.Unknown() })), modelAliasEntries: Type.Optional(Type.Array(WORKFLOW_CATALOG_ALIAS_OUTPUT)), settings: Type.Optional(Type.Unknown()) }),
|
|
144
|
+
Type.Object({ name: Type.String(), version: Type.String(), headline: Type.String(), description: Type.String(), input: Type.Unknown(), output: Type.Unknown() }),
|
|
145
|
+
WORKFLOW_CATALOG_ALIAS_OUTPUT,
|
|
146
|
+
Type.Object({ error: Type.Object({ code: Type.String(), name: Type.String(), message: Type.String() }) }),
|
|
147
|
+
]);
|
|
177
148
|
|
|
178
149
|
function workflowToolUpdate(run: PersistedRun): WorkflowToolUpdate {
|
|
179
150
|
//NOTE: renderers read details.run; the partial text stays O(1) so high-frequency updates never format the whole tree.
|
|
@@ -257,9 +228,46 @@ function modelInventory(root: ModelSpec | undefined, registry: ModelRegistryCapa
|
|
|
257
228
|
if (rootName) { knownModels.add(rootName); availableModels.add(rootName); }
|
|
258
229
|
return { knownModels, availableModels };
|
|
259
230
|
}
|
|
260
|
-
function resumeHostContext(ctx: unknown): { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext } {
|
|
231
|
+
function resumeHostContext(ctx: unknown): { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext; toolContext: ExtensionToolContext | undefined } {
|
|
261
232
|
const model = object(ctx) && object(ctx.model) && typeof ctx.model.provider === "string" && typeof ctx.model.id === "string" ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
|
|
262
|
-
return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry, deliveryContext: completionContext(ctx) };
|
|
233
|
+
return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry, deliveryContext: completionContext(ctx), toolContext: scriptToolContext(ctx) };
|
|
234
|
+
}
|
|
235
|
+
// State is rewritten on every update, so large shell inputs and errors are kept as prefixes.
|
|
236
|
+
const SCRIPT_CALL_TEXT_LIMIT_BYTES = 16 * 1024;
|
|
237
|
+
function scriptCallInput(input: JsonValue): Pick<WorkflowScriptCall, "input" | "inputBytes"> {
|
|
238
|
+
const text = JSON.stringify(input);
|
|
239
|
+
const bytes = Buffer.byteLength(text);
|
|
240
|
+
return bytes <= SCRIPT_CALL_TEXT_LIMIT_BYTES ? { input } : { input: utf8Prefix(text, SCRIPT_CALL_TEXT_LIMIT_BYTES), inputBytes: bytes };
|
|
241
|
+
}
|
|
242
|
+
/** Records a script call as started, replacing the record of an attempt that an interruption left unfinished. */
|
|
243
|
+
function startScriptCall(run: PersistedRun, call: WorkflowScriptCall): PersistedRun {
|
|
244
|
+
const recorded = { ...call, ...(run.phase === undefined ? {} : { phase: run.phase }) };
|
|
245
|
+
const calls = run.scriptCalls ?? [];
|
|
246
|
+
return { ...run, scriptCalls: calls.some(({ path }) => path === call.path) ? calls.map((current) => current.path === call.path ? recorded : current) : [...calls, recorded] };
|
|
247
|
+
}
|
|
248
|
+
function finishScriptCall(run: PersistedRun, path: string, error?: WorkflowError): PersistedRun {
|
|
249
|
+
const finishedAt = Date.now();
|
|
250
|
+
return { ...run, scriptCalls: (run.scriptCalls ?? []).map((call) => call.path === path ? { ...call, finishedAt, ...(error ? { error: { code: error.code, ...(call.kind === "shell" ? { message: utf8Prefix(error.message, SCRIPT_CALL_TEXT_LIMIT_BYTES) } : {}) } } : {}) } : call) };
|
|
251
|
+
}
|
|
252
|
+
type AliasVirtualModel = { provider: string; id: string; name: string; thinkingLevels: readonly string[]; route: (request: { thinkingLevel: string }, ctx: ExtensionContext) => { model: Model<Api>; thinkingLevel: string } };
|
|
253
|
+
/** Lists workflow model aliases in `/model` as virtual models; needs a Pi host with `registerVirtualModel`. */
|
|
254
|
+
function registerAliasVirtualModels(pi: unknown, names: readonly string[], settingsPath: string): void {
|
|
255
|
+
const register = object(pi) ? asFn(pi.registerVirtualModel) : undefined;
|
|
256
|
+
if (!register) return;
|
|
257
|
+
for (const name of names) {
|
|
258
|
+
const definition: AliasVirtualModel = {
|
|
259
|
+
provider: VIRTUAL_MODEL_PROVIDER, id: name, name: `${name} (workflow alias)`, thinkingLevels: THINKING_LEVELS,
|
|
260
|
+
route: (request, ctx) => {
|
|
261
|
+
// Settings are read per request so alias edits apply without a reload.
|
|
262
|
+
const aliases = resolveWorkflowSettings(ctx.cwd, projectTrusted(ctx), settingsPath).effective.modelAliases ?? {};
|
|
263
|
+
const target = resolveModelReference(name, aliases, new Set(ctx.modelRegistry.getAll().map((model) => `${model.provider}/${model.id}`)), settingsPath);
|
|
264
|
+
const model = ctx.modelRegistry.find(target.provider, target.model);
|
|
265
|
+
if (!model) throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${name} resolved to ${target.provider}/${target.model}`);
|
|
266
|
+
return { model, thinkingLevel: request.thinkingLevel };
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
Reflect.apply(register, pi, [definition]);
|
|
270
|
+
}
|
|
263
271
|
}
|
|
264
272
|
async function resolveLaunchAliases(registry: WorkflowRegistryApi, staticAliases: Readonly<Record<string, string>>, context: Readonly<WorkflowModelAliasResolverContext>, availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath: string): Promise<{ aliases: Readonly<Record<string, string>>; dynamicNames: readonly string[] }> {
|
|
265
273
|
const dynamic = typeof registry.resolveModelAliases === "function" ? await registry.resolveModelAliases(context, new Set(Object.keys(staticAliases))) : {};
|
|
@@ -276,6 +284,20 @@ async function resolveLaunchAliases(registry: WorkflowRegistryApi, staticAliases
|
|
|
276
284
|
}
|
|
277
285
|
}
|
|
278
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Prepares each completely static agent configuration in inspection mode, so preparation plugins resolve their own aliases
|
|
289
|
+
* and options without persisting a configuration. Any preparation failure rejects launch before effects; cancellation stops
|
|
290
|
+
* the remaining inspections.
|
|
291
|
+
*/
|
|
292
|
+
async function inspectStaticAgentConfigurations(executor: WorkflowAgentExecutor, script: string, workflowName: string, signal: AbortSignal, functions: Readonly<Record<string, WorkflowFunction>>): Promise<void> {
|
|
293
|
+
// NOTE: inspection uses the launch cwd; agents inside withWorktree(...) prepare against their checkout at runtime.
|
|
294
|
+
for (const agentOptions of staticAgentPreparationOptions(script, functions)) {
|
|
295
|
+
if (signal.aborted) throw new WorkflowError("CANCELLED", "Workflow launch cancelled");
|
|
296
|
+
await executor.prepare({ label: "agent", workflowName, agentOptions }, undefined, signal, "inspection");
|
|
297
|
+
}
|
|
298
|
+
if (signal.aborted) throw new WorkflowError("CANCELLED", "Workflow launch cancelled");
|
|
299
|
+
}
|
|
300
|
+
|
|
279
301
|
export default function workflowExtension(pi: WorkflowExtensionAPI, home?: string, clipboard = copyToClipboard, transport: AgentTransport = localAgentTransport, agentDir?: string, additionalSkillPaths: readonly string[] = []) {
|
|
280
302
|
beginWorkflowExtensionLoading();
|
|
281
303
|
const registry = loadingRegistry();
|
|
@@ -290,8 +312,14 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
290
312
|
return textBlock(data ? `Warning: ${data.message}` : "");
|
|
291
313
|
});
|
|
292
314
|
let backgroundWidgetEnabled = true;
|
|
293
|
-
try {
|
|
315
|
+
try {
|
|
316
|
+
const globalSettings = resolveWorkflowSettings(process.cwd(), false, workflowSettingsPath(extensionAgentDir)).effective;
|
|
317
|
+
backgroundWidgetEnabled = globalSettings.backgroundWidget ?? true;
|
|
318
|
+
// Registered at load, before Pi resolves --model and restores the session model. Project aliases need project trust, which is unknown here.
|
|
319
|
+
registerAliasVirtualModels(pi, Object.keys(globalSettings.modelAliases ?? {}), workflowSettingsPath(extensionAgentDir));
|
|
320
|
+
} catch { /* Keep the optional UI enabled; the launch path reports settings errors. */ }
|
|
294
321
|
const backgroundWidgetController = backgroundWidget(pi, backgroundWidgetEnabled);
|
|
322
|
+
const codemodeTools = loadCodemodeToolsSetting(extensionAgentDir);
|
|
295
323
|
const logBridge = (store: RunStore, lifecycle: RunLifecycle, workflowName: string) => async (message: string) => {
|
|
296
324
|
const timestamp = Date.now();
|
|
297
325
|
const bounded = utf8Prefix(message, DELIVERY_LIMIT_BYTES);
|
|
@@ -314,6 +342,9 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
314
342
|
return skillPath ? { skillPaths: [skillPath] } : undefined;
|
|
315
343
|
});
|
|
316
344
|
const runs = new Map<string, WorkflowRunRecord>();
|
|
345
|
+
// Extension load failures a headless launcher reported for this host. They hold for the whole process, so every executor,
|
|
346
|
+
// including those that paused resume, cold resume and retry create, keeps failing ownerless options closed.
|
|
347
|
+
let extensionLoad: AgentExecutionRoot["extensionLoad"];
|
|
317
348
|
const deliveryController = new ForegroundDeliveryController({ runs, ...(typeof pi.sendMessage === "function" ? { deliver: (content: string) => { deliver(pi, content); } } : {}) });
|
|
318
349
|
let releaseWorkflowRegistry: (() => void) | undefined;
|
|
319
350
|
const providerRecoveryLane = new SerialLane();
|
|
@@ -534,7 +565,11 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
534
565
|
if (replayed) return readShellResult(replayed.value);
|
|
535
566
|
const shellStartedAt = Date.now();
|
|
536
567
|
let shellPhaseIndex = -1;
|
|
537
|
-
|
|
568
|
+
// Env values stay out of the record: they may carry secrets, and a shared trajectory publishes run state.
|
|
569
|
+
const call: WorkflowScriptCall = { kind: "shell", name: "shell", path, ...scriptCallInput({ command, ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.env === undefined ? {} : { env: Object.keys(options.env) }), ...(identity.worktreeOwner ? { worktree: identity.worktreeOwner } : {}) }), startedAt: shellStartedAt };
|
|
570
|
+
let failure: WorkflowError | undefined;
|
|
571
|
+
const started = await persistRunState(store, metadata, (run) => {
|
|
572
|
+
const current = startScriptCall(run, call);
|
|
538
573
|
const history = current.phaseHistory ?? [];
|
|
539
574
|
if (current.phase !== undefined) shellPhaseIndex = current.phaseHistoryIndex ?? (history.length ? history.length - 1 : 0);
|
|
540
575
|
const phaseActivities = [...(current.activeShellsByPhase ?? [])];
|
|
@@ -552,8 +587,12 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
552
587
|
if (!jsonValue(result)) fail("SHELL_FAILED", "Shell result is not JSON-compatible");
|
|
553
588
|
await store.complete(path, result);
|
|
554
589
|
return result;
|
|
590
|
+
} catch (error) {
|
|
591
|
+
failure = asWorkflowError(error);
|
|
592
|
+
throw error;
|
|
555
593
|
} finally {
|
|
556
|
-
const stopped = await persistRunState(store, metadata, (
|
|
594
|
+
const stopped = await persistRunState(store, metadata, (run) => {
|
|
595
|
+
const current = finishScriptCall(run, path, failure);
|
|
557
596
|
const phaseActivities = [...(current.activeShellsByPhase ?? [])];
|
|
558
597
|
const phaseActivityIndex = phaseActivities.findIndex(({ phaseIndex }) => phaseIndex === shellPhaseIndex);
|
|
559
598
|
const phaseActivity = phaseActivities[phaseActivityIndex];
|
|
@@ -572,6 +611,44 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
572
611
|
}
|
|
573
612
|
} finally { await lifecycle.leave(); }
|
|
574
613
|
};
|
|
614
|
+
/** A script `tools.<id>(args)` call: replayed from the journal, otherwise run through the session's tool pipeline and journaled. */
|
|
615
|
+
const toolForRun = async (store: RunStore, lifecycle: RunLifecycle, context: ExtensionToolContext | undefined, identifier: string, args: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: ToolIdentity): Promise<JsonValue> => {
|
|
616
|
+
await lifecycle.enter();
|
|
617
|
+
try {
|
|
618
|
+
const path = toolIdentityPath(identifier, args, identity);
|
|
619
|
+
const replayed = await store.replay(path);
|
|
620
|
+
if (replayed) return replayed.value;
|
|
621
|
+
if (!context) fail("UNKNOWN_TOOL", `tools.${identifier} needs a run launched or recovered by a Pi tool call; runs resumed from /workflow or at session start, and headless launches, only replay journaled tool calls`);
|
|
622
|
+
// Rendering is best-effort: it must never lose or mask a completed call.
|
|
623
|
+
const render = async () => { try { const update = runs.get(store.runId)?.update; if (update) update(workflowToolUpdate(withLiveActivities((await store.load()).run))); } catch { /* best-effort progress */ } };
|
|
624
|
+
// Inspection writes must not prevent execution, mask errors, or reject a journaled success.
|
|
625
|
+
const record = async (update: (run: PersistedRun) => PersistedRun) => { try { await store.updateState(update); } catch { /* best-effort call metadata */ } };
|
|
626
|
+
let value: JsonValue;
|
|
627
|
+
try {
|
|
628
|
+
const tool = scriptTool(context, identifier);
|
|
629
|
+
if (!tool) fail("UNKNOWN_TOOL", `tools.${identifier} is not a tool this session can call`);
|
|
630
|
+
const cancelled = (): boolean => signal.aborted;
|
|
631
|
+
if (cancelled()) fail("CANCELLED", "Workflow cancelled");
|
|
632
|
+
await record((run) => startScriptCall(run, { kind: "tool", name: tool.name, path, startedAt: Date.now() }));
|
|
633
|
+
const finished = liveAgents.toolStarted(store.runId, tool.name);
|
|
634
|
+
try {
|
|
635
|
+
await render();
|
|
636
|
+
const outcome = await context.executeTool(tool.name, args, { signal });
|
|
637
|
+
if (cancelled()) fail("CANCELLED", "Workflow cancelled");
|
|
638
|
+
value = scriptToolValue(tool, outcome);
|
|
639
|
+
} finally { finished(); }
|
|
640
|
+
} catch (error) {
|
|
641
|
+
// A stale context throws plain errors from ctx.tools; report them as tool failures.
|
|
642
|
+
const failure = error instanceof WorkflowError ? error : new WorkflowError("TOOL_FAILED", `tools.${identifier}: ${errorText(error)}`);
|
|
643
|
+
await record((run) => finishScriptCall(run, path, failure));
|
|
644
|
+
throw failure;
|
|
645
|
+
}
|
|
646
|
+
await store.complete(path, value);
|
|
647
|
+
await record((run) => finishScriptCall(run, path));
|
|
648
|
+
await render();
|
|
649
|
+
return value;
|
|
650
|
+
} finally { await lifecycle.leave(); }
|
|
651
|
+
};
|
|
575
652
|
const lifecycleFor = (store: RunStore, state: RunState, metadata: WorkflowMetadata) => new RunLifecycle(state, async (next, previous, reason) => {
|
|
576
653
|
const run = runs.get(store.runId);
|
|
577
654
|
if (!run) throw new WorkflowError("INTERNAL_ERROR", `Unknown production run: ${store.runId}`);
|
|
@@ -646,7 +723,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
646
723
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, lastEventAt } : agent) }));
|
|
647
724
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
648
725
|
};
|
|
649
|
-
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, tuiIndex, tuiLabel: options.requestedLabel ?? options.label, agentNodeId: id, ...(options.extensionSettings === undefined ? {} : { inheritedExtensionSettings: options.extensionSettings }), onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.
|
|
726
|
+
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, tuiIndex, tuiLabel: options.requestedLabel ?? options.label, agentNodeId: id, ...(options.extensionSettings === undefined ? {} : { inheritedExtensionSettings: options.extensionSettings }), onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.contextFiles ? { contextFiles: options.contextFiles } : {}), tools: options.tools, ...(options.skills ? { skills: options.skills } : {}), ...(options.extensions ? { extensions: options.extensions } : {}), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.sessionPath ? { sessionPath: options.sessionPath } : {}), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}), ...(options.configuration ? { configuration: options.configuration } : {}), ...(options.capabilities ? { capabilities: options.capabilities } : {}), ...(options.projectTrusted === undefined ? {} : { projectTrusted: options.projectTrusted }), ...(options.inheritedExcludeTools ? { inheritedExcludeTools: options.inheritedExcludeTools } : {}), onConfiguration: (configuration) => scheduler.setConfiguration(id, configuration) }, signal, scheduler.toolsFor(id), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
|
|
650
727
|
cancelDeferredProgress();
|
|
651
728
|
const before = (await run.store.load()).run;
|
|
652
729
|
await persistAgentAttempts(run.store, id, result.attempts);
|
|
@@ -682,17 +759,16 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
682
759
|
const existing = new Map(current.agents.map((agent) => [agent.id, agent]));
|
|
683
760
|
const agents = ownership.map((node) => {
|
|
684
761
|
const previous = existing.get(node.id);
|
|
685
|
-
const requested = { label: node.options.label, workflowName: run.metadata.name, ...(node.options.skills ? { skills: node.options.skills } : {}), ...(node.options.extensions ? { extensions: node.options.extensions } : {}), ...(node.options.model ? { model: node.options.model } : {}), ...(node.options.
|
|
762
|
+
const requested = { label: node.options.label, workflowName: run.metadata.name, ...(node.options.skills ? { skills: node.options.skills } : {}), ...(node.options.extensions ? { extensions: node.options.extensions } : {}), ...(node.options.model ? { model: node.options.model } : {}), ...(node.options.contextFiles ? { contextFiles: node.options.contextFiles } : {}), ...(node.options.configuration ? { configuration: node.options.configuration } : {}) };
|
|
686
763
|
let effective: { model: ModelSpec; requestedModel?: string; tools: readonly string[] };
|
|
687
764
|
try { effective = { ...run.executor.resolve({ ...requested, effectiveTools: node.options.tools }), tools: node.options.tools }; }
|
|
688
765
|
catch { effective = previous ? { model: previous.model, ...(previous.requestedModel ? { requestedModel: previous.requestedModel } : {}), tools: previous.tools } : { model: node.options.model ? modelSpec(node.options.model, run.model) : run.model, ...(node.options.model ? { requestedModel: node.options.model } : {}), tools: node.options.tools }; }
|
|
689
766
|
const resultPath = !node.parentId && node.options.agentIdentity ? agentIdentityPath(node.options.agentIdentity) : undefined;
|
|
690
|
-
const nodeRole = roleNameOf(node.options.role);
|
|
691
767
|
const now = Date.now();
|
|
692
768
|
const lastEventAt = node.state === "running" ? previous?.state === "running" && previous.lastEventAt !== undefined ? previous.lastEventAt : now : previous?.lastEventAt;
|
|
693
769
|
const startedAt = previous?.startedAt ?? (node.state === "running" ? now : undefined);
|
|
694
770
|
const durationMs = previous?.durationMs ?? (SETTLED_AGENT_STATES.has(node.state) && startedAt !== undefined ? Math.max(0, now - startedAt) : undefined);
|
|
695
|
-
return { ...(previous?.systemPrompt === undefined ? {} : { systemPrompt: previous.systemPrompt }), ...(node.prompt !== undefined ? { prompt: node.prompt } : previous?.prompt !== undefined ? { prompt: previous.prompt } : {}), id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.agentIdentity?.handle === undefined ? {} : { handle: node.options.agentIdentity.handle, ...(node.options.agentIdentity.turn === undefined ? {} : { turn: node.options.agentIdentity.turn }), ...(node.options.continuity ? { continuity: node.options.continuity } : {}) }), ...(
|
|
771
|
+
return { ...(previous?.systemPrompt === undefined ? {} : { systemPrompt: previous.systemPrompt }), ...(node.prompt !== undefined ? { prompt: node.prompt } : previous?.prompt !== undefined ? { prompt: previous.prompt } : {}), id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.agentIdentity?.handle === undefined ? {} : { handle: node.options.agentIdentity.handle, ...(node.options.agentIdentity.turn === undefined ? {} : { turn: node.options.agentIdentity.turn }), ...(node.options.continuity ? { continuity: node.options.continuity } : {}) }), ...(effective.requestedModel ? { requestedModel: effective.requestedModel } : {}), model: effective.model, tools: effective.tools, attempts: previous?.attempts ?? 0, ...(startedAt === undefined ? {} : { startedAt }), ...(durationMs === undefined ? {} : { durationMs }), ...(previous?.attemptDetails ? { attemptDetails: previous.attemptDetails } : {}), ...(previous?.accounting ? { accounting: previous.accounting } : {}), ...(previous?.toolCalls ? { toolCalls: previous.toolCalls } : {}), ...(previous?.activity ? { activity: previous.activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
|
|
696
772
|
});
|
|
697
773
|
return { ...current, agents };
|
|
698
774
|
});
|
|
@@ -822,17 +898,20 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
822
898
|
label: string,
|
|
823
899
|
description: string,
|
|
824
900
|
parameters: P,
|
|
825
|
-
|
|
901
|
+
outputSchema: TSchema,
|
|
902
|
+
run: (params: Static<P>, signal: AbortSignal, ctx: unknown) => Promise<{ text: string; details: unknown; structured: unknown }>,
|
|
826
903
|
) => {
|
|
827
904
|
pi.registerTool({
|
|
828
905
|
name,
|
|
829
906
|
label,
|
|
830
907
|
description,
|
|
831
908
|
parameters,
|
|
909
|
+
outputSchema,
|
|
910
|
+
...workflowToolExposure(name, codemodeTools),
|
|
832
911
|
async execute(_id, params, signal: AbortSignal, _onUpdate, ctx) {
|
|
833
912
|
try {
|
|
834
913
|
const result = await run(params, signal, ctx);
|
|
835
|
-
return { content: [{ type: "text" as const, text: result.text }], details: result.details };
|
|
914
|
+
return { content: [{ type: "text" as const, text: result.text }], details: result.details, structuredContent: result.structured as JsonValue };
|
|
836
915
|
} catch (error) {
|
|
837
916
|
throw mainAgentError(error);
|
|
838
917
|
}
|
|
@@ -847,15 +926,18 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
847
926
|
"Workflow Respond",
|
|
848
927
|
"Approve or reject one pending workflow checkpoint or budget decision",
|
|
849
928
|
Type.Object({ runId: Type.String(), name: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), approved: Type.Boolean() }, { additionalProperties: false }),
|
|
929
|
+
WORKFLOW_RESPOND_OUTPUT,
|
|
850
930
|
async (params, signal, ctx) => {
|
|
851
931
|
if (params.proposalId) {
|
|
852
932
|
const result = await recovery.answerBudgetDecision(params.runId, params.proposalId, params.approved, false, ctx, signal);
|
|
853
|
-
if (!result) { const denied = { state: "budget_exhausted" as const, approved: false, reason: "proposal_not_pending" }; return { text: JSON.stringify(denied), details: denied }; }
|
|
854
|
-
|
|
933
|
+
if (!result) { const denied = { state: "budget_exhausted" as const, approved: false, reason: "proposal_not_pending" }; return { text: JSON.stringify(denied), details: denied, structured: denied }; }
|
|
934
|
+
const reason = params.approved ? "approved" : "rejected";
|
|
935
|
+
return { text: completionControlContent(result, params.runId), details: { ...result, reason }, structured: { ...completionControlValue(result, params.runId), reason } };
|
|
855
936
|
}
|
|
856
937
|
if (!params.name) throw new WorkflowError("INVALID_METADATA", "workflow_respond requires name or proposalId");
|
|
857
938
|
const accepted = await answerCheckpoint(params.runId, params.name, params.approved);
|
|
858
|
-
|
|
939
|
+
const details = { accepted, state: accepted ? "checkpoint_answered" : "not_pending", approved: params.approved, reason: "checkpoint" };
|
|
940
|
+
return { text: accepted ? "Checkpoint response accepted." : "Checkpoint is not awaiting a response.", details, structured: details };
|
|
859
941
|
},
|
|
860
942
|
);
|
|
861
943
|
registerControlTool(
|
|
@@ -863,9 +945,10 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
863
945
|
"Workflow Stop",
|
|
864
946
|
"Stop an active workflow run by ID",
|
|
865
947
|
Type.Object({ runId: Type.String() }, { additionalProperties: false }),
|
|
948
|
+
WORKFLOW_STOP_OUTPUT,
|
|
866
949
|
async (params) => {
|
|
867
950
|
const result = await stopWorkflowRun(params.runId);
|
|
868
|
-
return { text: JSON.stringify(result), details: result };
|
|
951
|
+
return { text: JSON.stringify(result), details: result, structured: result };
|
|
869
952
|
},
|
|
870
953
|
);
|
|
871
954
|
registerControlTool(
|
|
@@ -873,9 +956,10 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
873
956
|
"Workflow Status",
|
|
874
957
|
"Read a compact summary of a workflow run in the current project",
|
|
875
958
|
WORKFLOW_STATUS_PARAMETERS,
|
|
959
|
+
WORKFLOW_STATUS_OUTPUT,
|
|
876
960
|
async (params, _signal, ctx) => {
|
|
877
961
|
const result = await workflowStatusRun(params.runId, ctx);
|
|
878
|
-
return { text: JSON.stringify(result), details: result };
|
|
962
|
+
return { text: JSON.stringify(result), details: result, structured: result };
|
|
879
963
|
},
|
|
880
964
|
);
|
|
881
965
|
let catalogRegistered = false;
|
|
@@ -888,13 +972,15 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
888
972
|
if (!catalog.functions.length && !hasAliases && !hasSettings) return;
|
|
889
973
|
pi.registerTool({
|
|
890
974
|
name: "workflow_catalog",
|
|
975
|
+
...workflowToolExposure("workflow_catalog", codemodeTools),
|
|
976
|
+
outputSchema: WORKFLOW_CATALOG_OUTPUT,
|
|
891
977
|
label: "Workflow Catalog",
|
|
892
978
|
description: "List reusable workflow functions and model aliases; pass `name` to load one entry in full",
|
|
893
979
|
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or model alias name for full detail" })) }, { additionalProperties: false }),
|
|
894
980
|
async execute(_id, params = {}) {
|
|
895
981
|
const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
|
|
896
982
|
const result = params.name === undefined ? registry.catalogIndex(context) : registry.catalogDetail(params.name, context);
|
|
897
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
983
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result, structuredContent: result as unknown as JsonValue };
|
|
898
984
|
},
|
|
899
985
|
renderCall(args, theme) {
|
|
900
986
|
const title = theme.fg("toolTitle", theme.bold("workflow_catalog"));
|
|
@@ -906,22 +992,9 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
906
992
|
});
|
|
907
993
|
catalogRegistered = true;
|
|
908
994
|
};
|
|
909
|
-
const createAgentExecutor = (root: Omit<AgentExecutionRoot, "agentDir" | "agentSetupHooks">) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks(), validateExtensionSettings: registry.validateExtensionSettings, onResourceWarning: (message) => { deliverWarning(pi, message); }, onAgentSettings: (agentId, settings) => { scheduler.setExtensionSettings(agentId, settings); } }, transport);
|
|
910
|
-
// Resumed runs keep the role definitions frozen in their snapshot and fall back to the current definitions for roles a
|
|
911
|
-
// registered function reaches for the first time after the restart; those are captured into the snapshot like at launch.
|
|
912
|
-
const resumeRoles = (store: RunStore, snapshot: Readonly<LaunchSnapshot>, cwd: string, trustedProject: boolean) => {
|
|
913
|
-
let current: Readonly<Record<string, AgentDefinition>> = {};
|
|
914
|
-
let project: Readonly<Record<string, AgentDefinition>> = {};
|
|
915
|
-
try {
|
|
916
|
-
current = loadAgentDefinitions(cwd, extensionAgentDir, trustedProject, registry.roleDirectoryRegistrations());
|
|
917
|
-
project = trustedProject ? loadProjectAgentDefinitions(cwd) : {};
|
|
918
|
-
} catch { /* Unreadable current roles leave the resume with the snapshot roles, as before. */ }
|
|
919
|
-
const definitions = { ...current, ...(snapshot.roles ?? {}) };
|
|
920
|
-
const captured = roleCapture(store, snapshot, definitions, project);
|
|
921
|
-
return { definitions, capture: (role: string, model: ModelSpec) => captured.capture(role, model) };
|
|
922
|
-
};
|
|
995
|
+
const createAgentExecutor = (root: Omit<AgentExecutionRoot, "agentDir" | "agentSetupHooks">) => new WorkflowAgentExecutor({ ...root, ...(extensionLoad ? { extensionLoad } : {}), agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentPreparationHooks: registry.agentPreparationHooks(), agentSetupHooks: registry.agentSetupHooks(), validateExtensionSettings: registry.validateExtensionSettings, onResourceWarning: root.onResourceWarning ?? ((message) => { deliverWarning(pi, message); }), onAgentSettings: (agentId, settings) => { scheduler.setExtensionSettings(agentId, settings); }, onAgentCapabilities: (agentId, capabilities, projectTrusted) => { scheduler.setAttemptCapabilities(agentId, capabilities, projectTrusted); } }, transport);
|
|
923
996
|
const activeSnapshotTools = (tools: readonly string[], active: ReadonlySet<string> | "session") => active === "session"
|
|
924
|
-
? new Set(tools.filter((tool) => pi
|
|
997
|
+
? new Set(tools.filter((tool) => reachableTools(pi).includes(tool) && tool !== "workflow_catalog"))
|
|
925
998
|
: new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
|
|
926
999
|
const resumeLaunchPrologue = async (input: {
|
|
927
1000
|
snapshot: Readonly<LaunchSnapshot>;
|
|
@@ -931,11 +1004,12 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
931
1004
|
modelRegistry?: ModelRegistryCapability | undefined;
|
|
932
1005
|
signal: AbortSignal;
|
|
933
1006
|
resolvedAliases?: Readonly<Record<string, string>>;
|
|
1007
|
+
resolvedDynamicAliasNames?: readonly string[];
|
|
934
1008
|
blockedAliases?: ReadonlySet<string>;
|
|
935
1009
|
blockedAliasTargets?: Readonly<Record<string, string>>;
|
|
936
1010
|
withPreflight: boolean;
|
|
937
1011
|
}) => {
|
|
938
|
-
const active = new Set(pi
|
|
1012
|
+
const active = new Set(reachableTools(pi).filter((tool) => !INTERNAL_WORKFLOW_TOOLS.includes(tool)));
|
|
939
1013
|
const missing = input.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
940
1014
|
if (missing) throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
941
1015
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
@@ -943,26 +1017,27 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
943
1017
|
const extensionSettings = input.snapshot.settings.extensionSettings;
|
|
944
1018
|
const extensionSettingsPath = input.snapshot.settingsSources?.extensionSettings ?? input.snapshot.settingsPath ?? settingsPath;
|
|
945
1019
|
registry.validateExtensionSettings(extensionSettings, { source: "effective", cwd: input.cwd, projectTrusted: input.trustedProject, settingsPath: extensionSettingsPath });
|
|
946
|
-
for (const [role, definition] of Object.entries(input.snapshot.roles ?? {})) registry.validateExtensionSettings(mergeWorkflowExtensionSettings(extensionSettings, definition.extensionSettings), { source: "role", cwd: input.cwd, projectTrusted: input.trustedProject, settingsPath: extensionSettingsPath, role });
|
|
947
1020
|
const currentPolicy = resolveAgentResourcePolicy(input.cwd, input.trustedProject, settingsPath);
|
|
948
1021
|
const staticAliases = resolution.effective.modelAliases ?? {};
|
|
949
1022
|
const previousAliases = input.snapshot.modelAliases ?? input.snapshot.settings.modelAliases ?? {};
|
|
950
1023
|
const inventory = modelInventory(input.rootModel, input.modelRegistry);
|
|
951
1024
|
const knownModels = input.modelRegistry ? inventory.knownModels : new Set([...input.snapshot.models, ...inventory.knownModels]);
|
|
952
1025
|
const availableModels = input.modelRegistry ? inventory.availableModels : new Set([...input.snapshot.models, ...inventory.availableModels]);
|
|
953
|
-
const
|
|
1026
|
+
const resolved = input.resolvedAliases ? { aliases: input.resolvedAliases, dynamicNames: input.resolvedDynamicAliasNames ?? [] } : await resolveLaunchAliases(registry, staticAliases, { cwd: input.cwd, projectTrusted: input.trustedProject, rootModel: input.rootModel, knownModels, availableModels, signal: input.signal }, availableModels, knownModels, settingsPath);
|
|
1027
|
+
const currentAliases = resolved.aliases;
|
|
1028
|
+
const dynamicAliasNames = resolved.dynamicNames;
|
|
954
1029
|
const blockedAliases = input.blockedAliases ?? new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
955
1030
|
const blockedAliasTargets = input.blockedAliasTargets ?? Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
956
1031
|
const script = input.withPreflight ? input.snapshot.script : undefined;
|
|
957
1032
|
if (script !== undefined) {
|
|
958
1033
|
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
959
|
-
preflight(script, { models: availableModels, tools: active,
|
|
1034
|
+
preflight(script, { models: availableModels, tools: active, modelAliases: resumeAliases, knownModels, settingsPath, skipModelAvailability: true }, input.snapshot.schemas, input.snapshot.metadata, true);
|
|
960
1035
|
}
|
|
961
1036
|
const refreshed = resumedSnapshotSettings(input.snapshot, resolution, currentAliases);
|
|
962
1037
|
const snapshot = createLaunchSnapshot({ ...input.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
|
|
963
|
-
return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script };
|
|
1038
|
+
return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, dynamicAliasNames, blockedAliases, blockedAliasTargets, snapshot, script };
|
|
964
1039
|
};
|
|
965
|
-
const workflowAgentHandler = (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string
|
|
1040
|
+
const workflowAgentHandler = (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string) => {
|
|
966
1041
|
// Replay lookup and spawn are serialised per run so concurrent agent(...) calls reach the scheduler in call order.
|
|
967
1042
|
const admission = new SerialLane();
|
|
968
1043
|
return async (prompt: string, options: Readonly<Record<string, JsonValue>>, agentSignal: AbortSignal, identity: AgentIdentity) => {
|
|
@@ -974,20 +1049,21 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
974
1049
|
if (replayed) return { replayed };
|
|
975
1050
|
const worktree = agentWorktree(identity);
|
|
976
1051
|
const agentCwd = worktree.worktreeOwner ? (await persistWorktree(store, metadata, worktree.worktreeOwner)).cwd : cwd;
|
|
977
|
-
const role = typeof options.role === "string" ? options.role : undefined;
|
|
978
1052
|
const model = typeof options.model === "string" ? options.model : undefined;
|
|
979
1053
|
const requestedLabel = typeof options.label === "string" ? options.label : undefined;
|
|
980
1054
|
const skills = Array.isArray(options.skills) ? options.skills as string[] : undefined;
|
|
981
1055
|
const extensions = Array.isArray(options.extensions) ? options.extensions as string[] : undefined;
|
|
982
1056
|
const contextFiles = Array.isArray(options.contextFiles) && options.contextFiles.every(isContextFileScope) ? options.contextFiles : undefined;
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const
|
|
1057
|
+
// A paused resume replaces the run executor with refreshed aliases; new identities must prepare against it.
|
|
1058
|
+
const current = runs.get(runId)?.executor ?? executor;
|
|
1059
|
+
const configuration = await current.prepare({ label: requestedLabel ?? "agent", workflowName: metadata.name, agentOptions: options, agentIdentity: identity, ...(worktree.worktreeOwner ? { worktreeOwner: worktree.worktreeOwner } : {}) }, agentCwd, agentSignal);
|
|
1060
|
+
const resolved = configuration;
|
|
1061
|
+
const label = requestedLabel ?? configuration.label ?? resolved.model.model;
|
|
986
1062
|
const tools = resolved.tools;
|
|
987
1063
|
const schema = object(options.outputSchema) ? options.outputSchema : undefined;
|
|
988
1064
|
const sessionPath = identity.handle !== undefined && identity.turn !== undefined ? await handleTurnInput(store, identity.handle, identity.turn) : undefined;
|
|
989
1065
|
const continuity = identity.handle === undefined ? undefined : sessionPath ? "continued" as const : "fresh" as const;
|
|
990
|
-
return { spawned: scheduler.spawn(runId, prompt, { label, ...(requestedLabel ? { requestedLabel } : {}), ...(identity.parentBreadcrumb ? { parentBreadcrumb: identity.parentBreadcrumb } : {}), cwd: agentCwd, tools, ...(skills ? { skills } : {}), ...(extensions ? { extensions } : {}), ...worktree, ...(model ? { model } : {}), ...(
|
|
1066
|
+
return { spawned: scheduler.spawn(runId, prompt, { label, ...(requestedLabel ? { requestedLabel } : {}), ...(identity.parentBreadcrumb ? { parentBreadcrumb: identity.parentBreadcrumb } : {}), cwd: agentCwd, tools, ...(skills ? { skills } : {}), ...(extensions ? { extensions } : {}), ...worktree, ...(model ? { model } : {}), ...(contextFiles ? { contextFiles } : {}), ...(schema ? { schema } : {}), ...(typeof options.retries === "number" ? { retries: options.retries } : {}), ...(positiveInteger(options.timeoutMs) || options.timeoutMs === null ? { timeoutMs: options.timeoutMs } : {}), ...(sessionPath ? { sessionPath } : {}), ...(continuity ? { continuity } : {}), agentOptions: options, agentIdentity: identity, configuration }) };
|
|
991
1067
|
});
|
|
992
1068
|
if ("replayed" in admitted) return admitted.replayed.value;
|
|
993
1069
|
const { spawned } = admitted;
|
|
@@ -1002,7 +1078,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1002
1078
|
};
|
|
1003
1079
|
};
|
|
1004
1080
|
const recovery = createWorkflowRecovery({
|
|
1005
|
-
pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, coordinateRunMutation, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue: resumeLaunchPrologue,
|
|
1081
|
+
pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, coordinateRunMutation, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue: resumeLaunchPrologue, workflowAgentHandler, shellForRun, toolForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver: (content) => { deliver(pi, content); }, deliverTerminal: deliveryController.deliverTerminal, workflowToolUpdate, registry, modelSpec,
|
|
1006
1082
|
});
|
|
1007
1083
|
const resumeSelectedWorkflow = async (runId: string, foreground: boolean, context: unknown, budgetPatch?: unknown): Promise<{ workflowName: string; state: "running" | "completed" | "awaiting_approval"; attached: boolean; value?: JsonValue }> => {
|
|
1008
1084
|
const run = runs.get(runId);
|
|
@@ -1021,9 +1097,8 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1021
1097
|
if (!foreground && wasAttached) await deliveryController.moveForegroundToBackground(runId);
|
|
1022
1098
|
if (foreground && !wasAttached) {
|
|
1023
1099
|
run.foreground = true;
|
|
1024
|
-
const loaded = await run.store.load();
|
|
1025
1100
|
await persistRunState(run.store, run.metadata, (current) => ({ ...current, delivery: { ...(current.delivery ?? {}), mode: "foreground", state: "attached" } }));
|
|
1026
|
-
await run.store.
|
|
1101
|
+
await run.store.setLaunchMode("foreground");
|
|
1027
1102
|
} else if (!foreground) run.foreground = false;
|
|
1028
1103
|
else run.foreground = true;
|
|
1029
1104
|
await recovery.refreshPausedRunAliases(run, { ...recoveryContext, projectTrusted: projectTrusted(context) });
|
|
@@ -1058,9 +1133,10 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1058
1133
|
"Workflow Retry",
|
|
1059
1134
|
"Retry a failed workflow run by replaying its completed structural operations",
|
|
1060
1135
|
WORKFLOW_RETRY_PARAMETERS,
|
|
1136
|
+
WORKFLOW_RECOVERY_OUTPUT,
|
|
1061
1137
|
async (params, signal, ctx) => {
|
|
1062
1138
|
const result = await recovery.retryWorkflowRun(params.runId, ctx, signal, params.foreground, params.expectedState);
|
|
1063
|
-
return { text: completionControlContent(result), details: result };
|
|
1139
|
+
return { text: completionControlContent(result), details: result, structured: completionControlValue(result) };
|
|
1064
1140
|
},
|
|
1065
1141
|
);
|
|
1066
1142
|
registerControlTool(
|
|
@@ -1068,9 +1144,10 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1068
1144
|
"Workflow Resume",
|
|
1069
1145
|
"Resume an exhausted workflow with unchanged or patched aggregate budgets",
|
|
1070
1146
|
Type.Object({ runId: Type.String(), expectedState: Type.Optional(Type.String({ description: "Persisted source state observed before recovery" })), budget: Type.Optional(Type.Unknown()), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) }, { additionalProperties: false }),
|
|
1147
|
+
WORKFLOW_RECOVERY_OUTPUT,
|
|
1071
1148
|
async (params, signal, ctx) => {
|
|
1072
1149
|
const result = await recovery.resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground, true, params.expectedState);
|
|
1073
|
-
return { text: completionControlContent(result), details: result };
|
|
1150
|
+
return { text: completionControlContent(result), details: result, structured: completionControlValue(result) };
|
|
1074
1151
|
},
|
|
1075
1152
|
);
|
|
1076
1153
|
const deliverStaleTerminal = async (store: RunStore, run: PersistedRun): Promise<void> => {
|
|
@@ -1127,10 +1204,9 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1127
1204
|
const budgetRuntime = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents, { active: loaded.run.state === "running" });
|
|
1128
1205
|
const lifecycle = lifecycleFor(store, loaded.run.state, loaded.snapshot.metadata);
|
|
1129
1206
|
const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
1130
|
-
const roleDefinitions = loaded.snapshot.roles ?? {};
|
|
1131
1207
|
const abortController = new AbortController();
|
|
1132
1208
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, new Set(loaded.snapshot.models), () => { abortController.abort(); });
|
|
1133
|
-
runs.set(runId, { executor: createAgentExecutor({ cwd: ctx.cwd, model, tools: activeSnapshotTools(loaded.snapshot.tools, "session"), resourceSelectors: snapshotResourcePolicy(loaded.snapshot, store.cwd, projectTrusted(ctx), workflowSettingsPath(extensionAgentDir)).effective, extensionSettings: loaded.snapshot.settings.extensionSettings, availableModels: new Set(loaded.snapshot.models), knownModels: new Set(loaded.snapshot.models), ...(loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ? { modelAliases: loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases } : {}), ...(loaded.snapshot.settingsSources?.modelAliases ? { settingsPath: loaded.snapshot.settingsSources.modelAliases } : loaded.snapshot.settingsPath ? { settingsPath: loaded.snapshot.settingsPath } : {}),
|
|
1209
|
+
runs.set(runId, { executor: createAgentExecutor({ cwd: ctx.cwd, projectTrusted: projectTrusted(ctx), model, tools: activeSnapshotTools(loaded.snapshot.tools, "session"), resourceSelectors: snapshotResourcePolicy(loaded.snapshot, store.cwd, projectTrusted(ctx), workflowSettingsPath(extensionAgentDir)).effective, extensionSettings: loaded.snapshot.settings.extensionSettings, availableModels: new Set(loaded.snapshot.models), knownModels: new Set(loaded.snapshot.models), ...(loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ? { modelAliases: loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases } : {}), ...(loaded.snapshot.settingsSources?.modelAliases ? { settingsPath: loaded.snapshot.settingsSources.modelAliases } : loaded.snapshot.settingsPath ? { settingsPath: loaded.snapshot.settingsPath } : {}), runStore: store, providerPause, agentResourcePolicy: frozenResourcePolicy(snapshotResourcePolicy(loaded.snapshot, store.cwd, projectTrusted(ctx), workflowSettingsPath(extensionAgentDir))) }), store, metadata: loaded.snapshot.metadata, model, lifecycle, budget: budgetRuntime, abortController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) });
|
|
1134
1210
|
for (const checkpoint of await store.awaitingCheckpoints()) deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
|
|
1135
1211
|
for (const decision of await store.pendingWorkflowDecisions()) deliver(pi, recovery.budgetDecisionDelivery(loaded.snapshot.metadata, decision));
|
|
1136
1212
|
scheduler.restoreRun(runId, loaded.snapshot.settings.concurrency, loaded.snapshot.identityVersion === LAUNCH_SNAPSHOT_IDENTITY_VERSION ? await store.loadOwnership() : [], () => runs.get(runId)?.budget.checkAgentLaunch(), loaded.snapshot.settings.extensionSettings);
|
|
@@ -1159,19 +1235,14 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1159
1235
|
throw error;
|
|
1160
1236
|
}
|
|
1161
1237
|
});
|
|
1162
|
-
pi.on("before_agent_start", (event, ctx) => {
|
|
1163
|
-
if (!pi.getActiveTools().includes("workflow")) return;
|
|
1164
|
-
const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx), typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : undefined)).filter(([, definition]) => definition.description);
|
|
1165
|
-
if (!roles.length) return;
|
|
1166
|
-
const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
|
|
1167
|
-
return { systemPrompt: `${event.systemPrompt}\n\n${content}` };
|
|
1168
|
-
});
|
|
1169
1238
|
const workflowTool: ToolDefinition<typeof WORKFLOW_TOOL_PARAMETERS, WorkflowToolResult, WorkflowProgressRenderState> = {
|
|
1170
1239
|
name: "workflow",
|
|
1171
1240
|
label: WORKFLOW_TOOL_LABEL,
|
|
1172
1241
|
description: WORKFLOW_TOOL_DESCRIPTION,
|
|
1173
1242
|
promptSnippet: WORKFLOW_TOOL_PROMPT_SNIPPET,
|
|
1174
1243
|
parameters: WORKFLOW_TOOL_PARAMETERS,
|
|
1244
|
+
outputSchema: WORKFLOW_LAUNCH_OUTPUT,
|
|
1245
|
+
prepareLoadout: prepareScriptToolLoadout,
|
|
1175
1246
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1176
1247
|
let resolveDetached: ((result: ForegroundDetachResult) => void) | undefined;
|
|
1177
1248
|
let foregroundStore: RunStore | undefined;
|
|
@@ -1179,7 +1250,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1179
1250
|
const detachedResult = params.foreground ? new Promise<ForegroundDetachResult>((resolve) => { resolveDetached = resolve; }) : undefined;
|
|
1180
1251
|
const detachedToolResult = (run: PersistedRun) => {
|
|
1181
1252
|
const detached = { runId: run.id, state: "running" as const, detached: true as const };
|
|
1182
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(detached) }], details: { ...detached, run, preview: `Moved workflow ${run.id} to background.` } };
|
|
1253
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(detached) }], details: { ...detached, run, preview: `Moved workflow ${run.id} to background.` }, structuredContent: detached };
|
|
1183
1254
|
};
|
|
1184
1255
|
try {
|
|
1185
1256
|
const headless = object(ctx) && ctx.headless === true;
|
|
@@ -1192,7 +1263,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1192
1263
|
const inventory = modelInventory(rootModel, modelRegistry);
|
|
1193
1264
|
const knownModels = inventory.knownModels;
|
|
1194
1265
|
const availableModels = inventory.availableModels;
|
|
1195
|
-
const rootTools = pi
|
|
1266
|
+
const rootTools = reachableTools(pi).filter((name) => !INTERNAL_WORKFLOW_TOOLS.includes(name));
|
|
1196
1267
|
const trustedProject = projectTrusted(ctx);
|
|
1197
1268
|
const launchCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
1198
1269
|
const launch = workflowLaunchSettings(launchCwd, trustedProject, settingsPath, params.concurrency);
|
|
@@ -1206,23 +1277,31 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1206
1277
|
if (launch.resolution.global.extensionSettings !== undefined) registry.validateExtensionSettings(launch.resolution.global.extensionSettings, { source: "global", cwd: ctx.cwd, projectTrusted: trustedProject, settingsPath: launch.resolution.globalSettingsPath });
|
|
1207
1278
|
if (trustedProject && launch.resolution.project.extensionSettings !== undefined) registry.validateExtensionSettings(launch.resolution.project.extensionSettings, { source: "project", cwd: ctx.cwd, projectTrusted: true, settingsPath: launch.resolution.projectSettingsPath });
|
|
1208
1279
|
const validated = validateWorkflowLaunchWithRegistry(params, { cwd: ctx.cwd, agentDir: extensionAgentDir, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases, knownModels, settingsPath, ...(settings.extensionSettings === undefined ? {} : { extensionSettings: settings.extensionSettings }) }, registry);
|
|
1209
|
-
const { script, checked
|
|
1280
|
+
const { script, checked } = validated;
|
|
1281
|
+
const toolContext = scriptToolContext(ctx);
|
|
1282
|
+
validateScriptToolReferences(script, toolContext);
|
|
1283
|
+
// Headless hosts that tolerated extension load failures report them with the extensions they loaded.
|
|
1284
|
+
const strings = (value: unknown) => Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : [];
|
|
1285
|
+
const loadErrors = object(ctx) ? strings(ctx.extensionLoadErrors) : [];
|
|
1286
|
+
if (loadErrors.length) extensionLoad = Object.freeze({ errors: loadErrors, loaded: object(ctx) ? strings(ctx.loadedExtensionPaths) : [] });
|
|
1287
|
+
const executorRoot = { cwd: ctx.cwd, projectTrusted: launch.resourcePolicy.projectTrusted, model: rootModel, tools: new Set(rootTools), resourceSelectors: launch.resourcePolicy.effective, extensionSettings: settings.extensionSettings, availableModels, knownModels, modelAliases, dynamicModelAliasNames: resolvedAliases.dynamicNames, settingsPath, agentResourcePolicy: frozenResourcePolicy(launch.resourcePolicy) };
|
|
1288
|
+
// Runtime emits the same resource warnings, so inspection stays silent.
|
|
1289
|
+
await inspectStaticAgentConfigurations(createAgentExecutor({ ...executorRoot, onResourceWarning: () => undefined }), script, checked.metadata.name, runController.signal, registry.functions());
|
|
1210
1290
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
1291
|
+
// Cancellation can arrive while the lease or a queued parent validation is pending; no run may be created after it.
|
|
1292
|
+
const assertLaunchActive = () => { if (runController.signal.aborted) throw new WorkflowError("CANCELLED", "Workflow launch cancelled"); };
|
|
1293
|
+
assertLaunchActive();
|
|
1211
1294
|
const runId = randomUUID();
|
|
1212
1295
|
const args = params.args ?? null;
|
|
1213
1296
|
encoded(args);
|
|
1214
1297
|
const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
|
|
1215
1298
|
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
1216
1299
|
const parentRunId = params.parentRunId;
|
|
1217
|
-
const
|
|
1218
|
-
const
|
|
1219
|
-
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
|
|
1220
|
-
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
1221
|
-
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, launchMode: params.foreground ? "foreground" : "background", settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
|
|
1222
|
-
const capturedRoles = roleCapture(store, snapshot, agentDefinitions, projectAgentDefinitions);
|
|
1300
|
+
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models])];
|
|
1301
|
+
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, launchMode: params.foreground ? "foreground" : "background", settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentConfigurations: {}, schemas: checked.schemas });
|
|
1223
1302
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
1224
1303
|
const initialBudget = budgetRuntime.snapshot();
|
|
1225
|
-
const createRun = () => store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], agentSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
1304
|
+
const createRun = () => { assertLaunchActive(); return store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], agentSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot); };
|
|
1226
1305
|
// Parent validation and persistence share the mutation lane so a manual deletion cannot remove the parent between the two.
|
|
1227
1306
|
if (parentRunId !== undefined) await coordinateRunMutation(async () => { await store.validateParentRun(parentRunId); await createRun(); });
|
|
1228
1307
|
else await createRun();
|
|
@@ -1243,7 +1322,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1243
1322
|
delivery.detached = true;
|
|
1244
1323
|
const activeRun = runs.get(runId);
|
|
1245
1324
|
if (activeRun) { activeRun.foreground = false; delete activeRun.update; }
|
|
1246
|
-
await store.
|
|
1325
|
+
await store.setLaunchMode("background");
|
|
1247
1326
|
for (const checkpoint of await store.awaitingCheckpoints()) deliverBackgroundCheckpoint(checked.metadata.name, runId, checkpoint);
|
|
1248
1327
|
signal?.removeEventListener("abort", onForegroundAbort);
|
|
1249
1328
|
const run = (await store.load()).run;
|
|
@@ -1258,12 +1337,12 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1258
1337
|
const backgroundLaunch = !params.foreground;
|
|
1259
1338
|
const providerPause = async () => { if (!foregroundAttached) deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
1260
1339
|
const providerErrorRecovery = createProviderErrorRecovery(ctx, availableModels, () => { runController.abort(); });
|
|
1261
|
-
const executor = createAgentExecutor({
|
|
1340
|
+
const executor = createAgentExecutor({ ...executorRoot, runStore: store, providerPause, runContext });
|
|
1262
1341
|
const runRecord: WorkflowRunRecord = { executor, store, metadata: checked.metadata, model: rootModel, lifecycle, budget: budgetRuntime, abortController: runController, foreground: foregroundAttached, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}), ...(params.foreground && onUpdate ? { update: onUpdate } : {}) };
|
|
1263
1342
|
runs.set(runId, runRecord);
|
|
1264
1343
|
if (params.foreground && onUpdate) onUpdate(workflowToolUpdate((await store.load()).run));
|
|
1265
1344
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch(), settings.extensionSettings);
|
|
1266
|
-
const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId
|
|
1345
|
+
const execution = runWorkflow(script, args, withWorkflowFunctions({ tool: (identifier, toolArgs, signal, identity) => toolForRun(store, lifecycle, toolContext, identifier, toolArgs, signal, identity), shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, () => runs.get(runId)?.foreground ?? foregroundAttached, ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(store, lifecycle, checked.metadata.name) }, store, runContext, registry, settings.extensionSettings), runController.signal);
|
|
1267
1346
|
runRecord.execution = execution;
|
|
1268
1347
|
await eventPublisher.runStarted(store, checked.metadata);
|
|
1269
1348
|
const finish = execution.result.then(async (value) => {
|
|
@@ -1302,7 +1381,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1302
1381
|
}, async (error: unknown) => {
|
|
1303
1382
|
await deliveryController.deliverTerminal(store, deliverFailureContent(error), true);
|
|
1304
1383
|
});
|
|
1305
|
-
return { content: [{ type: "text" as const, text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
1384
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` }, structuredContent: { runId, state: "running" } };
|
|
1306
1385
|
}
|
|
1307
1386
|
void completion.then(async (result) => {
|
|
1308
1387
|
await deliveryController.deliverDetachedTerminal(toolCallId, completionContent("background", result));
|
|
@@ -1327,7 +1406,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1327
1406
|
}
|
|
1328
1407
|
deliveryController.foregroundDeliveries.delete(toolCallId);
|
|
1329
1408
|
const run = (await store.load()).run;
|
|
1330
|
-
return { content: [{ type: "text" as const, text: foregroundDelivery.content }, ...(foregroundDelivery.inlined ? [{ type: "text" as const, text: `Workflow run ID: ${runId}` }] : [])], details: { runId, value, run } };
|
|
1409
|
+
return { content: [{ type: "text" as const, text: foregroundDelivery.content }, ...(foregroundDelivery.inlined ? [{ type: "text" as const, text: `Workflow run ID: ${runId}` }] : [])], details: { runId, value, run }, structuredContent: { runId, state: "completed", value } };
|
|
1331
1410
|
} catch (error) {
|
|
1332
1411
|
if (params.foreground && foregroundStore && completionInstalled) {
|
|
1333
1412
|
const claim = await deliveryController.claimForegroundDelivery(foregroundStore, toolCallId);
|
|
@@ -1406,7 +1485,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1406
1485
|
return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
|
|
1407
1486
|
},
|
|
1408
1487
|
};
|
|
1409
|
-
pi.registerTool(workflowTool);
|
|
1488
|
+
pi.registerTool({ ...workflowTool, ...workflowToolExposure(workflowTool.name, codemodeTools) });
|
|
1410
1489
|
registerWorkflowNavigator({ pi, home, clipboard, extensionAgentDir, runs, terminalRunStates, hardTerminalRunStates: HARD_TERMINAL_RUN_STATES, ensureSessionLease, coordinateRunMutation, answerCheckpoint, recovery, stopWorkflowRun, moveForegroundToBackground: deliveryController.moveForegroundToBackground, isForegroundAttached: deliveryController.isForegroundAttached, liveAgents, registry, projectTrusted, resumeHostContext, resumeSelectedWorkflow, reportBlocked: reportWorkflowBlocked, trajectoryProvider, setNavigatorOpen: (open) => { if (open) backgroundWidgetController.suspend(); else backgroundWidgetController.resume(); } });
|
|
1411
1490
|
pi.on("session_shutdown", async () => {
|
|
1412
1491
|
try {
|
|
@@ -1436,6 +1515,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1436
1515
|
type LiveAgentState = { session?: WorkflowAgentSession; prepared?: Readonly<PreparedAgentSession>; handoff?: LiveSessionHandoff; activity?: AgentActivity; lastEventAt?: number };
|
|
1437
1516
|
class LiveAgentRegistry {
|
|
1438
1517
|
readonly #byRun = new Map<string, Map<string, LiveAgentState>>();
|
|
1518
|
+
readonly #tools = new Map<string, Set<{ name: string; startedAt: number }>>();
|
|
1439
1519
|
get(runId: string, agentId: string): Readonly<LiveAgentState> | undefined { return this.#byRun.get(runId)?.get(agentId); }
|
|
1440
1520
|
setSession(runId: string, agentId: string, session?: WorkflowAgentSession): void {
|
|
1441
1521
|
if (session) this.#state(runId, agentId).session = session;
|
|
@@ -1452,17 +1532,26 @@ class LiveAgentRegistry {
|
|
|
1452
1532
|
setEventTime(runId: string, agentId: string, timestamp?: number): void {
|
|
1453
1533
|
if (timestamp !== undefined) this.#state(runId, agentId).lastEventAt = timestamp;
|
|
1454
1534
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1535
|
+
/** Marks a script tool call in flight until the returned callback runs. */
|
|
1536
|
+
toolStarted(runId: string, name: string): () => void {
|
|
1537
|
+
const call = { name, startedAt: Date.now() };
|
|
1538
|
+
const calls = this.#tools.get(runId) ?? new Set<{ name: string; startedAt: number }>();
|
|
1539
|
+
this.#tools.set(runId, calls.add(call));
|
|
1540
|
+
return () => { calls.delete(call); if (!calls.size && this.#tools.get(runId) === calls) this.#tools.delete(runId); };
|
|
1541
|
+
}
|
|
1542
|
+
deleteRun(runId: string): void { this.#byRun.delete(runId); this.#tools.delete(runId); }
|
|
1543
|
+
/** Overlays live activity, freshness, and script tool calls onto a persisted run; returns the input when nothing is live. */
|
|
1457
1544
|
overlay(run: PersistedRun): PersistedRun {
|
|
1545
|
+
const tools = this.#tools.get(run.id);
|
|
1546
|
+
const withTools = tools?.size ? { ...run, activeTools: [...tools].map((call) => ({ ...call })) } : run;
|
|
1458
1547
|
const agents = this.#byRun.get(run.id);
|
|
1459
|
-
if (!agents?.size) return
|
|
1548
|
+
if (!agents?.size) return withTools;
|
|
1460
1549
|
const next = run.agents.map((agent) => {
|
|
1461
1550
|
const live = agents.get(agent.id);
|
|
1462
1551
|
if (!live || (live.activity === undefined && live.lastEventAt === undefined)) return agent;
|
|
1463
1552
|
return { ...agent, ...(live.activity === undefined ? {} : { activity: live.activity }), ...(live.lastEventAt === undefined ? {} : { lastEventAt: live.lastEventAt }) };
|
|
1464
1553
|
});
|
|
1465
|
-
return next.some((agent, index) => agent !== run.agents[index]) ? { ...
|
|
1554
|
+
return next.some((agent, index) => agent !== run.agents[index]) ? { ...withTools, agents: next } : withTools;
|
|
1466
1555
|
}
|
|
1467
1556
|
#state(runId: string, agentId: string): LiveAgentState {
|
|
1468
1557
|
const agents = this.#byRun.get(runId) ?? new Map<string, LiveAgentState>();
|
|
@@ -1515,9 +1604,7 @@ async function handleTurnInput(store: RunStore, handle: string, turn: number): P
|
|
|
1515
1604
|
return target;
|
|
1516
1605
|
}
|
|
1517
1606
|
|
|
1518
|
-
|
|
1519
|
-
return label ?? role ?? model.model;
|
|
1520
|
-
}
|
|
1607
|
+
|
|
1521
1608
|
|
|
1522
1609
|
function modelSpec(value: string, fallback: ModelSpec): ModelSpec {
|
|
1523
1610
|
try {
|