@osolmaz/pi-workflows 0.13.1 → 0.13.3
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 +9 -7
- package/dist/builtins/autodoc.workflow.d.ts +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +69 -61
- package/dist/builtins/autoimplement.workflow.js +47 -8
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/autoplan.workflow.d.ts +11 -5
- package/dist/builtins/autoplan.workflow.js +68 -32
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +4 -4
- package/dist/builtins/change-verification.workflow.d.ts +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +1 -1
- package/dist/builtins/plain-summary.workflow.js +35 -24
- package/dist/builtins/plain-summary.workflow.js.map +1 -1
- package/dist/builtins/plan-approval.workflow.d.ts +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +25 -23
- package/dist/builtins/sanity-check.workflow.d.ts +3 -3
- package/dist/builtins/sanity-check.workflow.js +19 -22
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/builtins/workspace-preparation.workflow.d.ts +1 -1
- package/dist/controllers/index.d.ts +2 -2
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.js +88 -61
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/types.d.ts +34 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +5 -1
- package/dist/controllers/workflow-engine-scheduler.js +83 -1
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +8 -1
- package/dist/controllers/workflows.js +36 -0
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.d.ts +2 -1
- package/dist/extension/controller-host.js +1 -1
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/decision-channels.js +45 -7
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/follow-up-coordinator.d.ts +27 -0
- package/dist/extension/follow-up-coordinator.js +131 -0
- package/dist/extension/follow-up-coordinator.js.map +1 -0
- package/dist/extension/index.d.ts +9 -0
- package/dist/extension/index.js +462 -76
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +1 -1
- package/dist/extension/recorder.js +8 -8
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -3
- package/dist/extension/session-events.js +11 -10
- package/dist/extension/session-events.js.map +1 -1
- package/dist/extension/widget.js +9 -0
- package/dist/extension/widget.js.map +1 -1
- package/dist/state/database.d.ts +2 -1
- package/dist/state/database.js +11 -13
- package/dist/state/database.js.map +1 -1
- package/dist/state/json.js +6 -1
- package/dist/state/json.js.map +1 -1
- package/dist/state/mutation.d.ts +1 -1
- package/dist/state/mutation.js.map +1 -1
- package/dist/state/prune.d.ts +18 -0
- package/dist/state/prune.js +373 -0
- package/dist/state/prune.js.map +1 -0
- package/dist/state/schema.d.ts +1 -1
- package/dist/state/schema.js +130 -13
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/cli.d.ts +3 -1
- package/dist/viewer/cli.js +55 -4
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +19 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +0 -1
- package/dist/viewer/session-reducer.js +2 -24
- package/dist/viewer/session-reducer.js.map +1 -1
- package/dist/workflows/composition.d.ts +2 -0
- package/dist/workflows/composition.js +15 -0
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -3
- package/dist/workflows/definition.js +3 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -1
- package/dist/workflows/engine.js +303 -36
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +1 -0
- package/dist/workflows/human-decision.js +25 -9
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -1
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/json-patch.d.ts +47 -0
- package/dist/workflows/json-patch.js +298 -0
- package/dist/workflows/json-patch.js.map +1 -0
- package/dist/workflows/schema.js +29 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/settings.d.ts +128 -0
- package/dist/workflows/settings.js +199 -0
- package/dist/workflows/settings.js.map +1 -0
- package/dist/workflows/store.d.ts +81 -3
- package/dist/workflows/store.js +1563 -50
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +22 -0
- package/dist/workflows/tool-input.js +43 -0
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +41 -6
- package/docs/2026-08-25-workflow-follow-ups.md +132 -0
- package/docs/2026-08-25-workflow-settings.md +169 -0
- package/docs/DEFERRED_TURNS.md +6 -0
- package/docs/DESIGN_PHILOSOPHY.md +2 -0
- package/docs/SQLITE_STATE.md +36 -17
- package/docs/WORKFLOW_COMPOSITION.md +15 -0
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
- package/docs/plans/2026-08-25-autoplan-user-intent-capture-plan.md +107 -0
- package/docs/plans/2026-08-25-live-workflow-settings-plan.md +532 -0
- package/docs/plans/2026-08-25-workflow-run-storage-plan.md +67 -0
- package/docs/session-event-journal.md +2 -3
- package/docs/tui-viewer.md +7 -6
- package/docs/workflows.md +58 -10
- package/examples/workflows/live-settings.workflow.ts +93 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/builtins/autoimplement.workflow.ts +56 -8
- package/src/builtins/autoplan.workflow.ts +80 -32
- package/src/builtins/catalog.ts +4 -4
- package/src/builtins/plain-summary.workflow.ts +38 -40
- package/src/builtins/sanity-check.workflow.ts +19 -22
- package/src/controllers/index.ts +6 -0
- package/src/controllers/sqlite.ts +132 -72
- package/src/controllers/types.ts +40 -0
- package/src/controllers/workflow-engine-scheduler.ts +103 -1
- package/src/controllers/workflows.ts +68 -0
- package/src/extension/controller-host.ts +6 -1
- package/src/extension/decision-channels.ts +47 -7
- package/src/extension/follow-up-coordinator.ts +151 -0
- package/src/extension/index.ts +540 -89
- package/src/extension/recorder.ts +10 -8
- package/src/extension/session-events.ts +15 -13
- package/src/extension/widget.ts +8 -0
- package/src/state/database.ts +12 -12
- package/src/state/json.ts +6 -1
- package/src/state/mutation.ts +4 -1
- package/src/state/prune.ts +502 -0
- package/src/state/schema.ts +130 -13
- package/src/viewer/cli.ts +52 -5
- package/src/viewer/render.ts +43 -1
- package/src/viewer/session-reducer.ts +2 -29
- package/src/workflows/composition.ts +19 -0
- package/src/workflows/definition.ts +19 -5
- package/src/workflows/engine.ts +365 -36
- package/src/workflows/human-decision.ts +41 -11
- package/src/workflows/index.ts +43 -0
- package/src/workflows/json-patch.ts +375 -0
- package/src/workflows/schema.ts +31 -1
- package/src/workflows/settings.ts +430 -0
- package/src/workflows/store.ts +2268 -121
- package/src/workflows/tool-input.ts +58 -0
- package/src/workflows/types.ts +43 -5
package/src/extension/index.ts
CHANGED
|
@@ -9,9 +9,10 @@ import type {
|
|
|
9
9
|
} from "../controllers/sqlite.js";
|
|
10
10
|
import type { JsonObject } from "../controllers/types.js";
|
|
11
11
|
import type { WorkflowSchedulerResult } from "../controllers/workflows.js";
|
|
12
|
+
import { canonicalJson } from "../state/json.js";
|
|
12
13
|
import { compositionMetadata } from "../workflows/composition.js";
|
|
13
14
|
import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
|
|
14
|
-
import { WorkflowEngine } from "../workflows/engine.js";
|
|
15
|
+
import { WorkflowEngine, workflowIdentityMismatch } from "../workflows/engine.js";
|
|
15
16
|
import {
|
|
16
17
|
BuiltinWorkflowRevisionChangedError,
|
|
17
18
|
ClaimLostError,
|
|
@@ -77,6 +78,7 @@ import {
|
|
|
77
78
|
type DeferredTurnDescriptor,
|
|
78
79
|
} from "./deferred-turn.js";
|
|
79
80
|
import { ConversationStepExecutor } from "./executor.js";
|
|
81
|
+
import { FollowUpCoordinator, findSettledPresentationEntries } from "./follow-up-coordinator.js";
|
|
80
82
|
import {
|
|
81
83
|
HerdrWorkflowViewer,
|
|
82
84
|
parseViewerPlacement,
|
|
@@ -219,7 +221,7 @@ type StartRunOptions = {
|
|
|
219
221
|
};
|
|
220
222
|
|
|
221
223
|
function definitionDigest(snapshot: WorkflowDefinitionSnapshot): string {
|
|
222
|
-
return `sha256:${createHash("sha256").update(
|
|
224
|
+
return `sha256:${createHash("sha256").update(canonicalJson(snapshot)).digest("hex")}`;
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
function launchSourceIdentity(workflow: WorkflowDefinition, root: unknown): unknown {
|
|
@@ -291,6 +293,9 @@ export type ParsedWorkflowArgs =
|
|
|
291
293
|
| { kind: "resume" }
|
|
292
294
|
| { kind: "status"; runId?: string }
|
|
293
295
|
| { kind: "answer"; input: unknown; runId?: string | undefined }
|
|
296
|
+
| { kind: "change-settings"; patch: unknown }
|
|
297
|
+
| { kind: "queue-follow-up"; prompt: string }
|
|
298
|
+
| { kind: "remove-follow-up"; followUpId: string }
|
|
294
299
|
| { kind: "run"; ref: string; input: unknown };
|
|
295
300
|
|
|
296
301
|
/** Parse `/workflow` arguments. Exported for tests. */
|
|
@@ -305,6 +310,28 @@ export function parseWorkflowArgs(args: string): ParsedWorkflowArgs {
|
|
|
305
310
|
if (trimmed === "status") {
|
|
306
311
|
return { kind: "status" };
|
|
307
312
|
}
|
|
313
|
+
if (trimmed.startsWith("change-settings ")) {
|
|
314
|
+
const text = trimmed.slice("change-settings".length).trim();
|
|
315
|
+
try {
|
|
316
|
+
return { kind: "change-settings", patch: JSON.parse(text) as unknown };
|
|
317
|
+
} catch (error) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`change-settings requires a JSON Patch array: ${error instanceof Error ? error.message : String(error)}`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (trimmed.startsWith("queue-follow-up ")) {
|
|
324
|
+
const prompt = trimmed.slice("queue-follow-up".length).trim();
|
|
325
|
+
if (prompt.length === 0) throw new Error("queue-follow-up requires a prompt");
|
|
326
|
+
return { kind: "queue-follow-up", prompt };
|
|
327
|
+
}
|
|
328
|
+
if (trimmed.startsWith("remove-follow-up ")) {
|
|
329
|
+
const followUpId = trimmed.slice("remove-follow-up".length).trim();
|
|
330
|
+
if (!/^follow-up-[a-f0-9]{40}$/u.test(followUpId)) {
|
|
331
|
+
throw new Error("remove-follow-up requires one valid follow-up id");
|
|
332
|
+
}
|
|
333
|
+
return { kind: "remove-follow-up", followUpId };
|
|
334
|
+
}
|
|
308
335
|
if (trimmed.startsWith("status ")) {
|
|
309
336
|
const runId = trimmed.slice("status".length).trim();
|
|
310
337
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(runId)) {
|
|
@@ -364,7 +391,13 @@ export function parseWorkflowArgs(args: string): ParsedWorkflowArgs {
|
|
|
364
391
|
return { kind: "run", ref, input: rest.length > 0 ? { task: rest } : {} };
|
|
365
392
|
}
|
|
366
393
|
|
|
367
|
-
function workflowStateSummary(
|
|
394
|
+
function workflowStateSummary(
|
|
395
|
+
state: WorkflowRunState,
|
|
396
|
+
controls?: Pick<
|
|
397
|
+
NonNullable<ReturnType<typeof readWorkflowRun>>,
|
|
398
|
+
"settingsScopes" | "followUpQueue"
|
|
399
|
+
>,
|
|
400
|
+
): JsonObject {
|
|
368
401
|
const error =
|
|
369
402
|
state.error === undefined
|
|
370
403
|
? undefined
|
|
@@ -385,6 +418,33 @@ function workflowStateSummary(state: WorkflowRunState): JsonObject {
|
|
|
385
418
|
...(state.waitingOn !== undefined ? { waitingOn: state.waitingOn } : {}),
|
|
386
419
|
...(state.startedAt !== undefined ? { startedAt: state.startedAt } : {}),
|
|
387
420
|
...(state.finishedAt !== undefined ? { finishedAt: state.finishedAt } : {}),
|
|
421
|
+
...(controls !== undefined
|
|
422
|
+
? {
|
|
423
|
+
settingsScopes: (controls.settingsScopes ?? []).map((scope) => ({
|
|
424
|
+
scopeId: scope.scopeId,
|
|
425
|
+
mountPath: scope.mountPath,
|
|
426
|
+
invocation: scope.invocation,
|
|
427
|
+
changeNumber: scope.changeNumber,
|
|
428
|
+
settingsHash: scope.settingsHash,
|
|
429
|
+
activeRunId: scope.activeRunId,
|
|
430
|
+
})),
|
|
431
|
+
followUps:
|
|
432
|
+
controls.followUpQueue === null || controls.followUpQueue === undefined
|
|
433
|
+
? null
|
|
434
|
+
: {
|
|
435
|
+
presentationState: controls.followUpQueue.presentationState,
|
|
436
|
+
items: controls.followUpQueue.followUps.map((followUp) => ({
|
|
437
|
+
followUpId: followUp.followUpId,
|
|
438
|
+
order: followUp.order,
|
|
439
|
+
state: followUp.state,
|
|
440
|
+
source: followUp.source,
|
|
441
|
+
...(followUp.sessionEntryId !== undefined
|
|
442
|
+
? { sessionEntryId: followUp.sessionEntryId }
|
|
443
|
+
: {}),
|
|
444
|
+
})),
|
|
445
|
+
},
|
|
446
|
+
}
|
|
447
|
+
: {}),
|
|
388
448
|
...(error !== undefined ? { error } : {}),
|
|
389
449
|
};
|
|
390
450
|
}
|
|
@@ -544,6 +604,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
544
604
|
},
|
|
545
605
|
idle,
|
|
546
606
|
);
|
|
607
|
+
void followUpCoordinator?.synchronize(ctx, activeRun !== null).catch(() => undefined);
|
|
547
608
|
} catch {
|
|
548
609
|
// Delivery retries on the next poll. It never affects workflow execution.
|
|
549
610
|
}
|
|
@@ -575,7 +636,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
575
636
|
let sessionClosed = false;
|
|
576
637
|
let runGeneration = 0;
|
|
577
638
|
let presentationAbort: AbortController | null = null;
|
|
578
|
-
let presentationPending: number | null = null;
|
|
639
|
+
let presentationPending: { generation: number; runId: string } | null = null;
|
|
640
|
+
let followUpStore: WorkflowRunStore | null = null;
|
|
641
|
+
let followUpCoordinator: FollowUpCoordinator | null = null;
|
|
579
642
|
let controllerHost: PiControllerHost | undefined;
|
|
580
643
|
let controllerContext: ExtensionContext | null = null;
|
|
581
644
|
|
|
@@ -603,6 +666,47 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
603
666
|
leaseMs: TURN_INTENT_DELIVERY_LEASE_MS,
|
|
604
667
|
});
|
|
605
668
|
|
|
669
|
+
const ensureFollowUpDelivery = (): void => {
|
|
670
|
+
if (followUpStore !== null && followUpCoordinator !== null) return;
|
|
671
|
+
followUpStore?.close();
|
|
672
|
+
followUpStore = new WorkflowRunStore();
|
|
673
|
+
followUpCoordinator = new FollowUpCoordinator(followUpStore, `follow-up-${runnerId}`, (text) =>
|
|
674
|
+
pi.sendUserMessage(text),
|
|
675
|
+
);
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const settlePresentationFromBranch = (
|
|
679
|
+
ctx: ExtensionContext,
|
|
680
|
+
runId: string,
|
|
681
|
+
unavailableReason?: string,
|
|
682
|
+
): boolean => {
|
|
683
|
+
if (
|
|
684
|
+
followUpStore === null ||
|
|
685
|
+
followUpStore.readFollowUpQueue(runId)?.presentationState !== "pending"
|
|
686
|
+
) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const settled = findSettledPresentationEntries(ctx.sessionManager.getBranch(), runId);
|
|
690
|
+
if (settled !== undefined) {
|
|
691
|
+
followUpStore.settleFollowUpPresentation({
|
|
692
|
+
runId,
|
|
693
|
+
state: "settled",
|
|
694
|
+
presentationEntryId: settled.presentationEntryId,
|
|
695
|
+
assistantEntryId: settled.assistantEntryId,
|
|
696
|
+
});
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
if (unavailableReason !== undefined) {
|
|
700
|
+
followUpStore.settleFollowUpPresentation({
|
|
701
|
+
runId,
|
|
702
|
+
state: "unavailable",
|
|
703
|
+
reason: unavailableReason,
|
|
704
|
+
});
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
return false;
|
|
708
|
+
};
|
|
709
|
+
|
|
606
710
|
// UI updates are best-effort: a captured ctx becomes stale after session
|
|
607
711
|
// replacement or shutdown, and pi throws on any access (even `ctx.hasUI`).
|
|
608
712
|
// A workflow finishing right as the session goes away must not crash pi.
|
|
@@ -986,6 +1090,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
986
1090
|
return;
|
|
987
1091
|
}
|
|
988
1092
|
if (instructions === undefined || instructions.trim().length === 0) {
|
|
1093
|
+
settlePresentationFromBranch(ctx, run.runId, "No result presentation was available");
|
|
989
1094
|
if (run.abortProvenance !== undefined) {
|
|
990
1095
|
makeRunTurnIntentEligible(ctx, run, state.status, "No result presentation was available");
|
|
991
1096
|
}
|
|
@@ -997,7 +1102,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
997
1102
|
targetSessionId: originSessionId(ctx, run.runId),
|
|
998
1103
|
resolution: "presentation",
|
|
999
1104
|
send: (turnIntentId) => {
|
|
1000
|
-
presentationPending = run.generation;
|
|
1105
|
+
presentationPending = { generation: run.generation, runId: run.runId };
|
|
1001
1106
|
pi.sendMessage(
|
|
1002
1107
|
{
|
|
1003
1108
|
customType: PRESENTATION_MESSAGE_TYPE,
|
|
@@ -1005,6 +1110,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1005
1110
|
display: false,
|
|
1006
1111
|
details: {
|
|
1007
1112
|
schema: PRESENTATION_MESSAGE_SCHEMA,
|
|
1113
|
+
runId: run.runId,
|
|
1008
1114
|
...(turnIntentId === undefined ? {} : { turnIntentId }),
|
|
1009
1115
|
},
|
|
1010
1116
|
},
|
|
@@ -1018,7 +1124,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1018
1124
|
ctx.isIdle() && systemTurnAbort === null,
|
|
1019
1125
|
);
|
|
1020
1126
|
} catch (error) {
|
|
1021
|
-
if (presentationPending === run.generation) {
|
|
1127
|
+
if (presentationPending?.generation === run.generation) {
|
|
1022
1128
|
presentationPending = null;
|
|
1023
1129
|
}
|
|
1024
1130
|
if (
|
|
@@ -1033,6 +1139,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1033
1139
|
? `timed out after ${PRESENTATION_TIMEOUT_MS}ms`
|
|
1034
1140
|
: errorMessage(error);
|
|
1035
1141
|
notify(ctx, `Could not present workflow result: ${message}`, "warning");
|
|
1142
|
+
settlePresentationFromBranch(ctx, run.runId, message);
|
|
1036
1143
|
if (run.abortProvenance !== undefined) {
|
|
1037
1144
|
makeRunTurnIntentEligible(ctx, run, state.status, message);
|
|
1038
1145
|
}
|
|
@@ -1202,6 +1309,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1202
1309
|
if (options.signal?.aborted) {
|
|
1203
1310
|
throw options.signal.reason ?? new Error("Workflow startup aborted");
|
|
1204
1311
|
}
|
|
1312
|
+
ensureFollowUpDelivery();
|
|
1205
1313
|
if (activeRun) {
|
|
1206
1314
|
if (!options.quiet) {
|
|
1207
1315
|
notify(
|
|
@@ -1685,7 +1793,65 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1685
1793
|
if (controllerHost !== undefined) {
|
|
1686
1794
|
return controllerHost;
|
|
1687
1795
|
}
|
|
1688
|
-
controllerHost = await PiControllerHost.create({
|
|
1796
|
+
controllerHost = await PiControllerHost.create({
|
|
1797
|
+
cwd: ctx.cwd,
|
|
1798
|
+
startChild,
|
|
1799
|
+
workflowControls: {
|
|
1800
|
+
changeSettings: async (request) => {
|
|
1801
|
+
const result = await changeWorkflowSettings(ctx, {
|
|
1802
|
+
runId: request.runId,
|
|
1803
|
+
...(request.scopeId !== undefined ? { scopeId: request.scopeId } : {}),
|
|
1804
|
+
...(request.expectedChangeNumber !== undefined
|
|
1805
|
+
? { expectedChangeNumber: request.expectedChangeNumber }
|
|
1806
|
+
: {}),
|
|
1807
|
+
patch: request.patch,
|
|
1808
|
+
requestId: request.actorRequestKey,
|
|
1809
|
+
actor: "controller",
|
|
1810
|
+
actorId: request.controllerResourceUid,
|
|
1811
|
+
source: "controller-request",
|
|
1812
|
+
});
|
|
1813
|
+
return {
|
|
1814
|
+
runId: request.runId,
|
|
1815
|
+
scopeId: String(result.details.scopeId),
|
|
1816
|
+
changeNumber: Number(result.details.changeNumber),
|
|
1817
|
+
adopted: result.details.adopted === true,
|
|
1818
|
+
};
|
|
1819
|
+
},
|
|
1820
|
+
queueFollowUp: async (request) => {
|
|
1821
|
+
const result = await queueWorkflowFollowUp(ctx, {
|
|
1822
|
+
runId: request.runId,
|
|
1823
|
+
prompt: request.prompt,
|
|
1824
|
+
requestId: request.actorRequestKey,
|
|
1825
|
+
actor: "controller",
|
|
1826
|
+
actorId: request.controllerResourceUid,
|
|
1827
|
+
source: "controller-request",
|
|
1828
|
+
});
|
|
1829
|
+
return {
|
|
1830
|
+
runId: request.runId,
|
|
1831
|
+
followUpId: String(result.details.followUpId),
|
|
1832
|
+
order: Number(result.details.order),
|
|
1833
|
+
state: "queued",
|
|
1834
|
+
adopted: result.details.adopted === true,
|
|
1835
|
+
};
|
|
1836
|
+
},
|
|
1837
|
+
removeFollowUp: async (request) => {
|
|
1838
|
+
const result = await removeWorkflowFollowUp(ctx, {
|
|
1839
|
+
runId: request.runId,
|
|
1840
|
+
followUpId: request.followUpId,
|
|
1841
|
+
actor: "controller",
|
|
1842
|
+
actorId: request.controllerResourceUid,
|
|
1843
|
+
source: "controller-request",
|
|
1844
|
+
});
|
|
1845
|
+
return {
|
|
1846
|
+
runId: request.runId,
|
|
1847
|
+
followUpId: String(result.details.followUpId),
|
|
1848
|
+
order: Number(result.details.order),
|
|
1849
|
+
state: String(result.details.state),
|
|
1850
|
+
adopted: false,
|
|
1851
|
+
};
|
|
1852
|
+
},
|
|
1853
|
+
},
|
|
1854
|
+
});
|
|
1689
1855
|
controllerHost?.start();
|
|
1690
1856
|
updateControllerStatus(ctx);
|
|
1691
1857
|
return controllerHost;
|
|
@@ -1997,7 +2163,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1997
2163
|
const { state } = bundle;
|
|
1998
2164
|
return {
|
|
1999
2165
|
message: `Workflow ${state.workflowName} is ${state.status} (run ${state.runId}).`,
|
|
2000
|
-
details: workflowStateSummary(state),
|
|
2166
|
+
details: workflowStateSummary(state, bundle),
|
|
2001
2167
|
};
|
|
2002
2168
|
}
|
|
2003
2169
|
const state = activeRun?.lastState ?? widgetSource?.state;
|
|
@@ -2016,10 +2182,176 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2016
2182
|
}
|
|
2017
2183
|
return {
|
|
2018
2184
|
message: `Workflow ${state.workflowName} is ${state.status} (run ${state.runId}).`,
|
|
2019
|
-
details: workflowStateSummary(state),
|
|
2185
|
+
details: workflowStateSummary(state, readWorkflowRun(state.runId) ?? undefined),
|
|
2020
2186
|
};
|
|
2021
2187
|
};
|
|
2022
2188
|
|
|
2189
|
+
const resolveMutableWorkflow = async (
|
|
2190
|
+
ctx: ExtensionContext,
|
|
2191
|
+
requestedRunId?: string,
|
|
2192
|
+
): Promise<{
|
|
2193
|
+
runId: string;
|
|
2194
|
+
workflow: WorkflowDefinition;
|
|
2195
|
+
store: WorkflowRunStore;
|
|
2196
|
+
}> => {
|
|
2197
|
+
const runId = requestedRunId ?? activeRun?.runId ?? lastWaitingRunId ?? undefined;
|
|
2198
|
+
if (runId === undefined) {
|
|
2199
|
+
throw new Error("No running, parked, or waiting workflow is available for changes.");
|
|
2200
|
+
}
|
|
2201
|
+
const loaded = readWorkflowRun(runId);
|
|
2202
|
+
if (loaded === null) throw new Error(`Workflow run not found: ${runId}`);
|
|
2203
|
+
if (originSessionId(ctx, runId) !== ctx.sessionManager.getSessionId()) {
|
|
2204
|
+
throw new Error("Workflow changes must come from the Pi session that owns the run.");
|
|
2205
|
+
}
|
|
2206
|
+
const source = loaded.state.workflowSource;
|
|
2207
|
+
if (source === undefined) throw new Error(`Workflow run ${runId} has no saved source`);
|
|
2208
|
+
const ref = source.kind === "builtin" ? `builtin:${source.id}` : source.path;
|
|
2209
|
+
const resolved = await resolveWorkflowRef(ref, { cwd: ctx.cwd }, builtinWorkflowCatalog);
|
|
2210
|
+
if (workflowIdentityMismatch(loaded.state, resolved.definition, resolved.source)) {
|
|
2211
|
+
throw new Error(`Workflow source changed since run ${runId} started`);
|
|
2212
|
+
}
|
|
2213
|
+
return { runId, workflow: resolved.definition, store: new WorkflowRunStore() };
|
|
2214
|
+
};
|
|
2215
|
+
|
|
2216
|
+
const changeWorkflowSettings = async (
|
|
2217
|
+
ctx: ExtensionContext,
|
|
2218
|
+
options: {
|
|
2219
|
+
runId?: string;
|
|
2220
|
+
scopeId?: string;
|
|
2221
|
+
expectedChangeNumber?: number;
|
|
2222
|
+
patch: unknown;
|
|
2223
|
+
requestId: string;
|
|
2224
|
+
actor: "session" | "human" | "controller";
|
|
2225
|
+
actorId?: string;
|
|
2226
|
+
source: string;
|
|
2227
|
+
},
|
|
2228
|
+
): Promise<WorkflowControlResult> => {
|
|
2229
|
+
const target = await resolveMutableWorkflow(ctx, options.runId);
|
|
2230
|
+
try {
|
|
2231
|
+
const scopes = target.store.listSettingsScopes(target.runId);
|
|
2232
|
+
const scopeId =
|
|
2233
|
+
options.scopeId ??
|
|
2234
|
+
(activeRun?.runId === target.runId ? activeRun.engine.activeSettingsScopeId : undefined) ??
|
|
2235
|
+
(scopes.length === 1 ? scopes[0]?.scopeId : undefined);
|
|
2236
|
+
if (scopeId === undefined) {
|
|
2237
|
+
throw new Error("Specify scopeId because this workflow has more than one settings scope.");
|
|
2238
|
+
}
|
|
2239
|
+
const scope = target.store.getSettingsScope(scopeId);
|
|
2240
|
+
if (scope === undefined || scope.activeRunId !== target.runId) {
|
|
2241
|
+
throw new Error(`Workflow settings scope not found for run ${target.runId}: ${scopeId}`);
|
|
2242
|
+
}
|
|
2243
|
+
const definition =
|
|
2244
|
+
scope.mountPath === ""
|
|
2245
|
+
? target.workflow.settings
|
|
2246
|
+
: compositionMetadata(target.workflow)?.scopes[scope.mountPath]?.settings;
|
|
2247
|
+
if (definition === undefined) {
|
|
2248
|
+
throw new Error(`Workflow scope ${scopeId} does not declare editable settings.`);
|
|
2249
|
+
}
|
|
2250
|
+
const result = await target.store.changeSettings(definition, {
|
|
2251
|
+
runId: target.runId,
|
|
2252
|
+
scopeId,
|
|
2253
|
+
requestId: options.requestId,
|
|
2254
|
+
...(options.expectedChangeNumber !== undefined
|
|
2255
|
+
? { expectedChangeNumber: options.expectedChangeNumber }
|
|
2256
|
+
: {}),
|
|
2257
|
+
actor: {
|
|
2258
|
+
type: options.actor,
|
|
2259
|
+
id: options.actorId ?? ctx.sessionManager.getSessionId(),
|
|
2260
|
+
},
|
|
2261
|
+
source: options.source,
|
|
2262
|
+
patch: options.patch,
|
|
2263
|
+
});
|
|
2264
|
+
renderWidget(ctx);
|
|
2265
|
+
return {
|
|
2266
|
+
message: `Workflow settings changed to number ${result.scope.changeNumber}.`,
|
|
2267
|
+
details: {
|
|
2268
|
+
action: "change-settings",
|
|
2269
|
+
runId: target.runId,
|
|
2270
|
+
scopeId,
|
|
2271
|
+
changeNumber: result.scope.changeNumber,
|
|
2272
|
+
adopted: result.adopted,
|
|
2273
|
+
},
|
|
2274
|
+
};
|
|
2275
|
+
} finally {
|
|
2276
|
+
target.store.close();
|
|
2277
|
+
}
|
|
2278
|
+
};
|
|
2279
|
+
|
|
2280
|
+
const queueWorkflowFollowUp = async (
|
|
2281
|
+
ctx: ExtensionContext,
|
|
2282
|
+
options: {
|
|
2283
|
+
runId?: string;
|
|
2284
|
+
prompt: string;
|
|
2285
|
+
requestId: string;
|
|
2286
|
+
actor: "session" | "human" | "controller";
|
|
2287
|
+
actorId?: string;
|
|
2288
|
+
source: string;
|
|
2289
|
+
},
|
|
2290
|
+
): Promise<WorkflowControlResult> => {
|
|
2291
|
+
const target = await resolveMutableWorkflow(ctx, options.runId);
|
|
2292
|
+
try {
|
|
2293
|
+
const result = target.store.queueFollowUp({
|
|
2294
|
+
runId: target.runId,
|
|
2295
|
+
requestId: options.requestId,
|
|
2296
|
+
targetSessionId: ctx.sessionManager.getSessionId(),
|
|
2297
|
+
actor: {
|
|
2298
|
+
type: options.actor,
|
|
2299
|
+
id: options.actorId ?? ctx.sessionManager.getSessionId(),
|
|
2300
|
+
},
|
|
2301
|
+
source: options.source,
|
|
2302
|
+
prompt: options.prompt,
|
|
2303
|
+
});
|
|
2304
|
+
return {
|
|
2305
|
+
message: `Queued workflow follow-up ${result.followUp.order}.`,
|
|
2306
|
+
details: {
|
|
2307
|
+
action: "queue-follow-up",
|
|
2308
|
+
runId: target.runId,
|
|
2309
|
+
followUpId: result.followUp.followUpId,
|
|
2310
|
+
order: result.followUp.order,
|
|
2311
|
+
adopted: result.adopted,
|
|
2312
|
+
},
|
|
2313
|
+
};
|
|
2314
|
+
} finally {
|
|
2315
|
+
target.store.close();
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
|
|
2319
|
+
const removeWorkflowFollowUp = async (
|
|
2320
|
+
ctx: ExtensionContext,
|
|
2321
|
+
options: {
|
|
2322
|
+
runId?: string;
|
|
2323
|
+
followUpId: string;
|
|
2324
|
+
actor: "session" | "human" | "controller";
|
|
2325
|
+
actorId?: string;
|
|
2326
|
+
source: string;
|
|
2327
|
+
},
|
|
2328
|
+
): Promise<WorkflowControlResult> => {
|
|
2329
|
+
const target = await resolveMutableWorkflow(ctx, options.runId);
|
|
2330
|
+
try {
|
|
2331
|
+
const followUp = target.store.removeFollowUp({
|
|
2332
|
+
runId: target.runId,
|
|
2333
|
+
followUpId: options.followUpId,
|
|
2334
|
+
actor: {
|
|
2335
|
+
type: options.actor,
|
|
2336
|
+
id: options.actorId ?? ctx.sessionManager.getSessionId(),
|
|
2337
|
+
},
|
|
2338
|
+
source: options.source,
|
|
2339
|
+
});
|
|
2340
|
+
return {
|
|
2341
|
+
message: `Removed workflow follow-up ${followUp.order}.`,
|
|
2342
|
+
details: {
|
|
2343
|
+
action: "remove-follow-up",
|
|
2344
|
+
runId: target.runId,
|
|
2345
|
+
followUpId: followUp.followUpId,
|
|
2346
|
+
order: followUp.order,
|
|
2347
|
+
state: followUp.state,
|
|
2348
|
+
},
|
|
2349
|
+
};
|
|
2350
|
+
} finally {
|
|
2351
|
+
target.store.close();
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
|
|
2023
2355
|
const resolveWaitingWorkflow = async (
|
|
2024
2356
|
ctx: ExtensionContext,
|
|
2025
2357
|
requestedRunId?: string,
|
|
@@ -2622,7 +2954,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2622
2954
|
|
|
2623
2955
|
pi.registerCommand("workflow", {
|
|
2624
2956
|
description:
|
|
2625
|
-
"Run or manage a workflow: /workflow <name-or-path> [task | --input-json {…}]; also: status, pause, resume, cancel, answer",
|
|
2957
|
+
"Run or manage a workflow: /workflow <name-or-path> [task | --input-json {…}]; also: status, pause, resume, cancel, answer, change-settings, queue-follow-up, remove-follow-up",
|
|
2626
2958
|
getArgumentCompletions: async (prefix: string) => {
|
|
2627
2959
|
const discovered = await discoverWorkflows({ cwd: process.cwd() }, builtinWorkflowCatalog);
|
|
2628
2960
|
const items = [
|
|
@@ -2632,6 +2964,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2632
2964
|
{ value: "resume", label: "resume" },
|
|
2633
2965
|
{ value: "cancel", label: "cancel" },
|
|
2634
2966
|
{ value: "answer", label: "answer" },
|
|
2967
|
+
{ value: "change-settings", label: "change-settings" },
|
|
2968
|
+
{ value: "queue-follow-up", label: "queue-follow-up" },
|
|
2969
|
+
{ value: "remove-follow-up", label: "remove-follow-up" },
|
|
2635
2970
|
].filter((item) => item.value.startsWith(prefix));
|
|
2636
2971
|
return items.length > 0 ? items : null;
|
|
2637
2972
|
},
|
|
@@ -2671,6 +3006,47 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2671
3006
|
}
|
|
2672
3007
|
return;
|
|
2673
3008
|
}
|
|
3009
|
+
if (parsed.kind === "change-settings") {
|
|
3010
|
+
try {
|
|
3011
|
+
const result = await changeWorkflowSettings(ctx, {
|
|
3012
|
+
patch: parsed.patch,
|
|
3013
|
+
requestId: `interactive-${randomUUID()}`,
|
|
3014
|
+
actor: "human",
|
|
3015
|
+
source: "interactive-command",
|
|
3016
|
+
});
|
|
3017
|
+
notify(ctx, result.message, result.level);
|
|
3018
|
+
} catch (error) {
|
|
3019
|
+
notify(ctx, errorMessage(error), "error");
|
|
3020
|
+
}
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
if (parsed.kind === "queue-follow-up") {
|
|
3024
|
+
try {
|
|
3025
|
+
const result = await queueWorkflowFollowUp(ctx, {
|
|
3026
|
+
prompt: parsed.prompt,
|
|
3027
|
+
requestId: `interactive-${randomUUID()}`,
|
|
3028
|
+
actor: "human",
|
|
3029
|
+
source: "interactive-command",
|
|
3030
|
+
});
|
|
3031
|
+
notify(ctx, result.message, result.level);
|
|
3032
|
+
} catch (error) {
|
|
3033
|
+
notify(ctx, errorMessage(error), "error");
|
|
3034
|
+
}
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
if (parsed.kind === "remove-follow-up") {
|
|
3038
|
+
try {
|
|
3039
|
+
const result = await removeWorkflowFollowUp(ctx, {
|
|
3040
|
+
followUpId: parsed.followUpId,
|
|
3041
|
+
actor: "human",
|
|
3042
|
+
source: "interactive-command",
|
|
3043
|
+
});
|
|
3044
|
+
notify(ctx, result.message, result.level);
|
|
3045
|
+
} catch (error) {
|
|
3046
|
+
notify(ctx, errorMessage(error), "error");
|
|
3047
|
+
}
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
2674
3050
|
if (parsed.kind === "answer") {
|
|
2675
3051
|
try {
|
|
2676
3052
|
const result = await answerWorkflowControl(ctx, parsed.input, parsed.runId);
|
|
@@ -2829,11 +3205,26 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2829
3205
|
},
|
|
2830
3206
|
});
|
|
2831
3207
|
|
|
3208
|
+
let workflowToolTail = Promise.resolve();
|
|
3209
|
+
const withWorkflowToolOrder = async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
3210
|
+
const prior = workflowToolTail;
|
|
3211
|
+
let release: (() => void) | undefined;
|
|
3212
|
+
workflowToolTail = new Promise<void>((resolve) => {
|
|
3213
|
+
release = resolve;
|
|
3214
|
+
});
|
|
3215
|
+
await prior;
|
|
3216
|
+
try {
|
|
3217
|
+
return await operation();
|
|
3218
|
+
} finally {
|
|
3219
|
+
release?.();
|
|
3220
|
+
}
|
|
3221
|
+
};
|
|
3222
|
+
|
|
2832
3223
|
pi.registerTool({
|
|
2833
3224
|
name: "workflow",
|
|
2834
3225
|
label: "Workflow",
|
|
2835
3226
|
description: [
|
|
2836
|
-
"List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
|
|
3227
|
+
"List, start, inspect, change settings, queue or remove follow-ups, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
|
|
2837
3228
|
"When the user asks to monitor, watch, poll, or check something repeatedly, start the built-in monitor workflow with input keys task, stopWhen, everyMinutes, and optional maxChecks.",
|
|
2838
3229
|
"Put the exact goal, authority, limits, and recovery rules in task; Monitor observes first, performs only safe authorized actions, verifies them immediately, and waits only while target work is moving or an external event is pending.",
|
|
2839
3230
|
"Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
|
|
@@ -2841,89 +3232,121 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2841
3232
|
].join(" "),
|
|
2842
3233
|
parameters: WorkflowToolParameters,
|
|
2843
3234
|
async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
"This checkpoint requires a verified human answer from Pi UI or a configured decision channel.",
|
|
2871
|
-
);
|
|
2872
|
-
}
|
|
2873
|
-
control = await queueToolLaunch(ctx, waiting.workflowRef, params.input, {
|
|
2874
|
-
parentRunId: waiting.parentRunId,
|
|
2875
|
-
});
|
|
2876
|
-
break;
|
|
2877
|
-
}
|
|
2878
|
-
case "update": {
|
|
2879
|
-
if (!activeRun) throw new Error("No workflow step is active.");
|
|
2880
|
-
const receipt = await activeRun.engine.publishUpdate(
|
|
2881
|
-
params.step,
|
|
2882
|
-
params.attempt,
|
|
2883
|
-
params.update,
|
|
2884
|
-
toolCallId,
|
|
2885
|
-
);
|
|
2886
|
-
return {
|
|
2887
|
-
content: [
|
|
2888
|
-
{ type: "text", text: "Workflow update published; the step remains active." },
|
|
2889
|
-
],
|
|
2890
|
-
details: { action: "update", ...receipt },
|
|
2891
|
-
};
|
|
2892
|
-
}
|
|
2893
|
-
case "submit": {
|
|
2894
|
-
if (!activeRun) {
|
|
2895
|
-
if (
|
|
2896
|
-
lastExpiredAttempt?.contract.attemptId === params.attempt &&
|
|
2897
|
-
lastExpiredAttempt.contract.nodeId === params.step
|
|
2898
|
-
) {
|
|
3235
|
+
return await withWorkflowToolOrder(async () => {
|
|
3236
|
+
const params = parseWorkflowToolInput(rawParams);
|
|
3237
|
+
let control: WorkflowControlResult;
|
|
3238
|
+
switch (params.action) {
|
|
3239
|
+
case "list":
|
|
3240
|
+
control = await listWorkflowControl(ctx, params.offset);
|
|
3241
|
+
break;
|
|
3242
|
+
case "start":
|
|
3243
|
+
control = await queueToolLaunch(ctx, params.workflow, params.input ?? {});
|
|
3244
|
+
break;
|
|
3245
|
+
case "status":
|
|
3246
|
+
control = await statusWorkflowControl(ctx, params.runId);
|
|
3247
|
+
break;
|
|
3248
|
+
case "pause":
|
|
3249
|
+
control = pauseWorkflowControl(ctx);
|
|
3250
|
+
break;
|
|
3251
|
+
case "resume":
|
|
3252
|
+
control = resumeWorkflowControl(ctx);
|
|
3253
|
+
break;
|
|
3254
|
+
case "cancel":
|
|
3255
|
+
control = await cancelWorkflowControl(ctx, "agent", params.runId);
|
|
3256
|
+
break;
|
|
3257
|
+
case "answer": {
|
|
3258
|
+
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
3259
|
+
const parent = readWorkflowRun(waiting.parentRunId);
|
|
3260
|
+
if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
|
|
2899
3261
|
throw new Error(
|
|
2900
|
-
|
|
3262
|
+
"This checkpoint requires a verified human answer from Pi UI or a configured decision channel.",
|
|
2901
3263
|
);
|
|
2902
3264
|
}
|
|
2903
|
-
|
|
3265
|
+
control = await queueToolLaunch(ctx, waiting.workflowRef, params.input, {
|
|
3266
|
+
parentRunId: waiting.parentRunId,
|
|
3267
|
+
});
|
|
3268
|
+
break;
|
|
2904
3269
|
}
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
3270
|
+
case "change-settings":
|
|
3271
|
+
control = await changeWorkflowSettings(ctx, {
|
|
3272
|
+
...(params.runId !== undefined ? { runId: params.runId } : {}),
|
|
3273
|
+
...(params.scopeId !== undefined ? { scopeId: params.scopeId } : {}),
|
|
3274
|
+
...(params.expectedChangeNumber !== undefined
|
|
3275
|
+
? { expectedChangeNumber: params.expectedChangeNumber }
|
|
3276
|
+
: {}),
|
|
3277
|
+
patch: params.patch,
|
|
3278
|
+
requestId: `tool-${toolCallId}`,
|
|
3279
|
+
actor: "session",
|
|
3280
|
+
source: "workflow-tool",
|
|
3281
|
+
});
|
|
3282
|
+
break;
|
|
3283
|
+
case "queue-follow-up":
|
|
3284
|
+
control = await queueWorkflowFollowUp(ctx, {
|
|
3285
|
+
...(params.runId !== undefined ? { runId: params.runId } : {}),
|
|
3286
|
+
prompt: params.prompt,
|
|
3287
|
+
requestId: `tool-${toolCallId}`,
|
|
3288
|
+
actor: "session",
|
|
3289
|
+
source: "workflow-tool",
|
|
3290
|
+
});
|
|
3291
|
+
break;
|
|
3292
|
+
case "remove-follow-up":
|
|
3293
|
+
control = await removeWorkflowFollowUp(ctx, {
|
|
3294
|
+
...(params.runId !== undefined ? { runId: params.runId } : {}),
|
|
3295
|
+
followUpId: params.followUpId,
|
|
3296
|
+
actor: "session",
|
|
3297
|
+
source: "workflow-tool",
|
|
3298
|
+
});
|
|
3299
|
+
break;
|
|
3300
|
+
case "update": {
|
|
3301
|
+
if (!activeRun) throw new Error("No workflow step is active.");
|
|
3302
|
+
const receipt = await activeRun.engine.publishUpdate(
|
|
3303
|
+
params.step,
|
|
3304
|
+
params.attempt,
|
|
3305
|
+
params.update,
|
|
3306
|
+
toolCallId,
|
|
3307
|
+
);
|
|
3308
|
+
return {
|
|
3309
|
+
content: [
|
|
3310
|
+
{ type: "text", text: "Workflow update published; the step remains active." },
|
|
3311
|
+
],
|
|
3312
|
+
details: { action: "update", ...receipt },
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
3315
|
+
case "submit": {
|
|
3316
|
+
if (!activeRun) {
|
|
3317
|
+
if (
|
|
3318
|
+
lastExpiredAttempt?.contract.attemptId === params.attempt &&
|
|
3319
|
+
lastExpiredAttempt.contract.nodeId === params.step
|
|
3320
|
+
) {
|
|
3321
|
+
throw new Error(
|
|
3322
|
+
`Workflow step ${JSON.stringify(params.step)} attempt ${JSON.stringify(params.attempt)} ${lastExpiredAttempt.reason}; its output is no longer accepted.`,
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3325
|
+
throw new Error("No workflow step is waiting for output.");
|
|
3326
|
+
}
|
|
3327
|
+
// Flush the conversation into SQLite before accepting, so the
|
|
3328
|
+
// attempt range includes the assistant message carrying this call.
|
|
3329
|
+
await activeRun.recorder?.record(ctx).catch(() => undefined);
|
|
3330
|
+
await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
|
|
3331
|
+
const result = await activeRun.executor.submit(
|
|
3332
|
+
params.step,
|
|
3333
|
+
params.attempt,
|
|
3334
|
+
params.output,
|
|
3335
|
+
);
|
|
3336
|
+
if (!result.accepted) {
|
|
3337
|
+
throw new Error(result.message);
|
|
3338
|
+
}
|
|
3339
|
+
return {
|
|
3340
|
+
content: [{ type: "text", text: result.message }],
|
|
3341
|
+
details: { action: "submit", step: params.step, accepted: true },
|
|
3342
|
+
};
|
|
2916
3343
|
}
|
|
2917
|
-
return {
|
|
2918
|
-
content: [{ type: "text", text: result.message }],
|
|
2919
|
-
details: { action: "submit", step: params.step, accepted: true },
|
|
2920
|
-
};
|
|
2921
3344
|
}
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
};
|
|
3345
|
+
return {
|
|
3346
|
+
content: [{ type: "text", text: control.message }],
|
|
3347
|
+
details: control.details,
|
|
3348
|
+
};
|
|
3349
|
+
});
|
|
2927
3350
|
},
|
|
2928
3351
|
});
|
|
2929
3352
|
|
|
@@ -2949,6 +3372,22 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2949
3372
|
controllerContext = ctx;
|
|
2950
3373
|
if (herdrEnabled) void refreshHerdrCapability(ctx);
|
|
2951
3374
|
ensureRunQueueStore(ctx.cwd);
|
|
3375
|
+
ensureFollowUpDelivery();
|
|
3376
|
+
const sessionFollowUpStore = followUpStore;
|
|
3377
|
+
const sessionFollowUpCoordinator = followUpCoordinator;
|
|
3378
|
+
if (sessionFollowUpStore === null || sessionFollowUpCoordinator === null) {
|
|
3379
|
+
throw new Error("Workflow follow-up delivery did not initialize");
|
|
3380
|
+
}
|
|
3381
|
+
for (const runId of sessionFollowUpStore.listPendingPresentations(
|
|
3382
|
+
ctx.sessionManager.getSessionId(),
|
|
3383
|
+
)) {
|
|
3384
|
+
settlePresentationFromBranch(
|
|
3385
|
+
ctx,
|
|
3386
|
+
runId,
|
|
3387
|
+
"The final workflow response did not settle before the extension restarted",
|
|
3388
|
+
);
|
|
3389
|
+
}
|
|
3390
|
+
await sessionFollowUpCoordinator.synchronize(ctx, false).catch(() => undefined);
|
|
2952
3391
|
try {
|
|
2953
3392
|
await reloadDecisionChannels(ctx);
|
|
2954
3393
|
} catch {
|
|
@@ -3069,6 +3508,14 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
3069
3508
|
|
|
3070
3509
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
3071
3510
|
systemTurnAbort = null;
|
|
3511
|
+
if (presentationPending !== null) {
|
|
3512
|
+
settlePresentationFromBranch(
|
|
3513
|
+
ctx,
|
|
3514
|
+
presentationPending.runId,
|
|
3515
|
+
"The final workflow response settled without durable session evidence",
|
|
3516
|
+
);
|
|
3517
|
+
presentationPending = null;
|
|
3518
|
+
}
|
|
3072
3519
|
if (activeRun === null) {
|
|
3073
3520
|
try {
|
|
3074
3521
|
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
@@ -3090,7 +3537,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
3090
3537
|
const run = activeRun;
|
|
3091
3538
|
if (!run) {
|
|
3092
3539
|
turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
|
|
3093
|
-
|
|
3540
|
+
await followUpCoordinator?.synchronize(ctx, false).catch(() => undefined);
|
|
3094
3541
|
runSyncPass(ctx);
|
|
3095
3542
|
return;
|
|
3096
3543
|
}
|
|
@@ -3145,6 +3592,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
3145
3592
|
controllerContext = null;
|
|
3146
3593
|
runQueueStore?.close();
|
|
3147
3594
|
runQueueStore = null;
|
|
3595
|
+
followUpCoordinator?.clear();
|
|
3596
|
+
followUpCoordinator = null;
|
|
3597
|
+
followUpStore?.close();
|
|
3598
|
+
followUpStore = null;
|
|
3148
3599
|
presentationPending = null;
|
|
3149
3600
|
clearWidgetTimer();
|
|
3150
3601
|
stopWidgetTicker();
|