pi-extensible-workflows 1.0.0 → 1.0.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/dist/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { fork } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { basename, dirname, extname, join, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
@@ -12,11 +12,10 @@ import { Value } from "typebox/value";
12
12
  import { copyToClipboard, getAgentDir, parseFrontmatter, highlightCode, SessionManager, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
13
13
  import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
14
14
  import { transcriptLines } from "./session-inspector.js";
15
- import { acquireSessionLease, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
15
+ import { acquireSessionLease, atomicWriteFile, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
16
16
  export const RUN_STATES = ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"];
17
17
  export const AGENT_STATES = ["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"];
18
- export const WORKFLOW_ASYNC_STARTED_EVENT = "workflow:async-started";
19
- export const WORKFLOW_ASYNC_COMPLETE_EVENT = "workflow:async-complete";
18
+ export const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"];
20
19
  export const WORKFLOW_RUN_STARTED_EVENT = "workflow:run-started";
21
20
  export const WORKFLOW_RUN_RESUMED_EVENT = "workflow:run-resumed";
22
21
  export const WORKFLOW_RUN_STATE_CHANGED_EVENT = "workflow:run-state-changed";
@@ -188,6 +187,7 @@ export class RunLifecycle {
188
187
  export const DEFAULT_SETTINGS = Object.freeze({ concurrency: 8 });
189
188
  function fail(code, message) { throw new WorkflowError(code, message); }
190
189
  function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
190
+ export { object as isObject };
191
191
  function nonNegativeInteger(value) { return Number.isInteger(value) && value >= 0; }
192
192
  function nonNegativeFinite(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
193
193
  export function validateBudget(value) {
@@ -562,9 +562,7 @@ export function saveModelAliases(path = workflowSettingsPath(), aliases = {}) {
562
562
  throw error;
563
563
  }
564
564
  mkdirSync(dirname(path), { recursive: true });
565
- const temporary = `${path}.${String(process.pid)}.${randomUUID()}.tmp`;
566
- writeFileSync(temporary, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, { mode: 0o600 });
567
- renameSync(temporary, path);
565
+ atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
568
566
  }
569
567
  export function parseRoleMarkdown(content, strict = false, rolePath) {
570
568
  if (!strict) {
@@ -700,20 +698,32 @@ function parseWorkflow(script) {
700
698
  function astNode(value) {
701
699
  return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
702
700
  }
701
+ function astChildren(node) {
702
+ const children = [];
703
+ for (const value of Object.values(node)) {
704
+ if (Array.isArray(value)) {
705
+ for (const child of value)
706
+ if (astNode(child))
707
+ children.push(child);
708
+ }
709
+ else if (astNode(value))
710
+ children.push(value);
711
+ }
712
+ return children;
713
+ }
714
+ function workflowCallKind(node) {
715
+ if (node.type !== "CallExpression" || node.callee.type !== "Identifier")
716
+ return undefined;
717
+ const kind = node.callee.name;
718
+ return WORKFLOW_CALL_KINDS.includes(kind) ? kind : undefined;
719
+ }
703
720
  function workflowCalls(program) {
704
721
  const calls = [];
705
722
  const visit = (node) => {
706
- if (node.type === "CallExpression" && node.callee.type === "Identifier" && ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"].includes(node.callee.name))
723
+ if (workflowCallKind(node))
707
724
  calls.push(node);
708
- for (const value of Object.values(node)) {
709
- if (Array.isArray(value)) {
710
- for (const child of value)
711
- if (astNode(child))
712
- visit(child);
713
- }
714
- else if (astNode(value))
715
- visit(value);
716
- }
725
+ for (const child of astChildren(node))
726
+ visit(child);
717
727
  };
718
728
  visit(program);
719
729
  return calls.sort((left, right) => left.start - right.start);
@@ -728,9 +738,9 @@ function workflowCallsWithStructure(program) {
728
738
  if (scope?.key === null && key)
729
739
  current = { ...current, structure: [...current.structure.slice(0, -1), { ...scope, key }] };
730
740
  }
731
- if (node.type === "CallExpression" && node.callee.type === "Identifier" && ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"].includes(node.callee.name)) {
741
+ const operation = workflowCallKind(node);
742
+ if (operation) {
732
743
  const call = node;
733
- const operation = call.callee.name;
734
744
  const execution = operation === "parallel" ? "parallel" : operation === "pipeline" ? "sequential" : current.execution;
735
745
  calls.push({ call, execution, structure: current.structure });
736
746
  for (const [index, argument] of call.arguments.entries()) {
@@ -741,15 +751,8 @@ function workflowCallsWithStructure(program) {
741
751
  }
742
752
  return;
743
753
  }
744
- for (const value of Object.values(node)) {
745
- if (Array.isArray(value)) {
746
- for (const child of value)
747
- if (astNode(child))
748
- visit(child, current);
749
- }
750
- else if (astNode(value))
751
- visit(value, current);
752
- }
754
+ for (const child of astChildren(node))
755
+ visit(child, current);
753
756
  };
754
757
  visit(program, { execution: "sequential", structure: [] });
755
758
  return calls.sort((left, right) => left.call.start - right.call.start);
@@ -762,22 +765,15 @@ function validateDirectPrimitiveReferences(program, name) {
762
765
  if (!directCall && !propertyKey)
763
766
  fail("INVALID_METADATA", `${name} calls must use a direct ${name}(...) call; aliases and indirect calls are unsupported`);
764
767
  }
765
- for (const value of Object.values(node)) {
766
- if (Array.isArray(value)) {
767
- for (const child of value)
768
- if (astNode(child))
769
- visit(child, node);
770
- }
771
- else if (astNode(value))
772
- visit(value, node);
773
- }
768
+ for (const child of astChildren(node))
769
+ visit(child, node);
774
770
  };
775
771
  visit(program);
776
772
  }
777
773
  function hasIdentifier(node, name) {
778
774
  if (node.type === "Identifier" && node.name === name)
779
775
  return true;
780
- return Object.values(node).some((value) => Array.isArray(value) ? value.some((child) => astNode(child) && hasIdentifier(child, name)) : astNode(value) && hasIdentifier(value, name));
776
+ return astChildren(node).some((child) => hasIdentifier(child, name));
781
777
  }
782
778
  const INTERNAL_AGENT_NAME = "__pi_extensible_workflows_agent";
783
779
  const INTERNAL_CONVERSATION_NAME = "__pi_extensible_workflows_conversation";
@@ -1156,7 +1152,7 @@ export class WorkflowRegistry {
1156
1152
  fail("RESULT_INVALID", `Invalid replay for ${name}`);
1157
1153
  return structuredClone(replayed);
1158
1154
  }
1159
- const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, agent: context.agent, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
1155
+ const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, invoke: context.invoke, agent: context.agent, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
1160
1156
  if (!jsonValue(result) || !Value.Check(fn.output, result))
1161
1157
  fail("RESULT_INVALID", `Invalid output from ${name}`);
1162
1158
  const stored = structuredClone(result);
@@ -1333,8 +1329,6 @@ export function createLaunchSnapshot(input) {
1333
1329
  return deepFreeze(structuredClone({ ...input, identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION }));
1334
1330
  }
1335
1331
  export function loadLaunchSnapshot(input) {
1336
- if (object(input.settings) && Object.prototype.hasOwnProperty.call(input.settings, "maxAgentLaunches"))
1337
- fail("RESUME_INCOMPATIBLE", "Persisted workflow settings contain removed maxAgentLaunches");
1338
1332
  return deepFreeze(structuredClone(input));
1339
1333
  }
1340
1334
  export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
@@ -2121,30 +2115,17 @@ class WorkflowEventPublisher {
2121
2115
  seen.add(this.budgetKey(event));
2122
2116
  this.#budgetEvents.set(runId, seen);
2123
2117
  }
2124
- async runStarted(store, metadata, background) {
2125
- await this.#publish(store, metadata, WORKFLOW_RUN_STARTED_EVENT, {});
2126
- if (background)
2127
- await this.legacyStarted(store, metadata);
2128
- }
2129
- async legacyStarted(store, metadata) { await this.#publish(store, metadata, WORKFLOW_ASYNC_STARTED_EVENT, { id: store.runId, runId: store.runId, pid: process.pid, sessionId: store.sessionId, asyncDir: store.directory, agent: metadata.name }); }
2118
+ async runStarted(store, metadata) { await this.#publish(store, metadata, WORKFLOW_RUN_STARTED_EVENT, {}); }
2130
2119
  async runResumed(store, metadata) { await this.#publish(store, metadata, WORKFLOW_RUN_RESUMED_EVENT, {}); }
2131
2120
  async runState(store, metadata, previousState, state, reason) {
2132
2121
  await this.#publish(store, metadata, WORKFLOW_RUN_STATE_CHANGED_EVENT, { previousState, state, ...(reason ? { reason } : {}), ...(ERROR_CODES.includes(reason) ? { errorCode: reason } : {}) });
2133
2122
  if ((previousState === "paused" || previousState === "interrupted" || previousState === "budget_exhausted") && state === "running")
2134
2123
  await this.runResumed(store, metadata);
2135
2124
  }
2136
- async runCompleted(store, metadata, resultPath, background) {
2137
- await this.#publish(store, metadata, WORKFLOW_RUN_COMPLETED_EVENT, { resultPath });
2138
- if (background)
2139
- await this.#publish(store, metadata, WORKFLOW_ASYNC_COMPLETE_EVENT, { id: store.runId, runId: store.runId, sessionId: store.sessionId, asyncDir: store.directory, success: true, state: "complete" });
2140
- }
2141
- async runFailed(store, metadata, error, background, state) {
2125
+ async runCompleted(store, metadata, resultPath) { await this.#publish(store, metadata, WORKFLOW_RUN_COMPLETED_EVENT, { resultPath }); }
2126
+ async runFailed(store, metadata, error, state) {
2142
2127
  if (state === "failed")
2143
2128
  await this.#publish(store, metadata, WORKFLOW_RUN_FAILED_EVENT, { error: safeEventError(error) });
2144
- if (background) {
2145
- const legacyState = state === "interrupted" ? "failed" : state;
2146
- await this.#publish(store, metadata, WORKFLOW_ASYNC_COMPLETE_EVENT, { id: store.runId, runId: store.runId, sessionId: store.sessionId, asyncDir: store.directory, success: false, state: legacyState, ...(state === "stopped" ? { stopped: true } : {}), ...(legacyState === "budget_exhausted" || legacyState === "failed" ? { error: safeEventError(error).message } : {}) });
2147
- }
2148
2129
  }
2149
2130
  async agentState(store, metadata, previous, agent) {
2150
2131
  await this.#publish(store, metadata, WORKFLOW_AGENT_STATE_CHANGED_EVENT, { agentId: agent.id, displayLabel: agent.label ?? agent.name, ...(agent.role ? { role: agent.role } : {}), structuralPath: [...(agent.structuralPath ?? [])], ...(agent.parentId ? { parentId: agent.parentId } : {}), ...(agent.parentBreadcrumb ? { parentBreadcrumb: agent.parentBreadcrumb } : {}), ...(agent.worktreeOwner ? { worktreeOwner: agent.worktreeOwner } : {}), ...(previous ? { previousState: previous.state } : {}), state: agent.state, attempt: agent.attempts });
@@ -2321,46 +2302,100 @@ function nextNamedOccurrence(counters, label) {
2321
2302
  function withWorkflowFunctions(bridge, store, runContext, variables, registry) {
2322
2303
  const functionAgentOccurrences = new Map();
2323
2304
  const functionWorktreeOccurrences = new Map();
2324
- return { ...bridge, functions: registry.globals(), variables, function: async (name, input, path, signal, worktreeOwner, structuralPath = []) => {
2325
- const replayed = await store.replay(path);
2326
- let stored;
2327
- const sideEffects = [];
2328
- const context = {
2329
- run: runContext,
2330
- agent: async (...args) => {
2331
- if (!bridge.agent || typeof args[0] !== "string")
2332
- fail("AGENT_FAILED", "No agent bridge is available");
2333
- const options = validateAgentOptions(args[1] === undefined ? {} : args[1]);
2334
- const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
2335
- const inherited = inheritedHostAgentPath.getStore() ?? [];
2336
- const key = `${path}\0${JSON.stringify(inherited)}`;
2337
- const occurrence = (functionAgentOccurrences.get(key) ?? 0) + 1;
2338
- functionAgentOccurrences.set(key, occurrence);
2339
- return bridge.agent(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb: name, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
2340
- },
2341
- prompt: workflowPrompt,
2342
- parallel: (...args) => hostParallel(args[0], args[1]),
2343
- pipeline: (...args) => hostPipeline(args[0], args[1], args[2]),
2344
- withWorktree: (...args) => hostWithWorktree(args, path, functionWorktreeOccurrences),
2345
- checkpoint: async (...args) => {
2346
- if (!bridge.checkpoint || !object(args[0]) || !jsonValue(args[0]))
2347
- fail("INTERNAL_ERROR", "No checkpoint bridge is available");
2348
- return bridge.checkpoint(args[0], signal);
2349
- },
2350
- phase: (name) => { sideEffects.push(Promise.resolve(bridge.phase?.(name))); },
2351
- log: (message) => { sideEffects.push(Promise.resolve(bridge.log?.(message))); },
2352
- };
2353
- const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } }));
2354
- await Promise.all(sideEffects);
2355
- if (!replayed)
2356
- await store.complete(path, stored ?? result);
2357
- return result;
2358
- } };
2305
+ const functionInvokeOccurrences = new Map();
2306
+ const invokeFunction = async (name, input, path, signal, worktreeOwner, structuralPath = [], breadcrumb) => {
2307
+ const replayed = await store.replay(path);
2308
+ let stored;
2309
+ const sideEffects = [];
2310
+ const functionBreadcrumb = breadcrumb ?? name;
2311
+ const context = {
2312
+ run: runContext,
2313
+ invoke: async (targetName, targetInput) => {
2314
+ const inherited = inheritedHostAgentPath.getStore() ?? structuralPath;
2315
+ const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
2316
+ const key = JSON.stringify([path, inherited, targetName]);
2317
+ const occurrence = (functionInvokeOccurrences.get(key) ?? 0) + 1;
2318
+ functionInvokeOccurrences.set(key, occurrence);
2319
+ const nestedPath = operationPath("function", "nested", path, ...inherited, targetName, `occurrence:${String(occurrence)}`);
2320
+ return invokeFunction(targetName, targetInput, nestedPath, signal, scopedWorktreeOwner, inherited, `${functionBreadcrumb} > ${targetName}`);
2321
+ },
2322
+ agent: async (...args) => {
2323
+ if (!bridge.agent || typeof args[0] !== "string")
2324
+ fail("AGENT_FAILED", "No agent bridge is available");
2325
+ const options = validateAgentOptions(args[1] === undefined ? {} : args[1]);
2326
+ const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
2327
+ const inherited = inheritedHostAgentPath.getStore() ?? [];
2328
+ const key = `${path}\0${JSON.stringify(inherited)}`;
2329
+ const occurrence = (functionAgentOccurrences.get(key) ?? 0) + 1;
2330
+ functionAgentOccurrences.set(key, occurrence);
2331
+ return bridge.agent(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb: functionBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
2332
+ },
2333
+ prompt: workflowPrompt,
2334
+ parallel: (...args) => hostParallel(args[0], args[1]),
2335
+ pipeline: (...args) => hostPipeline(args[0], args[1], args[2]),
2336
+ withWorktree: (...args) => hostWithWorktree(args, path, functionWorktreeOccurrences),
2337
+ checkpoint: async (...args) => {
2338
+ if (!bridge.checkpoint || !object(args[0]) || !jsonValue(args[0]))
2339
+ fail("INTERNAL_ERROR", "No checkpoint bridge is available");
2340
+ return bridge.checkpoint(args[0], signal);
2341
+ },
2342
+ phase: (name) => { sideEffects.push(Promise.resolve(bridge.phase?.(name))); },
2343
+ log: (message) => { sideEffects.push(Promise.resolve(bridge.log?.(message))); },
2344
+ };
2345
+ const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } }));
2346
+ await Promise.all(sideEffects);
2347
+ if (!replayed)
2348
+ await store.complete(path, stored ?? result);
2349
+ return result;
2350
+ };
2351
+ return { ...bridge, functions: registry.globals(), variables, function: invokeFunction };
2359
2352
  }
2360
2353
  function projectTrusted(ctx) {
2361
2354
  const check = object(ctx) ? ctx.isProjectTrusted : undefined;
2362
2355
  return typeof check === "function" ? Boolean(Reflect.apply(check, ctx, [])) : true;
2363
2356
  }
2357
+ function isEntryRenderer(value) { return typeof value === "function"; }
2358
+ function isWorkflowEventSink(value) { return object(value) && typeof value.emit === "function"; }
2359
+ function piHostCapabilities(pi) {
2360
+ if (!object(pi))
2361
+ return {};
2362
+ const registerEntryRenderer = pi.registerEntryRenderer;
2363
+ const events = pi.events;
2364
+ return { ...(isEntryRenderer(registerEntryRenderer) ? { registerEntryRenderer } : {}), ...(isWorkflowEventSink(events) ? { events } : {}) };
2365
+ }
2366
+ function isModelRegistryGetter(value) { return typeof value === "function"; }
2367
+ function contextHostCapabilities(ctx) {
2368
+ if (!object(ctx) || !object(ctx.modelRegistry))
2369
+ return {};
2370
+ const registry = ctx.modelRegistry;
2371
+ const getAll = registry.getAll;
2372
+ const getAvailable = registry.getAvailable;
2373
+ return { modelRegistry: { ...(isModelRegistryGetter(getAll) ? { getAll: () => getAll.call(registry) } : {}), ...(isModelRegistryGetter(getAvailable) ? { getAvailable: () => getAvailable.call(registry) } : {}) } };
2374
+ }
2375
+ function isUiSelect(value) { return typeof value === "function"; }
2376
+ function isUiInput(value) { return typeof value === "function"; }
2377
+ function isUiSetStatus(value) { return typeof value === "function"; }
2378
+ function uiHostCapabilities(ui) {
2379
+ if (!object(ui))
2380
+ return undefined;
2381
+ const select = ui.select;
2382
+ const input = ui.input;
2383
+ const setStatus = ui.setStatus;
2384
+ return { ...(isUiSelect(select) ? { select } : {}), ...(isUiInput(input) ? { input } : {}), ...(isUiSetStatus(setStatus) ? { setStatus } : {}) };
2385
+ }
2386
+ function tuiHostCapabilities(tui) {
2387
+ if (!object(tui) || !object(tui.terminal))
2388
+ return {};
2389
+ return { terminal: { ...(tui.terminal.rows === undefined ? {} : { rows: tui.terminal.rows }) } };
2390
+ }
2391
+ function tuiRows(tui) { const rows = tuiHostCapabilities(tui).terminal?.rows; return typeof rows === "number" && Number.isFinite(rows) ? rows : 24; }
2392
+ function isKeybindingGetter(value) { return typeof value === "function"; }
2393
+ function keybindingsHostCapabilities(keybindings) {
2394
+ if (!object(keybindings) || !isKeybindingGetter(keybindings.getKeys))
2395
+ return {};
2396
+ return { getKeys: keybindings.getKeys };
2397
+ }
2398
+ function keybindingKeys(keybindings, name) { const getKeys = keybindingsHostCapabilities(keybindings).getKeys; return typeof getKeys === "function" ? getKeys.call(keybindings, name) : undefined; }
2364
2399
  function parseThinking(value) {
2365
2400
  switch (value) {
2366
2401
  case "off":
@@ -2376,7 +2411,7 @@ function parseThinking(value) {
2376
2411
  export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession) {
2377
2412
  beginWorkflowExtensionLoading();
2378
2413
  const registry = loadingRegistry();
2379
- const registerEntryRenderer = pi.registerEntryRenderer;
2414
+ const registerEntryRenderer = piHostCapabilities(pi).registerEntryRenderer;
2380
2415
  registerEntryRenderer?.(WORKFLOW_LOG_ENTRY, (entry) => {
2381
2416
  const data = entry.data;
2382
2417
  return textBlock(data ? `Workflow ${data.workflowName}: ${data.message}` : "");
@@ -2390,7 +2425,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2390
2425
  await lifecycle.leave();
2391
2426
  }
2392
2427
  };
2393
- const eventPublisher = new WorkflowEventPublisher(pi.events);
2428
+ const eventPublisher = new WorkflowEventPublisher(piHostCapabilities(pi).events);
2394
2429
  pi.on("resources_discover", () => {
2395
2430
  if (!pi.getActiveTools().includes("workflow"))
2396
2431
  return;
@@ -2399,6 +2434,28 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2399
2434
  return skillPath ? { skillPaths: [skillPath] } : undefined;
2400
2435
  });
2401
2436
  const runs = new Map();
2437
+ const liveActivities = new Map();
2438
+ const setLiveActivity = (runId, agentId, activity) => {
2439
+ const activities = liveActivities.get(runId);
2440
+ if (activity) {
2441
+ if (activities)
2442
+ activities.set(agentId, activity);
2443
+ else
2444
+ liveActivities.set(runId, new Map([[agentId, activity]]));
2445
+ }
2446
+ else {
2447
+ activities?.delete(agentId);
2448
+ if (activities?.size === 0)
2449
+ liveActivities.delete(runId);
2450
+ }
2451
+ };
2452
+ const withLiveActivities = (run) => {
2453
+ const activities = liveActivities.get(run.id);
2454
+ return activities?.size ? { ...run, agents: run.agents.map((agent) => {
2455
+ const activity = activities.get(agent.id);
2456
+ return activity ? { ...agent, activity } : agent;
2457
+ }) } : run;
2458
+ };
2402
2459
  const conversationLocks = new Set();
2403
2460
  const terminalRunStates = new Map();
2404
2461
  let sessionLease;
@@ -2443,7 +2500,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2443
2500
  return nextRun;
2444
2501
  });
2445
2502
  await eventPublisher.runState(store, metadata, previous, next, reason);
2446
- runs.get(store.runId)?.update?.(workflowToolUpdate(persisted));
2503
+ runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2447
2504
  });
2448
2505
  const scheduler = new FairAgentScheduler(async ({ id, runId, parentId, prompt, options, signal, setSteer }) => {
2449
2506
  const run = runs.get(runId);
@@ -2464,7 +2521,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2464
2521
  }
2465
2522
  if (!runState.agents.some((agent) => agent.id === id))
2466
2523
  return;
2467
- run.update?.(workflowToolUpdate(runState));
2524
+ setLiveActivity(runId, id, progress.activity);
2525
+ run.update?.(workflowToolUpdate(withLiveActivities(runState)));
2468
2526
  };
2469
2527
  const onAttempt = async (attempt) => {
2470
2528
  await scheduler.flush();
@@ -2475,7 +2533,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2475
2533
  const active = (await run.store.load()).run;
2476
2534
  await eventPublisher.agentStates(run.store, run.metadata, before.agents, active.agents);
2477
2535
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
2478
- run.update?.(workflowToolUpdate(persisted));
2536
+ run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2479
2537
  };
2480
2538
  const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, onProgress, onAttempt, budget, ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.thinking ? { thinking: options.thinking } : {}), ...(options.role ? { role: options.role } : {}), ...(options.role ? {} : { tools: options.tools }), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}), ...(options.conversation ? { conversation: options.conversation } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, thinking) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
2481
2539
  const before = (await run.store.load()).run;
@@ -2483,7 +2541,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2483
2541
  const completed = (await run.store.load()).run;
2484
2542
  await eventPublisher.agentStates(run.store, run.metadata, before.agents, completed.agents);
2485
2543
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
2486
- run.update?.(workflowToolUpdate(persisted));
2544
+ setLiveActivity(runId, id);
2545
+ run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2487
2546
  return result.value;
2488
2547
  }
2489
2548
  catch (error) {
@@ -2495,7 +2554,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2495
2554
  await eventPublisher.agentStates(run.store, run.metadata, before.agents, failed.agents);
2496
2555
  }
2497
2556
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
2498
- run.update?.(workflowToolUpdate(persisted));
2557
+ setLiveActivity(runId, id);
2558
+ run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2499
2559
  throw error;
2500
2560
  }
2501
2561
  }, 16, async (runId, ownership) => {
@@ -2522,7 +2582,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2522
2582
  return { ...current, agents };
2523
2583
  });
2524
2584
  await eventPublisher.agentStates(run.store, run.metadata, previousAgents, runState.agents);
2525
- run.update?.(workflowToolUpdate(runState));
2585
+ run.update?.(workflowToolUpdate(withLiveActivities(runState)));
2526
2586
  });
2527
2587
  const stopWorkflowRun = async (runId) => {
2528
2588
  const run = runs.get(runId);
@@ -2756,7 +2816,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2756
2816
  if (!["completed", "failed", "stopped"].includes(run.lifecycle.state)) {
2757
2817
  await run.lifecycle.terminal("failed", typed.code).catch(() => undefined);
2758
2818
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, error: { code: typed.code, message: typed.message } }));
2759
- await eventPublisher.runFailed(run.store, run.metadata, typed, true, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
2819
+ await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
2760
2820
  run.update?.(workflowToolUpdate(persisted));
2761
2821
  }
