@osolmaz/pi-workflows 0.11.2 → 0.12.0
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/README.md +11 -7
- package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
- package/dist/builtins/autoimplement-command-batches.js +258 -0
- package/dist/builtins/autoimplement-command-batches.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +29 -3
- package/dist/builtins/autoimplement.workflow.js +876 -231
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +3 -1
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +2 -0
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/sanity-check-session.d.ts +17 -0
- package/dist/builtins/sanity-check-session.js +168 -0
- package/dist/builtins/sanity-check-session.js.map +1 -0
- package/dist/builtins/sanity-check.workflow.d.ts +83 -0
- package/dist/builtins/sanity-check.workflow.js +398 -0
- package/dist/builtins/sanity-check.workflow.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +90 -2
- package/dist/controllers/sqlite.js +380 -5
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.js +7 -7
- package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
- package/dist/extension/deferred-turn-coordinator.js +143 -0
- package/dist/extension/deferred-turn-coordinator.js.map +1 -0
- package/dist/extension/deferred-turn.d.ts +44 -0
- package/dist/extension/deferred-turn.js +110 -0
- package/dist/extension/deferred-turn.js.map +1 -0
- package/dist/extension/index.js +286 -47
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -0
- package/dist/extension/step-message.js.map +1 -1
- package/dist/herdr/setup.js +4 -4
- package/dist/workflows/command-batch.d.ts +38 -0
- package/dist/workflows/command-batch.js +176 -0
- package/dist/workflows/command-batch.js.map +1 -0
- package/dist/workflows/engine.js +8 -4
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -0
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +2 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/shell.d.ts +4 -0
- package/dist/workflows/shell.js +6 -0
- package/dist/workflows/shell.js.map +1 -1
- package/dist/workflows/store.js +3 -1
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +4 -3
- package/docs/2026-08-18-herdr-piw-plan.md +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
- package/docs/CONTROLLERS.md +2 -2
- package/docs/DEFERRED_TURNS.md +298 -0
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/HUMAN_DECISIONS.md +7 -7
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +3 -3
- package/docs/MONITOR.md +2 -2
- package/docs/WORKFLOW_COMPOSITION.md +2 -2
- package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
- package/docs/WORKFLOW_UPDATES.md +28 -5
- package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
- package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
- package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
- package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
- package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
- package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
- package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +175 -0
- package/docs/run-bundles.md +3 -1
- package/docs/workflows.md +62 -19
- package/examples/workflows/command-batch.workflow.ts +50 -0
- package/examples/workflows/sanity-check.workflow.ts +1 -0
- package/herdr-plugin.toml +3 -3
- package/package.json +1 -1
- package/schemas/decision-presentation-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v2.schema.json +1 -1
- package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
- package/schemas/human-decision-cancellation-v1.schema.json +1 -1
- package/schemas/human-decision-continuation-v1.schema.json +1 -1
- package/schemas/human-decision-delivery-v1.schema.json +1 -1
- package/schemas/human-decision-delivery-v2.schema.json +1 -1
- package/schemas/human-decision-receipt-v1.schema.json +1 -1
- package/schemas/human-decision-receipt-v2.schema.json +1 -1
- package/schemas/human-decision-request-v1.schema.json +1 -1
- package/schemas/human-decision-request-v2.schema.json +1 -1
- package/schemas/human-decision-resolution-v1.schema.json +1 -1
- package/schemas/human-decision-resolution-v2.schema.json +1 -1
- package/schemas/human-decision-settlement-v1.schema.json +1 -1
- package/skills/autodoc/SKILL.md +33 -2
- package/skills/autoimplement/SKILL.md +66 -12
- package/skills/autoplan/SKILL.md +26 -2
- package/skills/monitor/SKILL.md +35 -21
- package/skills/pi-workflows/SKILL.md +19 -5
- package/src/builtins/autoimplement-command-batches.ts +364 -0
- package/src/builtins/autoimplement.workflow.ts +1065 -261
- package/src/builtins/catalog.ts +3 -1
- package/src/builtins/index.ts +13 -0
- package/src/builtins/sanity-check-session.ts +205 -0
- package/src/builtins/sanity-check.workflow.ts +624 -0
- package/src/controllers/sqlite.ts +580 -7
- package/src/extension/decision-channels.ts +7 -7
- package/src/extension/deferred-turn-coordinator.ts +171 -0
- package/src/extension/deferred-turn.ts +166 -0
- package/src/extension/index.ts +359 -49
- package/src/extension/step-message.ts +1 -0
- package/src/herdr/setup.ts +4 -4
- package/src/workflows/command-batch.ts +254 -0
- package/src/workflows/engine.ts +8 -5
- package/src/workflows/index.ts +15 -0
- package/src/workflows/schema.ts +2 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +3 -1
- package/src/workflows/types.ts +4 -3
package/dist/extension/index.js
CHANGED
|
@@ -14,6 +14,8 @@ import { appendProgressHistory, progressRecordsFromTrace } from "../workflows/pr
|
|
|
14
14
|
import { createRunId, listRunBundles, readLastTraceEvent, readRunBundle, WorkflowRunStore, createDefinitionSnapshot, } from "../workflows/store.js";
|
|
15
15
|
import { PiControllerHost, parseControllerArgs, } from "./controller-host.js";
|
|
16
16
|
import { PiDecisionChannel, TelegramDecisionChannel, audienceChannels, createTelegramChannels, loadDecisionChannelConfig, verifyTelegramTokenFile, writeDecisionChannelProfile, } from "./decision-channels.js";
|
|
17
|
+
import { DeferredTurnCoordinator, } from "./deferred-turn-coordinator.js";
|
|
18
|
+
import { buildDeferredTurnContent, createDeferredTurnDescriptor, DEFERRED_TURN_MESSAGE_TYPE, deferredTurnMessageDetails, deferredTurnMessageId, deferredTurnSourceEventId, } from "./deferred-turn.js";
|
|
17
19
|
import { ConversationStepExecutor } from "./executor.js";
|
|
18
20
|
import { HerdrWorkflowViewer, parseViewerPlacement, PIW_SHORTCUT, PIW_SHORTCUT_HINT, VIEWER_PLACEMENTS, } from "./herdr-viewer.js";
|
|
19
21
|
import { SessionRecorder } from "./recorder.js";
|
|
@@ -25,8 +27,10 @@ const RUN_CLAIM_LEASE_MS = 30_000;
|
|
|
25
27
|
const RUN_CLAIM_RENEW_MS = 10_000;
|
|
26
28
|
const RUN_SYNC_POLL_MS = 3_000;
|
|
27
29
|
const NOTIFICATION_DELIVERY_LEASE_MS = 30_000;
|
|
30
|
+
const TURN_INTENT_DELIVERY_LEASE_MS = 30_000;
|
|
28
31
|
const WIDGET_KEY = "pi-workflows";
|
|
29
32
|
const PRESENTATION_MESSAGE_TYPE = "pi-workflows-presentation";
|
|
33
|
+
const PRESENTATION_MESSAGE_SCHEMA = "pi-workflows.presentation-message.v1";
|
|
30
34
|
const FINAL_WIDGET_TTL_MS = 60_000;
|
|
31
35
|
const WIDGET_SCROLL_STEP = 3;
|
|
32
36
|
const MAX_PRESENTATION_RESULT_CHARS = 50_000;
|
|
@@ -220,6 +224,7 @@ export default function piWorkflows(pi) {
|
|
|
220
224
|
let activationRecovery;
|
|
221
225
|
let decisionRecoveryTimer = null;
|
|
222
226
|
let decisionRecoveryActive = false;
|
|
227
|
+
let turnCoordinator;
|
|
223
228
|
const recordRunEvent = (event) => {
|
|
224
229
|
try {
|
|
225
230
|
runQueueStore?.recordRunEvent({ ...event, runnerId });
|
|
@@ -266,9 +271,7 @@ export default function piWorkflows(pi) {
|
|
|
266
271
|
runId: notification.runId,
|
|
267
272
|
kind: notification.kind,
|
|
268
273
|
},
|
|
269
|
-
},
|
|
270
|
-
? { triggerTurn: true, deliverAs: "followUp" }
|
|
271
|
-
: { triggerTurn: false });
|
|
274
|
+
}, { triggerTurn: false });
|
|
272
275
|
alreadyDelivered.add(notification.notificationId);
|
|
273
276
|
}
|
|
274
277
|
runQueueStore.markWorkflowNotificationDelivered({
|
|
@@ -277,6 +280,19 @@ export default function piWorkflows(pi) {
|
|
|
277
280
|
claimToken,
|
|
278
281
|
});
|
|
279
282
|
}
|
|
283
|
+
const idle = ctx.isIdle() && systemTurnAbort === null && !sessionClosed && !runHeld();
|
|
284
|
+
turnCoordinator.flushNatural(idle);
|
|
285
|
+
turnCoordinator.deliverFallbacks({
|
|
286
|
+
targetSessionId: sessionId,
|
|
287
|
+
send: (intent) => {
|
|
288
|
+
pi.sendMessage({
|
|
289
|
+
customType: DEFERRED_TURN_MESSAGE_TYPE,
|
|
290
|
+
content: buildDeferredTurnContent(intent),
|
|
291
|
+
display: true,
|
|
292
|
+
details: deferredTurnMessageDetails(intent),
|
|
293
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
294
|
+
},
|
|
295
|
+
}, idle);
|
|
280
296
|
}
|
|
281
297
|
catch {
|
|
282
298
|
// Delivery retries on the next poll. It never affects workflow execution.
|
|
@@ -312,6 +328,35 @@ export default function piWorkflows(pi) {
|
|
|
312
328
|
let presentationPending = null;
|
|
313
329
|
let controllerHost;
|
|
314
330
|
let controllerContext = null;
|
|
331
|
+
const branchIntentResolution = (intentId) => {
|
|
332
|
+
if (controllerContext === null)
|
|
333
|
+
return null;
|
|
334
|
+
for (const entry of controllerContext.sessionManager.getBranch()) {
|
|
335
|
+
if (entry.type !== "custom_message")
|
|
336
|
+
continue;
|
|
337
|
+
const details = entry.details;
|
|
338
|
+
if (details === null || typeof details !== "object" || Array.isArray(details))
|
|
339
|
+
continue;
|
|
340
|
+
if (details.turnIntentId !== intentId)
|
|
341
|
+
continue;
|
|
342
|
+
let resolution = null;
|
|
343
|
+
if (entry.customType === WORKFLOW_AGENT_STEP_MESSAGE_TYPE)
|
|
344
|
+
resolution = "workflowPrompt";
|
|
345
|
+
if (entry.customType === PRESENTATION_MESSAGE_TYPE)
|
|
346
|
+
resolution = "presentation";
|
|
347
|
+
if (entry.customType === DEFERRED_TURN_MESSAGE_TYPE)
|
|
348
|
+
resolution = "fallback";
|
|
349
|
+
if (resolution !== null) {
|
|
350
|
+
return { resolution, messageId: deferredTurnMessageId(intentId, resolution) };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
};
|
|
355
|
+
turnCoordinator = new DeferredTurnCoordinator({
|
|
356
|
+
store: () => runQueueStore,
|
|
357
|
+
branchResolution: branchIntentResolution,
|
|
358
|
+
leaseMs: TURN_INTENT_DELIVERY_LEASE_MS,
|
|
359
|
+
});
|
|
315
360
|
// UI updates are best-effort: a captured ctx becomes stale after session
|
|
316
361
|
// replacement or shutdown, and pi throws on any access (even `ctx.hasUI`).
|
|
317
362
|
// A workflow finishing right as the session goes away must not crash pi.
|
|
@@ -352,6 +397,115 @@ export default function piWorkflows(pi) {
|
|
|
352
397
|
};
|
|
353
398
|
/** True when the run is held for the user (escape or /workflow pause). */
|
|
354
399
|
const runHeld = () => activeRun !== null && (activeRun.engine.pauseRequested || activeRun.executor.held);
|
|
400
|
+
const originSessionId = (ctx, runId) => ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId)?.originSessionId ??
|
|
401
|
+
ctx.sessionManager.getSessionId();
|
|
402
|
+
const storeTurnDescriptor = (ctx, descriptor, eligible) => {
|
|
403
|
+
ensureRunQueueStore(ctx.cwd).ensureWorkflowTurnIntent({ ...descriptor, eligible });
|
|
404
|
+
};
|
|
405
|
+
const ensureAbortTurnIntent = (ctx, run, cause, contract, reason) => {
|
|
406
|
+
if (run.abortProvenance?.descriptor !== undefined) {
|
|
407
|
+
return run.abortProvenance.descriptor;
|
|
408
|
+
}
|
|
409
|
+
const descriptor = createDeferredTurnDescriptor({
|
|
410
|
+
runId: run.runId,
|
|
411
|
+
workflowName: run.workflowName,
|
|
412
|
+
targetSessionId: originSessionId(ctx, run.runId),
|
|
413
|
+
cause,
|
|
414
|
+
sourceEventId: deferredTurnSourceEventId({
|
|
415
|
+
runId: run.runId,
|
|
416
|
+
cause,
|
|
417
|
+
nodeId: contract.nodeId,
|
|
418
|
+
attemptId: contract.attemptId,
|
|
419
|
+
source: "agent-step-abort",
|
|
420
|
+
}),
|
|
421
|
+
observedState: cause === "claimLost" ? "handedOff" : "interrupted",
|
|
422
|
+
nodeId: contract.nodeId,
|
|
423
|
+
attemptId: contract.attemptId,
|
|
424
|
+
reason: errorMessage(reason),
|
|
425
|
+
handoff: cause === "claimLost",
|
|
426
|
+
});
|
|
427
|
+
run.abortProvenance = { cause, descriptor };
|
|
428
|
+
try {
|
|
429
|
+
storeTurnDescriptor(ctx, descriptor, false);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
run.abortProvenance.storageError = errorMessage(error);
|
|
433
|
+
}
|
|
434
|
+
return descriptor;
|
|
435
|
+
};
|
|
436
|
+
const makeRunTurnIntentEligible = (ctx, run, observedState, reason) => {
|
|
437
|
+
if (sessionClosed ||
|
|
438
|
+
run.suppressTurnIntent === true ||
|
|
439
|
+
run.abortProvenance?.cause === "claimLost") {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const targetSessionId = originSessionId(ctx, run.runId);
|
|
443
|
+
const pending = ensureRunQueueStore(ctx.cwd).findPendingWorkflowTurnIntent({
|
|
444
|
+
runId: run.runId,
|
|
445
|
+
targetSessionId,
|
|
446
|
+
});
|
|
447
|
+
if (run.childKey !== undefined &&
|
|
448
|
+
run.abortProvenance === undefined &&
|
|
449
|
+
pending?.cause !== "claimLost") {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const cause = pending?.cause ??
|
|
453
|
+
run.abortProvenance?.cause ??
|
|
454
|
+
(observedState === "timed_out" ? "timedOut" : "failed");
|
|
455
|
+
if (observedState === "cancelled" &&
|
|
456
|
+
run.abortProvenance === undefined &&
|
|
457
|
+
pending === undefined) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const previous = run.abortProvenance?.descriptor;
|
|
461
|
+
const sourceEventId = pending?.sourceEventId ??
|
|
462
|
+
previous?.sourceEventId ??
|
|
463
|
+
deferredTurnSourceEventId({
|
|
464
|
+
runId: run.runId,
|
|
465
|
+
cause,
|
|
466
|
+
nodeId: "$terminal",
|
|
467
|
+
source: "terminal",
|
|
468
|
+
});
|
|
469
|
+
const descriptor = createDeferredTurnDescriptor({
|
|
470
|
+
runId: run.runId,
|
|
471
|
+
workflowName: pending?.workflowRef ?? run.workflowName,
|
|
472
|
+
targetSessionId,
|
|
473
|
+
cause,
|
|
474
|
+
sourceEventId,
|
|
475
|
+
observedState,
|
|
476
|
+
nodeId: pending?.nodeId ?? previous?.nodeId ?? "$terminal",
|
|
477
|
+
attemptId: pending?.attemptId ?? previous?.attemptId ?? null,
|
|
478
|
+
reason: reason ?? null,
|
|
479
|
+
handoff: false,
|
|
480
|
+
});
|
|
481
|
+
run.abortProvenance = {
|
|
482
|
+
cause,
|
|
483
|
+
descriptor,
|
|
484
|
+
...(run.abortProvenance?.storageError === undefined
|
|
485
|
+
? {}
|
|
486
|
+
: { storageError: run.abortProvenance.storageError }),
|
|
487
|
+
};
|
|
488
|
+
try {
|
|
489
|
+
storeTurnDescriptor(ctx, descriptor, false);
|
|
490
|
+
ensureRunQueueStore(ctx.cwd).makeWorkflowTurnIntentEligible({
|
|
491
|
+
intentId: descriptor.intentId,
|
|
492
|
+
fallbackFacts: descriptor.fallbackFacts,
|
|
493
|
+
});
|
|
494
|
+
delete run.abortProvenance.storageError;
|
|
495
|
+
syncArmed = true;
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
const message = errorMessage(error);
|
|
499
|
+
run.abortProvenance.storageError = message;
|
|
500
|
+
recordRunEvent({
|
|
501
|
+
runId: run.runId,
|
|
502
|
+
workflowRef: run.workflowName,
|
|
503
|
+
type: "turn_intent_failed",
|
|
504
|
+
payload: { error: message },
|
|
505
|
+
});
|
|
506
|
+
notify(ctx, `Workflow ${run.workflowName} ended, but Pi Workflows could not preserve its successor turn: ${message}`, "warning");
|
|
507
|
+
}
|
|
508
|
+
};
|
|
355
509
|
const footerStatus = (state) => {
|
|
356
510
|
const label = runHeld() || state.paused ? "paused" : state.status;
|
|
357
511
|
const node = state.currentNode ?? state.waitingOn;
|
|
@@ -512,20 +666,36 @@ export default function piWorkflows(pi) {
|
|
|
512
666
|
const instructions = typeof run.presentationPrompt === "function"
|
|
513
667
|
? await resolvePresentationPrompt(run.presentationPrompt, state, abort.signal)
|
|
514
668
|
: run.presentationPrompt;
|
|
515
|
-
if (sessionClosed ||
|
|
516
|
-
run.generation !== runGeneration ||
|
|
517
|
-
abort.signal.aborted ||
|
|
518
|
-
instructions === undefined ||
|
|
519
|
-
instructions.trim().length === 0) {
|
|
669
|
+
if (sessionClosed || run.generation !== runGeneration || abort.signal.aborted) {
|
|
520
670
|
return;
|
|
521
671
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
672
|
+
if (instructions === undefined || instructions.trim().length === 0) {
|
|
673
|
+
if (run.abortProvenance !== undefined) {
|
|
674
|
+
makeRunTurnIntentEligible(ctx, run, state.status, "No result presentation was available");
|
|
675
|
+
}
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
turnCoordinator.sendNatural({
|
|
679
|
+
runId: run.runId,
|
|
680
|
+
targetSessionId: originSessionId(ctx, run.runId),
|
|
681
|
+
resolution: "presentation",
|
|
682
|
+
send: (turnIntentId) => {
|
|
683
|
+
presentationPending = run.generation;
|
|
684
|
+
suppressWorkflowAssistantTail = false;
|
|
685
|
+
pi.sendMessage({
|
|
686
|
+
customType: PRESENTATION_MESSAGE_TYPE,
|
|
687
|
+
content: buildPresentationMessage(instructions, state),
|
|
688
|
+
display: false,
|
|
689
|
+
details: {
|
|
690
|
+
schema: PRESENTATION_MESSAGE_SCHEMA,
|
|
691
|
+
...(turnIntentId === undefined ? {} : { turnIntentId }),
|
|
692
|
+
},
|
|
693
|
+
}, {
|
|
694
|
+
deliverAs: turnIntentId === undefined ? "steer" : "followUp",
|
|
695
|
+
triggerTurn: true,
|
|
696
|
+
});
|
|
697
|
+
},
|
|
698
|
+
}, ctx.isIdle() && systemTurnAbort === null);
|
|
529
699
|
}
|
|
530
700
|
catch (error) {
|
|
531
701
|
if (presentationPending === run.generation) {
|
|
@@ -540,6 +710,9 @@ export default function piWorkflows(pi) {
|
|
|
540
710
|
? `timed out after ${PRESENTATION_TIMEOUT_MS}ms`
|
|
541
711
|
: errorMessage(error);
|
|
542
712
|
notify(ctx, `Could not present workflow result: ${message}`, "warning");
|
|
713
|
+
if (run.abortProvenance !== undefined) {
|
|
714
|
+
makeRunTurnIntentEligible(ctx, run, state.status, message);
|
|
715
|
+
}
|
|
543
716
|
}
|
|
544
717
|
finally {
|
|
545
718
|
clearTimeout(timer);
|
|
@@ -590,6 +763,19 @@ export default function piWorkflows(pi) {
|
|
|
590
763
|
clearWidget(ctx);
|
|
591
764
|
return;
|
|
592
765
|
}
|
|
766
|
+
if (run.abortProvenance?.cause === "claimLost") {
|
|
767
|
+
await run.recorder?.stop().catch(() => undefined);
|
|
768
|
+
releaseClaim(run, "lost");
|
|
769
|
+
recordRunEvent({
|
|
770
|
+
runId: run.runId,
|
|
771
|
+
workflowRef: run.workflowName,
|
|
772
|
+
type: "claim_lost",
|
|
773
|
+
});
|
|
774
|
+
stopWidgetTicker();
|
|
775
|
+
clearWidget(ctx);
|
|
776
|
+
notify(ctx, `Workflow ${run.workflowName} continues under another runner (run ${run.runId}).`);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
593
779
|
releaseClaim(run, "done");
|
|
594
780
|
recordRunEvent({
|
|
595
781
|
runId: run.runId,
|
|
@@ -638,6 +824,14 @@ export default function piWorkflows(pi) {
|
|
|
638
824
|
catch (error) {
|
|
639
825
|
notify(ctx, `Could not record child workflow completion: ${errorMessage(error)}`, "warning");
|
|
640
826
|
}
|
|
827
|
+
if (state.status === "failed" || state.status === "timed_out" || state.status === "cancelled") {
|
|
828
|
+
makeRunTurnIntentEligible(ctx, run, state.status, state.error);
|
|
829
|
+
}
|
|
830
|
+
else if (run.abortProvenance !== undefined &&
|
|
831
|
+
run.presentationPrompt === undefined &&
|
|
832
|
+
(state.status === "completed" || state.status === "waiting")) {
|
|
833
|
+
makeRunTurnIntentEligible(ctx, run, state.status, state.error);
|
|
834
|
+
}
|
|
641
835
|
void presentRun(ctx, run, state);
|
|
642
836
|
if (pendingDecision !== null && state.status === "waiting") {
|
|
643
837
|
const channels = audienceChannels(decisionChannelConfig, pendingDecision.audience);
|
|
@@ -744,21 +938,29 @@ export default function piWorkflows(pi) {
|
|
|
744
938
|
}
|
|
745
939
|
const executor = new ConversationStepExecutor({
|
|
746
940
|
sendPrompt: ({ prompt, contract, presentation, kind, streaming }) => {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
941
|
+
turnCoordinator.sendNatural({
|
|
942
|
+
runId,
|
|
943
|
+
targetSessionId: originSessionId(ctx, runId),
|
|
944
|
+
resolution: "workflowPrompt",
|
|
945
|
+
send: (turnIntentId) => {
|
|
946
|
+
const details = {
|
|
947
|
+
schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
948
|
+
kind,
|
|
949
|
+
contract,
|
|
950
|
+
...(presentation !== undefined ? { presentation } : {}),
|
|
951
|
+
...(turnIntentId === undefined ? {} : { turnIntentId }),
|
|
952
|
+
};
|
|
953
|
+
pi.sendMessage({
|
|
954
|
+
customType: WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
955
|
+
content: prompt,
|
|
956
|
+
display: true,
|
|
957
|
+
details,
|
|
958
|
+
}, {
|
|
959
|
+
triggerTurn: true,
|
|
960
|
+
deliverAs: streaming ? "steer" : "followUp",
|
|
961
|
+
});
|
|
962
|
+
},
|
|
963
|
+
}, ctx.isIdle() && systemTurnAbort === null);
|
|
762
964
|
},
|
|
763
965
|
onAbort: (contract, reason) => {
|
|
764
966
|
lastExpiredAttempt = {
|
|
@@ -766,7 +968,15 @@ export default function piWorkflows(pi) {
|
|
|
766
968
|
reason: reason instanceof TimeoutError ? "timed out" : `ended: ${errorMessage(reason)}`,
|
|
767
969
|
};
|
|
768
970
|
if (!executor.held && !ctx.isIdle()) {
|
|
769
|
-
|
|
971
|
+
const cause = reason instanceof TimeoutError ? "timedOut" : run.pendingAbortCause;
|
|
972
|
+
const descriptor = cause === undefined
|
|
973
|
+
? undefined
|
|
974
|
+
: ensureAbortTurnIntent(ctx, run, cause, contract, reason);
|
|
975
|
+
run.pendingAbortCause = undefined;
|
|
976
|
+
systemTurnAbort = {
|
|
977
|
+
contract,
|
|
978
|
+
...(descriptor === undefined ? {} : { intentId: descriptor.intentId }),
|
|
979
|
+
};
|
|
770
980
|
ctx.abort();
|
|
771
981
|
}
|
|
772
982
|
},
|
|
@@ -859,6 +1069,7 @@ export default function piWorkflows(pi) {
|
|
|
859
1069
|
run.renewTimer = setInterval(() => {
|
|
860
1070
|
try {
|
|
861
1071
|
if (!store.renewWorkflowRunClaim({ runId, claimToken: token, leaseMs: RUN_CLAIM_LEASE_MS })) {
|
|
1072
|
+
run.pendingAbortCause = "claimLost";
|
|
862
1073
|
run.engine.cancel();
|
|
863
1074
|
}
|
|
864
1075
|
}
|
|
@@ -952,6 +1163,11 @@ export default function piWorkflows(pi) {
|
|
|
952
1163
|
}
|
|
953
1164
|
if (terminalStatus !== undefined) {
|
|
954
1165
|
recordRunEvent({ runId, workflowRef: workflow.name, type: terminalStatus, payload: {} });
|
|
1166
|
+
if (terminalStatus === "failed" ||
|
|
1167
|
+
terminalStatus === "timed_out" ||
|
|
1168
|
+
terminalStatus === "cancelled") {
|
|
1169
|
+
makeRunTurnIntentEligible(ctx, run, terminalStatus, message);
|
|
1170
|
+
}
|
|
955
1171
|
notify(ctx, `Workflow ${workflow.name} ${terminalStatus} (run ${runId}).`);
|
|
956
1172
|
return;
|
|
957
1173
|
}
|
|
@@ -961,6 +1177,7 @@ export default function piWorkflows(pi) {
|
|
|
961
1177
|
type: "failed",
|
|
962
1178
|
payload: { error: message },
|
|
963
1179
|
});
|
|
1180
|
+
makeRunTurnIntentEligible(ctx, run, "failed", message);
|
|
964
1181
|
try {
|
|
965
1182
|
run.onFinish?.({ state: "failed", runId, error: message });
|
|
966
1183
|
}
|
|
@@ -1155,10 +1372,12 @@ export default function piWorkflows(pi) {
|
|
|
1155
1372
|
}
|
|
1156
1373
|
return null;
|
|
1157
1374
|
};
|
|
1158
|
-
const cancelWorkflowControl = async (ctx, requestedRunId) => {
|
|
1375
|
+
const cancelWorkflowControl = async (ctx, origin, requestedRunId) => {
|
|
1159
1376
|
if (activeRun && (requestedRunId === undefined || requestedRunId === activeRun.runId)) {
|
|
1160
1377
|
const workflowName = activeRun.workflowName;
|
|
1161
1378
|
const runId = activeRun.runId;
|
|
1379
|
+
activeRun.pendingAbortCause = origin === "agent" ? "agentCancelled" : undefined;
|
|
1380
|
+
activeRun.suppressTurnIntent = origin === "user";
|
|
1162
1381
|
activeRun.engine.cancel();
|
|
1163
1382
|
return {
|
|
1164
1383
|
message: `Cancelling workflow ${workflowName}…`,
|
|
@@ -1250,6 +1469,7 @@ export default function piWorkflows(pi) {
|
|
|
1250
1469
|
},
|
|
1251
1470
|
};
|
|
1252
1471
|
}
|
|
1472
|
+
activeRun.suppressTurnIntent = true;
|
|
1253
1473
|
activeRun.engine.pause();
|
|
1254
1474
|
renderWidget(ctx);
|
|
1255
1475
|
return {
|
|
@@ -1282,6 +1502,7 @@ export default function piWorkflows(pi) {
|
|
|
1282
1502
|
level: "warning",
|
|
1283
1503
|
};
|
|
1284
1504
|
}
|
|
1505
|
+
activeRun.suppressTurnIntent = false;
|
|
1285
1506
|
activeRun.engine.resume();
|
|
1286
1507
|
activeRun.executor.release();
|
|
1287
1508
|
renderWidget(ctx);
|
|
@@ -1758,20 +1979,28 @@ export default function piWorkflows(pi) {
|
|
|
1758
1979
|
payload: { errorCode: safe.code, error: safe.message },
|
|
1759
1980
|
});
|
|
1760
1981
|
const content = `Workflow ${claimed.workflowName} failed to start (run ${claimed.runId}): ${safe.message}. Inspect the error and call workflow start again only after you correct the cause.`;
|
|
1761
|
-
|
|
1762
|
-
|
|
1982
|
+
const cause = "launchFailed";
|
|
1983
|
+
const descriptor = createDeferredTurnDescriptor({
|
|
1984
|
+
runId: claimed.runId,
|
|
1985
|
+
workflowName: claimed.workflowName,
|
|
1986
|
+
targetSessionId: claimed.originSessionId ?? ctx.sessionManager.getSessionId(),
|
|
1987
|
+
cause,
|
|
1988
|
+
sourceEventId: deferredTurnSourceEventId({
|
|
1763
1989
|
runId: claimed.runId,
|
|
1990
|
+
cause,
|
|
1764
1991
|
nodeId: "$launch",
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1992
|
+
source: "queued-launch",
|
|
1993
|
+
}),
|
|
1994
|
+
observedState: "failedToStart",
|
|
1995
|
+
nodeId: "$launch",
|
|
1996
|
+
reason: safe.message,
|
|
1997
|
+
});
|
|
1998
|
+
try {
|
|
1999
|
+
storeTurnDescriptor(ctx, descriptor, true);
|
|
2000
|
+
syncArmed = true;
|
|
1772
2001
|
}
|
|
1773
|
-
catch {
|
|
1774
|
-
|
|
2002
|
+
catch (intentError) {
|
|
2003
|
+
notify(ctx, `Workflow ${claimed.workflowName} failed to start, but Pi Workflows could not preserve its successor turn: ${errorMessage(intentError)}`, "warning");
|
|
1775
2004
|
}
|
|
1776
2005
|
notify(ctx, content, "error");
|
|
1777
2006
|
runSyncPass(ctx);
|
|
@@ -1830,7 +2059,7 @@ export default function piWorkflows(pi) {
|
|
|
1830
2059
|
return;
|
|
1831
2060
|
}
|
|
1832
2061
|
if (parsed.kind === "cancel") {
|
|
1833
|
-
const result = await cancelWorkflowControl(ctx);
|
|
2062
|
+
const result = await cancelWorkflowControl(ctx, "user");
|
|
1834
2063
|
notify(ctx, result.message, result.level);
|
|
1835
2064
|
return;
|
|
1836
2065
|
}
|
|
@@ -1990,6 +2219,7 @@ export default function piWorkflows(pi) {
|
|
|
1990
2219
|
case "stop":
|
|
1991
2220
|
if (activeRun?.childKey !== undefined) {
|
|
1992
2221
|
activeRun.interruptionRequested = true;
|
|
2222
|
+
activeRun.pendingAbortCause = "controllerInterrupted";
|
|
1993
2223
|
activeRun.engine.cancel();
|
|
1994
2224
|
}
|
|
1995
2225
|
await host.stop();
|
|
@@ -2006,7 +2236,7 @@ export default function piWorkflows(pi) {
|
|
|
2006
2236
|
name: "workflow",
|
|
2007
2237
|
label: "Workflow",
|
|
2008
2238
|
description: [
|
|
2009
|
-
"List, start, inspect, pause, resume, cancel, answer, update, or complete
|
|
2239
|
+
"List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
|
|
2010
2240
|
"When the user asks to monitor, watch, poll, or check something repeatedly, start the built-in monitor workflow with input keys task, everyMinutes, stopWhen, and optional maxChecks.",
|
|
2011
2241
|
"Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
|
|
2012
2242
|
"Do not start repeated work without the user's request, and keep monitoring observation-only unless the user authorizes mutations.",
|
|
@@ -2032,7 +2262,7 @@ export default function piWorkflows(pi) {
|
|
|
2032
2262
|
control = resumeWorkflowControl(ctx);
|
|
2033
2263
|
break;
|
|
2034
2264
|
case "cancel":
|
|
2035
|
-
control = await cancelWorkflowControl(ctx, params.runId);
|
|
2265
|
+
control = await cancelWorkflowControl(ctx, "agent", params.runId);
|
|
2036
2266
|
break;
|
|
2037
2267
|
case "answer": {
|
|
2038
2268
|
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
@@ -2190,6 +2420,7 @@ export default function piWorkflows(pi) {
|
|
|
2190
2420
|
if (!aborted || runHeld()) {
|
|
2191
2421
|
return;
|
|
2192
2422
|
}
|
|
2423
|
+
run.suppressTurnIntent = true;
|
|
2193
2424
|
run.engine.pause();
|
|
2194
2425
|
run.executor.hold();
|
|
2195
2426
|
renderWidget(ctx);
|
|
@@ -2228,6 +2459,7 @@ export default function piWorkflows(pi) {
|
|
|
2228
2459
|
activeRun?.recorder?.handleToolEnd(event);
|
|
2229
2460
|
});
|
|
2230
2461
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
2462
|
+
systemTurnAbort = null;
|
|
2231
2463
|
if (activeRun === null) {
|
|
2232
2464
|
try {
|
|
2233
2465
|
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
|
|
@@ -2241,23 +2473,30 @@ export default function piWorkflows(pi) {
|
|
|
2241
2473
|
return;
|
|
2242
2474
|
}
|
|
2243
2475
|
}
|
|
2476
|
+
const flushedNatural = turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
|
|
2244
2477
|
const run = activeRun;
|
|
2245
2478
|
if (!run) {
|
|
2246
2479
|
presentationPending = null;
|
|
2480
|
+
runSyncPass(ctx);
|
|
2247
2481
|
return;
|
|
2248
2482
|
}
|
|
2249
2483
|
await run.recorder?.synchronize(ctx).catch(() => undefined);
|
|
2250
2484
|
run.recorder?.settleAttempt();
|
|
2251
2485
|
run.executor.setStreaming(false);
|
|
2252
|
-
|
|
2486
|
+
if (flushedNatural === 0)
|
|
2487
|
+
run.executor.handleAgentSettled();
|
|
2488
|
+
runSyncPass(ctx);
|
|
2253
2489
|
});
|
|
2254
2490
|
pi.on("session_shutdown", async () => {
|
|
2255
2491
|
sessionClosed = true;
|
|
2256
2492
|
herdrProbeGeneration += 1;
|
|
2257
2493
|
systemTurnAbort = null;
|
|
2258
2494
|
suppressWorkflowAssistantTail = false;
|
|
2495
|
+
turnCoordinator.clearDeferred();
|
|
2259
2496
|
supersedePresentation();
|
|
2260
2497
|
const run = activeRun;
|
|
2498
|
+
if (run !== null)
|
|
2499
|
+
run.suppressTurnIntent = true;
|
|
2261
2500
|
if (run !== null && run.claimToken !== undefined) {
|
|
2262
2501
|
// Queued interactive runs park: no terminal event, no recorded partial
|
|
2263
2502
|
// attempt, and the claim releases so another runner can resume.
|