@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/dist/src/host.js
CHANGED
|
@@ -12,17 +12,19 @@ import { registerWorkflowNavigator, uiHostCapabilities } from "./host-navigator.
|
|
|
12
12
|
import { acquireSessionLease, isPersistedRun, listPersistedSessionIds, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
|
|
13
13
|
import { retainTerminalRuns } from "./retention.js";
|
|
14
14
|
import { validateBudget, WorkflowBudgetRuntime } from "./budget.js";
|
|
15
|
-
import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue, mergeWorkflowExtensionSettings, modelAliasErrorName, modelCapability, object, parseModelReference, positiveInteger, sanitizeDisplayText, validateModelAliases } from "./utils.js";
|
|
16
|
-
import {
|
|
15
|
+
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";
|
|
16
|
+
import { loadCodemodeToolsSetting, loadSettings, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, validateCheckpoint, validateModelAliasAvailability, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowSettingsPath, workflowToolExposure } from "./validation.js";
|
|
17
|
+
import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
|
|
17
18
|
import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistryIfIdle, retainWorkflowRegistry } from "./registry.js";
|
|
18
|
-
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";
|
|
19
21
|
import backgroundWidget from "./background-widget.js";
|
|
20
22
|
import { showChangelogNotice } from "./changelog.js";
|
|
21
23
|
import { createTrajectoryRunLoader, createTrajectoryRunMetadataLoader, createTrajectorySubagentLoader, createTrajectorySubagentMetadataLoader, createTrajectoryTranscriptLoader } from "./trajectory.js";
|
|
22
24
|
import { getTrajectoryHost } from "./trajectory-host-handle.js";
|
|
23
25
|
import { getSubagentManager } from "./subagent-manager-handle.js";
|
|
24
|
-
import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_BLOCKED_EVENT, WorkflowError, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState, roleNameOf } from "./types.js";
|
|
25
|
-
import { SETTLED_AGENT_STATES, catalogResultValue, formatWorkflowCatalog, styledTextBlock, textBlock, workflowCatalogBlock, workflowControlCall, workflowControlResult, workflowProgressBlock,
|
|
26
|
+
import { HARD_TERMINAL_RUN_STATES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, THINKING_LEVELS, WORKFLOW_BLOCKED_EVENT, WorkflowError, isContextFileScope, isExternallyEndedRunState, isHardTerminalRunState, roleNameOf } from "./types.js";
|
|
27
|
+
import { SETTLED_AGENT_STATES, catalogResultValue, formatWorkflowCatalog, styledTextBlock, textBlock, workflowCatalogBlock, workflowControlCall, workflowControlResult, workflowProgressBlock, } from "./host-view.js";
|
|
26
28
|
import { DELIVERY_LIMIT_BYTES, ForegroundDeliveryController, markWorkflowFailureDiagnostics, WORKFLOW_LOG_ENTRY, completionDescriptor, completionDeliveryFromStore, createWorkflowFailureDiagnostics, failureDiagnosticsFrom, formatWorkflowFailure, formatWorkflowFailureDelivery, formatWorkflowFailureDeliveryFallback, formatWorkflowFailureDiagnostics, isWorkflowFailureDiagnostics, serializeWorkflowFailureDiagnostics, utf8Prefix, } from "./host-delivery.js";
|
|
27
29
|
export { agentBreadcrumb, agentBreadcrumbParts, formatBudgetStatus, formatNavigatorDashboard, formatNavigatorRun, formatStalledDuration, formatWorkflowPhaseDashboard, formatWorkflowProgress, navigatorAttentionSort, truncateWorkflowProgress, } from "./host-view.js";
|
|
28
30
|
export { buildWorkflowPhaseModel, buildWorkflowPhaseTree, navigateWorkflowPhaseTree, preserveWorkflowPhaseSelection, preserveWorkflowPhaseTreeSelection, workflowPhaseTreeInitialExpanded, workflowPhaseTreeVisibleNodes } from "./host-phases.js";
|
|
@@ -54,6 +56,13 @@ function mainAgentError(error) {
|
|
|
54
56
|
Object.assign(presented, typed);
|
|
55
57
|
return presented;
|
|
56
58
|
}
|
|
59
|
+
/** The structured counterpart of {@link completionControlContent}: the result without the persisted run, with the full value. */
|
|
60
|
+
function completionControlValue(result, controlRunId) {
|
|
61
|
+
const record = { ...(controlRunId === undefined ? {} : { runId: controlRunId }), ...result };
|
|
62
|
+
delete record.run;
|
|
63
|
+
delete record.completion;
|
|
64
|
+
return record;
|
|
65
|
+
}
|
|
57
66
|
function completionControlContent(result, controlRunId) {
|
|
58
67
|
const record = object(result) ? { ...result } : undefined;
|
|
59
68
|
if (record && controlRunId !== undefined && record.runId === undefined)
|
|
@@ -103,7 +112,7 @@ export function formatWorkflowPreview(args) {
|
|
|
103
112
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
104
113
|
}
|
|
105
114
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
106
|
-
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default";
|
|
115
|
+
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";
|
|
107
116
|
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.";
|
|
108
117
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
109
118
|
name: Type.String({ description: "Required non-empty workflow name" }),
|
|
@@ -118,8 +127,25 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
|
118
127
|
}, { additionalProperties: false });
|
|
119
128
|
export const WORKFLOW_STATUS_PARAMETERS = Type.Object({ runId: Type.String({ description: "Workflow run ID visible in the current project" }) }, { additionalProperties: false });
|
|
120
129
|
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" })) });
|
|
130
|
+
// Structured results codemode scripts receive instead of the JSON text; nested records stay loose so they cannot drift from persistence.
|
|
131
|
+
const WORKFLOW_ERROR_OUTPUT = Type.Object({ code: Type.String(), message: Type.String() });
|
|
132
|
+
const WORKFLOW_LAUNCH_OUTPUT = Type.Object({ runId: Type.String(), state: Type.String(), detached: Type.Optional(Type.Boolean()), value: Type.Optional(Type.Unknown()) });
|
|
133
|
+
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()) })) });
|
|
134
|
+
const WORKFLOW_STOP_OUTPUT = Type.Object({ runId: Type.String(), state: Type.String(), stopped: Type.Boolean(), reason: Type.Optional(Type.String()) });
|
|
135
|
+
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()) });
|
|
136
|
+
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()) });
|
|
137
|
+
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()) });
|
|
138
|
+
// The index without a name; with one, the function, the model alias, or { error }.
|
|
139
|
+
const WORKFLOW_CATALOG_OUTPUT = Type.Union([
|
|
140
|
+
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()) }),
|
|
141
|
+
Type.Object({ name: Type.String(), version: Type.String(), headline: Type.String(), description: Type.String(), input: Type.Unknown(), output: Type.Unknown() }),
|
|
142
|
+
WORKFLOW_CATALOG_ALIAS_OUTPUT,
|
|
143
|
+
Type.Object({ error: Type.Object({ code: Type.String(), name: Type.String(), message: Type.String() }) }),
|
|
144
|
+
]);
|
|
121
145
|
function workflowToolUpdate(run) {
|
|
122
|
-
|
|
146
|
+
//NOTE: renderers read details.run; the partial text stays O(1) so high-frequency updates never format the whole tree.
|
|
147
|
+
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
148
|
+
return { content: [{ type: "text", text: `Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done) [${run.state}]` }], details: { runId: run.id, run } };
|
|
123
149
|
}
|
|
124
150
|
function agentWithProgress(agent, progress) {
|
|
125
151
|
const next = { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls };
|
|
@@ -143,6 +169,7 @@ function deliver(pi, content) {
|
|
|
143
169
|
pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
144
170
|
}
|
|
145
171
|
const WORKFLOW_WARNING_ENTRY = "workflow-warning";
|
|
172
|
+
const PROGRESS_COALESCE_MS = 100;
|
|
146
173
|
function deliverWarning(pi, content) {
|
|
147
174
|
pi.appendEntry(WORKFLOW_WARNING_ENTRY, { message: content });
|
|
148
175
|
}
|
|
@@ -200,7 +227,45 @@ function modelInventory(root, registry) {
|
|
|
200
227
|
}
|
|
201
228
|
function resumeHostContext(ctx) {
|
|
202
229
|
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;
|
|
203
|
-
return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry, deliveryContext: completionContext(ctx) };
|
|
230
|
+
return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry, deliveryContext: completionContext(ctx), toolContext: scriptToolContext(ctx) };
|
|
231
|
+
}
|
|
232
|
+
// State is rewritten on every update, so large shell inputs and errors are kept as prefixes.
|
|
233
|
+
const SCRIPT_CALL_TEXT_LIMIT_BYTES = 16 * 1024;
|
|
234
|
+
function scriptCallInput(input) {
|
|
235
|
+
const text = JSON.stringify(input);
|
|
236
|
+
const bytes = Buffer.byteLength(text);
|
|
237
|
+
return bytes <= SCRIPT_CALL_TEXT_LIMIT_BYTES ? { input } : { input: utf8Prefix(text, SCRIPT_CALL_TEXT_LIMIT_BYTES), inputBytes: bytes };
|
|
238
|
+
}
|
|
239
|
+
/** Records a script call as started, replacing the record of an attempt that an interruption left unfinished. */
|
|
240
|
+
function startScriptCall(run, call) {
|
|
241
|
+
const recorded = { ...call, ...(run.phase === undefined ? {} : { phase: run.phase }) };
|
|
242
|
+
const calls = run.scriptCalls ?? [];
|
|
243
|
+
return { ...run, scriptCalls: calls.some(({ path }) => path === call.path) ? calls.map((current) => current.path === call.path ? recorded : current) : [...calls, recorded] };
|
|
244
|
+
}
|
|
245
|
+
function finishScriptCall(run, path, error) {
|
|
246
|
+
const finishedAt = Date.now();
|
|
247
|
+
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) };
|
|
248
|
+
}
|
|
249
|
+
/** Lists workflow model aliases in `/model` as virtual models; needs a Pi host with `registerVirtualModel`. */
|
|
250
|
+
function registerAliasVirtualModels(pi, names, settingsPath) {
|
|
251
|
+
const register = object(pi) ? asFn(pi.registerVirtualModel) : undefined;
|
|
252
|
+
if (!register)
|
|
253
|
+
return;
|
|
254
|
+
for (const name of names) {
|
|
255
|
+
const definition = {
|
|
256
|
+
provider: VIRTUAL_MODEL_PROVIDER, id: name, name: `${name} (workflow alias)`, thinkingLevels: THINKING_LEVELS,
|
|
257
|
+
route: (request, ctx) => {
|
|
258
|
+
// Settings are read per request so alias edits apply without a reload.
|
|
259
|
+
const aliases = resolveWorkflowSettings(ctx.cwd, projectTrusted(ctx), settingsPath).effective.modelAliases ?? {};
|
|
260
|
+
const target = resolveModelReference(name, aliases, new Set(ctx.modelRegistry.getAll().map((model) => `${model.provider}/${model.id}`)), settingsPath);
|
|
261
|
+
const model = ctx.modelRegistry.find(target.provider, target.model);
|
|
262
|
+
if (!model)
|
|
263
|
+
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${name} resolved to ${target.provider}/${target.model}`);
|
|
264
|
+
return { model, thinkingLevel: request.thinkingLevel };
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
Reflect.apply(register, pi, [definition]);
|
|
268
|
+
}
|
|
204
269
|
}
|
|
205
270
|
async function resolveLaunchAliases(registry, staticAliases, context, availableModels, knownModels, settingsPath) {
|
|
206
271
|
const dynamic = typeof registry.resolveModelAliases === "function" ? await registry.resolveModelAliases(context, new Set(Object.keys(staticAliases))) : {};
|
|
@@ -233,10 +298,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
233
298
|
});
|
|
234
299
|
let backgroundWidgetEnabled = true;
|
|
235
300
|
try {
|
|
236
|
-
|
|
301
|
+
const globalSettings = loadSettings(workflowSettingsPath(extensionAgentDir));
|
|
302
|
+
backgroundWidgetEnabled = globalSettings.backgroundWidget ?? true;
|
|
303
|
+
// Registered at load, before Pi resolves --model and restores the session model. Project aliases need project trust, which is unknown here.
|
|
304
|
+
registerAliasVirtualModels(pi, Object.keys(globalSettings.modelAliases ?? {}), workflowSettingsPath(extensionAgentDir));
|
|
237
305
|
}
|
|
238
306
|
catch { /* Keep the optional UI enabled; the launch path reports settings errors. */ }
|
|
239
307
|
const backgroundWidgetController = backgroundWidget(pi, backgroundWidgetEnabled);
|
|
308
|
+
const codemodeTools = loadCodemodeToolsSetting(extensionAgentDir);
|
|
240
309
|
const logBridge = (store, lifecycle, workflowName) => async (message) => {
|
|
241
310
|
const timestamp = Date.now();
|
|
242
311
|
const bounded = utf8Prefix(message, DELIVERY_LIMIT_BYTES);
|
|
@@ -406,7 +475,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
406
475
|
const trusted = projectTrusted(context);
|
|
407
476
|
const settings = resolveWorkflowSettings(cwd, trusted, workflowSettingsPath(extensionAgentDir)).effective.extensionSettings?.trajectory;
|
|
408
477
|
const port = settings?.port;
|
|
409
|
-
const themes = settings?.themes ?? false;
|
|
410
478
|
const loadRuns = createTrajectoryRunLoader(cwd, sessionId, home, overlayLiveRun);
|
|
411
479
|
const loadSubagents = createTrajectorySubagentLoader(cwd, sessionId, extensionAgentDir, overlayLiveSubagent);
|
|
412
480
|
const loadRunsMetadata = createTrajectoryRunMetadataLoader(cwd, sessionId, home, overlayLiveRun);
|
|
@@ -415,7 +483,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
415
483
|
const live = liveSubagents.get(subagentId);
|
|
416
484
|
return live?.status.sessionId === sessionId ? live.status.attemptDetails?.at(-1)?.session : undefined;
|
|
417
485
|
});
|
|
418
|
-
return { cwd, sessionId, ...(port === undefined ? {} : { port }),
|
|
486
|
+
return { cwd, sessionId, ...(port === undefined ? {} : { port }), loadRuns, loadSubagents, loadMetadata: async () => ({ runs: await loadRunsMetadata(), subagents: await loadSubagentsMetadata() }), loadTranscript, handleAction: (request) => trajectoryAction(request, context) };
|
|
419
487
|
};
|
|
420
488
|
const withLiveActivities = (run) => liveAgents.overlay(run);
|
|
421
489
|
/** Overlays the in-memory lifecycle state and budget usage of an active run onto its persisted record. */
|
|
@@ -549,7 +617,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
549
617
|
return readShellResult(replayed.value);
|
|
550
618
|
const shellStartedAt = Date.now();
|
|
551
619
|
let shellPhaseIndex = -1;
|
|
552
|
-
|
|
620
|
+
// Env values stay out of the record: they may carry secrets, and a shared trajectory publishes run state.
|
|
621
|
+
const call = { 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 };
|
|
622
|
+
let failure;
|
|
623
|
+
const started = await persistRunState(store, metadata, (run) => {
|
|
624
|
+
const current = startScriptCall(run, call);
|
|
553
625
|
const history = current.phaseHistory ?? [];
|
|
554
626
|
if (current.phase !== undefined)
|
|
555
627
|
shellPhaseIndex = current.phaseHistoryIndex ?? (history.length ? history.length - 1 : 0);
|
|
@@ -570,8 +642,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
570
642
|
await store.complete(path, result);
|
|
571
643
|
return result;
|
|
572
644
|
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
failure = asWorkflowError(error);
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
573
649
|
finally {
|
|
574
|
-
const stopped = await persistRunState(store, metadata, (
|
|
650
|
+
const stopped = await persistRunState(store, metadata, (run) => {
|
|
651
|
+
const current = finishScriptCall(run, path, failure);
|
|
575
652
|
const phaseActivities = [...(current.activeShellsByPhase ?? [])];
|
|
576
653
|
const phaseActivityIndex = phaseActivities.findIndex(({ phaseIndex }) => phaseIndex === shellPhaseIndex);
|
|
577
654
|
const phaseActivity = phaseActivities[phaseActivityIndex];
|
|
@@ -596,6 +673,64 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
596
673
|
await lifecycle.leave();
|
|
597
674
|
}
|
|
598
675
|
};
|
|
676
|
+
/** A script `tools.<id>(args)` call: replayed from the journal, otherwise run through the session's tool pipeline and journaled. */
|
|
677
|
+
const toolForRun = async (store, lifecycle, context, identifier, args, signal, identity) => {
|
|
678
|
+
await lifecycle.enter();
|
|
679
|
+
try {
|
|
680
|
+
const path = toolIdentityPath(identifier, args, identity);
|
|
681
|
+
const replayed = await store.replay(path);
|
|
682
|
+
if (replayed)
|
|
683
|
+
return replayed.value;
|
|
684
|
+
if (!context)
|
|
685
|
+
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`);
|
|
686
|
+
// Rendering is best-effort: it must never lose or mask a completed call.
|
|
687
|
+
const render = async () => { try {
|
|
688
|
+
const update = runs.get(store.runId)?.update;
|
|
689
|
+
if (update)
|
|
690
|
+
update(workflowToolUpdate(withLiveActivities((await store.load()).run)));
|
|
691
|
+
}
|
|
692
|
+
catch { /* best-effort progress */ } };
|
|
693
|
+
// Inspection writes must not prevent execution, mask errors, or reject a journaled success.
|
|
694
|
+
const record = async (update) => { try {
|
|
695
|
+
await store.updateState(update);
|
|
696
|
+
}
|
|
697
|
+
catch { /* best-effort call metadata */ } };
|
|
698
|
+
let value;
|
|
699
|
+
try {
|
|
700
|
+
const tool = scriptTool(context, identifier);
|
|
701
|
+
if (!tool)
|
|
702
|
+
fail("UNKNOWN_TOOL", `tools.${identifier} is not a tool this session can call`);
|
|
703
|
+
const cancelled = () => signal.aborted;
|
|
704
|
+
if (cancelled())
|
|
705
|
+
fail("CANCELLED", "Workflow cancelled");
|
|
706
|
+
await record((run) => startScriptCall(run, { kind: "tool", name: tool.name, path, startedAt: Date.now() }));
|
|
707
|
+
const finished = liveAgents.toolStarted(store.runId, tool.name);
|
|
708
|
+
try {
|
|
709
|
+
await render();
|
|
710
|
+
const outcome = await context.executeTool(tool.name, args, { signal });
|
|
711
|
+
if (cancelled())
|
|
712
|
+
fail("CANCELLED", "Workflow cancelled");
|
|
713
|
+
value = scriptToolValue(tool, outcome);
|
|
714
|
+
}
|
|
715
|
+
finally {
|
|
716
|
+
finished();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
// A stale context throws plain errors from ctx.tools; report them as tool failures.
|
|
721
|
+
const failure = error instanceof WorkflowError ? error : new WorkflowError("TOOL_FAILED", `tools.${identifier}: ${errorText(error)}`);
|
|
722
|
+
await record((run) => finishScriptCall(run, path, failure));
|
|
723
|
+
throw failure;
|
|
724
|
+
}
|
|
725
|
+
await store.complete(path, value);
|
|
726
|
+
await record((run) => finishScriptCall(run, path));
|
|
727
|
+
await render();
|
|
728
|
+
return value;
|
|
729
|
+
}
|
|
730
|
+
finally {
|
|
731
|
+
await lifecycle.leave();
|
|
732
|
+
}
|
|
733
|
+
};
|
|
599
734
|
const lifecycleFor = (store, state, metadata) => new RunLifecycle(state, async (next, previous, reason) => {
|
|
600
735
|
const run = runs.get(store.runId);
|
|
601
736
|
if (!run)
|
|
@@ -619,25 +754,54 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
619
754
|
const run = runs.get(runId);
|
|
620
755
|
if (!run)
|
|
621
756
|
throw new WorkflowError("INTERNAL_ERROR", `Unknown production run: ${runId}`);
|
|
757
|
+
const applyProgress = async (progress) => {
|
|
758
|
+
let runState;
|
|
759
|
+
if (progress.persist) {
|
|
760
|
+
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);
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
const loaded = await run.store.loadStatus();
|
|
764
|
+
if (!loaded.agents.some((agent) => agent.id === id))
|
|
765
|
+
return;
|
|
766
|
+
runState = { ...loaded, ...run.budget.snapshot(), agents: loaded.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) };
|
|
767
|
+
}
|
|
768
|
+
if (!runState.agents.some((agent) => agent.id === id))
|
|
769
|
+
return;
|
|
770
|
+
liveAgents.setActivity(runId, id, progress.activity);
|
|
771
|
+
liveAgents.setEventTime(runId, id, progress.lastEventAt);
|
|
772
|
+
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
773
|
+
};
|
|
774
|
+
// Display-only progress bursts coalesce to one trailing apply per window; persisted progress always applies immediately.
|
|
775
|
+
let deferred;
|
|
776
|
+
let deferredTimer;
|
|
777
|
+
let appliedAt = 0;
|
|
778
|
+
const cancelDeferredProgress = () => {
|
|
779
|
+
if (deferredTimer)
|
|
780
|
+
clearTimeout(deferredTimer);
|
|
781
|
+
deferredTimer = undefined;
|
|
782
|
+
deferred = undefined;
|
|
783
|
+
};
|
|
784
|
+
const onProgress = async (progress) => {
|
|
785
|
+
const now = Date.now();
|
|
786
|
+
if (progress.persist || (!deferredTimer && now - appliedAt >= PROGRESS_COALESCE_MS)) {
|
|
787
|
+
cancelDeferredProgress();
|
|
788
|
+
appliedAt = now;
|
|
789
|
+
await applyProgress(progress);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
deferred = progress;
|
|
793
|
+
deferredTimer ??= setTimeout(() => {
|
|
794
|
+
deferredTimer = undefined;
|
|
795
|
+
const next = deferred;
|
|
796
|
+
deferred = undefined;
|
|
797
|
+
if (!next)
|
|
798
|
+
return;
|
|
799
|
+
appliedAt = Date.now();
|
|
800
|
+
void applyProgress(next).catch(() => undefined);
|
|
801
|
+
}, Math.max(0, PROGRESS_COALESCE_MS - (now - appliedAt))).unref();
|
|
802
|
+
};
|
|
622
803
|
try {
|
|
623
804
|
const budget = run.budget.forAgent(id);
|
|
624
|
-
const onProgress = async (progress) => {
|
|
625
|
-
let runState;
|
|
626
|
-
if (progress.persist) {
|
|
627
|
-
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);
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
const loaded = await run.store.load();
|
|
631
|
-
if (!loaded.run.agents.some((agent) => agent.id === id))
|
|
632
|
-
return;
|
|
633
|
-
runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) };
|
|
634
|
-
}
|
|
635
|
-
if (!runState.agents.some((agent) => agent.id === id))
|
|
636
|
-
return;
|
|
637
|
-
liveAgents.setActivity(runId, id, progress.activity);
|
|
638
|
-
liveAgents.setEventTime(runId, id, progress.lastEventAt);
|
|
639
|
-
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
640
|
-
};
|
|
641
805
|
const onAttempt = async (attempt) => {
|
|
642
806
|
liveAgents.setSession(runId, id, attempt.liveSession);
|
|
643
807
|
liveAgents.setHandoff(runId, id, attempt);
|
|
@@ -654,6 +818,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
654
818
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
655
819
|
};
|
|
656
820
|
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); });
|
|
821
|
+
cancelDeferredProgress();
|
|
657
822
|
const before = (await run.store.load()).run;
|
|
658
823
|
await persistAgentAttempts(run.store, id, result.attempts);
|
|
659
824
|
const completed = (await run.store.load()).run;
|
|
@@ -665,6 +830,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
665
830
|
return result.value;
|
|
666
831
|
}
|
|
667
832
|
catch (error) {
|
|
833
|
+
cancelDeferredProgress();
|
|
668
834
|
liveAgents.setSession(runId, id);
|
|
669
835
|
const attempts = getAgentAttempts(error);
|
|
670
836
|
if (attempts?.length) {
|
|
@@ -850,16 +1016,18 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
850
1016
|
return decision;
|
|
851
1017
|
};
|
|
852
1018
|
};
|
|
853
|
-
const registerControlTool = (name, label, description, parameters, run) => {
|
|
1019
|
+
const registerControlTool = (name, label, description, parameters, outputSchema, run) => {
|
|
854
1020
|
pi.registerTool({
|
|
855
1021
|
name,
|
|
856
1022
|
label,
|
|
857
1023
|
description,
|
|
858
1024
|
parameters,
|
|
1025
|
+
outputSchema,
|
|
1026
|
+
...workflowToolExposure(name, codemodeTools),
|
|
859
1027
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
860
1028
|
try {
|
|
861
1029
|
const result = await run(params, signal, ctx);
|
|
862
|
-
return { content: [{ type: "text", text: result.text }], details: result.details };
|
|
1030
|
+
return { content: [{ type: "text", text: result.text }], details: result.details, structuredContent: result.structured };
|
|
863
1031
|
}
|
|
864
1032
|
catch (error) {
|
|
865
1033
|
throw mainAgentError(error);
|
|
@@ -869,27 +1037,29 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
869
1037
|
renderResult(result, options, theme, context) { return workflowCatalogBlock(workflowControlResult(name, context.args, result, options.expanded, theme, context.isError), options.expanded); },
|
|
870
1038
|
});
|
|
871
1039
|
};
|
|
872
|
-
registerControlTool("workflow_respond", "Workflow Respond", "Approve or reject one pending workflow checkpoint or budget decision", Type.Object({ runId: Type.String(), name: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), approved: Type.Boolean() }, { additionalProperties: false }), async (params, signal, ctx) => {
|
|
1040
|
+
registerControlTool("workflow_respond", "Workflow Respond", "Approve or reject one pending workflow checkpoint or budget decision", Type.Object({ runId: Type.String(), name: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), approved: Type.Boolean() }, { additionalProperties: false }), WORKFLOW_RESPOND_OUTPUT, async (params, signal, ctx) => {
|
|
873
1041
|
if (params.proposalId) {
|
|
874
1042
|
const result = await recovery.answerBudgetDecision(params.runId, params.proposalId, params.approved, false, ctx, signal);
|
|
875
1043
|
if (!result) {
|
|
876
1044
|
const denied = { state: "budget_exhausted", approved: false, reason: "proposal_not_pending" };
|
|
877
|
-
return { text: JSON.stringify(denied), details: denied };
|
|
1045
|
+
return { text: JSON.stringify(denied), details: denied, structured: denied };
|
|
878
1046
|
}
|
|
879
|
-
|
|
1047
|
+
const reason = params.approved ? "approved" : "rejected";
|
|
1048
|
+
return { text: completionControlContent(result, params.runId), details: { ...result, reason }, structured: { ...completionControlValue(result, params.runId), reason } };
|
|
880
1049
|
}
|
|
881
1050
|
if (!params.name)
|
|
882
1051
|
throw new WorkflowError("INVALID_METADATA", "workflow_respond requires name or proposalId");
|
|
883
1052
|
const accepted = await answerCheckpoint(params.runId, params.name, params.approved);
|
|
884
|
-
|
|
1053
|
+
const details = { accepted, state: accepted ? "checkpoint_answered" : "not_pending", approved: params.approved, reason: "checkpoint" };
|
|
1054
|
+
return { text: accepted ? "Checkpoint response accepted." : "Checkpoint is not awaiting a response.", details, structured: details };
|
|
885
1055
|
});
|
|
886
|
-
registerControlTool("workflow_stop", "Workflow Stop", "Stop an active workflow run by ID", Type.Object({ runId: Type.String() }, { additionalProperties: false }), async (params) => {
|
|
1056
|
+
registerControlTool("workflow_stop", "Workflow Stop", "Stop an active workflow run by ID", Type.Object({ runId: Type.String() }, { additionalProperties: false }), WORKFLOW_STOP_OUTPUT, async (params) => {
|
|
887
1057
|
const result = await stopWorkflowRun(params.runId);
|
|
888
|
-
return { text: JSON.stringify(result), details: result };
|
|
1058
|
+
return { text: JSON.stringify(result), details: result, structured: result };
|
|
889
1059
|
});
|
|
890
|
-
registerControlTool("workflow_status", "Workflow Status", "Read a compact summary of a workflow run in the current project", WORKFLOW_STATUS_PARAMETERS, async (params, _signal, ctx) => {
|
|
1060
|
+
registerControlTool("workflow_status", "Workflow Status", "Read a compact summary of a workflow run in the current project", WORKFLOW_STATUS_PARAMETERS, WORKFLOW_STATUS_OUTPUT, async (params, _signal, ctx) => {
|
|
891
1061
|
const result = await workflowStatusRun(params.runId, ctx);
|
|
892
|
-
return { text: JSON.stringify(result), details: result };
|
|
1062
|
+
return { text: JSON.stringify(result), details: result, structured: result };
|
|
893
1063
|
});
|
|
894
1064
|
let catalogRegistered = false;
|
|
895
1065
|
let sessionStarted = false;
|
|
@@ -903,13 +1073,15 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
903
1073
|
return;
|
|
904
1074
|
pi.registerTool({
|
|
905
1075
|
name: "workflow_catalog",
|
|
1076
|
+
...workflowToolExposure("workflow_catalog", codemodeTools),
|
|
1077
|
+
outputSchema: WORKFLOW_CATALOG_OUTPUT,
|
|
906
1078
|
label: "Workflow Catalog",
|
|
907
1079
|
description: "List reusable workflow functions and model aliases; pass `name` to load one entry in full",
|
|
908
1080
|
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or model alias name for full detail" })) }, { additionalProperties: false }),
|
|
909
1081
|
async execute(_id, params = {}) {
|
|
910
1082
|
const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
|
|
911
1083
|
const result = params.name === undefined ? registry.catalogIndex(context) : registry.catalogDetail(params.name, context);
|
|
912
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
1084
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result, structuredContent: result };
|
|
913
1085
|
},
|
|
914
1086
|
renderCall(args, theme) {
|
|
915
1087
|
const title = theme.fg("toolTitle", theme.bold("workflow_catalog"));
|
|
@@ -937,10 +1109,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
937
1109
|
return { definitions, capture: (role, model) => captured.capture(role, model) };
|
|
938
1110
|
};
|
|
939
1111
|
const activeSnapshotTools = (tools, active) => active === "session"
|
|
940
|
-
? new Set(tools.filter((tool) => pi
|
|
1112
|
+
? new Set(tools.filter((tool) => reachableTools(pi).includes(tool) && tool !== "workflow_catalog"))
|
|
941
1113
|
: new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
|
|
942
1114
|
const resumeLaunchPrologue = async (input) => {
|
|
943
|
-
const active = new Set(pi
|
|
1115
|
+
const active = new Set(reachableTools(pi).filter((tool) => !INTERNAL_WORKFLOW_TOOLS.includes(tool)));
|
|
944
1116
|
const missing = input.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
945
1117
|
if (missing)
|
|
946
1118
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
@@ -1019,7 +1191,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1019
1191
|
};
|
|
1020
1192
|
};
|
|
1021
1193
|
const recovery = createWorkflowRecovery({
|
|
1022
|
-
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,
|
|
1194
|
+
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,
|
|
1023
1195
|
});
|
|
1024
1196
|
const resumeSelectedWorkflow = async (runId, foreground, context, budgetPatch) => {
|
|
1025
1197
|
const run = runs.get(runId);
|
|
@@ -1086,13 +1258,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1086
1258
|
const completed = await recovery.coldResumeRun(run, hasUI, ui, projectTrusted(context), recoveryContext, foreground, foreground);
|
|
1087
1259
|
return completed ? { workflowName: run.metadata.name, state: "completed", attached: false, value: completed.value } : { workflowName: run.metadata.name, state: "running", attached: false };
|
|
1088
1260
|
};
|
|
1089
|
-
registerControlTool("workflow_retry", "Workflow Retry", "Retry a failed workflow run by replaying its completed structural operations", WORKFLOW_RETRY_PARAMETERS, async (params, signal, ctx) => {
|
|
1261
|
+
registerControlTool("workflow_retry", "Workflow Retry", "Retry a failed workflow run by replaying its completed structural operations", WORKFLOW_RETRY_PARAMETERS, WORKFLOW_RECOVERY_OUTPUT, async (params, signal, ctx) => {
|
|
1090
1262
|
const result = await recovery.retryWorkflowRun(params.runId, ctx, signal, params.foreground, params.expectedState);
|
|
1091
|
-
return { text: completionControlContent(result), details: result };
|
|
1263
|
+
return { text: completionControlContent(result), details: result, structured: completionControlValue(result) };
|
|
1092
1264
|
});
|
|
1093
|
-
registerControlTool("workflow_resume", "Workflow Resume", "Resume an exhausted workflow with unchanged or patched aggregate budgets", 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 }), async (params, signal, ctx) => {
|
|
1265
|
+
registerControlTool("workflow_resume", "Workflow Resume", "Resume an exhausted workflow with unchanged or patched aggregate budgets", 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 }), WORKFLOW_RECOVERY_OUTPUT, async (params, signal, ctx) => {
|
|
1094
1266
|
const result = await recovery.resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground, true, params.expectedState);
|
|
1095
|
-
return { text: completionControlContent(result), details: result };
|
|
1267
|
+
return { text: completionControlContent(result), details: result, structured: completionControlValue(result) };
|
|
1096
1268
|
});
|
|
1097
1269
|
const deliverStaleTerminal = async (store, run) => {
|
|
1098
1270
|
if (!((run.delivery?.mode === "foreground" && run.delivery.state === "attached") || (run.delivery?.mode === "background" && run.delivery.state === "pending")))
|
|
@@ -1234,6 +1406,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1234
1406
|
description: WORKFLOW_TOOL_DESCRIPTION,
|
|
1235
1407
|
promptSnippet: WORKFLOW_TOOL_PROMPT_SNIPPET,
|
|
1236
1408
|
parameters: WORKFLOW_TOOL_PARAMETERS,
|
|
1409
|
+
outputSchema: WORKFLOW_LAUNCH_OUTPUT,
|
|
1410
|
+
prepareLoadout: prepareScriptToolLoadout,
|
|
1237
1411
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1238
1412
|
let resolveDetached;
|
|
1239
1413
|
let foregroundStore;
|
|
@@ -1241,7 +1415,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1241
1415
|
const detachedResult = params.foreground ? new Promise((resolve) => { resolveDetached = resolve; }) : undefined;
|
|
1242
1416
|
const detachedToolResult = (run) => {
|
|
1243
1417
|
const detached = { runId: run.id, state: "running", detached: true };
|
|
1244
|
-
return { content: [{ type: "text", text: JSON.stringify(detached) }], details: { ...detached, run, preview: `Moved workflow ${run.id} to background.` } };
|
|
1418
|
+
return { content: [{ type: "text", text: JSON.stringify(detached) }], details: { ...detached, run, preview: `Moved workflow ${run.id} to background.` }, structuredContent: detached };
|
|
1245
1419
|
};
|
|
1246
1420
|
try {
|
|
1247
1421
|
const headless = object(ctx) && ctx.headless === true;
|
|
@@ -1255,7 +1429,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1255
1429
|
const inventory = modelInventory(rootModel, modelRegistry);
|
|
1256
1430
|
const knownModels = inventory.knownModels;
|
|
1257
1431
|
const availableModels = inventory.availableModels;
|
|
1258
|
-
const rootTools = pi
|
|
1432
|
+
const rootTools = reachableTools(pi).filter((name) => !INTERNAL_WORKFLOW_TOOLS.includes(name));
|
|
1259
1433
|
const trustedProject = projectTrusted(ctx);
|
|
1260
1434
|
const launchCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
|
|
1261
1435
|
const launch = workflowLaunchSettings(launchCwd, trustedProject, settingsPath, params.concurrency);
|
|
@@ -1275,6 +1449,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1275
1449
|
registry.validateExtensionSettings(launch.resolution.project.extensionSettings, { source: "project", cwd: ctx.cwd, projectTrusted: true, settingsPath: launch.resolution.projectSettingsPath });
|
|
1276
1450
|
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);
|
|
1277
1451
|
const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames } = validated;
|
|
1452
|
+
const toolContext = scriptToolContext(ctx);
|
|
1453
|
+
validateScriptToolReferences(script, toolContext);
|
|
1278
1454
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
1279
1455
|
const runId = randomUUID();
|
|
1280
1456
|
const args = params.args ?? null;
|
|
@@ -1341,7 +1517,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1341
1517
|
if (params.foreground && onUpdate)
|
|
1342
1518
|
onUpdate(workflowToolUpdate((await store.load()).run));
|
|
1343
1519
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch(), settings.extensionSettings);
|
|
1344
|
-
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);
|
|
1520
|
+
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);
|
|
1345
1521
|
runRecord.execution = execution;
|
|
1346
1522
|
await eventPublisher.runStarted(store, checked.metadata);
|
|
1347
1523
|
const finish = execution.result.then(async (value) => {
|
|
@@ -1382,7 +1558,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1382
1558
|
}, async (error) => {
|
|
1383
1559
|
await deliveryController.deliverTerminal(store, deliverFailureContent(error), true);
|
|
1384
1560
|
});
|
|
1385
|
-
return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
1561
|
+
return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` }, structuredContent: { runId, state: "running" } };
|
|
1386
1562
|
}
|
|
1387
1563
|
void completion.then(async (result) => {
|
|
1388
1564
|
await deliveryController.deliverDetachedTerminal(toolCallId, completionContent("background", result));
|
|
@@ -1408,7 +1584,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1408
1584
|
}
|
|
1409
1585
|
deliveryController.foregroundDeliveries.delete(toolCallId);
|
|
1410
1586
|
const run = (await store.load()).run;
|
|
1411
|
-
return { content: [{ type: "text", text: foregroundDelivery.content }, ...(foregroundDelivery.inlined ? [{ type: "text", text: `Workflow run ID: ${runId}` }] : [])], details: { runId, value, run } };
|
|
1587
|
+
return { content: [{ type: "text", text: foregroundDelivery.content }, ...(foregroundDelivery.inlined ? [{ type: "text", text: `Workflow run ID: ${runId}` }] : [])], details: { runId, value, run }, structuredContent: { runId, state: "completed", value } };
|
|
1412
1588
|
}
|
|
1413
1589
|
catch (error) {
|
|
1414
1590
|
if (params.foreground && foregroundStore && completionInstalled) {
|
|
@@ -1484,8 +1660,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1484
1660
|
state.workflowProgressComponent = workflowProgressBlock(currentProgress?.run ?? incoming, theme, currentProgress, async () => {
|
|
1485
1661
|
const active = runs.get(incoming.id);
|
|
1486
1662
|
const store = active?.store ?? new RunStore(incoming.cwd, incoming.sessionId, incoming.id, home);
|
|
1487
|
-
|
|
1488
|
-
return withLiveActivities(loaded.run);
|
|
1663
|
+
return withLiveActivities(await store.loadStatus());
|
|
1489
1664
|
}, () => { if (state.workflowProgress === currentProgress)
|
|
1490
1665
|
requestRender(); }, undefined, state.workflowProgressFrozenAt);
|
|
1491
1666
|
}
|
|
@@ -1496,7 +1671,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1496
1671
|
return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
|
|
1497
1672
|
},
|
|
1498
1673
|
};
|
|
1499
|
-
pi.registerTool(workflowTool);
|
|
1674
|
+
pi.registerTool({ ...workflowTool, ...workflowToolExposure(workflowTool.name, codemodeTools) });
|
|
1500
1675
|
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)
|
|
1501
1676
|
backgroundWidgetController.suspend();
|
|
1502
1677
|
else
|
|
@@ -1528,6 +1703,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
1528
1703
|
}
|
|
1529
1704
|
class LiveAgentRegistry {
|
|
1530
1705
|
#byRun = new Map();
|
|
1706
|
+
#tools = new Map();
|
|
1531
1707
|
get(runId, agentId) { return this.#byRun.get(runId)?.get(agentId); }
|
|
1532
1708
|
setSession(runId, agentId, session) {
|
|
1533
1709
|
if (session)
|
|
@@ -1554,19 +1730,29 @@ class LiveAgentRegistry {
|
|
|
1554
1730
|
if (timestamp !== undefined)
|
|
1555
1731
|
this.#state(runId, agentId).lastEventAt = timestamp;
|
|
1556
1732
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1733
|
+
/** Marks a script tool call in flight until the returned callback runs. */
|
|
1734
|
+
toolStarted(runId, name) {
|
|
1735
|
+
const call = { name, startedAt: Date.now() };
|
|
1736
|
+
const calls = this.#tools.get(runId) ?? new Set();
|
|
1737
|
+
this.#tools.set(runId, calls.add(call));
|
|
1738
|
+
return () => { calls.delete(call); if (!calls.size && this.#tools.get(runId) === calls)
|
|
1739
|
+
this.#tools.delete(runId); };
|
|
1740
|
+
}
|
|
1741
|
+
deleteRun(runId) { this.#byRun.delete(runId); this.#tools.delete(runId); }
|
|
1742
|
+
/** Overlays live activity, freshness, and script tool calls onto a persisted run; returns the input when nothing is live. */
|
|
1559
1743
|
overlay(run) {
|
|
1744
|
+
const tools = this.#tools.get(run.id);
|
|
1745
|
+
const withTools = tools?.size ? { ...run, activeTools: [...tools].map((call) => ({ ...call })) } : run;
|
|
1560
1746
|
const agents = this.#byRun.get(run.id);
|
|
1561
1747
|
if (!agents?.size)
|
|
1562
|
-
return
|
|
1748
|
+
return withTools;
|
|
1563
1749
|
const next = run.agents.map((agent) => {
|
|
1564
1750
|
const live = agents.get(agent.id);
|
|
1565
1751
|
if (!live || (live.activity === undefined && live.lastEventAt === undefined))
|
|
1566
1752
|
return agent;
|
|
1567
1753
|
return { ...agent, ...(live.activity === undefined ? {} : { activity: live.activity }), ...(live.lastEventAt === undefined ? {} : { lastEventAt: live.lastEventAt }) };
|
|
1568
1754
|
});
|
|
1569
|
-
return next.some((agent, index) => agent !== run.agents[index]) ? { ...
|
|
1755
|
+
return next.some((agent, index) => agent !== run.agents[index]) ? { ...withTools, agents: next } : withTools;
|
|
1570
1756
|
}
|
|
1571
1757
|
#state(runId, agentId) {
|
|
1572
1758
|
const agents = this.#byRun.get(runId) ?? new Map();
|
package/dist/src/index.d.ts
CHANGED
|
@@ -6,12 +6,14 @@ export * from "./utils.js";
|
|
|
6
6
|
export { canonicalPath, sameFilesystemPath } from "./paths.js";
|
|
7
7
|
export * from "./budget.js";
|
|
8
8
|
export * from "./validation.js";
|
|
9
|
+
export * from "./roles.js";
|
|
9
10
|
export * from "./registry.js";
|
|
10
11
|
export * from "./execution.js";
|
|
11
12
|
export * from "./host.js";
|
|
12
13
|
export * from "./workflow-artifacts.js";
|
|
13
14
|
export { formatCost } from "./background-widget.js";
|
|
14
|
-
export { agentActionLabels, formatAgentDetail, navigatorAttentionSortByState, themeWorkflowProgressStyles, visibleStandaloneAgentAttemptActions } from "./host-view.js";
|
|
15
|
+
export { agentActionLabels, formatAgentDetail, formatAgentError, formatNavigatorColumns, formatWorkflowRuntime, navigatorAttentionSortByState, PLAIN_WORKFLOW_PROGRESS_STYLES, progressStyleForState, runStateGlyph, themeWorkflowProgressStyles, visibleStandaloneAgentAttemptActions } from "./host-view.js";
|
|
16
|
+
export { createInlineConfirm, workflowKeyLabel, workflowKeyMatches } from "./host-navigator.js";
|
|
15
17
|
export type { AgentDetailPresentation, WorkflowProgressStyles } from "./host-view.js";
|
|
16
18
|
export { default } from "./host.js";
|
|
17
19
|
export { acquireSessionLease, hasLiveSessionLease, listPersistedSessionIds, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
|