@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/dist/extension/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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 { builtinWorkflowCatalog } from "../builtins/catalog.js";
|
|
5
|
-
import { projectControllerStorePath, SqliteControllerStore } from "../controllers/index.js";
|
|
5
|
+
import { projectControllerStorePath, SqliteControllerStore, } from "../controllers/index.js";
|
|
6
|
+
import { compositionMetadata } from "../workflows/composition.js";
|
|
6
7
|
import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
|
|
7
8
|
import { WorkflowEngine } from "../workflows/engine.js";
|
|
8
9
|
import { ClaimLostError, errorMessage, isClaimLostError, TimeoutError, } from "../workflows/errors.js";
|
|
@@ -13,6 +14,8 @@ import { appendProgressHistory, progressRecordsFromTrace } from "../workflows/pr
|
|
|
13
14
|
import { createRunId, listRunBundles, readLastTraceEvent, readRunBundle, WorkflowRunStore, createDefinitionSnapshot, } from "../workflows/store.js";
|
|
14
15
|
import { PiControllerHost, parseControllerArgs, } from "./controller-host.js";
|
|
15
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";
|
|
16
19
|
import { ConversationStepExecutor } from "./executor.js";
|
|
17
20
|
import { HerdrWorkflowViewer, parseViewerPlacement, PIW_SHORTCUT, PIW_SHORTCUT_HINT, VIEWER_PLACEMENTS, } from "./herdr-viewer.js";
|
|
18
21
|
import { SessionRecorder } from "./recorder.js";
|
|
@@ -24,8 +27,10 @@ const RUN_CLAIM_LEASE_MS = 30_000;
|
|
|
24
27
|
const RUN_CLAIM_RENEW_MS = 10_000;
|
|
25
28
|
const RUN_SYNC_POLL_MS = 3_000;
|
|
26
29
|
const NOTIFICATION_DELIVERY_LEASE_MS = 30_000;
|
|
30
|
+
const TURN_INTENT_DELIVERY_LEASE_MS = 30_000;
|
|
27
31
|
const WIDGET_KEY = "pi-workflows";
|
|
28
32
|
const PRESENTATION_MESSAGE_TYPE = "pi-workflows-presentation";
|
|
33
|
+
const PRESENTATION_MESSAGE_SCHEMA = "pi-workflows.presentation-message.v1";
|
|
29
34
|
const FINAL_WIDGET_TTL_MS = 60_000;
|
|
30
35
|
const WIDGET_SCROLL_STEP = 3;
|
|
31
36
|
const MAX_PRESENTATION_RESULT_CHARS = 50_000;
|
|
@@ -56,6 +61,44 @@ function humanDecisionRequest(value) {
|
|
|
56
61
|
}
|
|
57
62
|
return value;
|
|
58
63
|
}
|
|
64
|
+
function definitionDigest(snapshot) {
|
|
65
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
|
|
66
|
+
}
|
|
67
|
+
function launchSourceIdentity(workflow, root) {
|
|
68
|
+
return {
|
|
69
|
+
root,
|
|
70
|
+
mounted: compositionMetadata(workflow)?.sources ?? [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function preparedLaunchOptions(options) {
|
|
74
|
+
return {
|
|
75
|
+
...(options.presentation !== undefined ? { presentation: options.presentation } : {}),
|
|
76
|
+
...(options.parentRunId !== undefined ? { parentRunId: options.parentRunId } : {}),
|
|
77
|
+
...(options.humanDecision !== undefined ? { humanDecision: options.humanDecision } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function parsePreparedLaunchOptions(value) {
|
|
81
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
82
|
+
throw new Error("Stored workflow launch options are invalid");
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
function safeLaunchError(error) {
|
|
87
|
+
const raw = errorMessage(error)
|
|
88
|
+
.replace(/Bearer\s+\S+/giu, "Bearer [redacted]")
|
|
89
|
+
.replace(/(token|api[_-]?key|secret|password)(\s*[:=]\s*)\S+/giu, "$1$2[redacted]")
|
|
90
|
+
.replaceAll("\n", " ")
|
|
91
|
+
.trim();
|
|
92
|
+
const code = /not found|cannot find|unknown workflow/iu.test(raw)
|
|
93
|
+
? "workflow_not_found"
|
|
94
|
+
: /source changed|source mismatch/iu.test(raw)
|
|
95
|
+
? "source_changed"
|
|
96
|
+
: /invalid|must be/iu.test(raw)
|
|
97
|
+
? "workflow_invalid"
|
|
98
|
+
: "activation_failed";
|
|
99
|
+
const message = raw.length <= 500 ? raw : `${raw.slice(0, 480)}… [error truncated]`;
|
|
100
|
+
return { code, message: message || "The deferred workflow could not start" };
|
|
101
|
+
}
|
|
59
102
|
/** Parse `/workflow` arguments. Exported for tests. */
|
|
60
103
|
export function parseWorkflowArgs(args) {
|
|
61
104
|
const trimmed = args.trim();
|
|
@@ -178,8 +221,10 @@ export default function piWorkflows(pi) {
|
|
|
178
221
|
// Run events remain an audit feed and never enter a conversation.
|
|
179
222
|
let syncArmed = false;
|
|
180
223
|
let runSyncTimer = null;
|
|
224
|
+
let activationRecovery;
|
|
181
225
|
let decisionRecoveryTimer = null;
|
|
182
226
|
let decisionRecoveryActive = false;
|
|
227
|
+
let turnCoordinator;
|
|
183
228
|
const recordRunEvent = (event) => {
|
|
184
229
|
try {
|
|
185
230
|
runQueueStore?.recordRunEvent({ ...event, runnerId });
|
|
@@ -207,6 +252,7 @@ export default function piWorkflows(pi) {
|
|
|
207
252
|
if (runQueueStore === null || !syncArmed)
|
|
208
253
|
return;
|
|
209
254
|
try {
|
|
255
|
+
activationRecovery?.(ctx);
|
|
210
256
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
211
257
|
const alreadyDelivered = deliveredNotificationIds(ctx);
|
|
212
258
|
const claimToken = randomUUID();
|
|
@@ -234,6 +280,19 @@ export default function piWorkflows(pi) {
|
|
|
234
280
|
claimToken,
|
|
235
281
|
});
|
|
236
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);
|
|
237
296
|
}
|
|
238
297
|
catch {
|
|
239
298
|
// Delivery retries on the next poll. It never affects workflow execution.
|
|
@@ -252,7 +311,6 @@ export default function piWorkflows(pi) {
|
|
|
252
311
|
let systemTurnAbort = null;
|
|
253
312
|
let suppressWorkflowAssistantTail = false;
|
|
254
313
|
let lastExpiredAttempt = null;
|
|
255
|
-
let pendingToolLaunch = null;
|
|
256
314
|
// The interactive run currently parked at a checkpoint, if any.
|
|
257
315
|
let lastWaitingRunId = null;
|
|
258
316
|
let widgetTimer = null;
|
|
@@ -270,6 +328,35 @@ export default function piWorkflows(pi) {
|
|
|
270
328
|
let presentationPending = null;
|
|
271
329
|
let controllerHost;
|
|
272
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
|
+
});
|
|
273
360
|
// UI updates are best-effort: a captured ctx becomes stale after session
|
|
274
361
|
// replacement or shutdown, and pi throws on any access (even `ctx.hasUI`).
|
|
275
362
|
// A workflow finishing right as the session goes away must not crash pi.
|
|
@@ -310,6 +397,115 @@ export default function piWorkflows(pi) {
|
|
|
310
397
|
};
|
|
311
398
|
/** True when the run is held for the user (escape or /workflow pause). */
|
|
312
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
|
+
};
|
|
313
509
|
const footerStatus = (state) => {
|
|
314
510
|
const label = runHeld() || state.paused ? "paused" : state.status;
|
|
315
511
|
const node = state.currentNode ?? state.waitingOn;
|
|
@@ -470,20 +666,36 @@ export default function piWorkflows(pi) {
|
|
|
470
666
|
const instructions = typeof run.presentationPrompt === "function"
|
|
471
667
|
? await resolvePresentationPrompt(run.presentationPrompt, state, abort.signal)
|
|
472
668
|
: run.presentationPrompt;
|
|
473
|
-
if (sessionClosed ||
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
669
|
+
if (sessionClosed || run.generation !== runGeneration || abort.signal.aborted) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
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
|
+
}
|
|
478
676
|
return;
|
|
479
677
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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);
|
|
487
699
|
}
|
|
488
700
|
catch (error) {
|
|
489
701
|
if (presentationPending === run.generation) {
|
|
@@ -498,6 +710,9 @@ export default function piWorkflows(pi) {
|
|
|
498
710
|
? `timed out after ${PRESENTATION_TIMEOUT_MS}ms`
|
|
499
711
|
: errorMessage(error);
|
|
500
712
|
notify(ctx, `Could not present workflow result: ${message}`, "warning");
|
|
713
|
+
if (run.abortProvenance !== undefined) {
|
|
714
|
+
makeRunTurnIntentEligible(ctx, run, state.status, message);
|
|
715
|
+
}
|
|
501
716
|
}
|
|
502
717
|
finally {
|
|
503
718
|
clearTimeout(timer);
|
|
@@ -548,6 +763,19 @@ export default function piWorkflows(pi) {
|
|
|
548
763
|
clearWidget(ctx);
|
|
549
764
|
return;
|
|
550
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
|
+
}
|
|
551
779
|
releaseClaim(run, "done");
|
|
552
780
|
recordRunEvent({
|
|
553
781
|
runId: run.runId,
|
|
@@ -596,6 +824,14 @@ export default function piWorkflows(pi) {
|
|
|
596
824
|
catch (error) {
|
|
597
825
|
notify(ctx, `Could not record child workflow completion: ${errorMessage(error)}`, "warning");
|
|
598
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
|
+
}
|
|
599
835
|
void presentRun(ctx, run, state);
|
|
600
836
|
if (pendingDecision !== null && state.status === "waiting") {
|
|
601
837
|
const channels = audienceChannels(decisionChannelConfig, pendingDecision.audience);
|
|
@@ -671,7 +907,10 @@ export default function piWorkflows(pi) {
|
|
|
671
907
|
workflowSourceRef: workflowSource.kind === "builtin"
|
|
672
908
|
? `builtin:${workflowSource.id}`
|
|
673
909
|
: workflowSource.path,
|
|
910
|
+
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
911
|
+
definitionDigest: definitionDigest(snapshot),
|
|
674
912
|
input,
|
|
913
|
+
launchOptions: preparedLaunchOptions(options),
|
|
675
914
|
runnerId,
|
|
676
915
|
claimToken: token,
|
|
677
916
|
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
@@ -699,21 +938,29 @@ export default function piWorkflows(pi) {
|
|
|
699
938
|
}
|
|
700
939
|
const executor = new ConversationStepExecutor({
|
|
701
940
|
sendPrompt: ({ prompt, contract, presentation, kind, streaming }) => {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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);
|
|
717
964
|
},
|
|
718
965
|
onAbort: (contract, reason) => {
|
|
719
966
|
lastExpiredAttempt = {
|
|
@@ -721,7 +968,15 @@ export default function piWorkflows(pi) {
|
|
|
721
968
|
reason: reason instanceof TimeoutError ? "timed out" : `ended: ${errorMessage(reason)}`,
|
|
722
969
|
};
|
|
723
970
|
if (!executor.held && !ctx.isIdle()) {
|
|
724
|
-
|
|
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
|
+
};
|
|
725
980
|
ctx.abort();
|
|
726
981
|
}
|
|
727
982
|
},
|
|
@@ -799,6 +1054,11 @@ export default function piWorkflows(pi) {
|
|
|
799
1054
|
...(options.resume === true ? { resume: true } : {}),
|
|
800
1055
|
...(options.parentRunId !== undefined ? { parentRunId: options.parentRunId } : {}),
|
|
801
1056
|
};
|
|
1057
|
+
if (queueStore !== null &&
|
|
1058
|
+
claimToken !== undefined &&
|
|
1059
|
+
!queueStore.markWorkflowRunRunning({ runId, claimToken })) {
|
|
1060
|
+
throw new ClaimLostError(runId);
|
|
1061
|
+
}
|
|
802
1062
|
activeRun = run;
|
|
803
1063
|
if (queueStore !== null && claimToken !== undefined) {
|
|
804
1064
|
const store = queueStore;
|
|
@@ -809,6 +1069,7 @@ export default function piWorkflows(pi) {
|
|
|
809
1069
|
run.renewTimer = setInterval(() => {
|
|
810
1070
|
try {
|
|
811
1071
|
if (!store.renewWorkflowRunClaim({ runId, claimToken: token, leaseMs: RUN_CLAIM_LEASE_MS })) {
|
|
1072
|
+
run.pendingAbortCause = "claimLost";
|
|
812
1073
|
run.engine.cancel();
|
|
813
1074
|
}
|
|
814
1075
|
}
|
|
@@ -902,6 +1163,11 @@ export default function piWorkflows(pi) {
|
|
|
902
1163
|
}
|
|
903
1164
|
if (terminalStatus !== undefined) {
|
|
904
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
|
+
}
|
|
905
1171
|
notify(ctx, `Workflow ${workflow.name} ${terminalStatus} (run ${runId}).`);
|
|
906
1172
|
return;
|
|
907
1173
|
}
|
|
@@ -911,6 +1177,7 @@ export default function piWorkflows(pi) {
|
|
|
911
1177
|
type: "failed",
|
|
912
1178
|
payload: { error: message },
|
|
913
1179
|
});
|
|
1180
|
+
makeRunTurnIntentEligible(ctx, run, "failed", message);
|
|
914
1181
|
try {
|
|
915
1182
|
run.onFinish?.({ state: "failed", runId, error: message });
|
|
916
1183
|
}
|
|
@@ -948,8 +1215,10 @@ export default function piWorkflows(pi) {
|
|
|
948
1215
|
: bundle.state.workflowSource.kind === "builtin"
|
|
949
1216
|
? `builtin:${bundle.state.workflowSource.id}`
|
|
950
1217
|
: bundle.state.workflowSource.path;
|
|
1218
|
+
const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
|
|
951
1219
|
started = await startRun(ctx, sourceRef, claimed.input, {
|
|
952
|
-
|
|
1220
|
+
...launchOptions,
|
|
1221
|
+
resume: bundle !== null,
|
|
953
1222
|
runId: claimed.runId,
|
|
954
1223
|
claimToken,
|
|
955
1224
|
});
|
|
@@ -1103,22 +1372,42 @@ export default function piWorkflows(pi) {
|
|
|
1103
1372
|
}
|
|
1104
1373
|
return null;
|
|
1105
1374
|
};
|
|
1106
|
-
const cancelWorkflowControl = async (ctx) => {
|
|
1107
|
-
if (activeRun) {
|
|
1375
|
+
const cancelWorkflowControl = async (ctx, origin, requestedRunId) => {
|
|
1376
|
+
if (activeRun && (requestedRunId === undefined || requestedRunId === activeRun.runId)) {
|
|
1108
1377
|
const workflowName = activeRun.workflowName;
|
|
1109
1378
|
const runId = activeRun.runId;
|
|
1379
|
+
activeRun.pendingAbortCause = origin === "agent" ? "agentCancelled" : undefined;
|
|
1380
|
+
activeRun.suppressTurnIntent = origin === "user";
|
|
1110
1381
|
activeRun.engine.cancel();
|
|
1111
1382
|
return {
|
|
1112
1383
|
message: `Cancelling workflow ${workflowName}…`,
|
|
1113
1384
|
details: { action: "cancel", workflowName, runId },
|
|
1114
1385
|
};
|
|
1115
1386
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1387
|
+
const queue = ensureRunQueueStore(ctx.cwd);
|
|
1388
|
+
const queued = requestedRunId === undefined
|
|
1389
|
+
? queue.findSessionReservation(ctx.sessionManager.getSessionId())
|
|
1390
|
+
: queue.getWorkflowRun(requestedRunId);
|
|
1391
|
+
if (queued !== undefined &&
|
|
1392
|
+
["queued", "starting"].includes(queued.status) &&
|
|
1393
|
+
(queued.originSessionId === null ||
|
|
1394
|
+
queued.originSessionId === ctx.sessionManager.getSessionId())) {
|
|
1395
|
+
if (!queue.cancelWorkflowRun({ runId: queued.runId })) {
|
|
1396
|
+
throw new Error(`Workflow ${queued.runId} could not be cancelled because its state changed.`);
|
|
1397
|
+
}
|
|
1398
|
+
recordRunEvent({
|
|
1399
|
+
runId: queued.runId,
|
|
1400
|
+
workflowRef: queued.workflowName,
|
|
1401
|
+
type: "cancelled",
|
|
1402
|
+
});
|
|
1119
1403
|
return {
|
|
1120
|
-
message: `Cancelled
|
|
1121
|
-
details: {
|
|
1404
|
+
message: `Cancelled queued workflow ${queued.workflowName} (run ${queued.runId}).`,
|
|
1405
|
+
details: {
|
|
1406
|
+
action: "cancel",
|
|
1407
|
+
workflow: queued.workflowName,
|
|
1408
|
+
runId: queued.runId,
|
|
1409
|
+
queued: false,
|
|
1410
|
+
},
|
|
1122
1411
|
};
|
|
1123
1412
|
}
|
|
1124
1413
|
if (widgetSource) {
|
|
@@ -1180,6 +1469,7 @@ export default function piWorkflows(pi) {
|
|
|
1180
1469
|
},
|
|
1181
1470
|
};
|
|
1182
1471
|
}
|
|
1472
|
+
activeRun.suppressTurnIntent = true;
|
|
1183
1473
|
activeRun.engine.pause();
|
|
1184
1474
|
renderWidget(ctx);
|
|
1185
1475
|
return {
|
|
@@ -1212,6 +1502,7 @@ export default function piWorkflows(pi) {
|
|
|
1212
1502
|
level: "warning",
|
|
1213
1503
|
};
|
|
1214
1504
|
}
|
|
1505
|
+
activeRun.suppressTurnIntent = false;
|
|
1215
1506
|
activeRun.engine.resume();
|
|
1216
1507
|
activeRun.executor.release();
|
|
1217
1508
|
renderWidget(ctx);
|
|
@@ -1225,11 +1516,28 @@ export default function piWorkflows(pi) {
|
|
|
1225
1516
|
},
|
|
1226
1517
|
};
|
|
1227
1518
|
};
|
|
1519
|
+
const workflowLaunchStatus = (record) => ({
|
|
1520
|
+
message: `Workflow ${record.workflowName} is ${record.status} (run ${record.runId}).`,
|
|
1521
|
+
details: {
|
|
1522
|
+
action: "status",
|
|
1523
|
+
active: ["starting", "running"].includes(record.status),
|
|
1524
|
+
queued: record.status === "queued",
|
|
1525
|
+
workflowName: record.workflowName,
|
|
1526
|
+
runId: record.runId,
|
|
1527
|
+
status: record.status,
|
|
1528
|
+
...(record.errorCode === null ? {} : { errorCode: record.errorCode }),
|
|
1529
|
+
...(record.errorMessage === null ? {} : { error: record.errorMessage }),
|
|
1530
|
+
},
|
|
1531
|
+
...(["failed", "cancelled"].includes(record.status) ? { level: "warning" } : {}),
|
|
1532
|
+
});
|
|
1228
1533
|
const statusWorkflowControl = async (ctx, runId) => {
|
|
1229
1534
|
if (runId !== undefined) {
|
|
1230
1535
|
const bundle = await readRunBundle(new WorkflowRunStore().runDirFor(runId));
|
|
1231
1536
|
if (bundle === null) {
|
|
1232
|
-
|
|
1537
|
+
const launch = ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId);
|
|
1538
|
+
if (launch === undefined)
|
|
1539
|
+
throw new Error(`Workflow run not found: ${runId}`);
|
|
1540
|
+
return workflowLaunchStatus(launch);
|
|
1233
1541
|
}
|
|
1234
1542
|
const { state } = bundle;
|
|
1235
1543
|
return {
|
|
@@ -1238,11 +1546,10 @@ export default function piWorkflows(pi) {
|
|
|
1238
1546
|
};
|
|
1239
1547
|
}
|
|
1240
1548
|
const state = activeRun?.lastState ?? widgetSource?.state;
|
|
1241
|
-
if (
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
};
|
|
1549
|
+
if (state === undefined || state === null) {
|
|
1550
|
+
const queued = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
|
|
1551
|
+
if (queued !== undefined)
|
|
1552
|
+
return workflowLaunchStatus(queued);
|
|
1246
1553
|
}
|
|
1247
1554
|
if (state === undefined || state === null) {
|
|
1248
1555
|
return {
|
|
@@ -1545,8 +1852,9 @@ export default function piWorkflows(pi) {
|
|
|
1545
1852
|
if (activeRun !== null) {
|
|
1546
1853
|
throw new Error(`A workflow is already running: ${activeRun.workflowName}.`);
|
|
1547
1854
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1855
|
+
const reserved = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
|
|
1856
|
+
if (reserved !== undefined) {
|
|
1857
|
+
throw new Error(`Workflow ${reserved.workflowName} is already ${reserved.status} (run ${reserved.runId}).`);
|
|
1550
1858
|
}
|
|
1551
1859
|
if (presentationPending !== null) {
|
|
1552
1860
|
throw new Error("The previous workflow result is still being presented.");
|
|
@@ -1564,36 +1872,148 @@ export default function piWorkflows(pi) {
|
|
|
1564
1872
|
if (activeRun !== null) {
|
|
1565
1873
|
throw new Error(`A workflow is already running: ${activeRun.workflowName}. Cancel it before starting another.`);
|
|
1566
1874
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1875
|
+
const queue = ensureRunQueueStore(ctx.cwd);
|
|
1876
|
+
const existing = queue.findSessionReservation(ctx.sessionManager.getSessionId());
|
|
1877
|
+
if (existing !== undefined) {
|
|
1878
|
+
throw new Error(`Workflow ${existing.workflowName} is already ${existing.status} (run ${existing.runId}).`);
|
|
1569
1879
|
}
|
|
1570
1880
|
if (presentationPending !== null) {
|
|
1571
1881
|
throw new Error("The previous workflow result is still being presented.");
|
|
1572
1882
|
}
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
const
|
|
1578
|
-
if (
|
|
1579
|
-
throw new Error(
|
|
1883
|
+
const resolved = await resolveWorkflowRef(ref, { cwd: ctx.cwd }, builtinWorkflowCatalog);
|
|
1884
|
+
const workflow = resolved.definition;
|
|
1885
|
+
const workflowSource = resolved.source;
|
|
1886
|
+
if (options.parentRunId !== undefined) {
|
|
1887
|
+
const parent = await readRunBundle(new WorkflowRunStore().runDirFor(options.parentRunId));
|
|
1888
|
+
if (parent === null || parent.state.status !== "waiting") {
|
|
1889
|
+
throw new Error(`Workflow run ${options.parentRunId} is not waiting at a checkpoint`);
|
|
1580
1890
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1891
|
+
if (parent.state.workflowSource !== undefined &&
|
|
1892
|
+
!isDeepStrictEqual(parent.state.workflowSource, workflowSource)) {
|
|
1893
|
+
throw new Error(`Workflow source changed since run ${options.parentRunId} started; revert the edit to answer its checkpoint`);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
const snapshot = createDefinitionSnapshot(workflow);
|
|
1897
|
+
const runId = createRunId(workflow.name);
|
|
1898
|
+
try {
|
|
1899
|
+
queue.reserveWorkflowRun({
|
|
1900
|
+
runId,
|
|
1901
|
+
workflowName: workflow.name,
|
|
1902
|
+
workflowSourceRef: workflowSource.kind === "builtin" ? `builtin:${workflowSource.id}` : workflowSource.path,
|
|
1903
|
+
workflowSource: launchSourceIdentity(workflow, workflowSource),
|
|
1904
|
+
definitionDigest: definitionDigest(snapshot),
|
|
1905
|
+
input,
|
|
1906
|
+
launchOptions: preparedLaunchOptions(options),
|
|
1907
|
+
runnerId,
|
|
1908
|
+
originSessionId: ctx.sessionManager.getSessionId(),
|
|
1909
|
+
...(options.parentRunId !== undefined ? { parentRunId: options.parentRunId } : {}),
|
|
1910
|
+
});
|
|
1590
1911
|
}
|
|
1591
1912
|
catch (error) {
|
|
1592
|
-
|
|
1593
|
-
|
|
1913
|
+
const reserved = queue.findSessionReservation(ctx.sessionManager.getSessionId());
|
|
1914
|
+
if (reserved !== undefined) {
|
|
1915
|
+
throw new Error(`A workflow launch is already waiting: ${reserved.workflowName} (run ${reserved.runId}).`, { cause: error });
|
|
1594
1916
|
}
|
|
1595
1917
|
throw error;
|
|
1596
1918
|
}
|
|
1919
|
+
recordRunEvent({
|
|
1920
|
+
runId,
|
|
1921
|
+
workflowRef: workflow.name,
|
|
1922
|
+
type: "queued",
|
|
1923
|
+
payload: options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId },
|
|
1924
|
+
});
|
|
1925
|
+
syncArmed = true;
|
|
1926
|
+
return {
|
|
1927
|
+
message: `Workflow ${workflow.name} queued (run ${runId}).`,
|
|
1928
|
+
details: {
|
|
1929
|
+
action: "start",
|
|
1930
|
+
workflow: workflow.name,
|
|
1931
|
+
runId,
|
|
1932
|
+
source: workflowSource,
|
|
1933
|
+
queued: true,
|
|
1934
|
+
},
|
|
1935
|
+
};
|
|
1936
|
+
};
|
|
1937
|
+
const activatePreparedLaunch = async (ctx, prepared) => {
|
|
1938
|
+
const queue = ensureRunQueueStore(ctx.cwd);
|
|
1939
|
+
const claimToken = randomUUID();
|
|
1940
|
+
const claimed = queue.claimWorkflowRun({
|
|
1941
|
+
runId: prepared.runId,
|
|
1942
|
+
runnerId,
|
|
1943
|
+
claimToken,
|
|
1944
|
+
leaseMs: RUN_CLAIM_LEASE_MS,
|
|
1945
|
+
});
|
|
1946
|
+
if (claimed === undefined)
|
|
1947
|
+
return false;
|
|
1948
|
+
try {
|
|
1949
|
+
const resolved = await resolveWorkflowRef(claimed.workflowSourceRef, { cwd: ctx.cwd }, builtinWorkflowCatalog);
|
|
1950
|
+
const snapshot = createDefinitionSnapshot(resolved.definition);
|
|
1951
|
+
if (!isDeepStrictEqual(launchSourceIdentity(resolved.definition, resolved.source), claimed.workflowSource) ||
|
|
1952
|
+
definitionDigest(snapshot) !== claimed.definitionDigest) {
|
|
1953
|
+
throw new Error("Workflow source changed after the launch was queued");
|
|
1954
|
+
}
|
|
1955
|
+
const launchOptions = parsePreparedLaunchOptions(claimed.launchOptions);
|
|
1956
|
+
const started = await startRun(ctx, claimed.workflowSourceRef, claimed.input, {
|
|
1957
|
+
...launchOptions,
|
|
1958
|
+
runId: claimed.runId,
|
|
1959
|
+
claimToken,
|
|
1960
|
+
});
|
|
1961
|
+
if (started === undefined)
|
|
1962
|
+
throw new Error("The queued workflow could not start");
|
|
1963
|
+
if (launchOptions.parentRunId !== undefined)
|
|
1964
|
+
lastWaitingRunId = null;
|
|
1965
|
+
return true;
|
|
1966
|
+
}
|
|
1967
|
+
catch (error) {
|
|
1968
|
+
const safe = safeLaunchError(error);
|
|
1969
|
+
queue.failWorkflowRun({
|
|
1970
|
+
runId: claimed.runId,
|
|
1971
|
+
claimToken,
|
|
1972
|
+
errorCode: safe.code,
|
|
1973
|
+
errorMessage: safe.message,
|
|
1974
|
+
});
|
|
1975
|
+
recordRunEvent({
|
|
1976
|
+
runId: claimed.runId,
|
|
1977
|
+
workflowRef: claimed.workflowName,
|
|
1978
|
+
type: "launch_failed",
|
|
1979
|
+
payload: { errorCode: safe.code, error: safe.message },
|
|
1980
|
+
});
|
|
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.`;
|
|
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({
|
|
1989
|
+
runId: claimed.runId,
|
|
1990
|
+
cause,
|
|
1991
|
+
nodeId: "$launch",
|
|
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;
|
|
2001
|
+
}
|
|
2002
|
+
catch (intentError) {
|
|
2003
|
+
notify(ctx, `Workflow ${claimed.workflowName} failed to start, but Pi Workflows could not preserve its successor turn: ${errorMessage(intentError)}`, "warning");
|
|
2004
|
+
}
|
|
2005
|
+
notify(ctx, content, "error");
|
|
2006
|
+
runSyncPass(ctx);
|
|
2007
|
+
return false;
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
activationRecovery = (ctx) => {
|
|
2011
|
+
if (activeRun !== null)
|
|
2012
|
+
return;
|
|
2013
|
+
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
|
|
2014
|
+
if (prepared !== undefined && ["queued", "starting"].includes(prepared.status)) {
|
|
2015
|
+
void activatePreparedLaunch(ctx, prepared).catch(() => undefined);
|
|
2016
|
+
}
|
|
1597
2017
|
};
|
|
1598
2018
|
pi.registerCommand("piw", {
|
|
1599
2019
|
description: "Open the current workflow run in piw through Herdr",
|
|
@@ -1639,7 +2059,7 @@ export default function piWorkflows(pi) {
|
|
|
1639
2059
|
return;
|
|
1640
2060
|
}
|
|
1641
2061
|
if (parsed.kind === "cancel") {
|
|
1642
|
-
const result = await cancelWorkflowControl(ctx);
|
|
2062
|
+
const result = await cancelWorkflowControl(ctx, "user");
|
|
1643
2063
|
notify(ctx, result.message, result.level);
|
|
1644
2064
|
return;
|
|
1645
2065
|
}
|
|
@@ -1799,6 +2219,7 @@ export default function piWorkflows(pi) {
|
|
|
1799
2219
|
case "stop":
|
|
1800
2220
|
if (activeRun?.childKey !== undefined) {
|
|
1801
2221
|
activeRun.interruptionRequested = true;
|
|
2222
|
+
activeRun.pendingAbortCause = "controllerInterrupted";
|
|
1802
2223
|
activeRun.engine.cancel();
|
|
1803
2224
|
}
|
|
1804
2225
|
await host.stop();
|
|
@@ -1815,7 +2236,7 @@ export default function piWorkflows(pi) {
|
|
|
1815
2236
|
name: "workflow",
|
|
1816
2237
|
label: "Workflow",
|
|
1817
2238
|
description: [
|
|
1818
|
-
"List, start, inspect, pause, resume, cancel, answer, update, or complete
|
|
2239
|
+
"List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
|
|
1819
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.",
|
|
1820
2241
|
"Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
|
|
1821
2242
|
"Do not start repeated work without the user's request, and keep monitoring observation-only unless the user authorizes mutations.",
|
|
@@ -1841,7 +2262,7 @@ export default function piWorkflows(pi) {
|
|
|
1841
2262
|
control = resumeWorkflowControl(ctx);
|
|
1842
2263
|
break;
|
|
1843
2264
|
case "cancel":
|
|
1844
|
-
control = await cancelWorkflowControl(ctx);
|
|
2265
|
+
control = await cancelWorkflowControl(ctx, "agent", params.runId);
|
|
1845
2266
|
break;
|
|
1846
2267
|
case "answer": {
|
|
1847
2268
|
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
@@ -1951,7 +2372,13 @@ export default function piWorkflows(pi) {
|
|
|
1951
2372
|
notify(ctx, `Could not recover human decisions: ${errorMessage(error)}`, "warning");
|
|
1952
2373
|
}
|
|
1953
2374
|
try {
|
|
1954
|
-
|
|
2375
|
+
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
|
|
2376
|
+
if (prepared !== undefined && ["queued", "starting"].includes(prepared.status)) {
|
|
2377
|
+
await activatePreparedLaunch(ctx, prepared);
|
|
2378
|
+
}
|
|
2379
|
+
else {
|
|
2380
|
+
await resumeParkedRun(ctx);
|
|
2381
|
+
}
|
|
1955
2382
|
}
|
|
1956
2383
|
catch (error) {
|
|
1957
2384
|
notify(ctx, `Could not resume a parked workflow: ${errorMessage(error)}`, "warning");
|
|
@@ -1993,6 +2420,7 @@ export default function piWorkflows(pi) {
|
|
|
1993
2420
|
if (!aborted || runHeld()) {
|
|
1994
2421
|
return;
|
|
1995
2422
|
}
|
|
2423
|
+
run.suppressTurnIntent = true;
|
|
1996
2424
|
run.engine.pause();
|
|
1997
2425
|
run.executor.hold();
|
|
1998
2426
|
renderWidget(ctx);
|
|
@@ -2031,40 +2459,44 @@ export default function piWorkflows(pi) {
|
|
|
2031
2459
|
activeRun?.recorder?.handleToolEnd(event);
|
|
2032
2460
|
});
|
|
2033
2461
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
pendingToolLaunch = null;
|
|
2462
|
+
systemTurnAbort = null;
|
|
2463
|
+
if (activeRun === null) {
|
|
2037
2464
|
try {
|
|
2038
|
-
const
|
|
2039
|
-
if (
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
else if (launch.options?.parentRunId !== undefined) {
|
|
2043
|
-
lastWaitingRunId = null;
|
|
2465
|
+
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
|
|
2466
|
+
if (prepared !== undefined && ["queued", "starting"].includes(prepared.status)) {
|
|
2467
|
+
await activatePreparedLaunch(ctx, prepared);
|
|
2468
|
+
return;
|
|
2044
2469
|
}
|
|
2045
2470
|
}
|
|
2046
2471
|
catch (error) {
|
|
2047
|
-
notify(
|
|
2472
|
+
notify(ctx, `Could not activate a queued workflow: ${safeLaunchError(error).message}`, "warning");
|
|
2473
|
+
return;
|
|
2048
2474
|
}
|
|
2049
|
-
return;
|
|
2050
2475
|
}
|
|
2476
|
+
const flushedNatural = turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
|
|
2051
2477
|
const run = activeRun;
|
|
2052
2478
|
if (!run) {
|
|
2053
2479
|
presentationPending = null;
|
|
2480
|
+
runSyncPass(ctx);
|
|
2054
2481
|
return;
|
|
2055
2482
|
}
|
|
2056
2483
|
await run.recorder?.synchronize(ctx).catch(() => undefined);
|
|
2057
2484
|
run.recorder?.settleAttempt();
|
|
2058
2485
|
run.executor.setStreaming(false);
|
|
2059
|
-
|
|
2486
|
+
if (flushedNatural === 0)
|
|
2487
|
+
run.executor.handleAgentSettled();
|
|
2488
|
+
runSyncPass(ctx);
|
|
2060
2489
|
});
|
|
2061
2490
|
pi.on("session_shutdown", async () => {
|
|
2062
2491
|
sessionClosed = true;
|
|
2063
2492
|
herdrProbeGeneration += 1;
|
|
2064
2493
|
systemTurnAbort = null;
|
|
2065
2494
|
suppressWorkflowAssistantTail = false;
|
|
2495
|
+
turnCoordinator.clearDeferred();
|
|
2066
2496
|
supersedePresentation();
|
|
2067
2497
|
const run = activeRun;
|
|
2498
|
+
if (run !== null)
|
|
2499
|
+
run.suppressTurnIntent = true;
|
|
2068
2500
|
if (run !== null && run.claimToken !== undefined) {
|
|
2069
2501
|
// Queued interactive runs park: no terminal event, no recorded partial
|
|
2070
2502
|
// attempt, and the claim releases so another runner can resume.
|
|
@@ -2076,7 +2508,6 @@ export default function piWorkflows(pi) {
|
|
|
2076
2508
|
await run?.recorder?.stop().catch(() => undefined);
|
|
2077
2509
|
await run?.completion?.catch(() => undefined);
|
|
2078
2510
|
activeRun = null;
|
|
2079
|
-
pendingToolLaunch = null;
|
|
2080
2511
|
lastWaitingRunId = null;
|
|
2081
2512
|
if (runSyncTimer !== null) {
|
|
2082
2513
|
clearInterval(runSyncTimer);
|