pi-extensible-workflows 3.4.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.
@@ -17,6 +17,7 @@ function text(message) {
17
17
  return "";
18
18
  return message.content.filter((part) => typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string").map((part) => part.text).join("");
19
19
  }
20
+ function isEmptyAbortedAssistant(message) { return message?.stopReason === "aborted" && Array.isArray(message.content) && message.content.length === 0; }
20
21
  function hasToolCall(message) {
21
22
  return typeof message === "object" && message !== null && Array.isArray(message.content) && message.content.some((part) => typeof part === "object" && part !== null && part.type === "toolCall");
22
23
  }
@@ -145,6 +146,11 @@ export async function createLocalPiSession(input) {
145
146
  });
146
147
  }
147
148
  function workflowAgentMessage(message) { return message ? { role: message.role, ...(message.content === undefined ? {} : { content: message.content }), ...(message.stopReason === undefined ? {} : { stopReason: message.stopReason }), ...(message.errorMessage === undefined ? {} : { errorMessage: message.errorMessage }), ...(message.usage === undefined ? {} : { usage: message.usage }) } : undefined; }
149
+ function latestUsableAssistant(messages) { for (let index = messages.length - 1; index >= 0; index -= 1) {
150
+ const candidate = workflowAgentMessage(messages[index]);
151
+ if (candidate?.role === "assistant" && !isEmptyAbortedAssistant(candidate))
152
+ return candidate;
153
+ } return undefined; }
148
154
  function workflowAgentStats(stats) { return { tokens: { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, total: stats.tokens.total }, cost: stats.cost }; }