2762
2822
  throw typed;
@@ -2826,7 +2886,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2826
2886
  throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
2827
2887
  const resultPath = await run.store.saveResult(value);
2828
2888
  await run.lifecycle.terminal("completed", "completed");
2829
- await eventPublisher.runCompleted(run.store, run.metadata, resultPath, true);
2889
+ await eventPublisher.runCompleted(run.store, run.metadata, resultPath);
2830
2890
  return { value, resultPath };
2831
2891
  }).catch(async (error) => {
2832
2892
  await scheduler.flush();
@@ -2835,7 +2895,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2835
2895
  await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
2836
2896
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), error: { code: typed.code, message: typed.message } }));
2837
2897
  const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
2838
- await eventPublisher.runFailed(run.store, run.metadata, typed, true, state);
2898
+ await eventPublisher.runFailed(run.store, run.metadata, typed, state);
2839
2899
  run.update?.(workflowToolUpdate(persisted));
2840
2900
  if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
2841
2901
  deliverFailure(pi, run.metadata.name, error);
@@ -2962,7 +3022,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2962
3022
  deliver(pi, budgetDecisionDelivery(loaded.snapshot.metadata, decision));
2963
3023
  scheduler.restoreRun(runId, loaded.snapshot.settings.concurrency, loaded.snapshot.identityVersion === LAUNCH_SNAPSHOT_IDENTITY_VERSION ? await store.loadOwnership() : [], () => runs.get(runId)?.budget.checkAgentLaunch());
