pi-extensible-workflows 3.3.0 → 3.4.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.
@@ -29,6 +29,7 @@ export class WorkflowRegistry {
29
29
  #extensions = new Set();
30
30
  #globals = new Map();
31
31
  #hooks = new Map();
32
+ #agentAttemptActions = new Map();
32
33
  #roleDirectories = new Map();
33
34
  #modelAliases = new Map();
34
35
  #frozen = false;
@@ -39,18 +40,19 @@ export class WorkflowRegistry {
39
40
  fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
40
41
  if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows"))
41
42
  fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
42
- if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "modelAliases", "agentSetupHooks", "roleDirectories"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim())
43
+ if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "modelAliases", "agentSetupHooks", "agentAttemptActions", "roleDirectories"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim())
43
44
  fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
44
45
  const functions = extension.functions ?? {};
45
46
  const variables = extension.variables ?? {};
46
47
  const modelAliases = extension.modelAliases ?? {};
47
48
  const agentSetupHooks = extension.agentSetupHooks ?? {};
49
+ const agentAttemptActions = extension.agentAttemptActions ?? {};
48
50
  const roleDirectoryValues = extension.roleDirectories === undefined ? [] : extension.roleDirectories;
49
51
  if (!Array.isArray(roleDirectoryValues))
50
52
  fail("INVALID_METADATA", "Workflow extension roleDirectories must be an array");
51
53
  const roleDirectories = [...new Set(Array.from(roleDirectoryValues, (value) => normalizeRoleDirectory(value)))];
52
- if (!object(functions) || !object(variables) || !object(modelAliases) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentSetupHooks).length === 0 && roleDirectories.length === 0))
53
- fail("INVALID_METADATA", "Workflow extensions require functions, variables, model aliases, agent setup hooks, or role directories");
54
+ if (!object(functions) || !object(variables) || !object(modelAliases) || !object(agentSetupHooks) || !object(agentAttemptActions) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentSetupHooks).length === 0 && Object.keys(agentAttemptActions).length === 0 && roleDirectories.length === 0))
55
+ fail("INVALID_METADATA", "Workflow extensions require functions, variables, model aliases, agent setup hooks, agent attempt actions, or role directories");
54
56
  const names = [...Object.keys(functions), ...Object.keys(variables)];
55
57
  if (new Set(names).size !== names.length)
56
58
  fail("GLOBAL_COLLISION", "Global name collision inside extension");
