@osolmaz/pi-workflows 0.11.2 → 0.12.1
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 -10
- 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 +509 -124
- package/dist/builtins/autoimplement.workflow.js +884 -327
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +5 -3
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +4 -1
- package/dist/builtins/index.js +3 -1
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +2 -4
- package/dist/builtins/monitor.workflow.js +26 -128
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/pi-agent-group.d.ts +72 -0
- package/dist/builtins/pi-agent-group.js +1087 -0
- package/dist/builtins/pi-agent-group.js.map +1 -0
- package/dist/builtins/plan-approval.workflow.d.ts +39 -5
- package/dist/builtins/plan-approval.workflow.js +92 -14
- package/dist/builtins/plan-approval.workflow.js.map +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +301 -0
- package/dist/builtins/plan-change.workflow.js +256 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.js +2 -2
- package/dist/builtins/plan-presentation.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.d.ts +85 -0
- package/dist/builtins/sanity-check.workflow.js +482 -0
- package/dist/builtins/sanity-check.workflow.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +90 -2
- package/dist/controllers/sqlite.js +380 -5
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +2 -2
- package/dist/extension/decision-channels.js +29 -35
- package/dist/extension/decision-channels.js.map +1 -1
- 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 +348 -80
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -2
- package/dist/extension/step-message.d.ts +1 -0
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +23 -3
- package/dist/extension/widget.js.map +1 -1
- package/dist/herdr/setup.js +4 -4
- package/dist/render/graph-render.js +1 -2
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/render.js +7 -6
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/catalog.js +7 -2
- package/dist/workflows/catalog.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/composition.js +8 -0
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +1 -1
- package/dist/workflows/decision-presentation.js +51 -38
- package/dist/workflows/decision-presentation.js.map +1 -1
- package/dist/workflows/engine.d.ts +2 -2
- package/dist/workflows/engine.js +22 -17
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +13 -0
- package/dist/workflows/errors.js +15 -0
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +16 -4
- package/dist/workflows/human-decision.js +175 -72
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -2
- package/dist/workflows/index.js +2 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/progress.d.ts +1 -0
- package/dist/workflows/progress.js +15 -3
- package/dist/workflows/progress.js.map +1 -1
- package/dist/workflows/schema.js +12 -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 +8 -1
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +37 -48
- package/docs/2026-08-18-herdr-piw-plan.md +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
- package/docs/CONTROLLERS.md +2 -2
- package/docs/DEFERRED_TURNS.md +298 -0
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/HUMAN_DECISIONS.md +30 -40
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +17 -27
- package/docs/MONITOR.md +7 -13
- package/docs/WORKFLOW_COMPOSITION.md +10 -9
- 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-plan-change-approval-policy-plan.md +322 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +283 -0
- package/docs/run-bundles.md +8 -6
- package/docs/workflows.md +85 -22
- package/examples/workflows/approved-plan.workflow.ts +19 -46
- 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 +7 -7
- package/schemas/decision-presentation-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v1.schema.json +16 -4
- 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 +11 -2
- package/schemas/human-decision-delivery-v1.schema.json +9 -1
- package/schemas/human-decision-receipt-v1.schema.json +9 -1
- package/schemas/human-decision-request-v1.schema.json +25 -5
- package/schemas/human-decision-resolution-v1.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 +93 -12
- package/skills/autoplan/SKILL.md +26 -2
- package/skills/monitor/SKILL.md +66 -24
- package/skills/pi-workflows/SKILL.md +21 -6
- package/skills/sanity-check/SKILL.md +44 -0
- package/src/builtins/autoimplement-command-batches.ts +364 -0
- package/src/builtins/autoimplement.workflow.ts +1075 -370
- package/src/builtins/catalog.ts +5 -3
- package/src/builtins/index.ts +24 -0
- package/src/builtins/monitor.workflow.ts +27 -150
- package/src/builtins/pi-agent-group.ts +1407 -0
- package/src/builtins/plan-approval.workflow.ts +157 -24
- package/src/builtins/plan-change.workflow.ts +321 -0
- package/src/builtins/plan-presentation.ts +2 -2
- package/src/builtins/sanity-check.workflow.ts +769 -0
- package/src/controllers/sqlite.ts +580 -7
- package/src/extension/decision-channels.ts +36 -66
- package/src/extension/deferred-turn-coordinator.ts +171 -0
- package/src/extension/deferred-turn.ts +166 -0
- package/src/extension/index.ts +438 -90
- package/src/extension/session-events.ts +2 -2
- package/src/extension/step-message.ts +1 -0
- package/src/extension/widget.ts +24 -5
- package/src/herdr/setup.ts +4 -4
- package/src/render/graph-render.ts +1 -2
- package/src/viewer/render.ts +7 -6
- package/src/workflows/catalog.ts +7 -2
- package/src/workflows/command-batch.ts +254 -0
- package/src/workflows/composition.ts +9 -0
- package/src/workflows/decision-presentation.ts +56 -43
- package/src/workflows/engine.ts +25 -20
- package/src/workflows/errors.ts +24 -0
- package/src/workflows/human-decision.ts +218 -101
- package/src/workflows/index.ts +20 -11
- package/src/workflows/progress.ts +18 -3
- package/src/workflows/schema.ts +19 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +8 -1
- package/src/workflows/types.ts +43 -59
- package/schemas/human-decision-accepted-v2.schema.json +0 -50
- package/schemas/human-decision-delivery-v2.schema.json +0 -36
- package/schemas/human-decision-receipt-v2.schema.json +0 -39
- package/schemas/human-decision-request-v2.schema.json +0 -69
- package/schemas/human-decision-resolution-v2.schema.json +0 -27
package/src/extension/index.ts
CHANGED
|
@@ -8,12 +8,17 @@ import {
|
|
|
8
8
|
SqliteControllerStore,
|
|
9
9
|
type WorkflowRunQueueRecord,
|
|
10
10
|
} from "../controllers/index.js";
|
|
11
|
+
import type {
|
|
12
|
+
WorkflowTurnIntentCause,
|
|
13
|
+
WorkflowTurnIntentResolution,
|
|
14
|
+
} from "../controllers/sqlite.js";
|
|
11
15
|
import type { JsonObject } from "../controllers/types.js";
|
|
12
16
|
import type { WorkflowSchedulerResult } from "../controllers/workflows.js";
|
|
13
17
|
import { compositionMetadata } from "../workflows/composition.js";
|
|
14
18
|
import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
|
|
15
19
|
import { WorkflowEngine } from "../workflows/engine.js";
|
|
16
20
|
import {
|
|
21
|
+
BuiltinWorkflowRevisionChangedError,
|
|
17
22
|
ClaimLostError,
|
|
18
23
|
errorMessage,
|
|
19
24
|
isClaimLostError,
|
|
@@ -37,6 +42,7 @@ import {
|
|
|
37
42
|
} from "../workflows/store.js";
|
|
38
43
|
import type {
|
|
39
44
|
AcceptedHumanDecision,
|
|
45
|
+
ResolvedHumanDecision,
|
|
40
46
|
AgentStepContract,
|
|
41
47
|
HumanDecisionRequest,
|
|
42
48
|
HumanDecisionSubmission,
|
|
@@ -44,6 +50,7 @@ import type {
|
|
|
44
50
|
WorkflowDefinitionSnapshot,
|
|
45
51
|
WorkflowRunResult,
|
|
46
52
|
WorkflowRunState,
|
|
53
|
+
WorkflowSource,
|
|
47
54
|
WorkflowUpdateRecord,
|
|
48
55
|
} from "../workflows/types.js";
|
|
49
56
|
import {
|
|
@@ -63,6 +70,19 @@ import {
|
|
|
63
70
|
type HumanDecisionChannelAnswer,
|
|
64
71
|
type SettledHumanDecision,
|
|
65
72
|
} from "./decision-channels.js";
|
|
73
|
+
import {
|
|
74
|
+
DeferredTurnCoordinator,
|
|
75
|
+
type BranchIntentResolution,
|
|
76
|
+
} from "./deferred-turn-coordinator.js";
|
|
77
|
+
import {
|
|
78
|
+
buildDeferredTurnContent,
|
|
79
|
+
createDeferredTurnDescriptor,
|
|
80
|
+
DEFERRED_TURN_MESSAGE_TYPE,
|
|
81
|
+
deferredTurnMessageDetails,
|
|
82
|
+
deferredTurnMessageId,
|
|
83
|
+
deferredTurnSourceEventId,
|
|
84
|
+
type DeferredTurnDescriptor,
|
|
85
|
+
} from "./deferred-turn.js";
|
|
66
86
|
import { ConversationStepExecutor } from "./executor.js";
|
|
67
87
|
import {
|
|
68
88
|
HerdrWorkflowViewer,
|
|
@@ -108,8 +128,10 @@ const RUN_CLAIM_LEASE_MS = 30_000;
|
|
|
108
128
|
const RUN_CLAIM_RENEW_MS = 10_000;
|
|
109
129
|
const RUN_SYNC_POLL_MS = 3_000;
|
|
110
130
|
const NOTIFICATION_DELIVERY_LEASE_MS = 30_000;
|
|
131
|
+
const TURN_INTENT_DELIVERY_LEASE_MS = 30_000;
|
|
111
132
|
const WIDGET_KEY = "pi-workflows";
|
|
112
133
|
const PRESENTATION_MESSAGE_TYPE = "pi-workflows-presentation";
|
|
134
|
+
const PRESENTATION_MESSAGE_SCHEMA = "pi-workflows.presentation-message.v1";
|
|
113
135
|
const FINAL_WIDGET_TTL_MS = 60_000;
|
|
114
136
|
const WIDGET_SCROLL_STEP = 3;
|
|
115
137
|
const MAX_PRESENTATION_RESULT_CHARS = 50_000;
|
|
@@ -138,6 +160,12 @@ type PresentationPromptBuilder = Exclude<
|
|
|
138
160
|
string | undefined
|
|
139
161
|
>;
|
|
140
162
|
|
|
163
|
+
type AbortProvenance = {
|
|
164
|
+
cause: WorkflowTurnIntentCause;
|
|
165
|
+
descriptor?: DeferredTurnDescriptor;
|
|
166
|
+
storageError?: string;
|
|
167
|
+
};
|
|
168
|
+
|
|
141
169
|
type ActiveRun = {
|
|
142
170
|
runId: string;
|
|
143
171
|
workflowName: string;
|
|
@@ -153,6 +181,9 @@ type ActiveRun = {
|
|
|
153
181
|
onFinish?: (result: WorkflowSchedulerResult) => void;
|
|
154
182
|
completion?: Promise<void>;
|
|
155
183
|
interruptionRequested?: boolean;
|
|
184
|
+
pendingAbortCause?: WorkflowTurnIntentCause | undefined;
|
|
185
|
+
abortProvenance?: AbortProvenance | undefined;
|
|
186
|
+
suppressTurnIntent?: boolean | undefined;
|
|
156
187
|
claimToken?: string | undefined;
|
|
157
188
|
renewTimer?: ReturnType<typeof setInterval> | undefined;
|
|
158
189
|
/** True for runs this session resumed from the queue. */
|
|
@@ -166,19 +197,14 @@ function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
|
|
|
166
197
|
value !== null && typeof value === "object"
|
|
167
198
|
? (value as { schema?: unknown }).schema
|
|
168
199
|
: undefined;
|
|
169
|
-
if (
|
|
170
|
-
schema !== "pi-workflows.human-decision-request.v1" &&
|
|
171
|
-
schema !== "pi-workflows.human-decision-request.v2"
|
|
172
|
-
) {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
200
|
+
if (schema !== "pi-workflows.human-decision-request.v1") return null;
|
|
175
201
|
return value as HumanDecisionRequest;
|
|
176
202
|
}
|
|
177
203
|
|
|
178
204
|
type PreparedLaunchOptions = {
|
|
179
205
|
presentation?: boolean;
|
|
180
206
|
parentRunId?: string;
|
|
181
|
-
humanDecision?:
|
|
207
|
+
humanDecision?: ResolvedHumanDecision;
|
|
182
208
|
};
|
|
183
209
|
|
|
184
210
|
type StartRunOptions = {
|
|
@@ -190,8 +216,8 @@ type StartRunOptions = {
|
|
|
190
216
|
signal?: AbortSignal;
|
|
191
217
|
/** Continue a checkpointed run: input becomes the legacy answer payload. */
|
|
192
218
|
parentRunId?: string;
|
|
193
|
-
/**
|
|
194
|
-
humanDecision?:
|
|
219
|
+
/** Durable human or timeout response for a protected decision continuation. */
|
|
220
|
+
humanDecision?: ResolvedHumanDecision;
|
|
195
221
|
/** Resume a parked run at its stopped node instead of starting fresh. */
|
|
196
222
|
resume?: boolean;
|
|
197
223
|
/** An existing queue claim token, when the caller already claimed the run. */
|
|
@@ -209,6 +235,29 @@ function launchSourceIdentity(workflow: WorkflowDefinition, root: unknown): unkn
|
|
|
209
235
|
};
|
|
210
236
|
}
|
|
211
237
|
|
|
238
|
+
function continuationSourceChangedError(
|
|
239
|
+
runId: string,
|
|
240
|
+
previous: WorkflowSource,
|
|
241
|
+
current: WorkflowSource,
|
|
242
|
+
): Error {
|
|
243
|
+
if (
|
|
244
|
+
previous.kind === "builtin" &&
|
|
245
|
+
current.kind === "builtin" &&
|
|
246
|
+
previous.id === current.id &&
|
|
247
|
+
previous.revision !== current.revision
|
|
248
|
+
) {
|
|
249
|
+
return new BuiltinWorkflowRevisionChangedError({
|
|
250
|
+
runId,
|
|
251
|
+
workflowId: current.id,
|
|
252
|
+
previousRevision: previous.revision,
|
|
253
|
+
currentRevision: current.revision,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return new Error(
|
|
257
|
+
`Workflow source changed since run ${runId} started; revert the edit to answer its checkpoint`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
212
261
|
function preparedLaunchOptions(options: StartRunOptions): PreparedLaunchOptions {
|
|
213
262
|
return {
|
|
214
263
|
...(options.presentation !== undefined ? { presentation: options.presentation } : {}),
|
|
@@ -400,6 +449,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
400
449
|
let activationRecovery: ((ctx: ExtensionContext) => void) | undefined;
|
|
401
450
|
let decisionRecoveryTimer: ReturnType<typeof setInterval> | null = null;
|
|
402
451
|
let decisionRecoveryActive = false;
|
|
452
|
+
let turnCoordinator: DeferredTurnCoordinator;
|
|
403
453
|
|
|
404
454
|
const recordRunEvent = (event: {
|
|
405
455
|
runId: string;
|
|
@@ -453,9 +503,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
453
503
|
kind: notification.kind,
|
|
454
504
|
},
|
|
455
505
|
},
|
|
456
|
-
|
|
457
|
-
? { triggerTurn: true, deliverAs: "followUp" }
|
|
458
|
-
: { triggerTurn: false },
|
|
506
|
+
{ triggerTurn: false },
|
|
459
507
|
);
|
|
460
508
|
alreadyDelivered.add(notification.notificationId);
|
|
461
509
|
}
|
|
@@ -465,6 +513,25 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
465
513
|
claimToken,
|
|
466
514
|
});
|
|
467
515
|
}
|
|
516
|
+
const idle = ctx.isIdle() && systemTurnAbort === null && !sessionClosed && !runHeld();
|
|
517
|
+
turnCoordinator.flushNatural(idle);
|
|
518
|
+
turnCoordinator.deliverFallbacks(
|
|
519
|
+
{
|
|
520
|
+
targetSessionId: sessionId,
|
|
521
|
+
send: (intent) => {
|
|
522
|
+
pi.sendMessage(
|
|
523
|
+
{
|
|
524
|
+
customType: DEFERRED_TURN_MESSAGE_TYPE,
|
|
525
|
+
content: buildDeferredTurnContent(intent),
|
|
526
|
+
display: true,
|
|
527
|
+
details: deferredTurnMessageDetails(intent),
|
|
528
|
+
},
|
|
529
|
+
{ triggerTurn: true, deliverAs: "followUp" },
|
|
530
|
+
);
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
idle,
|
|
534
|
+
);
|
|
468
535
|
} catch {
|
|
469
536
|
// Delivery retries on the next poll. It never affects workflow execution.
|
|
470
537
|
}
|
|
@@ -480,7 +547,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
480
547
|
runSyncTimer.unref?.();
|
|
481
548
|
};
|
|
482
549
|
let activeRun: ActiveRun | null = null;
|
|
483
|
-
let systemTurnAbort: AgentStepContract | null = null;
|
|
550
|
+
let systemTurnAbort: { contract: AgentStepContract; intentId?: string } | null = null;
|
|
484
551
|
let suppressWorkflowAssistantTail = false;
|
|
485
552
|
let lastExpiredAttempt: { contract: AgentStepContract; reason: string } | null = null;
|
|
486
553
|
// The interactive run currently parked at a checkpoint, if any.
|
|
@@ -501,6 +568,30 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
501
568
|
let controllerHost: PiControllerHost | undefined;
|
|
502
569
|
let controllerContext: ExtensionContext | null = null;
|
|
503
570
|
|
|
571
|
+
const branchIntentResolution = (intentId: string): BranchIntentResolution | null => {
|
|
572
|
+
if (controllerContext === null) return null;
|
|
573
|
+
for (const entry of controllerContext.sessionManager.getBranch()) {
|
|
574
|
+
if (entry.type !== "custom_message") continue;
|
|
575
|
+
const details = entry.details;
|
|
576
|
+
if (details === null || typeof details !== "object" || Array.isArray(details)) continue;
|
|
577
|
+
if ((details as { turnIntentId?: unknown }).turnIntentId !== intentId) continue;
|
|
578
|
+
let resolution: WorkflowTurnIntentResolution | null = null;
|
|
579
|
+
if (entry.customType === WORKFLOW_AGENT_STEP_MESSAGE_TYPE) resolution = "workflowPrompt";
|
|
580
|
+
if (entry.customType === PRESENTATION_MESSAGE_TYPE) resolution = "presentation";
|
|
581
|
+
if (entry.customType === DEFERRED_TURN_MESSAGE_TYPE) resolution = "fallback";
|
|
582
|
+
if (resolution !== null) {
|
|
583
|
+
return { resolution, messageId: deferredTurnMessageId(intentId, resolution) };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
turnCoordinator = new DeferredTurnCoordinator({
|
|
590
|
+
store: () => runQueueStore,
|
|
591
|
+
branchResolution: branchIntentResolution,
|
|
592
|
+
leaseMs: TURN_INTENT_DELIVERY_LEASE_MS,
|
|
593
|
+
});
|
|
594
|
+
|
|
504
595
|
// UI updates are best-effort: a captured ctx becomes stale after session
|
|
505
596
|
// replacement or shutdown, and pi throws on any access (even `ctx.hasUI`).
|
|
506
597
|
// A workflow finishing right as the session goes away must not crash pi.
|
|
@@ -542,6 +633,145 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
542
633
|
const runHeld = (): boolean =>
|
|
543
634
|
activeRun !== null && (activeRun.engine.pauseRequested || activeRun.executor.held);
|
|
544
635
|
|
|
636
|
+
const originSessionId = (ctx: ExtensionContext, runId: string): string =>
|
|
637
|
+
ensureRunQueueStore(ctx.cwd).getWorkflowRun(runId)?.originSessionId ??
|
|
638
|
+
ctx.sessionManager.getSessionId();
|
|
639
|
+
|
|
640
|
+
const storeTurnDescriptor = (
|
|
641
|
+
ctx: ExtensionContext,
|
|
642
|
+
descriptor: DeferredTurnDescriptor,
|
|
643
|
+
eligible: boolean,
|
|
644
|
+
): void => {
|
|
645
|
+
ensureRunQueueStore(ctx.cwd).ensureWorkflowTurnIntent({ ...descriptor, eligible });
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const ensureAbortTurnIntent = (
|
|
649
|
+
ctx: ExtensionContext,
|
|
650
|
+
run: ActiveRun,
|
|
651
|
+
cause: WorkflowTurnIntentCause,
|
|
652
|
+
contract: AgentStepContract,
|
|
653
|
+
reason: unknown,
|
|
654
|
+
): DeferredTurnDescriptor => {
|
|
655
|
+
if (run.abortProvenance?.descriptor !== undefined) {
|
|
656
|
+
return run.abortProvenance.descriptor;
|
|
657
|
+
}
|
|
658
|
+
const descriptor = createDeferredTurnDescriptor({
|
|
659
|
+
runId: run.runId,
|
|
660
|
+
workflowName: run.workflowName,
|
|
661
|
+
targetSessionId: originSessionId(ctx, run.runId),
|
|
662
|
+
cause,
|
|
663
|
+
sourceEventId: deferredTurnSourceEventId({
|
|
664
|
+
runId: run.runId,
|
|
665
|
+
cause,
|
|
666
|
+
nodeId: contract.nodeId,
|
|
667
|
+
attemptId: contract.attemptId,
|
|
668
|
+
source: "agent-step-abort",
|
|
669
|
+
}),
|
|
670
|
+
observedState: cause === "claimLost" ? "handedOff" : "interrupted",
|
|
671
|
+
nodeId: contract.nodeId,
|
|
672
|
+
attemptId: contract.attemptId,
|
|
673
|
+
reason: errorMessage(reason),
|
|
674
|
+
handoff: cause === "claimLost",
|
|
675
|
+
});
|
|
676
|
+
run.abortProvenance = { cause, descriptor };
|
|
677
|
+
try {
|
|
678
|
+
storeTurnDescriptor(ctx, descriptor, false);
|
|
679
|
+
} catch (error) {
|
|
680
|
+
run.abortProvenance.storageError = errorMessage(error);
|
|
681
|
+
}
|
|
682
|
+
return descriptor;
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const makeRunTurnIntentEligible = (
|
|
686
|
+
ctx: ExtensionContext,
|
|
687
|
+
run: ActiveRun,
|
|
688
|
+
observedState: string,
|
|
689
|
+
reason?: string,
|
|
690
|
+
): void => {
|
|
691
|
+
if (
|
|
692
|
+
sessionClosed ||
|
|
693
|
+
run.suppressTurnIntent === true ||
|
|
694
|
+
run.abortProvenance?.cause === "claimLost"
|
|
695
|
+
) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const targetSessionId = originSessionId(ctx, run.runId);
|
|
699
|
+
const pending = ensureRunQueueStore(ctx.cwd).findPendingWorkflowTurnIntent({
|
|
700
|
+
runId: run.runId,
|
|
701
|
+
targetSessionId,
|
|
702
|
+
});
|
|
703
|
+
if (
|
|
704
|
+
run.childKey !== undefined &&
|
|
705
|
+
run.abortProvenance === undefined &&
|
|
706
|
+
pending?.cause !== "claimLost"
|
|
707
|
+
) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const cause =
|
|
711
|
+
pending?.cause ??
|
|
712
|
+
run.abortProvenance?.cause ??
|
|
713
|
+
(observedState === "timed_out" ? "timedOut" : "failed");
|
|
714
|
+
if (
|
|
715
|
+
observedState === "cancelled" &&
|
|
716
|
+
run.abortProvenance === undefined &&
|
|
717
|
+
pending === undefined
|
|
718
|
+
) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const previous = run.abortProvenance?.descriptor;
|
|
722
|
+
const sourceEventId =
|
|
723
|
+
pending?.sourceEventId ??
|
|
724
|
+
previous?.sourceEventId ??
|
|
725
|
+
deferredTurnSourceEventId({
|
|
726
|
+
runId: run.runId,
|
|
727
|
+
cause,
|
|
728
|
+
nodeId: "$terminal",
|
|
729
|
+
source: "terminal",
|
|
730
|
+
});
|
|
731
|
+
const descriptor = createDeferredTurnDescriptor({
|
|
732
|
+
runId: run.runId,
|
|
733
|
+
workflowName: pending?.workflowRef ?? run.workflowName,
|
|
734
|
+
targetSessionId,
|
|
735
|
+
cause,
|
|
736
|
+
sourceEventId,
|
|
737
|
+
observedState,
|
|
738
|
+
nodeId: pending?.nodeId ?? previous?.nodeId ?? "$terminal",
|
|
739
|
+
attemptId: pending?.attemptId ?? previous?.attemptId ?? null,
|
|
740
|
+
reason: reason ?? null,
|
|
741
|
+
handoff: false,
|
|
742
|
+
});
|
|
743
|
+
run.abortProvenance = {
|
|
744
|
+
cause,
|
|
745
|
+
descriptor,
|
|
746
|
+
...(run.abortProvenance?.storageError === undefined
|
|
747
|
+
? {}
|
|
748
|
+
: { storageError: run.abortProvenance.storageError }),
|
|
749
|
+
};
|
|
750
|
+
try {
|
|
751
|
+
storeTurnDescriptor(ctx, descriptor, false);
|
|
752
|
+
ensureRunQueueStore(ctx.cwd).makeWorkflowTurnIntentEligible({
|
|
753
|
+
intentId: descriptor.intentId,
|
|
754
|
+
fallbackFacts: descriptor.fallbackFacts,
|
|
755
|
+
});
|
|
756
|
+
delete run.abortProvenance.storageError;
|
|
757
|
+
syncArmed = true;
|
|
758
|
+
} catch (error) {
|
|
759
|
+
const message = errorMessage(error);
|
|
760
|
+
run.abortProvenance.storageError = message;
|
|
761
|
+
recordRunEvent({
|
|
762
|
+
runId: run.runId,
|
|
763
|
+
workflowRef: run.workflowName,
|
|
764
|
+
type: "turn_intent_failed",
|
|
765
|
+
payload: { error: message },
|
|
766
|
+
});
|
|
767
|
+
notify(
|
|
768
|
+
ctx,
|
|
769
|
+
`Workflow ${run.workflowName} ended, but Pi Workflows could not preserve its successor turn: ${message}`,
|
|
770
|
+
"warning",
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
545
775
|
const footerStatus = (state: WorkflowRunState): string => {
|
|
546
776
|
const label = runHeld() || state.paused ? "paused" : state.status;
|
|
547
777
|
const node = state.currentNode ?? state.waitingOn;
|
|
@@ -742,24 +972,41 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
742
972
|
typeof run.presentationPrompt === "function"
|
|
743
973
|
? await resolvePresentationPrompt(run.presentationPrompt, state, abort.signal)
|
|
744
974
|
: run.presentationPrompt;
|
|
745
|
-
if (
|
|
746
|
-
sessionClosed ||
|
|
747
|
-
run.generation !== runGeneration ||
|
|
748
|
-
abort.signal.aborted ||
|
|
749
|
-
instructions === undefined ||
|
|
750
|
-
instructions.trim().length === 0
|
|
751
|
-
) {
|
|
975
|
+
if (sessionClosed || run.generation !== runGeneration || abort.signal.aborted) {
|
|
752
976
|
return;
|
|
753
977
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
978
|
+
if (instructions === undefined || instructions.trim().length === 0) {
|
|
979
|
+
if (run.abortProvenance !== undefined) {
|
|
980
|
+
makeRunTurnIntentEligible(ctx, run, state.status, "No result presentation was available");
|
|
981
|
+
}
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
turnCoordinator.sendNatural(
|
|
757
985
|
{
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
986
|
+
runId: run.runId,
|
|
987
|
+
targetSessionId: originSessionId(ctx, run.runId),
|
|
988
|
+
resolution: "presentation",
|
|
989
|
+
send: (turnIntentId) => {
|
|
990
|
+
presentationPending = run.generation;
|
|
991
|
+
suppressWorkflowAssistantTail = false;
|
|
992
|
+
pi.sendMessage(
|
|
993
|
+
{
|
|
994
|
+
customType: PRESENTATION_MESSAGE_TYPE,
|
|
995
|
+
content: buildPresentationMessage(instructions, state),
|
|
996
|
+
display: false,
|
|
997
|
+
details: {
|
|
998
|
+
schema: PRESENTATION_MESSAGE_SCHEMA,
|
|
999
|
+
...(turnIntentId === undefined ? {} : { turnIntentId }),
|
|
1000
|
+
},
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
deliverAs: turnIntentId === undefined ? "steer" : "followUp",
|
|
1004
|
+
triggerTurn: true,
|
|
1005
|
+
},
|
|
1006
|
+
);
|
|
1007
|
+
},
|
|
761
1008
|
},
|
|
762
|
-
|
|
1009
|
+
ctx.isIdle() && systemTurnAbort === null,
|
|
763
1010
|
);
|
|
764
1011
|
} catch (error) {
|
|
765
1012
|
if (presentationPending === run.generation) {
|
|
@@ -777,6 +1024,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
777
1024
|
? `timed out after ${PRESENTATION_TIMEOUT_MS}ms`
|
|
778
1025
|
: errorMessage(error);
|
|
779
1026
|
notify(ctx, `Could not present workflow result: ${message}`, "warning");
|
|
1027
|
+
if (run.abortProvenance !== undefined) {
|
|
1028
|
+
makeRunTurnIntentEligible(ctx, run, state.status, message);
|
|
1029
|
+
}
|
|
780
1030
|
} finally {
|
|
781
1031
|
clearTimeout(timer);
|
|
782
1032
|
if (presentationAbort === abort) {
|
|
@@ -829,6 +1079,22 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
829
1079
|
clearWidget(ctx);
|
|
830
1080
|
return;
|
|
831
1081
|
}
|
|
1082
|
+
if (run.abortProvenance?.cause === "claimLost") {
|
|
1083
|
+
await run.recorder?.stop().catch(() => undefined);
|
|
1084
|
+
releaseClaim(run, "lost");
|
|
1085
|
+
recordRunEvent({
|
|
1086
|
+
runId: run.runId,
|
|
1087
|
+
workflowRef: run.workflowName,
|
|
1088
|
+
type: "claim_lost",
|
|
1089
|
+
});
|
|
1090
|
+
stopWidgetTicker();
|
|
1091
|
+
clearWidget(ctx);
|
|
1092
|
+
notify(
|
|
1093
|
+
ctx,
|
|
1094
|
+
`Workflow ${run.workflowName} continues under another runner (run ${run.runId}).`,
|
|
1095
|
+
);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
832
1098
|
releaseClaim(run, "done");
|
|
833
1099
|
recordRunEvent({
|
|
834
1100
|
runId: run.runId,
|
|
@@ -880,6 +1146,15 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
880
1146
|
} catch (error) {
|
|
881
1147
|
notify(ctx, `Could not record child workflow completion: ${errorMessage(error)}`, "warning");
|
|
882
1148
|
}
|
|
1149
|
+
if (state.status === "failed" || state.status === "timed_out" || state.status === "cancelled") {
|
|
1150
|
+
makeRunTurnIntentEligible(ctx, run, state.status, state.error);
|
|
1151
|
+
} else if (
|
|
1152
|
+
run.abortProvenance !== undefined &&
|
|
1153
|
+
run.presentationPrompt === undefined &&
|
|
1154
|
+
(state.status === "completed" || state.status === "waiting")
|
|
1155
|
+
) {
|
|
1156
|
+
makeRunTurnIntentEligible(ctx, run, state.status, state.error);
|
|
1157
|
+
}
|
|
883
1158
|
void presentRun(ctx, run, state);
|
|
884
1159
|
if (pendingDecision !== null && state.status === "waiting") {
|
|
885
1160
|
const channels = audienceChannels(decisionChannelConfig, pendingDecision.audience);
|
|
@@ -956,8 +1231,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
956
1231
|
parent.state.workflowSource !== undefined &&
|
|
957
1232
|
!isDeepStrictEqual(parent.state.workflowSource, workflowSource)
|
|
958
1233
|
) {
|
|
959
|
-
throw
|
|
960
|
-
|
|
1234
|
+
throw continuationSourceChangedError(
|
|
1235
|
+
options.parentRunId,
|
|
1236
|
+
parent.state.workflowSource,
|
|
1237
|
+
workflowSource,
|
|
961
1238
|
);
|
|
962
1239
|
}
|
|
963
1240
|
}
|
|
@@ -1011,23 +1288,34 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1011
1288
|
|
|
1012
1289
|
const executor = new ConversationStepExecutor({
|
|
1013
1290
|
sendPrompt: ({ prompt, contract, presentation, kind, streaming }) => {
|
|
1014
|
-
|
|
1015
|
-
schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
1016
|
-
kind,
|
|
1017
|
-
contract,
|
|
1018
|
-
...(presentation !== undefined ? { presentation } : {}),
|
|
1019
|
-
};
|
|
1020
|
-
pi.sendMessage(
|
|
1291
|
+
turnCoordinator.sendNatural(
|
|
1021
1292
|
{
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1293
|
+
runId,
|
|
1294
|
+
targetSessionId: originSessionId(ctx, runId),
|
|
1295
|
+
resolution: "workflowPrompt",
|
|
1296
|
+
send: (turnIntentId) => {
|
|
1297
|
+
const details: WorkflowAgentStepMessageDetails = {
|
|
1298
|
+
schema: WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA,
|
|
1299
|
+
kind,
|
|
1300
|
+
contract,
|
|
1301
|
+
...(presentation !== undefined ? { presentation } : {}),
|
|
1302
|
+
...(turnIntentId === undefined ? {} : { turnIntentId }),
|
|
1303
|
+
};
|
|
1304
|
+
pi.sendMessage(
|
|
1305
|
+
{
|
|
1306
|
+
customType: WORKFLOW_AGENT_STEP_MESSAGE_TYPE,
|
|
1307
|
+
content: prompt,
|
|
1308
|
+
display: true,
|
|
1309
|
+
details,
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
triggerTurn: true,
|
|
1313
|
+
deliverAs: streaming ? "steer" : "followUp",
|
|
1314
|
+
},
|
|
1315
|
+
);
|
|
1316
|
+
},
|
|
1030
1317
|
},
|
|
1318
|
+
ctx.isIdle() && systemTurnAbort === null,
|
|
1031
1319
|
);
|
|
1032
1320
|
},
|
|
1033
1321
|
onAbort: (contract, reason) => {
|
|
@@ -1036,7 +1324,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1036
1324
|
reason: reason instanceof TimeoutError ? "timed out" : `ended: ${errorMessage(reason)}`,
|
|
1037
1325
|
};
|
|
1038
1326
|
if (!executor.held && !ctx.isIdle()) {
|
|
1039
|
-
|
|
1327
|
+
const cause = reason instanceof TimeoutError ? "timedOut" : run.pendingAbortCause;
|
|
1328
|
+
const descriptor =
|
|
1329
|
+
cause === undefined
|
|
1330
|
+
? undefined
|
|
1331
|
+
: ensureAbortTurnIntent(ctx, run, cause, contract, reason);
|
|
1332
|
+
run.pendingAbortCause = undefined;
|
|
1333
|
+
systemTurnAbort = {
|
|
1334
|
+
contract,
|
|
1335
|
+
...(descriptor === undefined ? {} : { intentId: descriptor.intentId }),
|
|
1336
|
+
};
|
|
1040
1337
|
ctx.abort();
|
|
1041
1338
|
}
|
|
1042
1339
|
},
|
|
@@ -1137,6 +1434,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1137
1434
|
if (
|
|
1138
1435
|
!store.renewWorkflowRunClaim({ runId, claimToken: token, leaseMs: RUN_CLAIM_LEASE_MS })
|
|
1139
1436
|
) {
|
|
1437
|
+
run.pendingAbortCause = "claimLost";
|
|
1140
1438
|
run.engine.cancel();
|
|
1141
1439
|
}
|
|
1142
1440
|
} catch {
|
|
@@ -1239,6 +1537,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1239
1537
|
}
|
|
1240
1538
|
if (terminalStatus !== undefined) {
|
|
1241
1539
|
recordRunEvent({ runId, workflowRef: workflow.name, type: terminalStatus, payload: {} });
|
|
1540
|
+
if (
|
|
1541
|
+
terminalStatus === "failed" ||
|
|
1542
|
+
terminalStatus === "timed_out" ||
|
|
1543
|
+
terminalStatus === "cancelled"
|
|
1544
|
+
) {
|
|
1545
|
+
makeRunTurnIntentEligible(ctx, run, terminalStatus, message);
|
|
1546
|
+
}
|
|
1242
1547
|
notify(ctx, `Workflow ${workflow.name} ${terminalStatus} (run ${runId}).`);
|
|
1243
1548
|
return;
|
|
1244
1549
|
}
|
|
@@ -1248,6 +1553,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1248
1553
|
type: "failed",
|
|
1249
1554
|
payload: { error: message },
|
|
1250
1555
|
});
|
|
1556
|
+
makeRunTurnIntentEligible(ctx, run, "failed", message);
|
|
1251
1557
|
try {
|
|
1252
1558
|
run.onFinish?.({ state: "failed", runId, error: message });
|
|
1253
1559
|
} catch {
|
|
@@ -1478,11 +1784,14 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1478
1784
|
|
|
1479
1785
|
const cancelWorkflowControl = async (
|
|
1480
1786
|
ctx: ExtensionContext,
|
|
1787
|
+
origin: "agent" | "user",
|
|
1481
1788
|
requestedRunId?: string,
|
|
1482
1789
|
): Promise<WorkflowControlResult> => {
|
|
1483
1790
|
if (activeRun && (requestedRunId === undefined || requestedRunId === activeRun.runId)) {
|
|
1484
1791
|
const workflowName = activeRun.workflowName;
|
|
1485
1792
|
const runId = activeRun.runId;
|
|
1793
|
+
activeRun.pendingAbortCause = origin === "agent" ? "agentCancelled" : undefined;
|
|
1794
|
+
activeRun.suppressTurnIntent = origin === "user";
|
|
1486
1795
|
activeRun.engine.cancel();
|
|
1487
1796
|
return {
|
|
1488
1797
|
message: `Cancelling workflow ${workflowName}…`,
|
|
@@ -1581,6 +1890,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1581
1890
|
},
|
|
1582
1891
|
};
|
|
1583
1892
|
}
|
|
1893
|
+
activeRun.suppressTurnIntent = true;
|
|
1584
1894
|
activeRun.engine.pause();
|
|
1585
1895
|
renderWidget(ctx);
|
|
1586
1896
|
return {
|
|
@@ -1614,6 +1924,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1614
1924
|
level: "warning",
|
|
1615
1925
|
};
|
|
1616
1926
|
}
|
|
1927
|
+
activeRun.suppressTurnIntent = false;
|
|
1617
1928
|
activeRun.engine.resume();
|
|
1618
1929
|
activeRun.executor.release();
|
|
1619
1930
|
renderWidget(ctx);
|
|
@@ -1773,7 +2084,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1773
2084
|
};
|
|
1774
2085
|
const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
|
|
1775
2086
|
const acceptance = await store.accept(request, submission);
|
|
1776
|
-
if (acceptance.status === "conflict") {
|
|
2087
|
+
if (acceptance.status === "conflict" || acceptance.decision.provenance !== "human") {
|
|
1777
2088
|
throw new Error("That human decision was already answered differently.");
|
|
1778
2089
|
}
|
|
1779
2090
|
accepted = acceptance.decision;
|
|
@@ -1805,6 +2116,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1805
2116
|
schema: "pi-workflows.human-decision-continuation.v1",
|
|
1806
2117
|
decisionId: request.decisionId,
|
|
1807
2118
|
requestDigest: request.requestDigest,
|
|
2119
|
+
provenance: accepted.provenance,
|
|
1808
2120
|
parentRunId: waiting.parentRunId,
|
|
1809
2121
|
runId: continuationRunId,
|
|
1810
2122
|
createdAt: accepted.acceptedAt,
|
|
@@ -1930,60 +2242,72 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1930
2242
|
queueRecord !== undefined &&
|
|
1931
2243
|
(queueRecord.originSessionId === null ||
|
|
1932
2244
|
queueRecord.originSessionId === ctx.sessionManager.getSessionId());
|
|
1933
|
-
|
|
1934
|
-
if (
|
|
2245
|
+
let resolved = await store.readResolved(request.decisionId);
|
|
2246
|
+
if (resolved === null) {
|
|
1935
2247
|
let cancellation = await store.readCancellation(request.decisionId);
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2248
|
+
const expired =
|
|
2249
|
+
request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now();
|
|
2250
|
+
if (cancellation === null && expired) {
|
|
2251
|
+
if (request.defaultResponse === undefined) {
|
|
2252
|
+
await store.cancel(request, "expired");
|
|
2253
|
+
cancellation = await store.readCancellation(request.decisionId);
|
|
2254
|
+
} else {
|
|
2255
|
+
try {
|
|
2256
|
+
resolved = (await store.resolveTimeout(request)).decision;
|
|
2257
|
+
} catch {
|
|
2258
|
+
cancellation = await store.readCancellation(request.decisionId);
|
|
2259
|
+
resolved = await store.readResolved(request.decisionId);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
1943
2262
|
}
|
|
1944
2263
|
if (cancellation !== null) {
|
|
1945
2264
|
await settleHumanDecisionChannels(cancellation);
|
|
1946
2265
|
continue;
|
|
1947
2266
|
}
|
|
1948
|
-
if (
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
const
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
lastWaitingRunId
|
|
1956
|
-
|
|
2267
|
+
if (resolved === null) {
|
|
2268
|
+
if (!deliverPending || !ownedBySession) continue;
|
|
2269
|
+
const channels = audienceChannels(decisionChannelConfig, request.audience);
|
|
2270
|
+
for (const channelId of channels) {
|
|
2271
|
+
const channel = telegramDecisionChannels.get(channelId);
|
|
2272
|
+
if (channel !== undefined) await channel.deliver(humanDecisionChannelRequest(request));
|
|
2273
|
+
}
|
|
2274
|
+
if (ctx.mode === "tui" && channels.includes("pi") && lastWaitingRunId === null) {
|
|
2275
|
+
lastWaitingRunId = request.runId;
|
|
2276
|
+
queueMicrotask(() => void promptHumanDecision(ctx, request));
|
|
2277
|
+
}
|
|
2278
|
+
continue;
|
|
1957
2279
|
}
|
|
1958
|
-
continue;
|
|
1959
2280
|
}
|
|
1960
2281
|
|
|
1961
2282
|
if (!ownedBySession) continue;
|
|
2283
|
+
const currentParent = await readRunBundle(runStore.runDirFor(request.runId));
|
|
2284
|
+
if (currentParent === null || currentParent.state.status !== "waiting") continue;
|
|
1962
2285
|
const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
|
|
1963
2286
|
const continuation = (await store.readContinuation(request.decisionId)) ?? {
|
|
1964
2287
|
schema: "pi-workflows.human-decision-continuation.v1" as const,
|
|
1965
2288
|
decisionId: request.decisionId,
|
|
1966
2289
|
requestDigest: request.requestDigest,
|
|
2290
|
+
provenance: resolved.provenance,
|
|
1967
2291
|
parentRunId: request.runId,
|
|
1968
2292
|
runId,
|
|
1969
|
-
createdAt:
|
|
2293
|
+
createdAt: resolved.acceptedAt,
|
|
1970
2294
|
};
|
|
1971
2295
|
await store.recordContinuation(request.decisionId, continuation);
|
|
1972
2296
|
const existing = await readRunBundle(runStore.runDirFor(continuation.runId));
|
|
1973
2297
|
if (existing === null && activeRun === null) {
|
|
1974
|
-
if (
|
|
2298
|
+
if (currentParent.state.workflowSource === undefined) continue;
|
|
1975
2299
|
const workflowRef =
|
|
1976
|
-
|
|
1977
|
-
? `builtin:${
|
|
1978
|
-
:
|
|
1979
|
-
await startRun(ctx, workflowRef,
|
|
2300
|
+
currentParent.state.workflowSource.kind === "builtin"
|
|
2301
|
+
? `builtin:${currentParent.state.workflowSource.id}`
|
|
2302
|
+
: currentParent.state.workflowSource.path;
|
|
2303
|
+
await startRun(ctx, workflowRef, currentParent.state.input, {
|
|
1980
2304
|
parentRunId: request.runId,
|
|
1981
|
-
humanDecision:
|
|
2305
|
+
humanDecision: resolved,
|
|
1982
2306
|
runId: continuation.runId,
|
|
1983
2307
|
quiet: true,
|
|
1984
2308
|
});
|
|
1985
2309
|
}
|
|
1986
|
-
await settleHumanDecisionChannels(
|
|
2310
|
+
await settleHumanDecisionChannels(resolved);
|
|
1987
2311
|
if (activeRun !== null) break;
|
|
1988
2312
|
}
|
|
1989
2313
|
};
|
|
@@ -2064,8 +2388,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2064
2388
|
parent.state.workflowSource !== undefined &&
|
|
2065
2389
|
!isDeepStrictEqual(parent.state.workflowSource, workflowSource)
|
|
2066
2390
|
) {
|
|
2067
|
-
throw
|
|
2068
|
-
|
|
2391
|
+
throw continuationSourceChangedError(
|
|
2392
|
+
options.parentRunId,
|
|
2393
|
+
parent.state.workflowSource,
|
|
2394
|
+
workflowSource,
|
|
2069
2395
|
);
|
|
2070
2396
|
}
|
|
2071
2397
|
}
|
|
@@ -2167,19 +2493,31 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2167
2493
|
payload: { errorCode: safe.code, error: safe.message },
|
|
2168
2494
|
});
|
|
2169
2495
|
const content = `Workflow ${claimed.workflowName} failed to start (run ${claimed.runId}): ${safe.message}. Inspect the error and call workflow start again only after you correct the cause.`;
|
|
2170
|
-
|
|
2171
|
-
|
|
2496
|
+
const cause = "launchFailed" as const;
|
|
2497
|
+
const descriptor = createDeferredTurnDescriptor({
|
|
2498
|
+
runId: claimed.runId,
|
|
2499
|
+
workflowName: claimed.workflowName,
|
|
2500
|
+
targetSessionId: claimed.originSessionId ?? ctx.sessionManager.getSessionId(),
|
|
2501
|
+
cause,
|
|
2502
|
+
sourceEventId: deferredTurnSourceEventId({
|
|
2172
2503
|
runId: claimed.runId,
|
|
2504
|
+
cause,
|
|
2173
2505
|
nodeId: "$launch",
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2506
|
+
source: "queued-launch",
|
|
2507
|
+
}),
|
|
2508
|
+
observedState: "failedToStart",
|
|
2509
|
+
nodeId: "$launch",
|
|
2510
|
+
reason: safe.message,
|
|
2511
|
+
});
|
|
2512
|
+
try {
|
|
2513
|
+
storeTurnDescriptor(ctx, descriptor, true);
|
|
2514
|
+
syncArmed = true;
|
|
2515
|
+
} catch (intentError) {
|
|
2516
|
+
notify(
|
|
2517
|
+
ctx,
|
|
2518
|
+
`Workflow ${claimed.workflowName} failed to start, but Pi Workflows could not preserve its successor turn: ${errorMessage(intentError)}`,
|
|
2519
|
+
"warning",
|
|
2520
|
+
);
|
|
2183
2521
|
}
|
|
2184
2522
|
notify(ctx, content, "error");
|
|
2185
2523
|
runSyncPass(ctx);
|
|
@@ -2244,7 +2582,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2244
2582
|
return;
|
|
2245
2583
|
}
|
|
2246
2584
|
if (parsed.kind === "cancel") {
|
|
2247
|
-
const result = await cancelWorkflowControl(ctx);
|
|
2585
|
+
const result = await cancelWorkflowControl(ctx, "user");
|
|
2248
2586
|
notify(ctx, result.message, result.level);
|
|
2249
2587
|
return;
|
|
2250
2588
|
}
|
|
@@ -2412,6 +2750,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2412
2750
|
case "stop":
|
|
2413
2751
|
if (activeRun?.childKey !== undefined) {
|
|
2414
2752
|
activeRun.interruptionRequested = true;
|
|
2753
|
+
activeRun.pendingAbortCause = "controllerInterrupted";
|
|
2415
2754
|
activeRun.engine.cancel();
|
|
2416
2755
|
}
|
|
2417
2756
|
await host.stop();
|
|
@@ -2428,7 +2767,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2428
2767
|
name: "workflow",
|
|
2429
2768
|
label: "Workflow",
|
|
2430
2769
|
description: [
|
|
2431
|
-
"List, start, inspect, pause, resume, cancel, answer, update, or complete
|
|
2770
|
+
"List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
|
|
2432
2771
|
"When the user asks to monitor, watch, poll, or check something repeatedly, start the built-in monitor workflow with input keys task, everyMinutes, stopWhen, and optional maxChecks.",
|
|
2433
2772
|
"Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
|
|
2434
2773
|
"Do not start repeated work without the user's request, and keep monitoring observation-only unless the user authorizes mutations.",
|
|
@@ -2454,7 +2793,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2454
2793
|
control = resumeWorkflowControl(ctx);
|
|
2455
2794
|
break;
|
|
2456
2795
|
case "cancel":
|
|
2457
|
-
control = await cancelWorkflowControl(ctx, params.runId);
|
|
2796
|
+
control = await cancelWorkflowControl(ctx, "agent", params.runId);
|
|
2458
2797
|
break;
|
|
2459
2798
|
case "answer": {
|
|
2460
2799
|
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
@@ -2636,6 +2975,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2636
2975
|
if (!aborted || runHeld()) {
|
|
2637
2976
|
return;
|
|
2638
2977
|
}
|
|
2978
|
+
run.suppressTurnIntent = true;
|
|
2639
2979
|
run.engine.pause();
|
|
2640
2980
|
run.executor.hold();
|
|
2641
2981
|
renderWidget(ctx);
|
|
@@ -2686,6 +3026,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2686
3026
|
});
|
|
2687
3027
|
|
|
2688
3028
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
3029
|
+
systemTurnAbort = null;
|
|
2689
3030
|
if (activeRun === null) {
|
|
2690
3031
|
try {
|
|
2691
3032
|
const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(
|
|
@@ -2704,15 +3045,20 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2704
3045
|
return;
|
|
2705
3046
|
}
|
|
2706
3047
|
}
|
|
3048
|
+
const flushedNatural = turnCoordinator.flushNatural(
|
|
3049
|
+
ctx.isIdle() && !sessionClosed && !runHeld(),
|
|
3050
|
+
);
|
|
2707
3051
|
const run = activeRun;
|
|
2708
3052
|
if (!run) {
|
|
2709
3053
|
presentationPending = null;
|
|
3054
|
+
runSyncPass(ctx);
|
|
2710
3055
|
return;
|
|
2711
3056
|
}
|
|
2712
3057
|
await run.recorder?.synchronize(ctx).catch(() => undefined);
|
|
2713
3058
|
run.recorder?.settleAttempt();
|
|
2714
3059
|
run.executor.setStreaming(false);
|
|
2715
|
-
run.executor.handleAgentSettled();
|
|
3060
|
+
if (flushedNatural === 0) run.executor.handleAgentSettled();
|
|
3061
|
+
runSyncPass(ctx);
|
|
2716
3062
|
});
|
|
2717
3063
|
|
|
2718
3064
|
pi.on("session_shutdown", async () => {
|
|
@@ -2720,8 +3066,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2720
3066
|
herdrProbeGeneration += 1;
|
|
2721
3067
|
systemTurnAbort = null;
|
|
2722
3068
|
suppressWorkflowAssistantTail = false;
|
|
3069
|
+
turnCoordinator.clearDeferred();
|
|
2723
3070
|
supersedePresentation();
|
|
2724
3071
|
const run = activeRun;
|
|
3072
|
+
if (run !== null) run.suppressTurnIntent = true;
|
|
2725
3073
|
if (run !== null && run.claimToken !== undefined) {
|
|
2726
3074
|
// Queued interactive runs park: no terminal event, no recorded partial
|
|
2727
3075
|
// attempt, and the claim releases so another runner can resume.
|