2964
3024
  }
2965
- const resumeSelect = ctx.ui?.select;
3025
+ const resumeSelect = uiHostCapabilities(ctx.ui)?.select;
2966
3026
  if (ctx.hasUI && resumeSelect) {
2967
3027
  const interrupted = [...runs.values()].filter((r) => r.lifecycle.state === "interrupted");
2968
3028
  if (interrupted.length > 0) {
@@ -3014,7 +3074,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3014
3074
  const budget = validateBudget(params.budget);
3015
3075
  const rootModel = { provider: ctx.model.provider, model: ctx.model.id, thinking: pi.getThinkingLevel() };
3016
3076
  const rootModelName = `${rootModel.provider}/${rootModel.model}`;
3017
- const modelRegistry = ctx.modelRegistry;
3077
+ const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
3018
3078
  const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? [ctx.model]).map((model) => `${model.provider}/${model.id}`));
3019
3079
  knownModels.add(rootModelName);
3020
3080
  const availableModels = knownModels;
@@ -3116,14 +3176,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3116
3176
  }
3117
3177
  }, log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
3118
3178
  runs.get(runId).execution = execution;
3119
- await eventPublisher.runStarted(store, checked.metadata, background);
3179
+ await eventPublisher.runStarted(store, checked.metadata);
3120
3180
  const finish = execution.result.then(async (value) => {
3121
3181
  await scheduler.flush();
3122
3182
  if (budgetRuntime.hardExhausted)
3123
3183
  throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
3124
3184
  const resultPath = await store.saveResult(value);
3125
3185
  await lifecycle.terminal("completed", "completed");
3126
- await eventPublisher.runCompleted(store, checked.metadata, resultPath, background);
3186
+ await eventPublisher.runCompleted(store, checked.metadata, resultPath);
3127
3187
  return { value, resultPath };
3128
3188
  }).catch(async (error) => {
3129
3189
  await scheduler.flush();
@@ -3132,7 +3192,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3132
3192
  await lifecycle.terminal(typed.code === "CANCELLED" ? "stopped" : typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
3133
3193
  await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot(), error: { code: typed.code, message: typed.message } }));