@@ -89,7 +91,13 @@ export class WorkflowRegistry {
89
91
  if (this.#hooks.has(name))
90
92
  fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
91
93
  }
92
- const stored = deepFreeze({ ...extension, functions, variables, modelAliases, agentSetupHooks, ...(roleDirectories.length ? { roleDirectories } : {}) });
94
+ for (const [name, action] of Object.entries(agentAttemptActions)) {
95
+ if (!IDENTIFIER.test(name) || !object(action) || Object.keys(action).some((key) => !["label", "visible", "run"].includes(key)) || typeof action.label !== "string" || !action.label.trim() || typeof action.visible !== "function" || typeof action.run !== "function")
96
+ fail("INVALID_METADATA", `Invalid agent attempt action: ${name}`);
97
+ if (this.#agentAttemptActions.has(name))
98
+ fail("DUPLICATE_NAME", `Agent attempt action already registered: ${name}`);
99
+ }
100
+ const stored = deepFreeze({ ...extension, functions, variables, modelAliases, agentSetupHooks, agentAttemptActions, ...(roleDirectories.length ? { roleDirectories } : {}) });
93
101
  this.#extensions.add(stored);
94
102
  for (const directory of roleDirectories)
95
103
  if (!this.#roleDirectories.has(directory))
@@ -100,6 +108,8 @@ export class WorkflowRegistry {
100
108
  this.#modelAliases.set(name, { name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, resolve: alias.resolve });
101
109
  for (const [name, hook] of Object.entries(agentSetupHooks))
102
110
  this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
111
+ for (const [name, action] of Object.entries(agentAttemptActions))
112
+ this.#agentAttemptActions.set(name, action);
103
113
  }
104
114
  function(name) {
105
115
  if (!IDENTIFIER.test(name))
@@ -197,6 +207,7 @@ export class WorkflowRegistry {
197
207
  agentSetupHooks() {
198
208
  return [...this.#hooks.values()].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
199
209
  }
210
+ agentAttemptActions() { return Object.freeze(Object.fromEntries(this.#agentAttemptActions.entries())); }
200
211
  roleDirectories() {
201
212
  return [...this.#roleDirectories.keys()];
202
213
  }
@@ -250,6 +261,7 @@ function createWorkflowRegistryApi(registry) {
250
261
  roleDirectories: () => registry.roleDirectories(),
251
262
  roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
252
263
  agentSetupHooks: () => registry.agentSetupHooks(),
264
+ agentAttemptActions: () => registry.agentAttemptActions(),
253
265
  };
254
266
  }
255
267
  function workflowRegistryHost() {
@@ -115,6 +115,13 @@ function readTranscript(path) {
115
115
  return undefined;
116
116
  }
117
117
  }
118
+ function attemptTranscriptPath(attempt) {
119
+ const session = attempt.session;
120
+ if (!session || session.transport !== "local" || typeof session.locator !== "object" || session.locator === null || Array.isArray(session.locator))
121
+ return undefined;
122
+ const path = session.locator.sessionFile;
123
+ return typeof path === "string" && path ? path : undefined;
124
+ }
118
125
  function resultRunId(result) {
119
126
  if (!result)
120
127
  return undefined;
@@ -161,33 +168,18 @@ async function agentReport(agent) {
161
168
  const fallbackThinking = agent.model.thinking;
162
169
  const attempts = [];
163
170
  for (const attempt of agent.attemptDetails ?? []) {
164
- const log = readTranscript(attempt.sessionFile);
171
+ const setup = attempt.setup;
172
+ const path = attemptTranscriptPath(attempt);
173
+ const log = path ? readTranscript(path) : undefined;
165
174
  if (log) {
166
- const model = log.model ?? fallbackModel;
175
+ const model = log.model ?? `${setup.model.provider}/${setup.model.model}`;
167
176
  const cost = log.cost;
168
- attempts.push({
169
- attempt: attempt.attempt,
170
- prompt: log.prompt ?? "(transcript unavailable)",
171
- model,
172
- ...(log.thinking !== undefined ? { thinking: log.thinking } : {}),
173
- cost,
174
- models: log.models.length ? log.models : [{ model, cost }],
175
- ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}),
176
- ...(attempt.setup ? { setup: attempt.setup } : {}),
177
- });
177
+ attempts.push({ attempt: attempt.attempt, prompt: log.prompt ?? "(transcript unavailable)", model, ...(log.thinking !== undefined ? { thinking: log.thinking } : {}), cost, models: log.models.length ? log.models : [{ model, cost }], ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}), setup });
178
178
  continue;
179
179
  }
180
+ const model = `${setup.model.provider}/${setup.model.model}`;
180
181
  const cost = attempt.accounting.cost;
181
- attempts.push({
182
- attempt: attempt.attempt,
183
- prompt: "(transcript unavailable)",
184
- model: fallbackModel,
185
- ...(fallbackThinking !== undefined ? { thinking: fallbackThinking } : {}),
186
- cost,
187
- models: [{ model: fallbackModel, cost }],
188
- ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}),
189
- ...(attempt.setup ? { setup: attempt.setup } : {}),
190
- });
182
+ attempts.push({ attempt: attempt.attempt, prompt: "(transcript unavailable)", model, ...(setup.model.thinking !== undefined ? { thinking: setup.model.thinking } : {}), cost, models: [{ model, cost }], ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}), setup });
191
183
  }
192
184
  if (!attempts.length) {
193
185
  const cost = agent.accounting?.cost ?? 0;
@@ -1,4 +1,4 @@
1
- import type { AgentSessionEvent, CreateAgentSessionOptions, InlineExtension, SessionStats, ToolDefinition } from "@earendil-works/pi-coding-agent";
1
+ import type { CreateAgentSessionOptions, InlineExtension, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  export declare const RUN_STATES: readonly ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"];
3
3
  export declare const AGENT_STATES: readonly ["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"];
4
4
  export declare const WORKFLOW_CALL_KINDS: readonly ["agent", "parallel", "pipeline", "checkpoint", "phase", "withWorktree", "shell"];
@@ -193,10 +193,10 @@ export interface AgentResourcePolicy {
193
193
  global: AgentResourceExclusions;
194
194
  project: AgentResourceExclusions;
195
195
  effective: AgentResourceExclusions;
196
- unmatchedSkills: string[];
197
- unmatchedExtensions: string[];
198
- excludedSkills?: string[];
199
- excludedExtensions?: string[];
196
+ unmatchedSkills: readonly string[];
197
+ unmatchedExtensions: readonly string[];
198
+ excludedSkills?: readonly string[];
199
+ excludedExtensions?: readonly string[];
200
200
  }
201
201
  export interface AgentActivity {
202
202
  kind: "reasoning" | "tool" | "text";
@@ -224,22 +224,17 @@ export interface AgentSetupSummary {
224
224
  unmatchedExtensions: readonly string[];
225
225
  };
226
226
  }
227
+ export interface AgentAttemptError {
228
+ code: string;
229
+ message: string;
230
+ }
227
231
  export interface AgentAttemptSummary {
228
232
  attempt: number;
229
- sessionId: string;
230
- sessionFile: string;
231
- error?: {
232
- code: string;
233
- message: string;
234
- };
235
- accounting: {
236
- input: number;
237
- output: number;
238
- cacheRead: number;
239
- cacheWrite: number;
240
- cost: number;
241
- };
242
- setup?: AgentSetupSummary;
233
+ transport: string;
234
+ session?: WorkflowAgentSessionReference;
235
+ setup: AgentSetupSummary;
236
+ error?: AgentAttemptError;
237
+ accounting: AgentAccounting;
243
238
  }
244
239
  export interface WorkflowWorktreeCreatedEvent extends WorkflowEventBase {
245
240
  owner: string;
@@ -252,6 +247,8 @@ export interface WorkflowWorktreeReference {
252
247
  readonly branch: string;
253
248
  }
254
249
  export interface AgentRecord {
250
+ systemPrompt?: string;
251
+ prompt?: string;
255
252
  id: string;
256
253
  name: string;
257
254
  label?: string;
@@ -311,6 +308,7 @@ export interface RunRecord {
311
308
  cwd: string;
312
309
  sessionId: string;
313
310
  state: RunState;
311
+ agentSessions: readonly WorkflowAgentSessionReference[];
314
312
  parentRunId?: string;
315
313
  retry?: WorkflowRetryProvenance;
316
314
  phase?: string;
@@ -413,47 +411,65 @@ export interface WorkflowVariable {
413
411
  schema: JsonSchema;
414
412
  resolve: (run: Readonly<WorkflowRunContext>) => Promise<JsonValue> | JsonValue;
415
413
  }
416
- type NativeAgentMessage = {
417
- role: string;
418
- content?: unknown;
419
- stopReason?: string;
420
- errorMessage?: string;
421
- usage?: {
422
- input: number;
423
- output: number;
424
- cacheRead: number;
425
- cacheWrite: number;
426
- cost: {
427
- total: number;
428
- };
429
- };
430
- };
431
- type NativeSessionStats = Pick<SessionStats, "tokens" | "cost">;
432
- export interface NativeSession {
414
+ export interface WorkflowAgentSessionReference {
415
+ readonly transport: string;
433
416
  readonly sessionId: string;
434
- readonly sessionFile: string | undefined;
435
- readonly messages: readonly NativeAgentMessage[];
436
- getSessionStats(): NativeSessionStats;
437
- readonly systemPrompt?: string;
438
- readonly model?: {
439
- provider: string;
440
- model?: string;
441
- id?: string;
417
+ readonly locator?: JsonValue;
418
+ }
419
+ export interface WorkflowAgentSessionStats {
420
+ readonly tokens: {
421
+ readonly input: number;
422
+ readonly output: number;
423
+ readonly cacheRead: number;
424
+ readonly cacheWrite: number;
425
+ readonly total: number;
442
426
  };
443
- readonly agent?: {
444
- state: {
445
- tools: readonly {
446
- name: string;
447
- }[];
427
+ readonly cost: number;
428
+ }
429
+ export interface WorkflowAgentMessage {
430
+ readonly role: string;
431
+ readonly content?: unknown;
432
+ readonly stopReason?: string;
433
+ readonly errorMessage?: string;
434
+ readonly usage?: {
435
+ readonly input: number;
436
+ readonly output: number;
437
+ readonly cacheRead: number;
438
+ readonly cacheWrite: number;
439
+ readonly cost: {
440
+ readonly total: number;
448
441
  };
449
442
  };
450
- getLeafId?: () => string | null;
451
- getToolDefinitions?: () => unknown;
452
- subscribe?(listener: (event: AgentSessionEvent) => void): () => void;
453
- prompt(text: string): Promise<void>;
454
- steer?(text: string): Promise<void>;
455
- abort?(): Promise<void>;
456
- dispose(): void;
443
+ }
444
+ export interface WorkflowAgentSessionState {
445
+ readonly model: ModelSpec;
446
+ readonly thinking?: ModelSpec["thinking"];
447
+ readonly tools: readonly string[];
448
+ readonly systemPrompt?: string;
449
+ }
450
+ export interface WorkflowAgentSessionEvent {
451
+ readonly type: string;
452
+ readonly state?: Readonly<WorkflowAgentSessionState>;
453
+ readonly message?: WorkflowAgentMessage;
454
+ readonly assistantMessageEvent?: {
455
+ readonly type: string;
456
+ };
457
+ readonly toolCallId?: string;
458
+ readonly toolName?: string;
459
+ readonly isError?: boolean;
460
+ }
461
+ export interface WorkflowAgentTurnResult {
462
+ readonly assistant?: WorkflowAgentMessage;
463
+ }
464
+ export interface WorkflowAgentSession {
465
+ readonly reference: WorkflowAgentSessionReference;
466
+ getState(): Readonly<WorkflowAgentSessionState>;
467
+ getSessionStats(): WorkflowAgentSessionStats;
468
+ subscribe(listener: (event: WorkflowAgentSessionEvent) => void): () => void;
469
+ prompt(text: string): Promise<WorkflowAgentTurnResult>;
470
+ steer(text: string): Promise<void>;
471
+ abort(): Promise<void>;
472
+ dispose(): Promise<void>;
457
473
  }
458
474
  type SessionTools = NonNullable<CreateAgentSessionOptions["tools"]>;
459
475
  type SessionCustomTools = NonNullable<CreateAgentSessionOptions["customTools"]>;
@@ -472,12 +488,37 @@ export interface SessionInput {
472
488
  resourcePolicy?: AgentResourcePolicy;
473
489
  options?: AgentOptions;
474
490
  }
475
- export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
491
+ export interface PreparedAgentSession {
492
+ readonly cwd: string;
493
+ readonly model: ModelSpec;
494
+ readonly tools: readonly string[];
495
+ readonly sessionLabel: string;
496
+ readonly agentDir?: string;
497
+ readonly customTools?: readonly ToolDefinition[];
498
+ readonly resultTool?: ToolDefinition;
499
+ readonly options?: Readonly<Record<string, JsonValue>>;
500
+ readonly systemPrompt?: string;
501
+ readonly systemPromptAppend?: string;
502
+ readonly extensionFactories?: readonly InlineExtension[];
503
+ readonly additionalSkillPaths?: readonly string[];
504
+ readonly resourcePolicy?: Readonly<AgentResourcePolicy>;
505
+ }
506
+ export interface AgentTransportContext {
507
+ readonly run: Readonly<WorkflowRunContext>;
508
+ readonly identity: Readonly<AgentIdentity>;
509
+ readonly attempt: number;
510
+ readonly signal: AbortSignal;
511
+ }
512
+ export interface AgentTransport {
513
+ readonly id: string;
514
+ createSession(prepared: Readonly<PreparedAgentSession>, context: Readonly<AgentTransportContext>): Promise<WorkflowAgentSession>;
515
+ }
476
516
  export interface AgentSetup {
477
517
  prompt: string;
478
518
  options: AgentOptions;
479
519
  sessionInput: SessionInput;
480
- createSession: SessionFactory;
520
+ prepared: Readonly<PreparedAgentSession>;
521
+ transport: AgentTransport;
481
522
  }
482
523
  export interface AgentSetupContext {
483
524
  readonly run: Readonly<WorkflowRunContext>;
@@ -503,11 +544,32 @@ export interface WorkflowRoleDirectoryRegistration {
503
544
  path: string;
504
545
  extension: WorkflowExtensionMetadata;
505
546
  }
547
+ export interface AgentAttemptActionUi {
548
+ notify(message: string, level?: "info" | "warning" | "error"): void;
549
+ confirm(title: string, message: string): Promise<boolean>;
550
+ select(title: string, options: readonly string[]): Promise<string | undefined>;
551
+ input(title: string, placeholder?: string): Promise<string | undefined>;
552
+ }
553
+ export interface AgentAttemptActionContext {
554
+ readonly run: Readonly<RunRecord>;
555
+ readonly agent: Readonly<AgentRecord>;
556
+ readonly attempt: Readonly<AgentAttemptSummary>;
557
+ readonly session?: WorkflowAgentSessionReference;
558
+ readonly liveSession?: WorkflowAgentSession;
559
+ readonly signal: AbortSignal;
560
+ readonly ui: Readonly<AgentAttemptActionUi>;
561
+ }
562
+ export interface AgentAttemptAction {
563
+ readonly label: string;
564
+ visible(context: Readonly<AgentAttemptActionContext>): boolean;
565
+ run(context: Readonly<AgentAttemptActionContext>): void | Promise<void>;
566
+ }
506
567
  export interface WorkflowExtension extends WorkflowExtensionMetadata {
507
568
  functions?: Readonly<Record<string, WorkflowFunction>>;
508
569
  variables?: Readonly<Record<string, WorkflowVariable>>;
509
570
  modelAliases?: Readonly<Record<string, WorkflowModelAlias>>;
510
571
  agentSetupHooks?: Readonly<Record<string, AgentSetupHook>>;
572
+ agentAttemptActions?: Readonly<Record<string, AgentAttemptAction>>;
511
573
  roleDirectories?: readonly (string | URL)[];
512
574
  }
513
575
  export interface WorkflowJournal {
@@ -524,8 +586,8 @@ export interface WorkflowFailureAgent {
524
586
  role?: string;
525
587
  structuralPath: readonly string[];
526
588
  attempt: number;
527
- sessionId?: string;
528
- sessionFile?: string;
589
+ transport?: string;
590
+ session?: WorkflowAgentSessionReference;
529
591
  }
530
592
  export interface WorkflowSiblingAgent {
531
593
  id: string;
@@ -687,6 +749,7 @@ export interface WorkflowValidationParameters {
687
749
  name?: string;
688
750
  description?: string;
689
751
  script?: string;
752
+ scriptPath?: string;
690
753
  workflow?: string;
691
754
  args?: unknown;
692
755
  }
@@ -825,10 +825,25 @@ export function validateWorkflowLaunchWithRegistry(params, context, registry) {
825
825
  fail("INVALID_METADATA", "maxAgentLaunches has been removed; use budget.agentLaunches");
826
826
  if (params.script !== undefined && params.workflow !== undefined)
827
827
  fail("INVALID_METADATA", "Provide either script or workflow, not both");
828
+ if (params.scriptPath !== undefined && (typeof params.scriptPath !== "string" || !params.scriptPath.trim()))
829
+ fail("INVALID_METADATA", "scriptPath must be a non-empty path");
830
+ if ((params.script !== undefined || params.workflow !== undefined) && params.scriptPath !== undefined)
831
+ fail("INVALID_METADATA", "Provide either script, scriptPath, or workflow, not more than one");
832
+ const scriptPath = typeof params.scriptPath === "string" ? params.scriptPath.trim() : undefined;
833
+ let fileScript;
834
+ if (scriptPath !== undefined) {
835
+ try {
836
+ fileScript = readFileSync(resolve(context.cwd, scriptPath), "utf8");
837
+ }
838
+ catch (error) {
839
+ fail("INVALID_SYNTAX", `Cannot read workflow script file ${scriptPath}: ${errorText(error)}`);
840
+ }
841
+ }
828
842
  const functionName = typeof params.workflow === "string" ? params.workflow : undefined;
829
- if (functionName !== undefined && params.name !== undefined)
830
- fail("INVALID_METADATA", "Registered function launches do not accept name; workflow is the run name");
831
- const workflowName = functionName ?? (typeof params.name === "string" ? params.name.trim() : "");
843
+ const explicitName = params.name === undefined ? undefined : typeof params.name === "string" ? params.name.trim() : "";
844
+ if (params.name !== undefined && !explicitName)
845
+ fail("INVALID_METADATA", "Workflow name must be non-empty when provided");
846
+ const workflowName = explicitName ?? functionName ?? "";
832
847
  if (functionName === undefined && !workflowName)
833
848
  fail("INVALID_METADATA", "Inline workflow launches require a non-empty name");
834
849
  const fn = functionName === undefined ? undefined : registry?.function(functionName);
@@ -837,9 +852,9 @@ export function validateWorkflowLaunchWithRegistry(params, context, registry) {
837
852
  const args = params.args === undefined ? null : params.args;
838
853
  if (functionName !== undefined && fn && (!object(args) || !jsonValue(args) || !Value.Check(fn.input, args)))
839
854
  fail("RESULT_INVALID", `Invalid input for ${functionName}`);
840
- const script = functionName !== undefined && fn ? functionLaunchScript(functionName) : typeof params.script === "string" && params.script.trim() ? params.script : "";
855
+ const script = functionName !== undefined && fn ? functionLaunchScript(functionName) : typeof params.script === "string" && params.script.trim() ? params.script : fileScript ?? "";
841
856
  if (!script)
842
- fail("INVALID_SYNTAX", "Provide script or registered function");
857
+ fail("INVALID_SYNTAX", "Provide script, scriptPath, or registered function");
843
858
  const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } : fn?.description ? { description: fn.description } : {}) });
844
859
  const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false, registry && typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : registry && typeof registry.roleDirectories === "function" ? registry.roleDirectories() : undefined);
845
860
  const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
@@ -9,5 +9,6 @@ export type WorkflowTui = {
9
9
  requestRender(force?: boolean): void;
10
10
  };
11
11
  export declare function workflowScriptArtifact(script: string): WorkflowArtifact;
12
+ export declare function workflowPromptArtifact(prompt: string): WorkflowArtifact;
12
13
  export declare function workflowResultArtifact(value: JsonValue): WorkflowArtifact;
13
14
  export declare function openWorkflowArtifact(tui: WorkflowTui, command: string, artifact: WorkflowArtifact): Promise<number | null>;
@@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  export function workflowScriptArtifact(script) { return { extension: ".js", content: script }; }
6
+ export function workflowPromptArtifact(prompt) { return { extension: ".md", content: prompt }; }
6
7
  export function workflowResultArtifact(value) { return typeof value === "string" ? { extension: ".md", content: value } : { extension: ".json", content: `${JSON.stringify(value, null, 2)}\n` }; }
7
8
  async function spawnWorkflowEditor(command, path) {
8
9
  const [editor, ...editorArgs] = command.split(" ");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extensible-workflows",
3
- "version": "3.3.0",
3
+ "version": "3.4.1",
4
4
  "description": "Deterministic multi-agent workflow orchestration for Pi",
5
5
  "homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "inspect": "node dist/src/cli.js inspect",
34
34
  "lint": "eslint .",
35
35
  "test": "npm run build && TEST_FILES='dist/test/*.test.js' npm run test:run",
36
- "test:run": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; TMPDIR=\"$tmp\" node --test --test-concurrency=1 $TEST_FILES",
36
+ "test:run": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; TMPDIR=\"$tmp\" node --test --test-concurrency=1 --test-reporter=dot $TEST_FILES",
37
37
  "acceptance": "npm run build && TEST_FILES='dist/test/runtime-acceptance.test.js' npm run test:run",
38
38
  "docs:check": "node scripts/check-docs.mjs",
39
39
  "check": "npm run lint && npm test && npm run docs:check",
@@ -27,15 +27,23 @@ Await `parallel(...)` or `pipeline(...)` results before interpolation. Runs are
27
27
  ## Runtime and safety rules
28
28
 
29
29
 
30
- Inline launches require a non-empty `name`. Registered function launches must omit `name`; they use `workflow` as the run name:
31
30
 
32
31
  ```json
33
32
  { "workflow": "workflowName", "args": { "issue": 42 } }
34
33
  ```
35
- Recovery map: `agent(..., { retries })` reruns one agent call in the same run for transient failures; `workflow_retry({ runId, foreground? })` replays a failed run into a child; `workflow_resume({ runId, budget?, foreground? })` continues a `budget_exhausted` run; recovery inherits the source snapshot's foreground/background launch mode, while legacy snapshots without `launchMode` recover in background; set `foreground: true` or `false` to override it; `parentRunId` on a new launch only borrows named worktrees and never replays or resumes. Use each only for its stated case.
36
- For an explicitly failed run, use the exact `runId` from failure diagnostics with `workflow_retry({ runId })`: diagnostics list replayable and incomplete paths, artifacts, and valid named worktrees; the tool creates a linked child, replays completed agent, shell, function, and checkpoint operations, and executes incomplete work. External side effects before failure are not guaranteed exactly once.
37
- Inside the workflow, read `args.issue` (`args` is `null` when omitted). `workflow_stop` requires the exact run ID; foreground launches and foreground recovery retain their terminal value and completed `runId`, while background launches and recovery return `runId` immediately and deliver completion or failure as a follow-up. Retry versus per-agent `retries` and `workflow_resume` is always explicit.
38
- Inspect tool `workflow_catalog` result at least once before creating the first workflow for a task.
34
+
35
+ A reviewed JavaScript file on disk can use `scriptPath` instead of `script`.
36
+
37
+ Recovery map:
38
+ - `agent(..., { retries })` reruns one agent call in the same run for transient failures;
39
+ - `workflow_retry({ runId, foreground? })` replays a failed run into a child;
40
+ - `workflow_resume({ runId, budget?, foreground? })` continues a `budget_exhausted` run;
41
+ - recovery inherits the source snapshot's foreground/background launch mode, while legacy snapshots without `launchMode` recover in background; set `foreground: true` or `false` to override it;
42
+ - `parentRunId` on a new launch only borrows named worktrees and never replays or resumes.
43
+
44
+ For an explicitly failed run, use the exact `runId` from failure diagnostics with `workflow_retry({ runId, foreground: <prevValue> })`: diagnostics list replayable and incomplete paths, artifacts, and valid named worktrees; the tool creates a linked child, replays completed agent, shell, function, and checkpoint operations, and executes incomplete work. External side effects before failure are not guaranteed exactly once.
45
+ `workflow_stop` requires the exact run ID; foreground launches and foreground recovery retain their terminal value and completed `runId`, while background launches and recovery return `runId` immediately and deliver completion or failure as a follow-up. Retry versus per-agent `retries` and `workflow_resume` is always explicit.
46
+ Inspect tool `workflow_catalog` result at least once before creating the first workflow for a task. Make sure to call workflow_catalog more than once if you need to inspect details about a global function.
39
47
 
40
48
  Workflow JavaScript has no imports, filesystem, network, process, or timers. Delegate that work to agents. `shell(command, options)` is the trusted host RPC for deterministic gates: it inherits the workflow or active-worktree cwd, merges string `env` overrides, and returns `{ exitCode, stdout, stderr }`; nonzero exits are results, but launch failures and timeouts fail with `SHELL_FAILED`.
41
49