@osolmaz/pi-workflows 0.11.1 → 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 +15 -6
- 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 +141 -5
- package/dist/controllers/sqlite.js +568 -46
- 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 +515 -84
- 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/host/runner.js +3 -0
- package/dist/host/runner.js.map +1 -1
- 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/migrate-sources.d.ts +1 -1
- package/dist/workflows/migrate-sources.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/tool-input.d.ts +1 -0
- package/dist/workflows/tool-input.js +2 -2
- package/dist/workflows/tool-input.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 +445 -0
- 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 +67 -17
- 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 +878 -51
- 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 +646 -91
- package/src/extension/step-message.ts +1 -0
- package/src/herdr/setup.ts +4 -4
- package/src/host/runner.ts +3 -0
- 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/migrate-sources.ts +5 -1
- package/src/workflows/schema.ts +2 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +3 -1
- package/src/workflows/tool-input.ts +5 -2
- package/src/workflows/types.ts +4 -3
package/src/extension/index.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { builtinWorkflowCatalog } from "../builtins/catalog.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
projectControllerStorePath,
|
|
8
|
+
SqliteControllerStore,
|
|
9
|
+
type WorkflowRunQueueRecord,
|
|
10
|
+
} from "../controllers/index.js";
|
|
11
|
+
import type {
|
|
12
|
+
WorkflowTurnIntentCause,
|
|
13
|
+
WorkflowTurnIntentResolution,
|
|
14
|
+
} from "../controllers/sqlite.js";
|
|
7
15
|
import type { JsonObject } from "../controllers/types.js";
|
|
8
16
|
import type { WorkflowSchedulerResult } from "../controllers/workflows.js";
|
|
17
|
+
import { compositionMetadata } from "../workflows/composition.js";
|
|
9
18
|
import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
|
|
10
19
|
import { WorkflowEngine } from "../workflows/engine.js";
|
|
11
20
|
import {
|
|
@@ -58,6 +67,19 @@ import {
|
|
|
58
67
|
type HumanDecisionChannelAnswer,
|
|
59
68
|
type SettledHumanDecision,
|
|
60
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";
|
|
61
83
|
import { ConversationStepExecutor } from "./executor.js";
|
|
62
84
|
import {
|
|
63
85
|
HerdrWorkflowViewer,
|
|
@@ -103,8 +125,10 @@ const RUN_CLAIM_LEASE_MS = 30_000;
|
|
|
103
125
|
const RUN_CLAIM_RENEW_MS = 10_000;
|
|
104
126
|
const RUN_SYNC_POLL_MS = 3_000;
|
|
105
127
|
const NOTIFICATION_DELIVERY_LEASE_MS = 30_000;
|
|
128
|
+
const TURN_INTENT_DELIVERY_LEASE_MS = 30_000;
|
|
106
129
|
const WIDGET_KEY = "pi-workflows";
|
|
107
130
|
const PRESENTATION_MESSAGE_TYPE = "pi-workflows-presentation";
|
|
131
|
+
const PRESENTATION_MESSAGE_SCHEMA = "pi-workflows.presentation-message.v1";
|
|
108
132
|
const FINAL_WIDGET_TTL_MS = 60_000;
|
|
109
133
|
const WIDGET_SCROLL_STEP = 3;
|
|
110
134
|
const MAX_PRESENTATION_RESULT_CHARS = 50_000;
|
|
@@ -133,6 +157,12 @@ type PresentationPromptBuilder = Exclude<
|
|
|
133
157
|
string | undefined
|
|
134
158
|
>;
|
|
135
159
|
|
|
160
|
+
type AbortProvenance = {
|
|
161
|
+
cause: WorkflowTurnIntentCause;
|
|
162
|
+
descriptor?: DeferredTurnDescriptor;
|
|
163
|
+
storageError?: string;
|
|
164
|
+
};
|
|
165
|
+
|
|
136
166
|
type ActiveRun = {
|
|
137
167
|
runId: string;
|
|
138
168
|
workflowName: string;
|
|
@@ -148,6 +178,9 @@ type ActiveRun = {
|
|
|
148
178
|
onFinish?: (result: WorkflowSchedulerResult) => void;
|
|
149
179
|
completion?: Promise<void>;
|
|
150
180
|
interruptionRequested?: boolean;
|
|
181
|
+
pendingAbortCause?: WorkflowTurnIntentCause | undefined;
|
|
182
|
+
abortProvenance?: AbortProvenance | undefined;
|
|
183
|
+
suppressTurnIntent?: boolean | undefined;
|
|
151
184
|
claimToken?: string | undefined;
|
|
152
185
|
renewTimer?: ReturnType<typeof setInterval> | undefined;
|
|
153
186
|
/** True for runs this session resumed from the queue. */
|
|
@@ -170,6 +203,12 @@ function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
|
|
|
170
203
|
return value as HumanDecisionRequest;
|
|
171
204
|
}
|
|
172
205
|
|
|
206
|
+
type PreparedLaunchOptions = {
|
|
207
|
+
presentation?: boolean;
|
|
208
|
+
parentRunId?: string;
|
|
209
|
+
humanDecision?: AcceptedHumanDecision;
|
|
210
|
+
};
|
|
211
|
+
|
|
173
212
|
type StartRunOptions = {
|
|
174
213
|
runId?: string;
|
|
175
214
|
childKey?: string;
|
|
@@ -187,6 +226,49 @@ type StartRunOptions = {
|
|
|
187
226
|
claimToken?: string;
|
|
188
227
|
};
|
|
189
228
|
|
|
229
|
+
function definitionDigest(snapshot: WorkflowDefinitionSnapshot): string {
|
|
230
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function launchSourceIdentity(workflow: WorkflowDefinition, root: unknown): unknown {
|
|
234
|
+
return {
|
|
235
|
+
root,
|
|
236
|
+
mounted: compositionMetadata(workflow)?.sources ?? [],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function preparedLaunchOptions(options: StartRunOptions): PreparedLaunchOptions {
|
|
241
|
+
return {
|
|
242
|
+
...(options.presentation !== undefined ? { presentation: options.presentation } : {}),
|
|
243
|
+
...(options.parentRunId !== undefined ? { parentRunId: options.parentRunId } : {}),
|
|
244
|
+
...(options.humanDecision !== undefined ? { humanDecision: options.humanDecision } : {}),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parsePreparedLaunchOptions(value: unknown): PreparedLaunchOptions {
|
|
249
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
250
|
+
throw new Error("Stored workflow launch options are invalid");
|
|
251
|
+
}
|
|
252
|
+
return value as PreparedLaunchOptions;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function safeLaunchError(error: unknown): { code: string; message: string } {
|
|
256
|
+
const raw = errorMessage(error)
|
|
257
|
+
.replace(/Bearer\s+\S+/giu, "Bearer [redacted]")
|
|
258
|
+
.replace(/(token|api[_-]?key|secret|password)(\s*[:=]\s*)\S+/giu, "$1$2[redacted]")
|
|
259
|
+
.replaceAll("\n", " ")
|
|
260
|
+
.trim();
|
|
261
|
+
const code = /not found|cannot find|unknown workflow/iu.test(raw)
|
|
262
|
+
? "workflow_not_found"
|
|
263
|
+
: /source changed|source mismatch/iu.test(raw)
|
|
264
|
+
? "source_changed"
|
|
265
|
+
: /invalid|must be/iu.test(raw)
|
|
266
|
+
? "workflow_invalid"
|
|
267
|
+
: "activation_failed";
|
|
268
|
+
const message = raw.length <= 500 ? raw : `${raw.slice(0, 480)}… [error truncated]`;
|
|
269
|
+
return { code, message: message || "The deferred workflow could not start" };
|
|
270
|
+
}
|
|
271
|
+
|
|
190
272
|
export type ParsedWorkflowArgs =
|
|
191
273
|
| { kind: "list" }
|
|
192
274
|
| { kind: "cancel" }
|
|
@@ -343,8 +425,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
343
425
|
// Run events remain an audit feed and never enter a conversation.
|
|
344
426
|
let syncArmed = false;
|
|
345
427
|
let runSyncTimer: ReturnType<typeof setInterval> | null = null;
|
|
428
|
+
let activationRecovery: ((ctx: ExtensionContext) => void) | undefined;
|
|
346
429
|
let decisionRecoveryTimer: ReturnType<typeof setInterval> | null = null;
|
|
347
430
|
let decisionRecoveryActive = false;
|
|
431
|
+
let turnCoordinator: DeferredTurnCoordinator;
|
|
348
432
|
|
|
349
433
|
const recordRunEvent = (event: {
|
|
350
434
|
runId: string;
|
|
@@ -377,6 +461,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
377
461
|
const runSyncPass = (ctx: ExtensionContext): void => {
|
|
378
462
|
if (runQueueStore === null || !syncArmed) return;
|
|
379
463
|
try {
|
|
464
|
+
activationRecovery?.(ctx);
|
|
380
465
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
381
466
|
const alreadyDelivered = deliveredNotificationIds(ctx);
|
|
382
467
|
const claimToken = randomUUID();
|
|
@@ -407,6 +492,25 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
407
492
|
claimToken,
|
|
408
493
|
});
|
|
409
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
|
+
);
|
|
410
514
|
} catch {
|
|
411
515
|
// Delivery retries on the next poll. It never affects workflow execution.
|
|
412
516
|
}
|
|
@@ -422,15 +526,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
422
526
|
runSyncTimer.unref?.();
|
|
423
527
|
};
|
|
424
528
|
let activeRun: ActiveRun | null = null;
|
|
425
|
-
let systemTurnAbort: AgentStepContract | null = null;
|
|
529
|
+
let systemTurnAbort: { contract: AgentStepContract; intentId?: string } | null = null;
|
|
426
530
|
let suppressWorkflowAssistantTail = false;
|
|
427
531
|
let lastExpiredAttempt: { contract: AgentStepContract; reason: string } | null = null;
|
|
428
|
-
let pendingToolLaunch: {
|
|
429
|
-
ctx: ExtensionContext;
|
|
430
|
-
ref: string;
|
|
431
|
-
input: unknown;
|
|
432
|
-
options?: StartRunOptions;
|
|
433
|
-
} | null = null;
|
|
434
532
|
// The interactive run currently parked at a checkpoint, if any.
|
|
435
533
|
let lastWaitingRunId: string | null = null;
|
|
436
534
|
let widgetTimer: NodeJS.Timeout | null = null;
|
|
@@ -449,6 +547,30 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
449
547
|
let controllerHost: PiControllerHost | undefined;
|
|
450
548
|
let controllerContext: ExtensionContext | null = null;
|
|
451
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
|
+
|
|
452
574
|
// UI updates are best-effort: a captured ctx becomes stale after session
|
|
453
575
|
// replacement or shutdown, and pi throws on any access (even `ctx.hasUI`).
|
|
454
576
|
// A workflow finishing right as the session goes away must not crash pi.
|
|
@@ -490,6 +612,145 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
490
612
|
const runHeld = (): boolean =>
|
|
491
613
|
activeRun !== null && (activeRun.engine.pauseRequested || activeRun.executor.held);
|
|
492
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
|
+
|
|
493
754
|
const footerStatus = (state: WorkflowRunState): string => {
|
|
494
755
|
const label = runHeld() || state.paused ? "paused" : state.status;
|
|
495
756
|
const node = state.currentNode ?? state.waitingOn;
|
|
@@ -690,24 +951,41 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
690
951
|
typeof run.presentationPrompt === "function"
|
|
691
952
|
? await resolvePresentationPrompt(run.presentationPrompt, state, abort.signal)
|
|
692
953
|
: run.presentationPrompt;
|
|
693
|
-
if (
|
|
694
|
-
sessionClosed ||
|
|
695
|
-
run.generation !== runGeneration ||
|
|
696
|
-
abort.signal.aborted ||
|
|
697
|
-
instructions === undefined ||
|
|
698
|
-
instructions.trim().length === 0
|
|
699
|
-
) {
|
|
954
|
+
if (sessionClosed || run.generation !== runGeneration || abort.signal.aborted) {
|
|
700
955
|
return;
|
|
701
956
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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(
|
|
705
964
|
{
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
+
},
|
|
709
987
|
},
|
|
710
|
-
|
|
988
|
+
ctx.isIdle() && systemTurnAbort === null,
|
|
711
989
|
);
|
|
712
990
|
} catch (error) {
|
|
713
991
|
if (presentationPending === run.generation) {
|
|
@@ -725,6 +1003,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
725
1003
|
? `timed out after ${PRESENTATION_TIMEOUT_MS}ms`
|
|
726
1004
|
: errorMessage(error);
|
|
727
1005
|
notify(ctx, `Could not present workflow result: ${message}`, "warning");
|
|
1006
|
+
if (run.abortProvenance !== undefined) {
|
|
1007
|
+
makeRunTurnIntentEligible(ctx, run, state.status, message);
|
|
1008
|
+
}
|
|
728
1009
|
} finally {
|
|
729
1010
|
clearTimeout(timer);
|
|
730
1011
|
if (presentationAbort === abort) {
|
|
@@ -777,6 +1058,22 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
777
1058
|
clearWidget(ctx);
|
|
778
1059
|
return;
|
|
779
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
|
+
}
|
|
780
1077
|
releaseClaim(run, "done");
|
|
781
1078
|
recordRunEvent({
|
|
782
1079
|
runId: run.runId,
|
|
@@ -828,6 +1125,15 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
828
1125
|
} catch (error) {
|
|
829
1126
|
notify(ctx, `Could not record child workflow completion: ${errorMessage(error)}`, "warning");
|
|
830
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
|
+
}
|
|
831
1137
|
void presentRun(ctx, run, state);
|
|
832
1138
|
if (pendingDecision !== null && state.status === "waiting") {
|
|
833
1139
|
const channels = audienceChannels(decisionChannelConfig, pendingDecision.audience);
|
|
@@ -927,7 +1233,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
927
1233
|
workflowSource.kind === "builtin"
|
|
928
1234
|
? `builtin:${workflowSource.id}`
|
|
929
1235
|
: workflowSource.path,
|
|
1236
|
+
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
1237
|
+
definitionDigest: definitionDigest(snapshot),
|
|
930
1238
|
input,
|
|
1239
|
+
launchOptions: preparedLaunchOptions(options),
|
|
931
1240
|
runnerId,
|
|
932
1241
|
claimToken: token,
|
|
933
1242
|
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
@@ -956,23 +1265,34 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
956
1265
|
|
|
957
1266
|
const executor = new ConversationStepExecutor({
|
|
958
1267
|
sendPrompt: ({ prompt, contract, presentation, kind, streaming }) => {
|
|
959
|
-
|
|
960
|
-
schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
961
|
-
kind,
|
|
962
|
-
contract,
|
|
963
|
-
...(presentation !== undefined ? { presentation } : {}),
|
|
964
|
-
};
|
|
965
|
-
pi.sendMessage(
|
|
1268
|
+
turnCoordinator.sendNatural(
|
|
966
1269
|
{
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
+
},
|
|
975
1294
|
},
|
|
1295
|
+
ctx.isIdle() && systemTurnAbort === null,
|
|
976
1296
|
);
|
|
977
1297
|
},
|
|
978
1298
|
onAbort: (contract, reason) => {
|
|
@@ -981,7 +1301,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
981
1301
|
reason: reason instanceof TimeoutError ? "timed out" : `ended: ${errorMessage(reason)}`,
|
|
982
1302
|
};
|
|
983
1303
|
if (!executor.held && !ctx.isIdle()) {
|
|
984
|
-
|
|
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
|
+
};
|
|
985
1314
|
ctx.abort();
|
|
986
1315
|
}
|
|
987
1316
|
},
|
|
@@ -1063,6 +1392,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1063
1392
|
...(options.resume === true ? { resume: true } : {}),
|
|
1064
1393
|
...(options.parentRunId !== undefined ? { parentRunId: options.parentRunId } : {}),
|
|
1065
1394
|
};
|
|
1395
|
+
if (
|
|
1396
|
+
queueStore !== null &&
|
|
1397
|
+
claimToken !== undefined &&
|
|
1398
|
+
!queueStore.markWorkflowRunRunning({ runId, claimToken })
|
|
1399
|
+
) {
|
|
1400
|
+
throw new ClaimLostError(runId);
|
|
1401
|
+
}
|
|
1066
1402
|
activeRun = run;
|
|
1067
1403
|
if (queueStore !== null && claimToken !== undefined) {
|
|
1068
1404
|
const store = queueStore;
|
|
@@ -1075,6 +1411,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1075
1411
|
if (
|
|
1076
1412
|
!store.renewWorkflowRunClaim({ runId, claimToken: token, leaseMs: RUN_CLAIM_LEASE_MS })
|
|
1077
1413
|
) {
|
|
1414
|
+
run.pendingAbortCause = "claimLost";
|
|
1078
1415
|
run.engine.cancel();
|
|
1079
1416
|
}
|
|
1080
1417
|
} catch {
|
|
@@ -1177,6 +1514,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1177
1514
|
}
|
|
1178
1515
|
if (terminalStatus !== undefined) {
|
|
1179
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
|
+
}
|
|
1180
1524
|
notify(ctx, `Workflow ${workflow.name} ${terminalStatus} (run ${runId}).`);
|
|
1181
1525
|
return;
|
|
1182
1526
|
}
|
|
@@ -1186,6 +1530,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1186
1530
|
type: "failed",
|
|
1187
1531
|
payload: { error: message },
|
|
1188
1532
|
});
|
|
1533
|
+
makeRunTurnIntentEligible(ctx, run, "failed", message);
|
|
1189
1534
|
try {
|
|
1190
1535
|
run.onFinish?.({ state: "failed", runId, error: message });
|
|
1191
1536
|
} catch {
|
|
@@ -1224,8 +1569,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1224
1569
|
: bundle.state.workflowSource.kind === "builtin"
|
|
1225
1570
|
? `builtin:${bundle.state.workflowSource.id}`
|
|
1226
1571
|
: bundle.state.workflowSource.path;
|
|
1572
|
+
const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
|
|
1227
1573
|
started = await startRun(ctx, sourceRef, claimed.input, {
|
|
1228
|
-
|
|
1574
|
+
...launchOptions,
|
|
1575
|
+
resume: bundle !== null,
|
|
1229
1576
|
runId: claimed.runId,
|
|
1230
1577
|
claimToken,
|
|
1231
1578
|
});
|
|
@@ -1412,22 +1759,51 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1412
1759
|
return null;
|
|
1413
1760
|
};
|
|
1414
1761
|
|
|
1415
|
-
const cancelWorkflowControl = async (
|
|
1416
|
-
|
|
1762
|
+
const cancelWorkflowControl = async (
|
|
1763
|
+
ctx: ExtensionContext,
|
|
1764
|
+
origin: "agent" | "user",
|
|
1765
|
+
requestedRunId?: string,
|
|
1766
|
+
): Promise<WorkflowControlResult> => {
|
|
1767
|
+
if (activeRun && (requestedRunId === undefined || requestedRunId === activeRun.runId)) {
|
|
1417
1768
|
const workflowName = activeRun.workflowName;
|
|
1418
1769
|
const runId = activeRun.runId;
|
|
1770
|
+
activeRun.pendingAbortCause = origin === "agent" ? "agentCancelled" : undefined;
|
|
1771
|
+
activeRun.suppressTurnIntent = origin === "user";
|
|
1419
1772
|
activeRun.engine.cancel();
|
|
1420
1773
|
return {
|
|
1421
1774
|
message: `Cancelling workflow ${workflowName}…`,
|
|
1422
1775
|
details: { action: "cancel", workflowName, runId },
|
|
1423
1776
|
};
|
|
1424
1777
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1778
|
+
const queue = ensureRunQueueStore(ctx.cwd);
|
|
1779
|
+
const queued =
|
|
1780
|
+
requestedRunId === undefined
|
|
1781
|
+
? queue.findSessionReservation(ctx.sessionManager.getSessionId())
|
|
1782
|
+
: queue.getWorkflowRun(requestedRunId);
|
|
1783
|
+
if (
|
|
1784
|
+
queued !== undefined &&
|
|
1785
|
+
["queued", "starting"].includes(queued.status) &&
|
|
1786
|
+
(queued.originSessionId === null ||
|
|
1787
|
+
queued.originSessionId === ctx.sessionManager.getSessionId())
|
|
1788
|
+
) {
|
|
1789
|
+
if (!queue.cancelWorkflowRun({ runId: queued.runId })) {
|
|
1790
|
+
throw new Error(
|
|
1791
|
+
`Workflow ${queued.runId} could not be cancelled because its state changed.`,
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
recordRunEvent({
|
|
1795
|
+
runId: queued.runId,
|
|
1796
|
+
workflowRef: queued.workflowName,
|
|
1797
|
+
type: "cancelled",
|
|
1798
|
+
});
|
|
1428
1799
|
return {
|
|
1429
|
-
message: `Cancelled
|
|
1430
|
-
details: {
|
|
1800
|
+
message: `Cancelled queued workflow ${queued.workflowName} (run ${queued.runId}).`,
|
|
1801
|
+
details: {
|
|
1802
|
+
action: "cancel",
|
|
1803
|
+
workflow: queued.workflowName,
|
|
1804
|
+
runId: queued.runId,
|
|
1805
|
+
queued: false,
|
|
1806
|
+
},
|
|
1431
1807
|
};
|
|
1432
1808
|
}
|
|
1433
1809
|
if (widgetSource) {
|
|
@@ -1491,6 +1867,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1491
1867
|
},
|
|
1492
1868
|
};
|
|
1493
1869
|
}
|
|
1870
|
+
activeRun.suppressTurnIntent = true;
|
|
1494
1871
|
activeRun.engine.pause();
|
|
1495
1872
|
renderWidget(ctx);
|
|
1496
1873
|
return {
|
|
@@ -1524,6 +1901,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1524
1901
|
level: "warning",
|
|
1525
1902
|
};
|
|
1526
1903
|
}
|
|
1904
|
+
activeRun.suppressTurnIntent = false;
|
|
1527
1905
|
activeRun.engine.resume();
|
|
1528
1906
|
activeRun.executor.release();
|
|
1529
1907
|
renderWidget(ctx);
|
|
@@ -1538,6 +1916,21 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1538
1916
|
};
|
|
1539
1917
|
};
|
|
1540
1918
|
|
|
1919
|
+
const workflowLaunchStatus = (record: WorkflowRunQueueRecord): WorkflowControlResult => ({
|
|
1920
|
+
message: `Workflow ${record.workflowName} is ${record.status} (run ${record.runId}).`,
|
|
1921
|
+
details: {
|
|
1922
|
+
action: "status",
|
|
1923
|
+
active: ["starting", "running"].includes(record.status),
|
|
1924
|
+
queued: record.status === "queued",
|
|
1925
|
+
workflowName: record.workflowName,
|
|
1926
|
+
runId: record.runId,
|
|
1927
|
+
status: record.status,
|
|
1928
|
+
...(record.errorCode === null ? {} : { errorCode: record.errorCode }),
|
|
1929
|
+
...(record.errorMessage === null ? {} : { error: record.errorMessage }),
|
|
1930
|
+
},
|
|
1931
|
+
...(["failed", "cancelled"].includes(record.status) ? { level: "warning" as const } : {}),
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1541
1934
|
const statusWorkflowControl = async (
|
|
1542
1935
|
ctx: ExtensionContext,
|
|
1543
1936
|
runId?: string,
|
|
@@ -1545,7 +1938,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1545
1938
|
if (runId !== undefined) {
|
|
1546
1939
|
const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
|
|
1547
1940
|
if (bundle === null) {
|
|
1548
|
-
|
|
1941
|
+
const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
|
|
1942
|
+
if (launch === undefined) throw new Error(`Workflow run not found: ${runId}`);
|
|
1943
|
+
return workflowLaunchStatus(launch);
|
|
1549
1944
|
}
|
|
1550
1945
|
const { state } = bundle;
|
|
1551
1946
|
return {
|
|
@@ -1554,11 +1949,11 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1554
1949
|
};
|
|
1555
1950
|
}
|
|
1556
1951
|
const state = activeRun?.lastState ?? widgetSource?.state;
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1952
|
+
if (state === undefined || state === null) {
|
|
1953
|
+
const queued = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
1954
|
+
ctx.sessionManager.getSessionId(),
|
|
1955
|
+
);
|
|
1956
|
+
if (queued !== undefined) return workflowLaunchStatus(queued);
|
|
1562
1957
|
}
|
|
1563
1958
|
if (state === undefined || state === null) {
|
|
1564
1959
|
return {
|
|
@@ -1903,8 +2298,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1903
2298
|
if (activeRun !== null) {
|
|
1904
2299
|
throw new Error(`A workflow is already running: ${activeRun.workflowName}.`);
|
|
1905
2300
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
2301
|
+
const reserved = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
2302
|
+
ctx.sessionManager.getSessionId(),
|
|
2303
|
+
);
|
|
2304
|
+
if (reserved !== undefined) {
|
|
2305
|
+
throw new Error(
|
|
2306
|
+
`Workflow ${reserved.workflowName} is already ${reserved.status} (run ${reserved.runId}).`,
|
|
2307
|
+
);
|
|
1908
2308
|
}
|
|
1909
2309
|
if (presentationPending !== null) {
|
|
1910
2310
|
throw new Error("The previous workflow result is still being presented.");
|
|
@@ -1930,35 +2330,171 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1930
2330
|
`A workflow is already running: ${activeRun.workflowName}. Cancel it before starting another.`,
|
|
1931
2331
|
);
|
|
1932
2332
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
2333
|
+
const queue = ensureRunQueueStore(ctx.cwd);
|
|
2334
|
+
const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
|
|
2335
|
+
if (existing !== undefined) {
|
|
2336
|
+
throw new Error(
|
|
2337
|
+
`Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`,
|
|
2338
|
+
);
|
|
1935
2339
|
}
|
|
1936
2340
|
if (presentationPending !== null) {
|
|
1937
2341
|
throw new Error("The previous workflow result is still being presented.");
|
|
1938
2342
|
}
|
|
1939
|
-
const
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
const
|
|
1944
|
-
if (
|
|
1945
|
-
throw new Error(
|
|
2343
|
+
const resolved = await resolveWorkflowRef(ref, { cwd: ctx.cwd }, builtinWorkflowCatalog);
|
|
2344
|
+
const workflow = resolved.definition;
|
|
2345
|
+
const workflowSource = resolved.source;
|
|
2346
|
+
if (options.parentRunId !== undefined) {
|
|
2347
|
+
const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
|
|
2348
|
+
if (parent === null || parent.state.status !== "waiting") {
|
|
2349
|
+
throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
|
|
1946
2350
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
source
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2351
|
+
if (
|
|
2352
|
+
parent.state.workflowSource !== undefined &&
|
|
2353
|
+
!isDeepStrictEqual(parent.state.workflowSource, workflowSource)
|
|
2354
|
+
) {
|
|
2355
|
+
throw new Error(
|
|
2356
|
+
`Workflow source changed since run ${options.parentRunId} started; revert the edit to answer its checkpoint`,
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const snapshot = createDefinitionSnapshot(workflow);
|
|
2361
|
+
const runId = createRunId(workflow.name);
|
|
2362
|
+
try {
|
|
2363
|
+
queue.reserveWorkflowRun({
|
|
2364
|
+
runId,
|
|
2365
|
+
workflowName: workflow.name,
|
|
2366
|
+
workflowSourceRef:
|
|
2367
|
+
workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
|
|
2368
|
+
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
2369
|
+
definitionDigest: definitionDigest(snapshot),
|
|
2370
|
+
input,
|
|
2371
|
+
launchOptions: preparedLaunchOptions(options),
|
|
2372
|
+
runnerId,
|
|
2373
|
+
originSessionId: ctx.sessionManager.getSessionId(),
|
|
2374
|
+
...(options.parentRunId !== undefined ? { parentRunId: options.parentRunId } : {}),
|
|
2375
|
+
});
|
|
1956
2376
|
} catch (error) {
|
|
1957
|
-
|
|
1958
|
-
|
|
2377
|
+
const reserved = queue.findSessionReservation(ctx.sessionManager.getSessionId());
|
|
2378
|
+
if (reserved !== undefined) {
|
|
2379
|
+
throw new Error(
|
|
2380
|
+
`A workflow launch is already waiting: ${reserved.workflowName} (run ${reserved.runId}).`,
|
|
2381
|
+
{ cause: error },
|
|
2382
|
+
);
|
|
1959
2383
|
}
|
|
1960
2384
|
throw error;
|
|
1961
2385
|
}
|
|
2386
|
+
recordRunEvent({
|
|
2387
|
+
runId,
|
|
2388
|
+
workflowRef: workflow.name,
|
|
2389
|
+
type: "queued",
|
|
2390
|
+
payload: options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId },
|
|
2391
|
+
});
|
|
2392
|
+
syncArmed = true;
|
|
2393
|
+
return {
|
|
2394
|
+
message: `Workflow ${workflow.name} queued (run ${runId}).`,
|
|
2395
|
+
details: {
|
|
2396
|
+
action: "start",
|
|
2397
|
+
workflow: workflow.name,
|
|
2398
|
+
runId,
|
|
2399
|
+
source: workflowSource,
|
|
2400
|
+
queued: true,
|
|
2401
|
+
},
|
|
2402
|
+
};
|
|
2403
|
+
};
|
|
2404
|
+
|
|
2405
|
+
const activatePreparedLaunch = async (
|
|
2406
|
+
ctx: ExtensionContext,
|
|
2407
|
+
prepared: WorkflowRunQueueRecord,
|
|
2408
|
+
): Promise<boolean> => {
|
|
2409
|
+
const queue = ensureRunQueueStore(ctx.cwd);
|
|
2410
|
+
const claimToken = randomUUID();
|
|
2411
|
+
const claimed = queue.claimWorkflowRun({
|
|
2412
|
+
runId: prepared.runId,
|
|
2413
|
+
runnerId,
|
|
2414
|
+
claimToken,
|
|
2415
|
+
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
2416
|
+
});
|
|
2417
|
+
if (claimed === undefined) return false;
|
|
2418
|
+
try {
|
|
2419
|
+
const resolved = await resolveWorkflowRef(
|
|
2420
|
+
claimed.workflowSourceRef,
|
|
2421
|
+
{ cwd: ctx.cwd },
|
|
2422
|
+
builtinWorkflowCatalog,
|
|
2423
|
+
);
|
|
2424
|
+
const snapshot = createDefinitionSnapshot(resolved.definition);
|
|
2425
|
+
if (
|
|
2426
|
+
!isDeepStrictEqual(
|
|
2427
|
+
launchSourceIdentity(resolved.definition, resolved.source),
|
|
2428
|
+
claimed.workflowSource,
|
|
2429
|
+
) ||
|
|
2430
|
+
definitionDigest(snapshot) !== claimed.definitionDigest
|
|
2431
|
+
) {
|
|
2432
|
+
throw new Error("Workflow source changed after the launch was queued");
|
|
2433
|
+
}
|
|
2434
|
+
const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
|
|
2435
|
+
const started = await startRun(ctx, claimed.workflowSourceRef, claimed.input, {
|
|
2436
|
+
...launchOptions,
|
|
2437
|
+
runId: claimed.runId,
|
|
2438
|
+
claimToken,
|
|
2439
|
+
});
|
|
2440
|
+
if (started === undefined) throw new Error("The queued workflow could not start");
|
|
2441
|
+
if (launchOptions.parentRunId !== undefined) lastWaitingRunId = null;
|
|
2442
|
+
return true;
|
|
2443
|
+
} catch (error) {
|
|
2444
|
+
const safe = safeLaunchError(error);
|
|
2445
|
+
queue.failWorkflowRun({
|
|
2446
|
+
runId: claimed.runId,
|
|
2447
|
+
claimToken,
|
|
2448
|
+
errorCode: safe.code,
|
|
2449
|
+
errorMessage: safe.message,
|
|
2450
|
+
});
|
|
2451
|
+
recordRunEvent({
|
|
2452
|
+
runId: claimed.runId,
|
|
2453
|
+
workflowRef: claimed.workflowName,
|
|
2454
|
+
type: "launch_failed",
|
|
2455
|
+
payload: { errorCode: safe.code, error: safe.message },
|
|
2456
|
+
});
|
|
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.`;
|
|
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({
|
|
2465
|
+
runId: claimed.runId,
|
|
2466
|
+
cause,
|
|
2467
|
+
nodeId: "$launch",
|
|
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
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
notify(ctx, content, "error");
|
|
2485
|
+
runSyncPass(ctx);
|
|
2486
|
+
return false;
|
|
2487
|
+
}
|
|
2488
|
+
};
|
|
2489
|
+
|
|
2490
|
+
activationRecovery = (ctx) => {
|
|
2491
|
+
if (activeRun !== null) return;
|
|
2492
|
+
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
2493
|
+
ctx.sessionManager.getSessionId(),
|
|
2494
|
+
);
|
|
2495
|
+
if (prepared !== undefined && ["queued", "starting"].includes(prepared.status)) {
|
|
2496
|
+
void activatePreparedLaunch(ctx, prepared).catch(() => undefined);
|
|
2497
|
+
}
|
|
1962
2498
|
};
|
|
1963
2499
|
|
|
1964
2500
|
pi.registerCommand("piw", {
|
|
@@ -2008,7 +2544,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2008
2544
|
return;
|
|
2009
2545
|
}
|
|
2010
2546
|
if (parsed.kind === "cancel") {
|
|
2011
|
-
const result = await cancelWorkflowControl(ctx);
|
|
2547
|
+
const result = await cancelWorkflowControl(ctx, "user");
|
|
2012
2548
|
notify(ctx, result.message, result.level);
|
|
2013
2549
|
return;
|
|
2014
2550
|
}
|
|
@@ -2176,6 +2712,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2176
2712
|
case "stop":
|
|
2177
2713
|
if (activeRun?.childKey !== undefined) {
|
|
2178
2714
|
activeRun.interruptionRequested = true;
|
|
2715
|
+
activeRun.pendingAbortCause = "controllerInterrupted";
|
|
2179
2716
|
activeRun.engine.cancel();
|
|
2180
2717
|
}
|
|
2181
2718
|
await host.stop();
|
|
@@ -2192,7 +2729,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2192
2729
|
name: "workflow",
|
|
2193
2730
|
label: "Workflow",
|
|
2194
2731
|
description: [
|
|
2195
|
-
"List, start, inspect, pause, resume, cancel, answer, update, or complete
|
|
2732
|
+
"List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
|
|
2196
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.",
|
|
2197
2734
|
"Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
|
|
2198
2735
|
"Do not start repeated work without the user's request, and keep monitoring observation-only unless the user authorizes mutations.",
|
|
@@ -2218,7 +2755,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2218
2755
|
control = resumeWorkflowControl(ctx);
|
|
2219
2756
|
break;
|
|
2220
2757
|
case "cancel":
|
|
2221
|
-
control = await cancelWorkflowControl(ctx);
|
|
2758
|
+
control = await cancelWorkflowControl(ctx, "agent", params.runId);
|
|
2222
2759
|
break;
|
|
2223
2760
|
case "answer": {
|
|
2224
2761
|
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
@@ -2348,7 +2885,14 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2348
2885
|
notify(ctx, `Could not recover human decisions: ${errorMessage(error)}`, "warning");
|
|
2349
2886
|
}
|
|
2350
2887
|
try {
|
|
2351
|
-
|
|
2888
|
+
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
2889
|
+
ctx.sessionManager.getSessionId(),
|
|
2890
|
+
);
|
|
2891
|
+
if (prepared !== undefined && ["queued", "starting"].includes(prepared.status)) {
|
|
2892
|
+
await activatePreparedLaunch(ctx, prepared);
|
|
2893
|
+
} else {
|
|
2894
|
+
await resumeParkedRun(ctx);
|
|
2895
|
+
}
|
|
2352
2896
|
} catch (error) {
|
|
2353
2897
|
notify(ctx, `Could not resume a parked workflow: ${errorMessage(error)}`, "warning");
|
|
2354
2898
|
}
|
|
@@ -2393,6 +2937,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2393
2937
|
if (!aborted || runHeld()) {
|
|
2394
2938
|
return;
|
|
2395
2939
|
}
|
|
2940
|
+
run.suppressTurnIntent = true;
|
|
2396
2941
|
run.engine.pause();
|
|
2397
2942
|
run.executor.hold();
|
|
2398
2943
|
renderWidget(ctx);
|
|
@@ -2443,30 +2988,39 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2443
2988
|
});
|
|
2444
2989
|
|
|
2445
2990
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
pendingToolLaunch = null;
|
|
2991
|
+
systemTurnAbort = null;
|
|
2992
|
+
if (activeRun === null) {
|
|
2449
2993
|
try {
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2994
|
+
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
2995
|
+
ctx.sessionManager.getSessionId(),
|
|
2996
|
+
);
|
|
2997
|
+
if (prepared !== undefined && ["queued", "starting"].includes(prepared.status)) {
|
|
2998
|
+
await activatePreparedLaunch(ctx, prepared);
|
|
2999
|
+
return;
|
|
2455
3000
|
}
|
|
2456
3001
|
} catch (error) {
|
|
2457
|
-
notify(
|
|
3002
|
+
notify(
|
|
3003
|
+
ctx,
|
|
3004
|
+
`Could not activate a queued workflow: ${safeLaunchError(error).message}`,
|
|
3005
|
+
"warning",
|
|
3006
|
+
);
|
|
3007
|
+
return;
|
|
2458
3008
|
}
|
|
2459
|
-
return;
|
|
2460
3009
|
}
|
|
3010
|
+
const flushedNatural = turnCoordinator.flushNatural(
|
|
3011
|
+
ctx.isIdle() && !sessionClosed && !runHeld(),
|
|
3012
|
+
);
|
|
2461
3013
|
const run = activeRun;
|
|
2462
3014
|
if (!run) {
|
|
2463
3015
|
presentationPending = null;
|
|
3016
|
+
runSyncPass(ctx);
|
|
2464
3017
|
return;
|
|
2465
3018
|
}
|
|
2466
3019
|
await run.recorder?.synchronize(ctx).catch(() => undefined);
|
|
2467
3020
|
run.recorder?.settleAttempt();
|
|
2468
3021
|
run.executor.setStreaming(false);
|
|
2469
|
-
run.executor.handleAgentSettled();
|
|
3022
|
+
if (flushedNatural === 0) run.executor.handleAgentSettled();
|
|
3023
|
+
runSyncPass(ctx);
|
|
2470
3024
|
});
|
|
2471
3025
|
|
|
2472
3026
|
pi.on("session_shutdown", async () => {
|
|
@@ -2474,8 +3028,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2474
3028
|
herdrProbeGeneration += 1;
|
|
2475
3029
|
systemTurnAbort = null;
|
|
2476
3030
|
suppressWorkflowAssistantTail = false;
|
|
3031
|
+
turnCoordinator.clearDeferred();
|
|
2477
3032
|
supersedePresentation();
|
|
2478
3033
|
const run = activeRun;
|
|
3034
|
+
if (run !== null) run.suppressTurnIntent = true;
|
|
2479
3035
|
if (run !== null && run.claimToken !== undefined) {
|
|
2480
3036
|
// Queued interactive runs park: no terminal event, no recorded partial
|
|
2481
3037
|
// attempt, and the claim releases so another runner can resume.
|
|
@@ -2486,7 +3042,6 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2486
3042
|
await run?.recorder?.stop().catch(() => undefined);
|
|
2487
3043
|
await run?.completion?.catch(() => undefined);
|
|
2488
3044
|
activeRun = null;
|
|
2489
|
-
pendingToolLaunch = null;
|
|
2490
3045
|
lastWaitingRunId = null;
|
|
2491
3046
|
if (runSyncTimer !== null) {
|
|
2492
3047
|
clearInterval(runSyncTimer);
|