149
155
  function workflowAgentState(native, prepared) {
150
156
  const tools = native.agent?.state.tools.map(({ name }) => name) ?? prepared.tools;
@@ -173,11 +179,11 @@ export async function createLocalWorkflowAgentSession(prepared, context) {
173
179
  getState: () => Object.freeze(workflowAgentState(native, prepared)),
174
180
  getSessionStats: () => workflowAgentStats(native.getSessionStats()),
175
181
  subscribe(listener) { listener({ type: "state_changed", state: workflowAgentState(native, prepared) }); return native.subscribe?.((event) => { listener(localSessionEvent(event)); }) ?? (() => undefined); },
176
- getLastAssistant: () => workflowAgentMessage([...native.messages].reverse().find((message) => message.role === "assistant")),
182
+ getLastAssistant: () => latestUsableAssistant(native.messages),
177
183
  async prompt(text) {
178
184
  if (disposed)
179
185
  throw new WorkflowError("INTERNAL_ERROR", "Local workflow session is disposed");
180
- const prompt = Promise.resolve().then(async () => { await native.prompt(text); const assistant = workflowAgentMessage([...native.messages].reverse().find((message) => message.role === "assistant")); return assistant ? { assistant } : {}; });
186
+ const prompt = Promise.resolve().then(async () => { await native.prompt(text); const assistant = latestUsableAssistant(native.messages); return assistant ? { assistant } : {}; });
181
187
  prompting = prompt;
182
188
  try {
183
189
  return await prompt;
@@ -477,11 +483,20 @@ export class WorkflowAgentExecutor {
477
483
  setupSummary = { ...setupSummary, disabledAgentResources: resourcePolicySummary(setup.sessionInput.resourcePolicy) };
478
484
  const activeSession = session;
479
485
  let lastAssistant;
486
+ const acceptAssistant = (candidate) => {
487
+ if (isEmptyAbortedAssistant(candidate)) {
488
+ const previous = activeSession.getLastAssistant?.();
489
+ if (previous && !isEmptyAbortedAssistant(previous))
490
+ lastAssistant = previous;
491
+ }
492
+ else if (candidate)
493
+ lastAssistant = candidate;
494
+ };
480
495
  const recoverTerminal = () => recoverTerminalProviderError(activeSession, options.label, options.providerErrorRecovery, async () => { try {
481
- lastAssistant = (await promptWithProviderPause(activeSession, providerContinuationPrompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant;
496
+ acceptAssistant((await promptWithProviderPause(activeSession, providerContinuationPrompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant);
482
497
  }
483
498
  catch (error) {
484
- lastAssistant = activeSession.getLastAssistant?.() ?? lastAssistant;
499
+ acceptAssistant(activeSession.getLastAssistant?.() ?? lastAssistant);
485
500
  if (!hasSchemaResult())
486
501
  throw error;
487
502
  } }, () => lastAssistant);
@@ -489,10 +504,10 @@ export class WorkflowAgentExecutor {
489
504
  let promptFailed = false;
490
505
  let promptError;
491
506
  try {
492
- lastAssistant = (await promptWithProviderPause(activeSession, prompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant;
507
+ acceptAssistant((await promptWithProviderPause(activeSession, prompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant);
493
508
  }
494
509
  catch (error) {
495
- lastAssistant = activeSession.getLastAssistant?.() ?? lastAssistant;
510
+ acceptAssistant(activeSession.getLastAssistant?.() ?? lastAssistant);
496
511
  promptFailed = true;
497
512
  promptError = error;
498
513
  }
@@ -556,7 +571,7 @@ export class WorkflowAgentExecutor {
556
571
  activity = undefined;
557
572
  shouldReport = activityChanged(previousActivity);
558
573
  if (event.message?.role === "assistant") {
559
- lastAssistant = event.message;
574
+ acceptAssistant(event.message);
560
575
  const needsMoreWork = hasToolCall(event.message);
561
576
  const final = !needsMoreWork || (options.schema !== undefined && hasSchemaResult());
562
577
  if (!budgetError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extensible-workflows",
3
- "version": "3.4.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",
@@ -94,6 +94,7 @@ function text(message: WorkflowAgentMessage | undefined): string {
94
94
  if (!message || !Array.isArray(message.content)) return "";
95
95
  return message.content.filter((part: unknown): part is { type: "text"; text: string } => typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string").map((part) => part.text).join("");
96
96
  }
97
+ function isEmptyAbortedAssistant(message: WorkflowAgentMessage | undefined): boolean { return message?.stopReason === "aborted" && Array.isArray(message.content) && message.content.length === 0; }
97
98
 
98
99
  function hasToolCall(message: unknown): boolean {
99
100
  return typeof message === "object" && message !== null && Array.isArray((message as { content?: unknown }).content) && (message as { content: unknown[] }).content.some((part) => typeof part === "object" && part !== null && (part as { type?: unknown }).type === "toolCall");
@@ -205,6 +206,7 @@ export async function createLocalPiSession(input: SessionInput): Promise<PiSessi
205
206
  }) as unknown as PiSession;
206
207
  }
207
208
  function workflowAgentMessage(message: AgentMessage | undefined): WorkflowAgentMessage | undefined { return message ? { role: message.role, ...(message.content === undefined ? {} : { content: message.content }), ...(message.stopReason === undefined ? {} : { stopReason: message.stopReason }), ...(message.errorMessage === undefined ? {} : { errorMessage: message.errorMessage }), ...(message.usage === undefined ? {} : { usage: message.usage }) } : undefined; }
209
+ function latestUsableAssistant(messages: readonly AgentMessage[]): WorkflowAgentMessage | undefined { for (let index = messages.length - 1; index >= 0; index -= 1) { const candidate = workflowAgentMessage(messages[index]); if (candidate?.role === "assistant" && !isEmptyAbortedAssistant(candidate)) return candidate; } return undefined; }
208
210
  function workflowAgentStats(stats: ReturnType<PiSession["getSessionStats"]>): WorkflowAgentSessionStats { return { tokens: { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, total: stats.tokens.total }, cost: stats.cost }; }
209
211
  function workflowAgentState(native: PiSession, prepared: Readonly<PreparedAgentSession>): WorkflowAgentSessionState {
210
212
  const tools = native.agent?.state.tools.map(({ name }) => name) ?? prepared.tools;
@@ -233,10 +235,10 @@ export async function createLocalWorkflowAgentSession(prepared: Readonly<Prepare
233
235
  getState: () => Object.freeze(workflowAgentState(native, prepared)),
234
236
  getSessionStats: () => workflowAgentStats(native.getSessionStats()),
235
237
  subscribe(listener: (event: WorkflowAgentSessionEvent) => void) { listener({ type: "state_changed", state: workflowAgentState(native, prepared) }); return native.subscribe?.((event) => { listener(localSessionEvent(event)); }) ?? (() => undefined); },
236
- getLastAssistant: () => workflowAgentMessage([...native.messages].reverse().find((message) => message.role === "assistant")),
238
+ getLastAssistant: () => latestUsableAssistant(native.messages),
237
239
  async prompt(text: string) {
238
240
  if (disposed) throw new WorkflowError("INTERNAL_ERROR", "Local workflow session is disposed");
239
- const prompt = Promise.resolve().then(async () => { await native.prompt(text); const assistant = workflowAgentMessage([...native.messages].reverse().find((message) => message.role === "assistant")); return assistant ? { assistant } : {}; });
241
+ const prompt = Promise.resolve().then(async () => { await native.prompt(text); const assistant = latestUsableAssistant(native.messages); return assistant ? { assistant } : {}; });
240
242
  prompting = prompt;
241
243
  try { return await prompt; } finally { if (prompting === prompt) prompting = undefined; }
242
244
  },
@@ -480,11 +482,17 @@ export class WorkflowAgentExecutor {
480
482
  if (setup.sessionInput.resourcePolicy) setupSummary = { ...setupSummary, disabledAgentResources: resourcePolicySummary(setup.sessionInput.resourcePolicy) };
481
483
  const activeSession = session;
482
484
  let lastAssistant: WorkflowAgentMessage | undefined;
483
- const recoverTerminal = () => recoverTerminalProviderError(activeSession, options.label, options.providerErrorRecovery, async () => { try { lastAssistant = (await promptWithProviderPause(activeSession, providerContinuationPrompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant; } catch (error) { lastAssistant = (activeSession as WorkflowAgentSession & { getLastAssistant?: () => WorkflowAgentMessage | undefined }).getLastAssistant?.() ?? lastAssistant; if (!hasSchemaResult()) throw error; } }, () => lastAssistant);
485
+ const acceptAssistant = (candidate: WorkflowAgentMessage | undefined) => {
486
+ if (isEmptyAbortedAssistant(candidate)) {
487
+ const previous = (activeSession as WorkflowAgentSession & { getLastAssistant?: () => WorkflowAgentMessage | undefined }).getLastAssistant?.();
488
+ if (previous && !isEmptyAbortedAssistant(previous)) lastAssistant = previous;
489
+ } else if (candidate) lastAssistant = candidate;
490
+ };
491
+ const recoverTerminal = () => recoverTerminalProviderError(activeSession, options.label, options.providerErrorRecovery, async () => { try { acceptAssistant((await promptWithProviderPause(activeSession, providerContinuationPrompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant); } catch (error) { acceptAssistant((activeSession as WorkflowAgentSession & { getLastAssistant?: () => WorkflowAgentMessage | undefined }).getLastAssistant?.() ?? lastAssistant); if (!hasSchemaResult()) throw error; } }, () => lastAssistant);
484
492
  const promptAndRecover = async (prompt: string): Promise<void> => {
485
493
  let promptFailed = false;
486
494
  let promptError: unknown;
487
- try { lastAssistant = (await promptWithProviderPause(activeSession, prompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant; } catch (error) { lastAssistant = (activeSession as WorkflowAgentSession & { getLastAssistant?: () => WorkflowAgentMessage | undefined }).getLastAssistant?.() ?? lastAssistant; promptFailed = true; promptError = error; }
495
+ try { acceptAssistant((await promptWithProviderPause(activeSession, prompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant); } catch (error) { acceptAssistant((activeSession as WorkflowAgentSession & { getLastAssistant?: () => WorkflowAgentMessage | undefined }).getLastAssistant?.() ?? lastAssistant); promptFailed = true; promptError = error; }
488
496
  const recovered = await recoverTerminal();
489
497
  if (promptFailed && !hasSchemaResult() && !recovered) throw promptError;
490
498
  };
@@ -519,7 +527,7 @@ export class WorkflowAgentExecutor {
519
527
  activity = undefined;
520
528
  shouldReport = activityChanged(previousActivity);
521
529
  if (event.message?.role === "assistant") {
522
- lastAssistant = event.message;
530
+ acceptAssistant(event.message);
523
531
  const needsMoreWork = hasToolCall(event.message);
524
532
  const final = !needsMoreWork || (options.schema !== undefined && hasSchemaResult());
525
533
  if (!budgetError) { try { options.budget?.afterTurn(accounting(activeSession.getSessionStats()), final); if (!final) { const instruction = options.budget?.instruction(); if (instruction) void activeSession.steer(instruction); } } catch (error) { budgetError ??= error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", error instanceof Error ? error.message : String(error)); void activeSession.abort(); } }