3134
3194
  const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
3135
- await eventPublisher.runFailed(store, checked.metadata, typed, background, state);
3195
+ await eventPublisher.runFailed(store, checked.metadata, typed, state);
3136
3196
  throw typed;
3137
3197
  });
3138
3198
  runs.get(runId).completion = finish;
@@ -3198,7 +3258,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3198
3258
  let stores = await loadStores();
3199
3259
  const usage = "Usage: /workflow [doctor|model-aliases], or /workflow pause|resume|stop|approve|reject|delete <run-id> [checkpoint or proposal-id]. Use workflow_resume for budget patches.";
3200
3260
  const setWorkflowStatus = (text) => {
3201
- const setStatus = ctx.ui.setStatus;
3261
+ const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
3202
3262
  setStatus?.call(ctx.ui, "workflow-stop", text);
3203
3263
  };
3204
3264
  const runAction = async (actionCommand, keepContext, status = setWorkflowStatus) => {
@@ -3254,7 +3314,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3254
3314
  return keepContext ? "dashboard" : "done";
3255
3315
  }
3256
3316
  if (action === "adjust" && run?.lifecycle.state === "budget_exhausted") {
3257
- const input = await ctx.ui.input?.("Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
3317
+ const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
3258
3318
  if (input === undefined)
3259
3319
  return keepContext ? "dashboard" : "done";
3260
3320
  const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input));
