@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/src/extension/index.ts
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
SqliteControllerStore,
|
|
9
9
|
type WorkflowRunQueueRecord,
|
|
10
10
|
} from "../controllers/index.js";
|
|
11
|
+
import type {
|
|
12
|
+
WorkflowTurnIntentCause,
|
|
13
|
+
WorkflowTurnIntentResolution,
|
|
14
|
+
} from "../controllers/sqlite.js";
|
|
11
15
|
import type { JsonObject } from "../controllers/types.js";
|
|
12
16
|
import type { WorkflowSchedulerResult } from "../controllers/workflows.js";
|
|
13
17
|
import { compositionMetadata } from "../workflows/composition.js";
|
|
@@ -63,6 +67,19 @@ import {
|
|
|
63
67
|
type HumanDecisionChannelAnswer,
|
|
64
68
|
type SettledHumanDecision,
|
|
65
69
|
} from "./decision-channels.js";
|
|
70
|
+
import {
|
|
71
|
+
DeferredTurnCoordinator,
|
|
72
|
+
type BranchIntentResolution,
|
|
73
|
+
} from "./deferred-turn-coordinator.js";
|
|
74
|
+
import {
|
|
75
|
+
buildDeferredTurnContent,
|
|
76
|
+
createDeferredTurnDescriptor,
|
|
77
|
+
DEFERRED_TURN_MESSAGE_TYPE,
|
|
78
|
+
deferredTurnMessageDetails,
|
|
79
|
+
deferredTurnMessageId,
|
|
80
|
+
deferredTurnSourceEventId,
|
|
81
|
+
type DeferredTurnDescriptor,
|
|
82
|
+
} from "./deferred-turn.js";
|
|
66
83
|
import { ConversationStepExecutor } from "./executor.js";
|
|
67
84
|
import {
|
|
68
85
|
HerdrWorkflowViewer,
|
|
@@ -108,8 +125,10 @@ const RUN_CLAIM_LEASE_MS = 30_000;
|
|
|
108
125
|
const RUN_CLAIM_RENEW_MS = 10_000;
|
|
109
126
|
const RUN_SYNC_POLL_MS = 3_000;
|
|
110
127
|
const NOTIFICATION_DELIVERY_LEASE_MS = 30_000;
|
|
128
|
+
const TURN_INTENT_DELIVERY_LEASE_MS = 30_000;
|
|
111
129
|
const WIDGET_KEY = "pi-workflows";
|
|
112
130
|
const PRESENTATION_MESSAGE_TYPE = "pi-workflows-presentation";
|
|
131
|
+
const PRESENTATION_MESSAGE_SCHEMA = "pi-workflows.presentation-message.v1";
|
|
113
132
|
const FINAL_WIDGET_TTL_MS = 60_000;
|
|
114
133
|
const WIDGET_SCROLL_STEP = 3;
|
|
115
134
|
const MAX_PRESENTATION_RESULT_CHARS = 50_000;
|
|
@@ -138,6 +157,12 @@ type PresentationPromptBuilder = Exclude<
|
|
|
138
157
|
string | undefined
|
|
139
158
|
>;
|
|
140
159
|
|
|
160
|
+
type AbortProvenance = {
|
|
161
|
+
cause: WorkflowTurnIntentCause;
|
|
162
|
+
descriptor?: DeferredTurnDescriptor;
|
|
163
|
+
storageError?: string;
|
|
164
|
+
};
|
|
165
|
+
|
|
141
166
|
type ActiveRun = {
|
|
142
167
|
runId: string;
|
|
143
168
|
workflowName: string;
|
|
@@ -153,6 +178,9 @@ type ActiveRun = {
|
|
|
153
178
|
onFinish?: (result: WorkflowSchedulerResult) => void;
|
|
154
179
|
completion?: Promise<void>;
|
|
155
180
|
interruptionRequested?: boolean;
|
|
181
|
+
pendingAbortCause?: WorkflowTurnIntentCause | undefined;
|
|
182
|
+
abortProvenance?: AbortProvenance | undefined;
|
|
183
|
+
suppressTurnIntent?: boolean | undefined;
|
|
156
184
|
claimToken?: string | undefined;
|
|
157
185
|
renewTimer?: ReturnType<typeof setInterval> | undefined;
|
|
158
186
|
/** True for runs this session resumed from the queue. */
|
|
@@ -400,6 +428,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
400
428
|
let activationRecovery: ((ctx: ExtensionContext) => void) | undefined;
|
|
401
429
|
let decisionRecoveryTimer: ReturnType<typeof setInterval> | null = null;
|
|
402
430
|
let decisionRecoveryActive = false;
|
|
431
|
+
let turnCoordinator: DeferredTurnCoordinator;
|
|
403
432
|
|
|
404
433
|
const recordRunEvent = (event: {
|
|
405
434
|
runId: string;
|
|
@@ -453,9 +482,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
453
482
|
kind: notification.kind,
|
|
454
483
|
},
|
|
455
484
|
},
|
|
456
|
-
|
|
457
|
-
? { triggerTurn: true, deliverAs: "followUp" }
|
|
458
|
-
: { triggerTurn: false },
|
|
485
|
+
{ triggerTurn: false },
|
|
459
486
|
);
|
|
460
487
|
alreadyDelivered.add(notification.notificationId);
|
|
461
488
|
}
|
|
@@ -465,6 +492,25 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
465
492
|
claimToken,
|
|
466
493
|
});
|
|
467
494
|
}
|
|
495
|
+
const idle = ctx.isIdle() && systemTurnAbort === null && !sessionClosed && !runHeld();
|
|
496
|
+
turnCoordinator.flushNatural(idle);
|
|
497
|
+
turnCoordinator.deliverFallbacks(
|
|
498
|
+
{
|
|
499
|
+
targetSessionId: sessionId,
|
|
500
|
+
send: (intent) => {
|
|
501
|
+
pi.sendMessage(
|
|
502
|
+
{
|
|
503
|
+
customType: DEFERRED_TURN_MESSAGE_TYPE,
|
|
504
|
+
content: buildDeferredTurnContent(intent),
|
|
505
|
+
display: true,
|
|
506
|
+
details: deferredTurnMessageDetails(intent),
|
|
507
|
+
},
|
|
508
|
+
{ triggerTurn: true, deliverAs: "followUp" },
|
|
509
|
+
);
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
idle,
|
|
513
|
+
);
|
|
468
514
|
} catch {
|
|
469
515
|
// Delivery retries on the next poll. It never affects workflow execution.
|
|
470
516
|
}
|
|
@@ -480,7 +526,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
480
526
|
runSyncTimer.unref?.();
|
|
481
527
|
};
|
|
482
528
|
let activeRun: ActiveRun | null = null;
|
|
483
|
-
let systemTurnAbort: AgentStepContract | null = null;
|
|
529
|
+
let systemTurnAbort: { contract: AgentStepContract; intentId?: string } | null = null;
|
|
484
530
|
let suppressWorkflowAssistantTail = false;
|
|
485
531
|
let lastExpiredAttempt: { contract: AgentStepContract; reason: string } | null = null;
|
|
486
532
|
// The interactive run currently parked at a checkpoint, if any.
|
|
@@ -501,6 +547,30 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
501
547
|
let controllerHost: PiControllerHost | undefined;
|
|
502
548
|
let controllerContext: ExtensionContext | null = null;
|
|
503
549
|
|
|
550
|
+
const branchIntentResolution = (intentId: string): BranchIntentResolution | null => {
|
|
551
|
+
if (controllerContext === null) return null;
|
|
552
|
+
for (const entry of controllerContext.sessionManager.getBranch()) {
|
|
553
|
+
if (entry.type !== "custom_message") continue;
|
|
554
|
+
const details = entry.details;
|
|
555
|
+
if (details === null || typeof details !== "object" || Array.isArray(details)) continue;
|
|
556
|
+
if ((details as { turnIntentId?: unknown }).turnIntentId !== intentId) continue;
|
|
557
|
+
let resolution: WorkflowTurnIntentResolution | null = null;
|
|
558
|
+
if (entry.customType === WORKFLOW_AGENT_STEP_MESSAGE_TYPE) resolution = "workflowPrompt";
|
|
559
|
+
if (entry.customType === PRESENTATION_MESSAGE_TYPE) resolution = "presentation";
|
|
560
|
+
if (entry.customType === DEFERRED_TURN_MESSAGE_TYPE) resolution = "fallback";
|
|
561
|
+
if (resolution !== null) {
|
|
562
|
+
return { resolution, messageId: deferredTurnMessageId(intentId, resolution) };
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
turnCoordinator = new DeferredTurnCoordinator({
|
|
569
|
+
store: () => runQueueStore,
|
|
570
|
+
branchResolution: branchIntentResolution,
|
|
571
|
+
leaseMs: TURN_INTENT_DELIVERY_LEASE_MS,
|
|
572
|
+
});
|
|
573
|
+
|
|
504
574
|
// UI updates are best-effort: a captured ctx becomes stale after session
|
|
505
575
|
// replacement or shutdown, and pi throws on any access (even `ctx.hasUI`).
|
|
506
576
|
// A workflow finishing right as the session goes away must not crash pi.
|
|
@@ -542,6 +612,145 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
542
612
|
const runHeld = (): boolean =>
|
|
543
613
|
activeRun !== null && (activeRun.engine.pauseRequested || activeRun.executor.held);
|
|
544
614
|
|
|
615
|
+
const originSessionId = (ctx: ExtensionContext, runId: string): string =>
|
|
616
|
+
ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId)?.originSessionId ??
|
|
617
|
+
ctx.sessionManager.getSessionId();
|
|
618
|
+
|
|
619
|
+
const storeTurnDescriptor = (
|
|
620
|
+
ctx: ExtensionContext,
|
|
621
|
+
descriptor: DeferredTurnDescriptor,
|
|
622
|
+
eligible: boolean,
|
|
623
|
+
): void => {
|
|
624
|
+
ensureRunQueueStore(ctx.cwd).ensureWorkflowTurnIntent({ ...descriptor, eligible });
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const ensureAbortTurnIntent = (
|
|
628
|
+
ctx: ExtensionContext,
|
|
629
|
+
run: ActiveRun,
|
|
630
|
+
cause: WorkflowTurnIntentCause,
|
|
631
|
+
contract: AgentStepContract,
|
|
632
|
+
reason: unknown,
|
|
633
|
+
): DeferredTurnDescriptor => {
|
|
634
|
+
if (run.abortProvenance?.descriptor !== undefined) {
|
|
635
|
+
return run.abortProvenance.descriptor;
|
|
636
|
+
}
|
|
637
|
+
const descriptor = createDeferredTurnDescriptor({
|
|
638
|
+
runId: run.runId,
|
|
639
|
+
workflowName: run.workflowName,
|
|
640
|
+
targetSessionId: originSessionId(ctx, run.runId),
|
|
641
|
+
cause,
|
|
642
|
+
sourceEventId: deferredTurnSourceEventId({
|
|
643
|
+
runId: run.runId,
|
|
644
|
+
cause,
|
|
645
|
+
nodeId: contract.nodeId,
|
|
646
|
+
attemptId: contract.attemptId,
|
|
647
|
+
source: "agent-step-abort",
|
|
648
|
+
}),
|
|
649
|
+
observedState: cause === "claimLost" ? "handedOff" : "interrupted",
|
|
650
|
+
nodeId: contract.nodeId,
|
|
651
|
+
attemptId: contract.attemptId,
|
|
652
|
+
reason: errorMessage(reason),
|
|
653
|
+
handoff: cause === "claimLost",
|
|
654
|
+
});
|
|
655
|
+
run.abortProvenance = { cause, descriptor };
|
|
656
|
+
try {
|
|
657
|
+
storeTurnDescriptor(ctx, descriptor, false);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
run.abortProvenance.storageError = errorMessage(error);
|
|
660
|
+
}
|
|
661
|
+
return descriptor;
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const makeRunTurnIntentEligible = (
|
|
665
|
+
ctx: ExtensionContext,
|
|
666
|
+
run: ActiveRun,
|
|
667
|
+
observedState: string,
|
|
668
|
+
reason?: string,
|
|
669
|
+
): void => {
|
|
670
|
+
if (
|
|
671
|
+
sessionClosed ||
|
|
672
|
+
run.suppressTurnIntent === true ||
|
|
673
|
+
run.abortProvenance?.cause === "claimLost"
|
|
674
|
+
) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const targetSessionId = originSessionId(ctx, run.runId);
|
|
678
|
+
const pending = ensureRunQueueStore(ctx.cwd).findPendingWorkflowTurnIntent({
|
|
679
|
+
runId: run.runId,
|
|
680
|
+
targetSessionId,
|
|
681
|
+
});
|
|
682
|
+
if (
|
|
683
|
+
run.childKey !== undefined &&
|
|
684
|
+
run.abortProvenance === undefined &&
|
|
685
|
+
pending?.cause !== "claimLost"
|
|
686
|
+
) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const cause =
|
|
690
|
+
pending?.cause ??
|
|
691
|
+
run.abortProvenance?.cause ??
|
|
692
|
+
(observedState === "timed_out" ? "timedOut" : "failed");
|
|
693
|
+
if (
|
|
694
|
+
observedState === "cancelled" &&
|
|
695
|
+
run.abortProvenance === undefined &&
|
|
696
|
+
pending === undefined
|
|
697
|
+
) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const previous = run.abortProvenance?.descriptor;
|
|
701
|
+
const sourceEventId =
|
|
702
|
+
pending?.sourceEventId ??
|
|
703
|
+
previous?.sourceEventId ??
|
|
704
|
+
deferredTurnSourceEventId({
|
|
705
|
+
runId: run.runId,
|
|
706
|
+
cause,
|
|
707
|
+
nodeId: "$terminal",
|
|
708
|
+
source: "terminal",
|
|
709
|
+
});
|
|
710
|
+
const descriptor = createDeferredTurnDescriptor({
|
|
711
|
+
runId: run.runId,
|
|
712
|
+
workflowName: pending?.workflowRef ?? run.workflowName,
|
|
713
|
+
targetSessionId,
|
|
714
|
+
cause,
|
|
715
|
+
sourceEventId,
|
|
716
|
+
observedState,
|
|
717
|
+
nodeId: pending?.nodeId ?? previous?.nodeId ?? "$terminal",
|
|
718
|
+
attemptId: pending?.attemptId ?? previous?.attemptId ?? null,
|
|
719
|
+
reason: reason ?? null,
|
|
720
|
+
handoff: false,
|
|
721
|
+
});
|
|
722
|
+
run.abortProvenance = {
|
|
723
|
+
cause,
|
|
724
|
+
descriptor,
|
|
725
|
+
...(run.abortProvenance?.storageError === undefined
|
|
726
|
+
? {}
|
|
727
|
+
: { storageError: run.abortProvenance.storageError }),
|
|
728
|
+
};
|
|
729
|
+
try {
|
|
730
|
+
storeTurnDescriptor(ctx, descriptor, false);
|
|
731
|
+
ensureRunQueueStore(ctx.cwd).makeWorkflowTurnIntentEligible({
|
|
732
|
+
intentId: descriptor.intentId,
|
|
733
|
+
fallbackFacts: descriptor.fallbackFacts,
|
|
734
|
+
});
|
|
735
|
+
delete run.abortProvenance.storageError;
|
|
736
|
+
syncArmed = true;
|
|
737
|
+
} catch (error) {
|
|
738
|
+
const message = errorMessage(error);
|
|
739
|
+
run.abortProvenance.storageError = message;
|
|
740
|
+
recordRunEvent({
|
|
741
|
+
runId: run.runId,
|
|
742
|
+
workflowRef: run.workflowName,
|
|
743
|
+
type: "turn_intent_failed",
|
|
744
|
+
payload: { error: message },
|
|
745
|
+
});
|
|
746
|
+
notify(
|
|
747
|
+
ctx,
|
|
748
|
+
`Workflow ${run.workflowName} ended, but Pi Workflows could not preserve its successor turn: ${message}`,
|
|
749
|
+
"warning",
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
545
754
|
const footerStatus = (state: WorkflowRunState): string => {
|
|
546
755
|
const label = runHeld() || state.paused ? "paused" : state.status;
|
|
547
756
|
const node = state.currentNode ?? state.waitingOn;
|
|
@@ -742,24 +951,41 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
742
951
|
typeof run.presentationPrompt === "function"
|
|
743
952
|
? await resolvePresentationPrompt(run.presentationPrompt, state, abort.signal)
|
|
744
953
|
: run.presentationPrompt;
|
|
745
|
-
if (
|
|
746
|
-
sessionClosed ||
|
|
747
|
-
run.generation !== runGeneration ||
|
|
748
|
-
abort.signal.aborted ||
|
|
749
|
-
instructions === undefined ||
|
|
750
|
-
instructions.trim().length === 0
|
|
751
|
-
) {
|
|
954
|
+
if (sessionClosed || run.generation !== runGeneration || abort.signal.aborted) {
|
|
752
955
|
return;
|
|
753
956
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
957
|
+
if (instructions === undefined || instructions.trim().length === 0) {
|
|
958
|
+
if (run.abortProvenance !== undefined) {
|
|
959
|
+
makeRunTurnIntentEligible(ctx, run, state.status, "No result presentation was available");
|
|
960
|
+
}
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
turnCoordinator.sendNatural(
|
|
757
964
|
{
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
965
|
+
runId: run.runId,
|
|
966
|
+
targetSessionId: originSessionId(ctx, run.runId),
|
|
967
|
+
resolution: "presentation",
|
|
968
|
+
send: (turnIntentId) => {
|
|
969
|
+
presentationPending = run.generation;
|
|
970
|
+
suppressWorkflowAssistantTail = false;
|
|
971
|
+
pi.sendMessage(
|
|
972
|
+
{
|
|
973
|
+
customType: PRESENTATION_MESSAGE_TYPE,
|
|
974
|
+
content: buildPresentationMessage(instructions, state),
|
|
975
|
+
display: false,
|
|
976
|
+
details: {
|
|
977
|
+
schema: PRESENTATION_MESSAGE_SCHEMA,
|
|
978
|
+
...(turnIntentId === undefined ? {} : { turnIntentId }),
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
deliverAs: turnIntentId === undefined ? "steer" : "followUp",
|
|
983
|
+
triggerTurn: true,
|
|
984
|
+
},
|
|
985
|
+
);
|
|
986
|
+
},
|
|
761
987
|
},
|
|
762
|
-
|
|
988
|
+
ctx.isIdle() && systemTurnAbort === null,
|
|
763
989
|
);
|
|
764
990
|
} catch (error) {
|
|
765
991
|
if (presentationPending === run.generation) {
|
|
@@ -777,6 +1003,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
777
1003
|
? `timed out after ${PRESENTATION_TIMEOUT_MS}ms`
|
|
778
1004
|
: errorMessage(error);
|
|
779
1005
|
notify(ctx, `Could not present workflow result: ${message}`, "warning");
|
|
1006
|
+
if (run.abortProvenance !== undefined) {
|
|
1007
|
+
makeRunTurnIntentEligible(ctx, run, state.status, message);
|
|
1008
|
+
}
|
|
780
1009
|
} finally {
|
|
781
1010
|
clearTimeout(timer);
|
|
782
1011
|
if (presentationAbort === abort) {
|
|
@@ -829,6 +1058,22 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
829
1058
|
clearWidget(ctx);
|
|
830
1059
|
return;
|
|
831
1060
|
}
|
|
1061
|
+
if (run.abortProvenance?.cause === "claimLost") {
|
|
1062
|
+
await run.recorder?.stop().catch(() => undefined);
|
|
1063
|
+
releaseClaim(run, "lost");
|
|
1064
|
+
recordRunEvent({
|
|
1065
|
+
runId: run.runId,
|
|
1066
|
+
workflowRef: run.workflowName,
|
|
1067
|
+
type: "claim_lost",
|
|
1068
|
+
});
|
|
1069
|
+
stopWidgetTicker();
|
|
1070
|
+
clearWidget(ctx);
|
|
1071
|
+
notify(
|
|
1072
|
+
ctx,
|
|
1073
|
+
`Workflow ${run.workflowName} continues under another runner (run ${run.runId}).`,
|
|
1074
|
+
);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
832
1077
|
releaseClaim(run, "done");
|
|
833
1078
|
recordRunEvent({
|
|
834
1079
|
runId: run.runId,
|
|
@@ -880,6 +1125,15 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
880
1125
|
} catch (error) {
|
|
881
1126
|
notify(ctx, `Could not record child workflow completion: ${errorMessage(error)}`, "warning");
|
|
882
1127
|
}
|
|
1128
|
+
if (state.status === "failed" || state.status === "timed_out" || state.status === "cancelled") {
|
|
1129
|
+
makeRunTurnIntentEligible(ctx, run, state.status, state.error);
|
|
1130
|
+
} else if (
|
|
1131
|
+
run.abortProvenance !== undefined &&
|
|
1132
|
+
run.presentationPrompt === undefined &&
|
|
1133
|
+
(state.status === "completed" || state.status === "waiting")
|
|
1134
|
+
) {
|
|
1135
|
+
makeRunTurnIntentEligible(ctx, run, state.status, state.error);
|
|
1136
|
+
}
|
|
883
1137
|
void presentRun(ctx, run, state);
|
|
884
1138
|
if (pendingDecision !== null && state.status === "waiting") {
|
|
885
1139
|
const channels = audienceChannels(decisionChannelConfig, pendingDecision.audience);
|
|
@@ -1011,23 +1265,34 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1011
1265
|
|
|
1012
1266
|
const executor = new ConversationStepExecutor({
|
|
1013
1267
|
sendPrompt: ({ prompt, contract, presentation, kind, streaming }) => {
|
|
1014
|
-
|
|
1015
|
-
schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
1016
|
-
kind,
|
|
1017
|
-
contract,
|
|
1018
|
-
...(presentation !== undefined ? { presentation } : {}),
|
|
1019
|
-
};
|
|
1020
|
-
pi.sendMessage(
|
|
1021
|
-
{
|
|
1022
|
-
customType: WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
1023
|
-
content: prompt,
|
|
1024
|
-
display: true,
|
|
1025
|
-
details,
|
|
1026
|
-
},
|
|
1268
|
+
turnCoordinator.sendNatural(
|
|
1027
1269
|
{
|
|
1028
|
-
|
|
1029
|
-
|
|
1270
|
+
runId,
|
|
1271
|
+
targetSessionId: originSessionId(ctx, runId),
|
|
1272
|
+
resolution: "workflowPrompt",
|
|
1273
|
+
send: (turnIntentId) => {
|
|
1274
|
+
const details: WorkflowAgentStepMessageDetails = {
|
|
1275
|
+
schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
1276
|
+
kind,
|
|
1277
|
+
contract,
|
|
1278
|
+
...(presentation !== undefined ? { presentation } : {}),
|
|
1279
|
+
...(turnIntentId === undefined ? {} : { turnIntentId }),
|
|
1280
|
+
};
|
|
1281
|
+
pi.sendMessage(
|
|
1282
|
+
{
|
|
1283
|
+
customType: WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
1284
|
+
content: prompt,
|
|
1285
|
+
display: true,
|
|
1286
|
+
details,
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
triggerTurn: true,
|
|
1290
|
+
deliverAs: streaming ? "steer" : "followUp",
|
|
1291
|
+
},
|
|
1292
|
+
);
|
|
1293
|
+
},
|
|
1030
1294
|
},
|
|
1295
|
+
ctx.isIdle() && systemTurnAbort === null,
|
|
1031
1296
|
);
|
|
1032
1297
|
},
|
|
1033
1298
|
onAbort: (contract, reason) => {
|
|
@@ -1036,7 +1301,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1036
1301
|
reason: reason instanceof TimeoutError ? "timed out" : `ended: ${errorMessage(reason)}`,
|
|
1037
1302
|
};
|
|
1038
1303
|
if (!executor.held && !ctx.isIdle()) {
|
|
1039
|
-
|
|
1304
|
+
const cause = reason instanceof TimeoutError ? "timedOut" : run.pendingAbortCause;
|
|
1305
|
+
const descriptor =
|
|
1306
|
+
cause === undefined
|
|
1307
|
+
? undefined
|
|
1308
|
+
: ensureAbortTurnIntent(ctx, run, cause, contract, reason);
|
|
1309
|
+
run.pendingAbortCause = undefined;
|
|
1310
|
+
systemTurnAbort = {
|
|
1311
|
+
contract,
|
|
1312
|
+
...(descriptor === undefined ? {} : { intentId: descriptor.intentId }),
|
|
1313
|
+
};
|
|
1040
1314
|
ctx.abort();
|
|
1041
1315
|
}
|
|
1042
1316
|
},
|
|
@@ -1137,6 +1411,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1137
1411
|
if (
|
|
1138
1412
|
!store.renewWorkflowRunClaim({ runId, claimToken: token, leaseMs: RUN_CLAIM_LEASE_MS })
|
|
1139
1413
|
) {
|
|
1414
|
+
run.pendingAbortCause = "claimLost";
|
|
1140
1415
|
run.engine.cancel();
|
|
1141
1416
|
}
|
|
1142
1417
|
} catch {
|
|
@@ -1239,6 +1514,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1239
1514
|
}
|
|
1240
1515
|
if (terminalStatus !== undefined) {
|
|
1241
1516
|
recordRunEvent({ runId, workflowRef: workflow.name, type: terminalStatus, payload: {} });
|
|
1517
|
+
if (
|
|
1518
|
+
terminalStatus === "failed" ||
|
|
1519
|
+
terminalStatus === "timed_out" ||
|
|
1520
|
+
terminalStatus === "cancelled"
|
|
1521
|
+
) {
|
|
1522
|
+
makeRunTurnIntentEligible(ctx, run, terminalStatus, message);
|
|
1523
|
+
}
|
|
1242
1524
|
notify(ctx, `Workflow ${workflow.name} ${terminalStatus} (run ${runId}).`);
|
|
1243
1525
|
return;
|
|
1244
1526
|
}
|
|
@@ -1248,6 +1530,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1248
1530
|
type: "failed",
|
|
1249
1531
|
payload: { error: message },
|
|
1250
1532
|
});
|
|
1533
|
+
makeRunTurnIntentEligible(ctx, run, "failed", message);
|
|
1251
1534
|
try {
|
|
1252
1535
|
run.onFinish?.({ state: "failed", runId, error: message });
|
|
1253
1536
|
} catch {
|
|
@@ -1478,11 +1761,14 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1478
1761
|
|
|
1479
1762
|
const cancelWorkflowControl = async (
|
|
1480
1763
|
ctx: ExtensionContext,
|
|
1764
|
+
origin: "agent" | "user",
|
|
1481
1765
|
requestedRunId?: string,
|
|
1482
1766
|
): Promise<WorkflowControlResult> => {
|
|
1483
1767
|
if (activeRun && (requestedRunId === undefined || requestedRunId === activeRun.runId)) {
|
|
1484
1768
|
const workflowName = activeRun.workflowName;
|
|
1485
1769
|
const runId = activeRun.runId;
|
|
1770
|
+
activeRun.pendingAbortCause = origin === "agent" ? "agentCancelled" : undefined;
|
|
1771
|
+
activeRun.suppressTurnIntent = origin === "user";
|
|
1486
1772
|
activeRun.engine.cancel();
|
|
1487
1773
|
return {
|
|
1488
1774
|
message: `Cancelling workflow ${workflowName}…`,
|
|
@@ -1581,6 +1867,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1581
1867
|
},
|
|
1582
1868
|
};
|
|
1583
1869
|
}
|
|
1870
|
+
activeRun.suppressTurnIntent = true;
|
|
1584
1871
|
activeRun.engine.pause();
|
|
1585
1872
|
renderWidget(ctx);
|
|
1586
1873
|
return {
|
|
@@ -1614,6 +1901,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1614
1901
|
level: "warning",
|
|
1615
1902
|
};
|
|
1616
1903
|
}
|
|
1904
|
+
activeRun.suppressTurnIntent = false;
|
|
1617
1905
|
activeRun.engine.resume();
|
|
1618
1906
|
activeRun.executor.release();
|
|
1619
1907
|
renderWidget(ctx);
|
|
@@ -2167,19 +2455,31 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2167
2455
|
payload: { errorCode: safe.code, error: safe.message },
|
|
2168
2456
|
});
|
|
2169
2457
|
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.`;
|
|
2170
|
-
|
|
2171
|
-
|
|
2458
|
+
const cause = "launchFailed" as const;
|
|
2459
|
+
const descriptor = createDeferredTurnDescriptor({
|
|
2460
|
+
runId: claimed.runId,
|
|
2461
|
+
workflowName: claimed.workflowName,
|
|
2462
|
+
targetSessionId: claimed.originSessionId ?? ctx.sessionManager.getSessionId(),
|
|
2463
|
+
cause,
|
|
2464
|
+
sourceEventId: deferredTurnSourceEventId({
|
|
2172
2465
|
runId: claimed.runId,
|
|
2466
|
+
cause,
|
|
2173
2467
|
nodeId: "$launch",
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2468
|
+
source: "queued-launch",
|
|
2469
|
+
}),
|
|
2470
|
+
observedState: "failedToStart",
|
|
2471
|
+
nodeId: "$launch",
|
|
2472
|
+
reason: safe.message,
|
|
2473
|
+
});
|
|
2474
|
+
try {
|
|
2475
|
+
storeTurnDescriptor(ctx, descriptor, true);
|
|
2476
|
+
syncArmed = true;
|
|
2477
|
+
} catch (intentError) {
|
|
2478
|
+
notify(
|
|
2479
|
+
ctx,
|
|
2480
|
+
`Workflow ${claimed.workflowName} failed to start, but Pi Workflows could not preserve its successor turn: ${errorMessage(intentError)}`,
|
|
2481
|
+
"warning",
|
|
2482
|
+
);
|
|
2183
2483
|
}
|
|
2184
2484
|
notify(ctx, content, "error");
|
|
2185
2485
|
runSyncPass(ctx);
|
|
@@ -2244,7 +2544,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2244
2544
|
return;
|
|
2245
2545
|
}
|
|
2246
2546
|
if (parsed.kind === "cancel") {
|
|
2247
|
-
const result = await cancelWorkflowControl(ctx);
|
|
2547
|
+
const result = await cancelWorkflowControl(ctx, "user");
|
|
2248
2548
|
notify(ctx, result.message, result.level);
|
|
2249
2549
|
return;
|
|
2250
2550
|
}
|
|
@@ -2412,6 +2712,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2412
2712
|
case "stop":
|
|
2413
2713
|
if (activeRun?.childKey !== undefined) {
|
|
2414
2714
|
activeRun.interruptionRequested = true;
|
|
2715
|
+
activeRun.pendingAbortCause = "controllerInterrupted";
|
|
2415
2716
|
activeRun.engine.cancel();
|
|
2416
2717
|
}
|
|
2417
2718
|
await host.stop();
|
|
@@ -2428,7 +2729,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2428
2729
|
name: "workflow",
|
|
2429
2730
|
label: "Workflow",
|
|
2430
2731
|
description: [
|
|
2431
|
-
"List, start, inspect, pause, resume, cancel, answer, update, or complete
|
|
2732
|
+
"List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
|
|
2432
2733
|
"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.",
|
|
2433
2734
|
"Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
|
|
2434
2735
|
"Do not start repeated work without the user's request, and keep monitoring observation-only unless the user authorizes mutations.",
|
|
@@ -2454,7 +2755,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2454
2755
|
control = resumeWorkflowControl(ctx);
|
|
2455
2756
|
break;
|
|
2456
2757
|
case "cancel":
|
|
2457
|
-
control = await cancelWorkflowControl(ctx, params.runId);
|
|
2758
|
+
control = await cancelWorkflowControl(ctx, "agent", params.runId);
|
|
2458
2759
|
break;
|
|
2459
2760
|
case "answer": {
|
|
2460
2761
|
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
@@ -2636,6 +2937,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2636
2937
|
if (!aborted || runHeld()) {
|
|
2637
2938
|
return;
|
|
2638
2939
|
}
|
|
2940
|
+
run.suppressTurnIntent = true;
|
|
2639
2941
|
run.engine.pause();
|
|
2640
2942
|
run.executor.hold();
|
|
2641
2943
|
renderWidget(ctx);
|
|
@@ -2686,6 +2988,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2686
2988
|
});
|
|
2687
2989
|
|
|
2688
2990
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
2991
|
+
systemTurnAbort = null;
|
|
2689
2992
|
if (activeRun === null) {
|
|
2690
2993
|
try {
|
|
2691
2994
|
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
@@ -2704,15 +3007,20 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2704
3007
|
return;
|
|
2705
3008
|
}
|
|
2706
3009
|
}
|
|
3010
|
+
const flushedNatural = turnCoordinator.flushNatural(
|
|
3011
|
+
ctx.isIdle() && !sessionClosed && !runHeld(),
|
|
3012
|
+
);
|
|
2707
3013
|
const run = activeRun;
|
|
2708
3014
|
if (!run) {
|
|
2709
3015
|
presentationPending = null;
|
|
3016
|
+
runSyncPass(ctx);
|
|
2710
3017
|
return;
|
|
2711
3018
|
}
|
|
2712
3019
|
await run.recorder?.synchronize(ctx).catch(() => undefined);
|
|
2713
3020
|
run.recorder?.settleAttempt();
|
|
2714
3021
|
run.executor.setStreaming(false);
|
|
2715
|
-
run.executor.handleAgentSettled();
|
|
3022
|
+
if (flushedNatural === 0) run.executor.handleAgentSettled();
|
|
3023
|
+
runSyncPass(ctx);
|
|
2716
3024
|
});
|
|
2717
3025
|
|
|
2718
3026
|
pi.on("session_shutdown", async () => {
|
|
@@ -2720,8 +3028,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2720
3028
|
herdrProbeGeneration += 1;
|
|
2721
3029
|
systemTurnAbort = null;
|
|
2722
3030
|
suppressWorkflowAssistantTail = false;
|
|
3031
|
+
turnCoordinator.clearDeferred();
|
|
2723
3032
|
supersedePresentation();
|
|
2724
3033
|
const run = activeRun;
|
|
3034
|
+
if (run !== null) run.suppressTurnIntent = true;
|
|
2725
3035
|
if (run !== null && run.claimToken !== undefined) {
|
|
2726
3036
|
// Queued interactive runs park: no terminal event, no recorded partial
|
|
2727
3037
|
// attempt, and the claim releases so another runner can resume.
|
package/src/herdr/setup.ts
CHANGED
|
@@ -115,20 +115,20 @@ function preflightPackage(packageRoot: string): PluginPackage {
|
|
|
115
115
|
try {
|
|
116
116
|
root = fs.realpathSync(requestedRoot);
|
|
117
117
|
} catch (error) {
|
|
118
|
-
throw new Error(`
|
|
118
|
+
throw new Error(`pi-workflows package root is missing: ${requestedRoot}`, { cause: error });
|
|
119
119
|
}
|
|
120
120
|
if (!fs.statSync(root).isDirectory()) {
|
|
121
|
-
throw new Error(`
|
|
121
|
+
throw new Error(`pi-workflows package root is not a directory: ${requestedRoot}`);
|
|
122
122
|
}
|
|
123
123
|
const packagePath = checkedRegularFile(root, "package.json");
|
|
124
124
|
const manifestPath = checkedRegularFile(root, MANIFEST_NAME);
|
|
125
125
|
const packageJson = parseJsonObject(fs.readFileSync(packagePath, "utf8"), "package.json");
|
|
126
126
|
if (packageJson["name"] !== PACKAGE_NAME) {
|
|
127
|
-
throw new Error(`Unexpected
|
|
127
|
+
throw new Error(`Unexpected pi-workflows package name in ${packagePath}.`);
|
|
128
128
|
}
|
|
129
129
|
const packageVersion = packageJson["version"];
|
|
130
130
|
if (typeof packageVersion !== "string" || packageVersion.length === 0) {
|
|
131
|
-
throw new Error(`
|
|
131
|
+
throw new Error(`pi-workflows package version is missing in ${packagePath}.`);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
const manifest = fs.readFileSync(manifestPath, "utf8");
|