@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.19.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 +135 -0
- package/dist/src/agent-execution.js +114 -87
- package/dist/src/background-widget.d.ts +61 -0
- package/dist/src/background-widget.js +23 -16
- package/dist/src/decoders.js +28 -1
- package/dist/src/execution.d.ts +4 -2
- package/dist/src/execution.js +43 -2
- package/dist/src/host-delivery.js +1 -0
- package/dist/src/host-navigator.d.ts +18 -0
- package/dist/src/host-navigator.js +71 -13
- package/dist/src/host-recovery.d.ts +5 -2
- package/dist/src/host-recovery.js +3 -3
- package/dist/src/host-view.d.ts +21 -1
- package/dist/src/host-view.js +138 -47
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +246 -60
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +1346 -622
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/roles.d.ts +70 -0
- package/dist/src/roles.js +326 -0
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/settings.d.ts +26 -0
- package/dist/src/settings.js +277 -0
- package/dist/src/trajectory.d.ts +39 -4
- package/dist/src/trajectory.js +82 -5
- package/dist/src/types.d.ts +30 -3
- package/dist/src/types.js +1 -1
- package/dist/src/utils.d.ts +21 -1
- package/dist/src/utils.js +40 -0
- package/dist/src/validation.d.ts +10 -17
- package/dist/src/validation.js +29 -458
- package/dist/starter/index.js +0 -2
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +0 -2
- package/dist/subagents/index.js +1344 -798
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +49 -29
- package/dist/subagents/src/contracts.js +18 -7
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +15 -7
- package/dist/subagents/src/manager.js +32 -13
- package/dist/subagents/src/navigator.js +396 -247
- package/dist/subagents/src/view.d.ts +24 -2
- package/dist/subagents/src/view.js +107 -80
- package/dist/trajectory/assets/index.html +370 -232
- package/dist/trajectory/assets/prism.min.js +13 -0
- package/dist/trajectory/index.js +445 -277
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +370 -232
- package/dist/trajectory/src/assets/prism.min.js +13 -0
- package/dist/trajectory/src/export.js +4 -2
- package/dist/trajectory/src/index.d.ts +2 -0
- package/dist/trajectory/src/index.js +151 -60
- package/dist/trajectory/src/server.js +138 -11
- package/dist/trajectory/src/server.js.map +2 -2
- package/package.json +7 -6
- package/skills/pi-extensible-workflows/SKILL.md +2 -0
- package/src/agent-execution.ts +110 -80
- package/src/background-widget.ts +26 -19
- package/src/decoders.ts +22 -1
- package/src/execution.ts +41 -4
- package/src/host-delivery.ts +1 -0
- package/src/host-navigator.ts +58 -14
- package/src/host-recovery.ts +8 -7
- package/src/host-view.ts +116 -42
- package/src/host.ts +223 -55
- package/src/index.ts +3 -1
- package/src/paths.ts +3 -0
- package/src/registry.ts +3 -2
- package/src/roles.ts +327 -0
- package/src/script-tools.ts +76 -0
- package/src/settings.ts +224 -0
- package/src/trajectory.ts +97 -9
- package/src/types.ts +12 -5
- package/src/utils.ts +36 -1
- package/src/validation.ts +30 -356
- package/starter/review-loop.ts +0 -2
- package/subagents/README.md +3 -3
- package/subagents/src/contracts.ts +22 -7
- package/subagents/src/index.ts +18 -7
- package/subagents/src/manager.ts +31 -13
- package/subagents/src/navigator.ts +336 -180
- package/subagents/src/view.ts +108 -76
- package/trajectory/src/assets/index.html +370 -232
- package/trajectory/src/assets/prism.min.js +13 -0
- package/trajectory/src/export.ts +4 -2
- package/trajectory/src/index.ts +124 -47
- package/trajectory/src/server.ts +114 -14
package/src/host.ts
CHANGED
|
@@ -4,7 +4,7 @@ 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";
|
|
7
|
+
import { copyToClipboard, getAgentDir, ModelSelectorComponent, type ExtensionAPI, type ExtensionContext, type ExtensionToolContext, type ModelRuntime, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { FairAgentScheduler, getAgentAttempts, WorkflowAgentExecutor, localAgentTransport, type AgentActivity, type AgentAttempt, type AgentDefinition, 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";
|
|
@@ -13,16 +13,18 @@ import { acquireSessionLease, isPersistedRun, listPersistedSessionIds, listRunId
|
|
|
13
13
|
import { retainTerminalRuns } from "./retention.js";
|
|
14
14
|
import type { PersistedRun, WorktreeReference } from "./persistence.js";
|
|
15
15
|
import { validateBudget, WorkflowBudgetRuntime } from "./budget.js";
|
|
16
|
-
import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue, mergeWorkflowExtensionSettings, modelAliasErrorName, modelCapability, object, parseModelReference, positiveInteger, sanitizeDisplayText, validateModelAliases } from "./utils.js";
|
|
17
|
-
import {
|
|
16
|
+
import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue, mergeWorkflowExtensionSettings, modelAliasErrorName, modelCapability, object, parseModelReference, positiveInteger, reachableTools, resolveModelReference, VIRTUAL_MODEL_PROVIDER, sanitizeDisplayText, validateModelAliases } from "./utils.js";
|
|
17
|
+
import { loadCodemodeToolsSetting, loadSettings, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, validateCheckpoint, validateModelAliasAvailability, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowSettingsPath, workflowToolExposure } from "./validation.js";
|
|
18
|
+
import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
|
|
18
19
|
import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistryIfIdle, retainWorkflowRegistry, type WorkflowRegistryApi } from "./registry.js";
|
|
19
|
-
import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
|
|
20
|
+
import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath, toolIdentityPath } from "./execution.js";
|
|
21
|
+
import { prepareScriptToolLoadout, scriptTool, scriptToolContext, scriptToolValue, validateScriptToolReferences } from "./script-tools.js";
|
|
20
22
|
import backgroundWidget, { type BackgroundWidgetAPI } from "./background-widget.js";
|
|
21
23
|
import { showChangelogNotice } from "./changelog.js";
|
|
22
24
|
import { createTrajectoryRunLoader, createTrajectoryRunMetadataLoader, createTrajectorySubagentLoader, createTrajectorySubagentMetadataLoader, createTrajectoryTranscriptLoader, type TrajectoryActionRequest, type TrajectoryActionResult, type TrajectorySubagent } from "./trajectory.js";
|
|
23
25
|
import { getTrajectoryHost, type TrajectoryPublisherProvider } from "./trajectory-host-handle.js";
|
|
24
26
|
import { getSubagentManager } from "./subagent-manager-handle.js";
|
|
25
|
-
import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_BLOCKED_EVENT, WorkflowError, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState, roleNameOf, type AgentAccounting, type AgentIdentity, type AgentRecord, type AgentResourcePolicy, type AgentTransport, type JsonValue, type LaunchSnapshot, type LiveSessionHandoff, type HardTerminalRunState, type ModelSpec, type PreparedAgentSession, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowAgentSession, type WorkflowErrorCode, type WorkflowMetadata, type WorkflowModelAliasResolverContext, type WorkflowSettings, type WorkflowSettingsResolution, type WorkflowWorktreeReference } from "./types.js";
|
|
27
|
+
import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, THINKING_LEVELS, WORKFLOW_BLOCKED_EVENT, WorkflowError, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState, roleNameOf, type AgentAccounting, type AgentIdentity, type AgentRecord, type AgentResourcePolicy, type AgentTransport, type JsonValue, type LaunchSnapshot, type LiveSessionHandoff, type HardTerminalRunState, type ModelSpec, type PreparedAgentSession, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type ToolIdentity, type WorkflowAgentSession, type WorkflowErrorCode, type WorkflowMetadata, type WorkflowModelAliasResolverContext, type WorkflowSettings, type WorkflowScriptCall, type WorkflowSettingsResolution, type WorkflowWorktreeReference } from "./types.js";
|
|
26
28
|
import type { SubagentManagerContext, SubagentRunRequest, SubagentStatus } from "../subagents/src/contracts.js";
|
|
27
29
|
import {
|
|
28
30
|
SETTLED_AGENT_STATES,
|
|
@@ -34,7 +36,6 @@ import {
|
|
|
34
36
|
workflowControlCall,
|
|
35
37
|
workflowControlResult,
|
|
36
38
|
workflowProgressBlock,
|
|
37
|
-
formatWorkflowProgress,
|
|
38
39
|
type WorkflowProgressRenderState,
|
|
39
40
|
} from "./host-view.js";
|
|
40
41
|
import {
|
|
@@ -59,7 +60,7 @@ import {
|
|
|
59
60
|
type WorkflowLogEntry,
|
|
60
61
|
} from "./host-delivery.js";
|
|
61
62
|
|
|
62
|
-
export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
|
|
63
|
+
export type WorkflowExtensionAPI = Pick<ExtensionAPI, "appendEntry" | "getActiveTools" | "getThinkingLevel" | "on" | "registerCommand" | "registerTool" | "sendMessage"> & Partial<Pick<ExtensionAPI, "getAllTools">> & Pick<BackgroundWidgetAPI, "events" | "registerEntryRenderer" | "registerShortcut">;
|
|
63
64
|
|
|
64
65
|
export {
|
|
65
66
|
agentBreadcrumb,
|
|
@@ -117,6 +118,13 @@ function mainAgentError(error: unknown): WorkflowError {
|
|
|
117
118
|
Object.assign(presented, typed);
|
|
118
119
|
return presented;
|
|
119
120
|
}
|
|
121
|
+
/** The structured counterpart of {@link completionControlContent}: the result without the persisted run, with the full value. */
|
|
122
|
+
function completionControlValue(result: Readonly<Record<string, unknown>>, controlRunId?: string): Record<string, unknown> {
|
|
123
|
+
const record: Record<string, unknown> = { ...(controlRunId === undefined ? {} : { runId: controlRunId }), ...result };
|
|
124
|
+
delete record.run;
|
|
125
|
+
delete record.completion;
|
|
126
|
+
return record;
|
|
127
|
+
}
|
|
120
128
|
function completionControlContent(result: unknown, controlRunId?: string): string {
|
|
121
129
|
const record = object(result) ? { ...result } : undefined;
|
|
122
130
|
if (record && controlRunId !== undefined && record.runId === undefined) record.runId = controlRunId;
|
|
@@ -159,7 +167,7 @@ export function formatWorkflowPreview(args: { script?: unknown; scriptPath?: unk
|
|
|
159
167
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
160
168
|
}
|
|
161
169
|
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"
|
|
170
|
+
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
171
|
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
172
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
165
173
|
name: Type.String({ description: "Required non-empty workflow name" }),
|
|
@@ -174,9 +182,26 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
|
174
182
|
}, { additionalProperties: false });
|
|
175
183
|
export const WORKFLOW_STATUS_PARAMETERS = Type.Object({ runId: Type.String({ description: "Workflow run ID visible in the current project" }) }, { additionalProperties: false });
|
|
176
184
|
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" })) });
|
|
185
|
+
// Structured results codemode scripts receive instead of the JSON text; nested records stay loose so they cannot drift from persistence.
|
|
186
|
+
const WORKFLOW_ERROR_OUTPUT = Type.Object({ code: Type.String(), message: Type.String() });
|
|
187
|
+
const WORKFLOW_LAUNCH_OUTPUT = Type.Object({ runId: Type.String(), state: Type.String(), detached: Type.Optional(Type.Boolean()), value: Type.Optional(Type.Unknown()) });
|
|
188
|
+
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()) })) });
|
|
189
|
+
const WORKFLOW_STOP_OUTPUT = Type.Object({ runId: Type.String(), state: Type.String(), stopped: Type.Boolean(), reason: Type.Optional(Type.String()) });
|
|
190
|
+
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()) });
|
|
191
|
+
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()) });
|
|
192
|
+
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()) });
|
|
193
|
+
// The index without a name; with one, the function, the model alias, or { error }.
|
|
194
|
+
const WORKFLOW_CATALOG_OUTPUT = Type.Union([
|
|
195
|
+
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()) }),
|
|
196
|
+
Type.Object({ name: Type.String(), version: Type.String(), headline: Type.String(), description: Type.String(), input: Type.Unknown(), output: Type.Unknown() }),
|
|
197
|
+
WORKFLOW_CATALOG_ALIAS_OUTPUT,
|
|
198
|
+
Type.Object({ error: Type.Object({ code: Type.String(), name: Type.String(), message: Type.String() }) }),
|
|
199
|
+
]);
|
|
177
200
|
|
|
178
201
|
function workflowToolUpdate(run: PersistedRun): WorkflowToolUpdate {
|
|
179
|
-
|
|
202
|
+
//NOTE: renderers read details.run; the partial text stays O(1) so high-frequency updates never format the whole tree.
|
|
203
|
+
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
204
|
+
return { content: [{ type: "text", text: `Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done) [${run.state}]` }], details: { runId: run.id, run } };
|
|
180
205
|
}
|
|
181
206
|
function agentWithProgress(agent: AgentRecord, progress: AgentProgress): AgentRecord {
|
|
182
207
|
const next = { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls };
|
|
@@ -200,6 +225,7 @@ function deliver(pi: WorkflowExtensionAPI, content: string): void {
|
|
|
200
225
|
pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
201
226
|
}
|
|
202
227
|
const WORKFLOW_WARNING_ENTRY = "workflow-warning";
|
|
228
|
+
const PROGRESS_COALESCE_MS = 100;
|
|
203
229
|
interface WorkflowWarningEntry { message: string }
|
|
204
230
|
function deliverWarning(pi: WorkflowExtensionAPI, content: string): void {
|
|
205
231
|
pi.appendEntry<WorkflowWarningEntry>(WORKFLOW_WARNING_ENTRY, { message: content });
|
|
@@ -254,9 +280,46 @@ function modelInventory(root: ModelSpec | undefined, registry: ModelRegistryCapa
|
|
|
254
280
|
if (rootName) { knownModels.add(rootName); availableModels.add(rootName); }
|
|
255
281
|
return { knownModels, availableModels };
|
|
256
282
|
}
|
|
257
|
-
function resumeHostContext(ctx: unknown): { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext } {
|
|
283
|
+
function resumeHostContext(ctx: unknown): { model: { provider: string; id: string } | undefined; modelRegistry: ModelRegistryCapability | undefined; deliveryContext: CompletionDeliveryContext; toolContext: ExtensionToolContext | undefined } {
|
|
258
284
|
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;
|
|
259
|
-
return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry, deliveryContext: completionContext(ctx) };
|
|
285
|
+
return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry, deliveryContext: completionContext(ctx), toolContext: scriptToolContext(ctx) };
|
|
286
|
+
}
|
|
287
|
+
// State is rewritten on every update, so large shell inputs and errors are kept as prefixes.
|
|
288
|
+
const SCRIPT_CALL_TEXT_LIMIT_BYTES = 16 * 1024;
|
|
289
|
+
function scriptCallInput(input: JsonValue): Pick<WorkflowScriptCall, "input" | "inputBytes"> {
|
|
290
|
+
const text = JSON.stringify(input);
|
|
291
|
+
const bytes = Buffer.byteLength(text);
|
|
292
|
+
return bytes <= SCRIPT_CALL_TEXT_LIMIT_BYTES ? { input } : { input: utf8Prefix(text, SCRIPT_CALL_TEXT_LIMIT_BYTES), inputBytes: bytes };
|
|
293
|
+
}
|
|
294
|
+
/** Records a script call as started, replacing the record of an attempt that an interruption left unfinished. */
|
|
295
|
+
function startScriptCall(run: PersistedRun, call: WorkflowScriptCall): PersistedRun {
|
|
296
|
+
const recorded = { ...call, ...(run.phase === undefined ? {} : { phase: run.phase }) };
|
|
297
|
+
const calls = run.scriptCalls ?? [];
|
|
298
|
+
return { ...run, scriptCalls: calls.some(({ path }) => path === call.path) ? calls.map((current) => current.path === call.path ? recorded : current) : [...calls, recorded] };
|
|
299
|
+
}
|
|
300
|
+
function finishScriptCall(run: PersistedRun, path: string, error?: WorkflowError): PersistedRun {
|
|
301
|
+
const finishedAt = Date.now();
|
|
302
|
+
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) };
|
|
303
|
+
}
|
|
304
|
+
type AliasVirtualModel = { provider: string; id: string; name: string; thinkingLevels: readonly string[]; route: (request: { thinkingLevel: string }, ctx: ExtensionContext) => { model: Model<Api>; thinkingLevel: string } };
|
|
305
|
+
/** Lists workflow model aliases in `/model` as virtual models; needs a Pi host with `registerVirtualModel`. */
|
|
306
|
+
function registerAliasVirtualModels(pi: unknown, names: readonly string[], settingsPath: string): void {
|
|
307
|
+
const register = object(pi) ? asFn(pi.registerVirtualModel) : undefined;
|
|
308
|
+
if (!register) return;
|
|
309
|
+
for (const name of names) {
|
|
310
|
+
const definition: AliasVirtualModel = {
|
|
311
|
+
provider: VIRTUAL_MODEL_PROVIDER, id: name, name: `${name} (workflow alias)`, thinkingLevels: THINKING_LEVELS,
|
|
312
|
+
route: (request, ctx) => {
|
|
313
|
+
// Settings are read per request so alias edits apply without a reload.
|
|
314
|
+
const aliases = resolveWorkflowSettings(ctx.cwd, projectTrusted(ctx), settingsPath).effective.modelAliases ?? {};
|
|
315
|
+
const target = resolveModelReference(name, aliases, new Set(ctx.modelRegistry.getAll().map((model) => `${model.provider}/${model.id}`)), settingsPath);
|
|
316
|
+
const model = ctx.modelRegistry.find(target.provider, target.model);
|
|
317
|
+
if (!model) throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${name} resolved to ${target.provider}/${target.model}`);
|
|
318
|
+
return { model, thinkingLevel: request.thinkingLevel };
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
Reflect.apply(register, pi, [definition]);
|
|
322
|
+
}
|
|
260
323
|
}
|
|
261
324
|
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[] }> {
|
|
262
325
|
const dynamic = typeof registry.resolveModelAliases === "function" ? await registry.resolveModelAliases(context, new Set(Object.keys(staticAliases))) : {};
|
|
@@ -287,8 +350,14 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
287
350
|
return textBlock(data ? `Warning: ${data.message}` : "");
|
|
288
351
|
});
|
|
289
352
|
let backgroundWidgetEnabled = true;
|
|
290
|
-
try {
|
|
353
|
+
try {
|
|
354
|
+
const globalSettings = loadSettings(workflowSettingsPath(extensionAgentDir));
|
|
355
|
+
backgroundWidgetEnabled = globalSettings.backgroundWidget ?? true;
|
|
356
|
+
// Registered at load, before Pi resolves --model and restores the session model. Project aliases need project trust, which is unknown here.
|
|
357
|
+
registerAliasVirtualModels(pi, Object.keys(globalSettings.modelAliases ?? {}), workflowSettingsPath(extensionAgentDir));
|
|
358
|
+
} catch { /* Keep the optional UI enabled; the launch path reports settings errors. */ }
|
|
291
359
|
const backgroundWidgetController = backgroundWidget(pi, backgroundWidgetEnabled);
|
|
360
|
+
const codemodeTools = loadCodemodeToolsSetting(extensionAgentDir);
|
|
292
361
|
const logBridge = (store: RunStore, lifecycle: RunLifecycle, workflowName: string) => async (message: string) => {
|
|
293
362
|
const timestamp = Date.now();
|
|
294
363
|
const bounded = utf8Prefix(message, DELIVERY_LIMIT_BYTES);
|
|
@@ -415,7 +484,6 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
415
484
|
const trusted = projectTrusted(context);
|
|
416
485
|
const settings = resolveWorkflowSettings(cwd, trusted, workflowSettingsPath(extensionAgentDir)).effective.extensionSettings?.trajectory;
|
|
417
486
|
const port = settings?.port;
|
|
418
|
-
const themes = settings?.themes ?? false;
|
|
419
487
|
const loadRuns = createTrajectoryRunLoader(cwd, sessionId, home, overlayLiveRun);
|
|
420
488
|
const loadSubagents = createTrajectorySubagentLoader(cwd, sessionId, extensionAgentDir, overlayLiveSubagent);
|
|
421
489
|
const loadRunsMetadata = createTrajectoryRunMetadataLoader(cwd, sessionId, home, overlayLiveRun);
|
|
@@ -424,7 +492,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
424
492
|
const live = liveSubagents.get(subagentId);
|
|
425
493
|
return live?.status.sessionId === sessionId ? live.status.attemptDetails?.at(-1)?.session : undefined;
|
|
426
494
|
});
|
|
427
|
-
return { cwd, sessionId, ...(port === undefined ? {} : { port }),
|
|
495
|
+
return { cwd, sessionId, ...(port === undefined ? {} : { port }), loadRuns, loadSubagents, loadMetadata: async () => ({ runs: await loadRunsMetadata(), subagents: await loadSubagentsMetadata() }), loadTranscript, handleAction: (request: Readonly<TrajectoryActionRequest>) => trajectoryAction(request, context) };
|
|
428
496
|
};
|
|
429
497
|
const withLiveActivities = (run: PersistedRun): PersistedRun => liveAgents.overlay(run);
|
|
430
498
|
/** Overlays the in-memory lifecycle state and budget usage of an active run onto its persisted record. */
|
|
@@ -532,7 +600,11 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
532
600
|
if (replayed) return readShellResult(replayed.value);
|
|
533
601
|
const shellStartedAt = Date.now();
|
|
534
602
|
let shellPhaseIndex = -1;
|
|
535
|
-
|
|
603
|
+
// Env values stay out of the record: they may carry secrets, and a shared trajectory publishes run state.
|
|
604
|
+
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 };
|
|
605
|
+
let failure: WorkflowError | undefined;
|
|
606
|
+
const started = await persistRunState(store, metadata, (run) => {
|
|
607
|
+
const current = startScriptCall(run, call);
|
|
536
608
|
const history = current.phaseHistory ?? [];
|
|
537
609
|
if (current.phase !== undefined) shellPhaseIndex = current.phaseHistoryIndex ?? (history.length ? history.length - 1 : 0);
|
|
538
610
|
const phaseActivities = [...(current.activeShellsByPhase ?? [])];
|
|
@@ -550,8 +622,12 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
550
622
|
if (!jsonValue(result)) fail("SHELL_FAILED", "Shell result is not JSON-compatible");
|
|
551
623
|
await store.complete(path, result);
|
|
552
624
|
return result;
|
|
625
|
+
} catch (error) {
|
|
626
|
+
failure = asWorkflowError(error);
|
|
627
|
+
throw error;
|
|
553
628
|
} finally {
|
|
554
|
-
const stopped = await persistRunState(store, metadata, (
|
|
629
|
+
const stopped = await persistRunState(store, metadata, (run) => {
|
|
630
|
+
const current = finishScriptCall(run, path, failure);
|
|
555
631
|
const phaseActivities = [...(current.activeShellsByPhase ?? [])];
|
|
556
632
|
const phaseActivityIndex = phaseActivities.findIndex(({ phaseIndex }) => phaseIndex === shellPhaseIndex);
|
|
557
633
|
const phaseActivity = phaseActivities[phaseActivityIndex];
|
|
@@ -570,6 +646,44 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
570
646
|
}
|
|
571
647
|
} finally { await lifecycle.leave(); }
|
|
572
648
|
};
|
|
649
|
+
/** A script `tools.<id>(args)` call: replayed from the journal, otherwise run through the session's tool pipeline and journaled. */
|
|
650
|
+
const toolForRun = async (store: RunStore, lifecycle: RunLifecycle, context: ExtensionToolContext | undefined, identifier: string, args: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: ToolIdentity): Promise<JsonValue> => {
|
|
651
|
+
await lifecycle.enter();
|
|
652
|
+
try {
|
|
653
|
+
const path = toolIdentityPath(identifier, args, identity);
|
|
654
|
+
const replayed = await store.replay(path);
|
|
655
|
+
if (replayed) return replayed.value;
|
|
656
|
+
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`);
|
|
657
|
+
// Rendering is best-effort: it must never lose or mask a completed call.
|
|
658
|
+
const render = async () => { try { const update = runs.get(store.runId)?.update; if (update) update(workflowToolUpdate(withLiveActivities((await store.load()).run))); } catch { /* best-effort progress */ } };
|
|
659
|
+
// Inspection writes must not prevent execution, mask errors, or reject a journaled success.
|
|
660
|
+
const record = async (update: (run: PersistedRun) => PersistedRun) => { try { await store.updateState(update); } catch { /* best-effort call metadata */ } };
|
|
661
|
+
let value: JsonValue;
|
|
662
|
+
try {
|
|
663
|
+
const tool = scriptTool(context, identifier);
|
|
664
|
+
if (!tool) fail("UNKNOWN_TOOL", `tools.${identifier} is not a tool this session can call`);
|
|
665
|
+
const cancelled = (): boolean => signal.aborted;
|
|
666
|
+
if (cancelled()) fail("CANCELLED", "Workflow cancelled");
|
|
667
|
+
await record((run) => startScriptCall(run, { kind: "tool", name: tool.name, path, startedAt: Date.now() }));
|
|
668
|
+
const finished = liveAgents.toolStarted(store.runId, tool.name);
|
|
669
|
+
try {
|
|
670
|
+
await render();
|
|
671
|
+
const outcome = await context.executeTool(tool.name, args, { signal });
|
|
672
|
+
if (cancelled()) fail("CANCELLED", "Workflow cancelled");
|
|
673
|
+
value = scriptToolValue(tool, outcome);
|
|
674
|
+
} finally { finished(); }
|
|
675
|
+
} catch (error) {
|
|
676
|
+
// A stale context throws plain errors from ctx.tools; report them as tool failures.
|
|
677
|
+
const failure = error instanceof WorkflowError ? error : new WorkflowError("TOOL_FAILED", `tools.${identifier}: ${errorText(error)}`);
|
|
678
|
+
await record((run) => finishScriptCall(run, path, failure));
|
|
679
|
+
throw failure;
|
|
680
|
+
}
|
|
681
|
+
await store.complete(path, value);
|
|
682
|
+
await record((run) => finishScriptCall(run, path));
|
|
683
|
+
await render();
|
|
684
|
+
return value;
|
|
685
|
+
} finally { await lifecycle.leave(); }
|
|
686
|
+
};
|
|
573
687
|
const lifecycleFor = (store: RunStore, state: RunState, metadata: WorkflowMetadata) => new RunLifecycle(state, async (next, previous, reason) => {
|
|
574
688
|
const run = runs.get(store.runId);
|
|
575
689
|
if (!run) throw new WorkflowError("INTERNAL_ERROR", `Unknown production run: ${store.runId}`);
|
|
@@ -586,22 +700,49 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
586
700
|
const scheduler = new FairAgentScheduler(async ({ id, runId, tuiIndex, parentId, prompt, options, signal, setSteer }) => {
|
|
587
701
|
const run = runs.get(runId);
|
|
588
702
|
if (!run) throw new WorkflowError("INTERNAL_ERROR", `Unknown production run: ${runId}`);
|
|
703
|
+
const applyProgress = async (progress: AgentProgress) => {
|
|
704
|
+
let runState: PersistedRun;
|
|
705
|
+
if (progress.persist) {
|
|
706
|
+
runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) } : current);
|
|
707
|
+
} else {
|
|
708
|
+
const loaded = await run.store.loadStatus();
|
|
709
|
+
if (!loaded.agents.some((agent) => agent.id === id)) return;
|
|
710
|
+
runState = { ...loaded, ...run.budget.snapshot(), agents: loaded.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) };
|
|
711
|
+
}
|
|
712
|
+
if (!runState.agents.some((agent) => agent.id === id)) return;
|
|
713
|
+
liveAgents.setActivity(runId, id, progress.activity);
|
|
714
|
+
liveAgents.setEventTime(runId, id, progress.lastEventAt);
|
|
715
|
+
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
716
|
+
};
|
|
717
|
+
// Display-only progress bursts coalesce to one trailing apply per window; persisted progress always applies immediately.
|
|
718
|
+
let deferred: AgentProgress | undefined;
|
|
719
|
+
let deferredTimer: NodeJS.Timeout | undefined;
|
|
720
|
+
let appliedAt = 0;
|
|
721
|
+
const cancelDeferredProgress = () => {
|
|
722
|
+
if (deferredTimer) clearTimeout(deferredTimer);
|
|
723
|
+
deferredTimer = undefined;
|
|
724
|
+
deferred = undefined;
|
|
725
|
+
};
|
|
726
|
+
const onProgress = async (progress: AgentProgress) => {
|
|
727
|
+
const now = Date.now();
|
|
728
|
+
if (progress.persist || (!deferredTimer && now - appliedAt >= PROGRESS_COALESCE_MS)) {
|
|
729
|
+
cancelDeferredProgress();
|
|
730
|
+
appliedAt = now;
|
|
731
|
+
await applyProgress(progress);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
deferred = progress;
|
|
735
|
+
deferredTimer ??= setTimeout(() => {
|
|
736
|
+
deferredTimer = undefined;
|
|
737
|
+
const next = deferred;
|
|
738
|
+
deferred = undefined;
|
|
739
|
+
if (!next) return;
|
|
740
|
+
appliedAt = Date.now();
|
|
741
|
+
void applyProgress(next).catch(() => undefined);
|
|
742
|
+
}, Math.max(0, PROGRESS_COALESCE_MS - (now - appliedAt))).unref();
|
|
743
|
+
};
|
|
589
744
|
try {
|
|
590
745
|
const budget = run.budget.forAgent(id);
|
|
591
|
-
const onProgress = async (progress: AgentProgress) => {
|
|
592
|
-
let runState: PersistedRun;
|
|
593
|
-
if (progress.persist) {
|
|
594
|
-
runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) } : current);
|
|
595
|
-
} else {
|
|
596
|
-
const loaded = await run.store.load();
|
|
597
|
-
if (!loaded.run.agents.some((agent) => agent.id === id)) return;
|
|
598
|
-
runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) };
|
|
599
|
-
}
|
|
600
|
-
if (!runState.agents.some((agent) => agent.id === id)) return;
|
|
601
|
-
liveAgents.setActivity(runId, id, progress.activity);
|
|
602
|
-
liveAgents.setEventTime(runId, id, progress.lastEventAt);
|
|
603
|
-
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
604
|
-
};
|
|
605
746
|
const onAttempt = async (attempt: AgentAttempt) => {
|
|
606
747
|
liveAgents.setSession(runId, id, attempt.liveSession);
|
|
607
748
|
liveAgents.setHandoff(runId, id, attempt);
|
|
@@ -618,6 +759,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
618
759
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
619
760
|
};
|
|
620
761
|
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, tuiIndex, tuiLabel: options.requestedLabel ?? options.label, agentNodeId: id, ...(options.extensionSettings === undefined ? {} : { inheritedExtensionSettings: options.extensionSettings }), onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.role ? { role: options.role } : {}), ...(options.contextFiles ? { contextFiles: options.contextFiles } : {}), tools: options.tools, ...(options.skills ? { skills: options.skills } : {}), ...(options.extensions ? { extensions: options.extensions } : {}), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.sessionPath ? { sessionPath: options.sessionPath } : {}), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, skills, extensions) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}), ...(skills !== undefined ? { skills } : {}), ...(extensions !== undefined ? { extensions } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
|
|
762
|
+
cancelDeferredProgress();
|
|
621
763
|
const before = (await run.store.load()).run;
|
|
622
764
|
await persistAgentAttempts(run.store, id, result.attempts);
|
|
623
765
|
const completed = (await run.store.load()).run;
|
|
@@ -628,6 +770,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
628
770
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
629
771
|
return result.value;
|
|
630
772
|
} catch (error) {
|
|
773
|
+
cancelDeferredProgress();
|
|
631
774
|
liveAgents.setSession(runId, id);
|
|
632
775
|
const attempts = getAgentAttempts(error);
|
|
633
776
|
if (attempts?.length) {
|
|
@@ -791,17 +934,20 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
791
934
|
label: string,
|
|
792
935
|
description: string,
|
|
793
936
|
parameters: P,
|
|
794
|
-
|
|
937
|
+
outputSchema: TSchema,
|
|
938
|
+
run: (params: Static<P>, signal: AbortSignal, ctx: unknown) => Promise<{ text: string; details: unknown; structured: unknown }>,
|
|
795
939
|
) => {
|
|
796
940
|
pi.registerTool({
|
|
797
941
|
name,
|
|
798
942
|
label,
|
|
799
943
|
description,
|
|
800
944
|
parameters,
|
|
945
|
+
outputSchema,
|
|
946
|
+
...workflowToolExposure(name, codemodeTools),
|
|
801
947
|
async execute(_id, params, signal: AbortSignal, _onUpdate, ctx) {
|
|
802
948
|
try {
|
|
803
949
|
const result = await run(params, signal, ctx);
|
|
804
|
-
return { content: [{ type: "text" as const, text: result.text }], details: result.details };
|
|
950
|
+
return { content: [{ type: "text" as const, text: result.text }], details: result.details, structuredContent: result.structured as JsonValue };
|
|
805
951
|
} catch (error) {
|
|
806
952
|
throw mainAgentError(error);
|
|
807
953
|
}
|
|
@@ -816,15 +962,18 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
816
962
|
"Workflow Respond",
|
|
817
963
|
"Approve or reject one pending workflow checkpoint or budget decision",
|
|
818
964
|
Type.Object({ runId: Type.String(), name: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), approved: Type.Boolean() }, { additionalProperties: false }),
|
|
965
|
+
WORKFLOW_RESPOND_OUTPUT,
|
|
819
966
|
async (params, signal, ctx) => {
|
|
820
967
|
if (params.proposalId) {
|
|
821
968
|
const result = await recovery.answerBudgetDecision(params.runId, params.proposalId, params.approved, false, ctx, signal);
|
|
822
|
-
if (!result) { const denied = { state: "budget_exhausted" as const, approved: false, reason: "proposal_not_pending" }; return { text: JSON.stringify(denied), details: denied }; }
|
|
823
|
-
|
|
969
|
+
if (!result) { const denied = { state: "budget_exhausted" as const, approved: false, reason: "proposal_not_pending" }; return { text: JSON.stringify(denied), details: denied, structured: denied }; }
|
|
970
|
+
const reason = params.approved ? "approved" : "rejected";
|
|
971
|
+
return { text: completionControlContent(result, params.runId), details: { ...result, reason }, structured: { ...completionControlValue(result, params.runId), reason } };
|
|
824
972
|
}
|
|
825
973
|
if (!params.name) throw new WorkflowError("INVALID_METADATA", "workflow_respond requires name or proposalId");
|
|
826
974
|
const accepted = await answerCheckpoint(params.runId, params.name, params.approved);
|
|
827
|
-
|
|
975
|
+
const details = { accepted, state: accepted ? "checkpoint_answered" : "not_pending", approved: params.approved, reason: "checkpoint" };
|
|
976
|
+
return { text: accepted ? "Checkpoint response accepted." : "Checkpoint is not awaiting a response.", details, structured: details };
|
|
828
977
|
},
|
|
829
978
|
);
|
|
830
979
|
registerControlTool(
|
|
@@ -832,9 +981,10 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
832
981
|
"Workflow Stop",
|
|
833
982
|
"Stop an active workflow run by ID",
|
|
834
983
|
Type.Object({ runId: Type.String() }, { additionalProperties: false }),
|
|
984
|
+
WORKFLOW_STOP_OUTPUT,
|
|
835
985
|
async (params) => {
|
|
836
986
|
const result = await stopWorkflowRun(params.runId);
|
|
837
|
-
return { text: JSON.stringify(result), details: result };
|
|
987
|
+
return { text: JSON.stringify(result), details: result, structured: result };
|
|
838
988
|
},
|
|
839
989
|
);
|
|
840
990
|
registerControlTool(
|
|
@@ -842,9 +992,10 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
842
992
|
"Workflow Status",
|
|
843
993
|
"Read a compact summary of a workflow run in the current project",
|
|
844
994
|
WORKFLOW_STATUS_PARAMETERS,
|
|
995
|
+
WORKFLOW_STATUS_OUTPUT,
|
|
845
996
|
async (params, _signal, ctx) => {
|
|
846
997
|
const result = await workflowStatusRun(params.runId, ctx);
|
|
847
|
-
return { text: JSON.stringify(result), details: result };
|
|
998
|
+
return { text: JSON.stringify(result), details: result, structured: result };
|
|
848
999
|
},
|
|
849
1000
|
);
|
|
850
1001
|
let catalogRegistered = false;
|
|
@@ -857,13 +1008,15 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
857
1008
|
if (!catalog.functions.length && !hasAliases && !hasSettings) return;
|
|
858
1009
|
pi.registerTool({
|
|
859
1010
|
name: "workflow_catalog",
|
|
1011
|
+
...workflowToolExposure("workflow_catalog", codemodeTools),
|
|
1012
|
+
outputSchema: WORKFLOW_CATALOG_OUTPUT,
|
|
860
1013
|
label: "Workflow Catalog",
|
|
861
1014
|
description: "List reusable workflow functions and model aliases; pass `name` to load one entry in full",
|
|
862
1015
|
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or model alias name for full detail" })) }, { additionalProperties: false }),
|
|
863
1016
|
async execute(_id, params = {}) {
|
|
864
1017
|
const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
|
|
865
1018
|
const result = params.name === undefined ? registry.catalogIndex(context) : registry.catalogDetail(params.name, context);
|
|
866
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
1019
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result, structuredContent: result as unknown as JsonValue };
|
|
867
1020
|
},
|
|
868
1021
|
renderCall(args, theme) {
|
|
869
1022
|
const title = theme.fg("toolTitle", theme.bold("workflow_catalog"));
|
|
@@ -890,7 +1043,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
890
1043
|
return { definitions, capture: (role: string, model: ModelSpec) => captured.capture(role, model) };
|
|
891
1044
|
};
|
|
892
1045
|
const activeSnapshotTools = (tools: readonly string[], active: ReadonlySet<string> | "session") => active === "session"
|
|
893
|
-
? new Set(tools.filter((tool) => pi
|
|
1046
|
+
? new Set(tools.filter((tool) => reachableTools(pi).includes(tool) && tool !== "workflow_catalog"))
|
|
894
1047
|
: new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
|
|
895
1048
|
const resumeLaunchPrologue = async (input: {
|
|
896
1049
|
snapshot: Readonly<LaunchSnapshot>;
|
|
@@ -904,7 +1057,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
904
1057
|
blockedAliasTargets?: Readonly<Record<string, string>>;
|
|
905
1058
|
withPreflight: boolean;
|
|
906
1059
|
}) => {
|
|
907
|
-
const active = new Set(pi
|
|
1060
|
+
const active = new Set(reachableTools(pi).filter((tool) => !INTERNAL_WORKFLOW_TOOLS.includes(tool)));
|
|
908
1061
|
const missing = input.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
909
1062
|
if (missing) throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
910
1063
|
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
@@ -971,7 +1124,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
971
1124
|
};
|
|
972
1125
|
};
|
|
973
1126
|
const recovery = createWorkflowRecovery({
|
|
974
|
-
pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, coordinateRunMutation, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue: resumeLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver: (content) => { deliver(pi, content); }, deliverTerminal: deliveryController.deliverTerminal, workflowToolUpdate, registry, modelSpec,
|
|
1127
|
+
pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, coordinateRunMutation, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue: resumeLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, toolForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver: (content) => { deliver(pi, content); }, deliverTerminal: deliveryController.deliverTerminal, workflowToolUpdate, registry, modelSpec,
|
|
975
1128
|
});
|
|
976
1129
|
const resumeSelectedWorkflow = async (runId: string, foreground: boolean, context: unknown, budgetPatch?: unknown): Promise<{ workflowName: string; state: "running" | "completed" | "awaiting_approval"; attached: boolean; value?: JsonValue }> => {
|
|
977
1130
|
const run = runs.get(runId);
|
|
@@ -1027,9 +1180,10 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1027
1180
|
"Workflow Retry",
|
|
1028
1181
|
"Retry a failed workflow run by replaying its completed structural operations",
|
|
1029
1182
|
WORKFLOW_RETRY_PARAMETERS,
|
|
1183
|
+
WORKFLOW_RECOVERY_OUTPUT,
|
|
1030
1184
|
async (params, signal, ctx) => {
|
|
1031
1185
|
const result = await recovery.retryWorkflowRun(params.runId, ctx, signal, params.foreground, params.expectedState);
|
|
1032
|
-
return { text: completionControlContent(result), details: result };
|
|
1186
|
+
return { text: completionControlContent(result), details: result, structured: completionControlValue(result) };
|
|
1033
1187
|
},
|
|
1034
1188
|
);
|
|
1035
1189
|
registerControlTool(
|
|
@@ -1037,9 +1191,10 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1037
1191
|
"Workflow Resume",
|
|
1038
1192
|
"Resume an exhausted workflow with unchanged or patched aggregate budgets",
|
|
1039
1193
|
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 }),
|
|
1194
|
+
WORKFLOW_RECOVERY_OUTPUT,
|
|
1040
1195
|
async (params, signal, ctx) => {
|
|
1041
1196
|
const result = await recovery.resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground, true, params.expectedState);
|
|
1042
|
-
return { text: completionControlContent(result), details: result };
|
|
1197
|
+
return { text: completionControlContent(result), details: result, structured: completionControlValue(result) };
|
|
1043
1198
|
},
|
|
1044
1199
|
);
|
|
1045
1200
|
const deliverStaleTerminal = async (store: RunStore, run: PersistedRun): Promise<void> => {
|
|
@@ -1141,6 +1296,8 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1141
1296
|
description: WORKFLOW_TOOL_DESCRIPTION,
|
|
1142
1297
|
promptSnippet: WORKFLOW_TOOL_PROMPT_SNIPPET,
|
|
1143
1298
|
parameters: WORKFLOW_TOOL_PARAMETERS,
|
|
1299
|
+
outputSchema: WORKFLOW_LAUNCH_OUTPUT,
|
|
1300
|
+
prepareLoadout: prepareScriptToolLoadout,
|
|
1144
1301
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1145
1302
|
let resolveDetached: ((result: ForegroundDetachResult) => void) | undefined;
|
|
1146
1303
|
let foregroundStore: RunStore | undefined;
|
|
@@ -1148,7 +1305,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1148
1305
|
const detachedResult = params.foreground ? new Promise<ForegroundDetachResult>((resolve) => { resolveDetached = resolve; }) : undefined;
|
|
1149
1306
|
const detachedToolResult = (run: PersistedRun) => {
|
|
1150
1307
|
const detached = { runId: run.id, state: "running" as const, detached: true as const };
|
|
1151
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(detached) }], details: { ...detached, run, preview: `Moved workflow ${run.id} to background.` } };
|
|
1308
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(detached) }], details: { ...detached, run, preview: `Moved workflow ${run.id} to background.` }, structuredContent: detached };
|
|
1152
1309
|
};
|
|
1153
1310
|
try {
|
|
1154
1311
|
const headless = object(ctx) && ctx.headless === true;
|
|
@@ -1161,7 +1318,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1161
1318
|
const inventory = modelInventory(rootModel, modelRegistry);
|
|
1162
1319
|
const knownModels = inventory.knownModels;
|
|
1163
1320
|
const availableModels = inventory.availableModels;
|
|
1164
|
-
const rootTools = pi
|
|
1321
|
+
const rootTools = reachableTools(pi).filter((name) => !INTERNAL_WORKFLOW_TOOLS.includes(name));
|
|
1165
1322
|
const trustedProject = projectTrusted(ctx);
|
|
1166
1323
|
const launchCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
1167
1324
|
const launch = workflowLaunchSettings(launchCwd, trustedProject, settingsPath, params.concurrency);
|
|
@@ -1176,6 +1333,8 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1176
1333
|
if (trustedProject && launch.resolution.project.extensionSettings !== undefined) registry.validateExtensionSettings(launch.resolution.project.extensionSettings, { source: "project", cwd: ctx.cwd, projectTrusted: true, settingsPath: launch.resolution.projectSettingsPath });
|
|
1177
1334
|
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);
|
|
1178
1335
|
const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames } = validated;
|
|
1336
|
+
const toolContext = scriptToolContext(ctx);
|
|
1337
|
+
validateScriptToolReferences(script, toolContext);
|
|
1179
1338
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
1180
1339
|
const runId = randomUUID();
|
|
1181
1340
|
const args = params.args ?? null;
|
|
@@ -1232,7 +1391,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1232
1391
|
runs.set(runId, runRecord);
|
|
1233
1392
|
if (params.foreground && onUpdate) onUpdate(workflowToolUpdate((await store.load()).run));
|
|
1234
1393
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch(), settings.extensionSettings);
|
|
1235
|
-
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, (role, model) => capturedRoles.capture(role, model)), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, () => runs.get(runId)?.foreground ?? foregroundAttached, ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(store, lifecycle, checked.metadata.name) }, store, runContext, registry, settings.extensionSettings), runController.signal);
|
|
1394
|
+
const execution = runWorkflow(script, args, withWorkflowFunctions({ tool: (identifier, toolArgs, signal, identity) => toolForRun(store, lifecycle, toolContext, identifier, toolArgs, signal, identity), shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId, (role, model) => capturedRoles.capture(role, model)), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, () => runs.get(runId)?.foreground ?? foregroundAttached, ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(store, lifecycle, checked.metadata.name) }, store, runContext, registry, settings.extensionSettings), runController.signal);
|
|
1236
1395
|
runRecord.execution = execution;
|
|
1237
1396
|
await eventPublisher.runStarted(store, checked.metadata);
|
|
1238
1397
|
const finish = execution.result.then(async (value) => {
|
|
@@ -1271,7 +1430,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1271
1430
|
}, async (error: unknown) => {
|
|
1272
1431
|
await deliveryController.deliverTerminal(store, deliverFailureContent(error), true);
|
|
1273
1432
|
});
|
|
1274
|
-
return { content: [{ type: "text" as const, text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
1433
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` }, structuredContent: { runId, state: "running" } };
|
|
1275
1434
|
}
|
|
1276
1435
|
void completion.then(async (result) => {
|
|
1277
1436
|
await deliveryController.deliverDetachedTerminal(toolCallId, completionContent("background", result));
|
|
@@ -1296,7 +1455,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1296
1455
|
}
|
|
1297
1456
|
deliveryController.foregroundDeliveries.delete(toolCallId);
|
|
1298
1457
|
const run = (await store.load()).run;
|
|
1299
|
-
return { content: [{ type: "text" as const, text: foregroundDelivery.content }, ...(foregroundDelivery.inlined ? [{ type: "text" as const, text: `Workflow run ID: ${runId}` }] : [])], details: { runId, value, run } };
|
|
1458
|
+
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 } };
|
|
1300
1459
|
} catch (error) {
|
|
1301
1460
|
if (params.foreground && foregroundStore && completionInstalled) {
|
|
1302
1461
|
const claim = await deliveryController.claimForegroundDelivery(foregroundStore, toolCallId);
|
|
@@ -1365,8 +1524,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1365
1524
|
state.workflowProgressComponent = workflowProgressBlock(currentProgress?.run ?? incoming, theme, currentProgress, async () => {
|
|
1366
1525
|
const active = runs.get(incoming.id);
|
|
1367
1526
|
const store = active?.store ?? new RunStore(incoming.cwd, incoming.sessionId, incoming.id, home);
|
|
1368
|
-
|
|
1369
|
-
return withLiveActivities(loaded.run);
|
|
1527
|
+
return withLiveActivities(await store.loadStatus());
|
|
1370
1528
|
}, () => { if (state.workflowProgress === currentProgress) requestRender(); }, undefined, state.workflowProgressFrozenAt);
|
|
1371
1529
|
}
|
|
1372
1530
|
state.workflowProgressComponent.setExpanded(expanded);
|
|
@@ -1376,7 +1534,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1376
1534
|
return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
|
|
1377
1535
|
},
|
|
1378
1536
|
};
|
|
1379
|
-
pi.registerTool(workflowTool);
|
|
1537
|
+
pi.registerTool({ ...workflowTool, ...workflowToolExposure(workflowTool.name, codemodeTools) });
|
|
1380
1538
|
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(); } });
|
|
1381
1539
|
pi.on("session_shutdown", async () => {
|
|
1382
1540
|
try {
|
|
@@ -1406,6 +1564,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1406
1564
|
type LiveAgentState = { session?: WorkflowAgentSession; prepared?: Readonly<PreparedAgentSession>; handoff?: LiveSessionHandoff; activity?: AgentActivity; lastEventAt?: number };
|
|
1407
1565
|
class LiveAgentRegistry {
|
|
1408
1566
|
readonly #byRun = new Map<string, Map<string, LiveAgentState>>();
|
|
1567
|
+
readonly #tools = new Map<string, Set<{ name: string; startedAt: number }>>();
|
|
1409
1568
|
get(runId: string, agentId: string): Readonly<LiveAgentState> | undefined { return this.#byRun.get(runId)?.get(agentId); }
|
|
1410
1569
|
setSession(runId: string, agentId: string, session?: WorkflowAgentSession): void {
|
|
1411
1570
|
if (session) this.#state(runId, agentId).session = session;
|
|
@@ -1422,17 +1581,26 @@ class LiveAgentRegistry {
|
|
|
1422
1581
|
setEventTime(runId: string, agentId: string, timestamp?: number): void {
|
|
1423
1582
|
if (timestamp !== undefined) this.#state(runId, agentId).lastEventAt = timestamp;
|
|
1424
1583
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1584
|
+
/** Marks a script tool call in flight until the returned callback runs. */
|
|
1585
|
+
toolStarted(runId: string, name: string): () => void {
|
|
1586
|
+
const call = { name, startedAt: Date.now() };
|
|
1587
|
+
const calls = this.#tools.get(runId) ?? new Set<{ name: string; startedAt: number }>();
|
|
1588
|
+
this.#tools.set(runId, calls.add(call));
|
|
1589
|
+
return () => { calls.delete(call); if (!calls.size && this.#tools.get(runId) === calls) this.#tools.delete(runId); };
|
|
1590
|
+
}
|
|
1591
|
+
deleteRun(runId: string): void { this.#byRun.delete(runId); this.#tools.delete(runId); }
|
|
1592
|
+
/** Overlays live activity, freshness, and script tool calls onto a persisted run; returns the input when nothing is live. */
|
|
1427
1593
|
overlay(run: PersistedRun): PersistedRun {
|
|
1594
|
+
const tools = this.#tools.get(run.id);
|
|
1595
|
+
const withTools = tools?.size ? { ...run, activeTools: [...tools].map((call) => ({ ...call })) } : run;
|
|
1428
1596
|
const agents = this.#byRun.get(run.id);
|
|
1429
|
-
if (!agents?.size) return
|
|
1597
|
+
if (!agents?.size) return withTools;
|
|
1430
1598
|
const next = run.agents.map((agent) => {
|
|
1431
1599
|
const live = agents.get(agent.id);
|
|
1432
1600
|
if (!live || (live.activity === undefined && live.lastEventAt === undefined)) return agent;
|
|
1433
1601
|
return { ...agent, ...(live.activity === undefined ? {} : { activity: live.activity }), ...(live.lastEventAt === undefined ? {} : { lastEventAt: live.lastEventAt }) };
|
|
1434
1602
|
});
|
|
1435
|
-
return next.some((agent, index) => agent !== run.agents[index]) ? { ...
|
|
1603
|
+
return next.some((agent, index) => agent !== run.agents[index]) ? { ...withTools, agents: next } : withTools;
|
|
1436
1604
|
}
|
|
1437
1605
|
#state(runId: string, agentId: string): LiveAgentState {
|
|
1438
1606
|
const agents = this.#byRun.get(runId) ?? new Map<string, LiveAgentState>();
|