@@ -3292,7 +3352,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3292
3352
  };
3293
3353
  const manageAliases = async () => {
3294
3354
  const settingsPath = workflowSettingsPath();
3295
- const modelRegistry = ctx.modelRegistry;
3355
+ const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
3296
3356
  const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
3297
3357
  const selectTarget = async () => {
3298
3358
  const models = available();
@@ -3458,7 +3518,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3458
3518
  await ctx.ui.custom((tui, theme, keybindings, done) => {
3459
3519
  let offset = 0;
3460
3520
  let renderedLines = [];
3461
- const viewport = () => Math.max(1, ((tui.terminal?.rows) ?? 24) - 3);
3521
+ const viewport = () => Math.max(1, tuiRows(tui) - 3);
3462
3522
  const move = (delta) => { offset = Math.max(0, Math.min(Math.max(0, renderedLines.length - viewport()), offset + delta)); };
3463
3523
  return {
3464
3524
  render(width) {
@@ -3615,11 +3675,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3615
3675
  let disposed = false;
3616
3676
  let stopRequested = false;
3617
3677
  let stopStatus;
3618
- const terminalRows = () => Math.max(1, (tui.terminal?.rows) ?? 24);
3678
+ const terminalRows = () => Math.max(1, tuiRows(tui));
3619
3679
  const keyLabels = { up: "↑", down: "↓", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
3620
3680
  const keyLabel = (binding, fallback) => {
3621
- const getKeys = keybindings.getKeys;
3622
- const keys = getKeys?.call(keybindings, binding);
3681
+ const keys = keybindingKeys(keybindings, binding);
3623
3682
  return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
3624
3683
  };
3625
3684
  const dashboardLayout = () => {
@@ -3733,7 +3792,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3733
3792
  const highlighted = highlightCode(view.script, "javascript");
3734
3793
  let offset = 0;
3735
3794
  let renderedLines = [];
3736
- const viewport = () => Math.max(1, ((tui.terminal?.rows) ?? 24) - 3);
3795
+ const viewport = () => Math.max(1, tuiRows(tui) - 3);
3737
3796
  const move = (delta) => {
3738
3797
  const maxOffset = Math.max(0, renderedLines.length - viewport());
3739
3798
  offset = Math.max(0, Math.min(maxOffset, offset + delta));
@@ -3799,7 +3858,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3799
3858
  let offset = 0;
3800
3859
  let renderedLines = [];
3801
3860
  const layout = () => {
3802
- const rows = Math.max(1, ((tui.terminal?.rows) ?? 24));
3861
+ const rows = Math.max(1, tuiRows(tui));
3803
3862
  const compactControls = rows < 4;
3804
3863
  const titleRows = rows >= 5 ? 1 : 0;
3805
3864
  const hintRows = rows >= 8 ? 1 : 0;
@@ -60,6 +60,8 @@ export declare class SessionLease {
60
60
  export declare function acquireSessionLease(cwd: string, sessionId: string, home?: string): Promise<SessionLease>;
61
61
  export declare function listRunIds(cwd: string, sessionId: string, home?: string): Promise<string[]>;
62
62
  export declare function structuralPath(...names: string[]): string;
63
+ export declare function atomicWriteFile(path: string, content: string): Promise<void>;
64
+ export declare function atomicWriteFile(path: string, content: string, sync: true): void;
63
65
  export declare class RunStore {
64
66
  readonly cwd: string;
65
67
  readonly sessionId: string;
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { execFile } from "node:child_process";
3
+ import { renameSync, rmSync, writeFileSync } from "node:fs";
3
4
  import { access, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
4
5
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
6
  import { homedir } from "node:os";
@@ -182,10 +183,32 @@ export function structuralPath(...names) {
182
183
  throw new WorkflowError("INVALID_METADATA", "Structural paths require non-empty explicit names");
183
184
  return names.map((name) => encodeURIComponent(name)).join("/");
184
185
  }
185
- async function atomicJson(path, value) {
186
+ export function atomicWriteFile(path, content, sync = false) {
186
187
  const temporary = `${path}.${String(process.pid)}.${randomUUID()}.tmp`;
187
- await writeFile(temporary, `${JSON.stringify(value)}\n`, { encoding: "utf8", mode: 0o600 });
188
- await rename(temporary, path);
188
+ if (sync) {
189
+ try {
190
+ writeFileSync(temporary, content, { encoding: "utf8", mode: 0o600 });
191
+ renameSync(temporary, path);
192
+ }
193
+ catch (error) {
194
+ try {
195
+ rmSync(temporary, { force: true });
196
+ }
197
+ catch { /* Preserve the original write error. */ }
198
+ throw error;
199
+ }
200
+ return;
201
+ }
202
+ return writeFile(temporary, content, { encoding: "utf8", mode: 0o600 }).then(() => rename(temporary, path)).catch(async (error) => {
203
+ try {
204
+ await rm(temporary, { force: true });
205
+ }
206
+ catch { /* Preserve the original write error. */ }
207
+ throw error;
208
+ });
209
+ }
210
+ async function atomicJson(path, value) {
211
+ await atomicWriteFile(path, `${JSON.stringify(value)}\n`);
189
212
  }
190
213
  async function json(path) { return JSON.parse(await readFile(path, "utf8")); }
191
214
  export class RunStore {
@@ -277,7 +300,7 @@ export class RunStore {
277
300
  await write;
278
301
  }
279
302
  async appendEvent(event) {
280
- await this.updateState((run) => ({ ...run, events: [...(run.events ?? []), ...(run.events?.some((current) => current.message === event.message) ? [] : [event])] }));
303
+ await this.updateState((run) => ({ ...run, events: [...(run.events ?? []), ...(run.events?.some((current) => current.type === event.type && current.message === event.message) ? [] : [event])] }));
281
304
  }
282
305
  async saveOwnership(nodes) {
283
306
  await atomicJson(join(this.directory, "ownership.json"), nodes);