@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
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "../workflows/decision-presentation.js";
|
|
15
15
|
import { HumanDecisionStore, createHumanDecisionAttemptId } from "../workflows/human-decision.js";
|
|
16
16
|
import type {
|
|
17
|
-
|
|
17
|
+
ResolvedHumanDecision,
|
|
18
18
|
HumanDecisionAnswerSource,
|
|
19
19
|
HumanDecisionCancellationRecord,
|
|
20
20
|
HumanDecisionChannelRequest,
|
|
@@ -47,7 +47,7 @@ export type HumanDecisionDeliveryResult = {
|
|
|
47
47
|
errorCode?: string;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
export type SettledHumanDecision =
|
|
50
|
+
export type SettledHumanDecision = ResolvedHumanDecision | HumanDecisionCancellationRecord;
|
|
51
51
|
|
|
52
52
|
export interface HumanDecisionChannel {
|
|
53
53
|
readonly id: string;
|
|
@@ -105,13 +105,13 @@ export async function loadDecisionChannelConfig(
|
|
|
105
105
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
106
106
|
throw error;
|
|
107
107
|
}
|
|
108
|
-
await requirePrivateFile(channelPath, "
|
|
108
|
+
await requirePrivateFile(channelPath, "pi-workflows channel profile");
|
|
109
109
|
const channels = parseChannelConfig(channelsRaw);
|
|
110
110
|
const profileNames = Object.keys(channels.telegramProfiles ?? {});
|
|
111
111
|
if (profileNames.length === 0) return { channels, credentials: {}, configDir };
|
|
112
112
|
|
|
113
113
|
const credentialPath = path.join(configDir, "credentials.json");
|
|
114
|
-
await requirePrivateFile(credentialPath, "
|
|
114
|
+
await requirePrivateFile(credentialPath, "pi-workflows credential profile");
|
|
115
115
|
const credentialConfig = parseCredentialConfig(
|
|
116
116
|
JSON.parse(await fsp.readFile(credentialPath, "utf8")),
|
|
117
117
|
);
|
|
@@ -162,7 +162,7 @@ export class PiDecisionChannel implements HumanDecisionChannel {
|
|
|
162
162
|
await this.options.store.recordDelivery(
|
|
163
163
|
request,
|
|
164
164
|
this.id,
|
|
165
|
-
|
|
165
|
+
deliveryRecord(request, this.id, attemptId, "intent", "intent", createdAt),
|
|
166
166
|
);
|
|
167
167
|
const entries = Object.entries(request.choices);
|
|
168
168
|
const selectedChoice = await this.options.ui.custom<string | undefined>(
|
|
@@ -241,7 +241,7 @@ export class PiDecisionChannel implements HumanDecisionChannel {
|
|
|
241
241
|
await this.options.store.recordDelivery(
|
|
242
242
|
request,
|
|
243
243
|
this.id,
|
|
244
|
-
|
|
244
|
+
deliveryRecord(
|
|
245
245
|
request,
|
|
246
246
|
this.id,
|
|
247
247
|
`${attemptId}-cancelled`,
|
|
@@ -267,7 +267,7 @@ export class PiDecisionChannel implements HumanDecisionChannel {
|
|
|
267
267
|
await this.options.store.recordDelivery(
|
|
268
268
|
request,
|
|
269
269
|
this.id,
|
|
270
|
-
|
|
270
|
+
deliveryRecord(
|
|
271
271
|
request,
|
|
272
272
|
this.id,
|
|
273
273
|
`${attemptId}-cancelled`,
|
|
@@ -291,7 +291,7 @@ export class PiDecisionChannel implements HumanDecisionChannel {
|
|
|
291
291
|
await this.options.store.recordDelivery(
|
|
292
292
|
request,
|
|
293
293
|
this.id,
|
|
294
|
-
|
|
294
|
+
deliveryRecord(
|
|
295
295
|
request,
|
|
296
296
|
this.id,
|
|
297
297
|
`${attemptId}-confirmed`,
|
|
@@ -684,7 +684,7 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
684
684
|
await this.store.recordDelivery(
|
|
685
685
|
request,
|
|
686
686
|
channelPath,
|
|
687
|
-
|
|
687
|
+
deliveryRecord(request, this.id, attemptId, "intent", "intent", createdAt, {
|
|
688
688
|
partCount: parts.length,
|
|
689
689
|
}),
|
|
690
690
|
);
|
|
@@ -704,7 +704,6 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
704
704
|
);
|
|
705
705
|
const confirmed = prior.some(
|
|
706
706
|
(record) =>
|
|
707
|
-
record.schema === "pi-workflows.human-decision-delivery.v2" &&
|
|
708
707
|
record.phase === "part" &&
|
|
709
708
|
record.state === "confirmed" &&
|
|
710
709
|
record.recipientIndex === recipientIndex &&
|
|
@@ -724,7 +723,7 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
724
723
|
await this.store.recordDelivery(
|
|
725
724
|
request,
|
|
726
725
|
channelPath,
|
|
727
|
-
|
|
726
|
+
deliveryRecord(
|
|
728
727
|
request,
|
|
729
728
|
this.id,
|
|
730
729
|
`${attemptId}-r${recipientIndex}-p${partIndex}-unknown`,
|
|
@@ -740,15 +739,12 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
740
739
|
await this.store.recordDelivery(
|
|
741
740
|
request,
|
|
742
741
|
channelPath,
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
createdAt,
|
|
750
|
-
{ recipientIndex, partIndex, partCount: parts.length, contentDigest },
|
|
751
|
-
),
|
|
742
|
+
deliveryRecord(request, this.id, `${partAttemptId}-intent`, "part", "intent", createdAt, {
|
|
743
|
+
recipientIndex,
|
|
744
|
+
partIndex,
|
|
745
|
+
partCount: parts.length,
|
|
746
|
+
contentDigest,
|
|
747
|
+
}),
|
|
752
748
|
);
|
|
753
749
|
try {
|
|
754
750
|
const result = await this.call("sendMessage", {
|
|
@@ -769,7 +765,7 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
769
765
|
await this.store.recordDelivery(
|
|
770
766
|
request,
|
|
771
767
|
channelPath,
|
|
772
|
-
|
|
768
|
+
deliveryRecord(
|
|
773
769
|
request,
|
|
774
770
|
this.id,
|
|
775
771
|
`${partAttemptId}-confirmed`,
|
|
@@ -785,7 +781,7 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
785
781
|
await this.store.recordDelivery(
|
|
786
782
|
request,
|
|
787
783
|
channelPath,
|
|
788
|
-
|
|
784
|
+
deliveryRecord(
|
|
789
785
|
request,
|
|
790
786
|
this.id,
|
|
791
787
|
`${partAttemptId}-${state}`,
|
|
@@ -804,7 +800,7 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
804
800
|
await this.store.recordDelivery(
|
|
805
801
|
request,
|
|
806
802
|
channelPath,
|
|
807
|
-
|
|
803
|
+
deliveryRecord(
|
|
808
804
|
request,
|
|
809
805
|
this.id,
|
|
810
806
|
`${attemptId}-${state}`,
|
|
@@ -821,7 +817,7 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
821
817
|
await this.store.recordDelivery(
|
|
822
818
|
request,
|
|
823
819
|
channelPath,
|
|
824
|
-
|
|
820
|
+
deliveryRecord(
|
|
825
821
|
request,
|
|
826
822
|
this.id,
|
|
827
823
|
`${attemptId}-confirmed`,
|
|
@@ -1163,9 +1159,9 @@ export async function writeDecisionChannelProfile(options: {
|
|
|
1163
1159
|
}
|
|
1164
1160
|
|
|
1165
1161
|
function parseChannelConfig(value: unknown): DecisionChannelConfig {
|
|
1166
|
-
const config = record(value, "
|
|
1162
|
+
const config = record(value, "pi-workflows channel config");
|
|
1167
1163
|
if (config.schema !== "pi-workflows.channels.v1") {
|
|
1168
|
-
throw new Error("
|
|
1164
|
+
throw new Error("pi-workflows channel config schema is invalid");
|
|
1169
1165
|
}
|
|
1170
1166
|
const telegramProfiles: NonNullable<DecisionChannelConfig["telegramProfiles"]> = {};
|
|
1171
1167
|
if (config.telegramProfiles !== undefined) {
|
|
@@ -1191,7 +1187,7 @@ function parseChannelConfig(value: unknown): DecisionChannelConfig {
|
|
|
1191
1187
|
};
|
|
1192
1188
|
}
|
|
1193
1189
|
}
|
|
1194
|
-
const audiencesRaw = record(config.audiences, "
|
|
1190
|
+
const audiencesRaw = record(config.audiences, "pi-workflows audiences");
|
|
1195
1191
|
const audiences: DecisionChannelConfig["audiences"] = {};
|
|
1196
1192
|
for (const [name, raw] of Object.entries(audiencesRaw)) {
|
|
1197
1193
|
requireSimpleId(name, "Decision audience");
|
|
@@ -1219,9 +1215,9 @@ function parseChannelConfig(value: unknown): DecisionChannelConfig {
|
|
|
1219
1215
|
}
|
|
1220
1216
|
|
|
1221
1217
|
function parseCredentialConfig(value: unknown): DecisionCredentialConfig {
|
|
1222
|
-
const config = record(value, "
|
|
1218
|
+
const config = record(value, "pi-workflows credential config");
|
|
1223
1219
|
if (config.schema !== "pi-workflows.credentials.v1") {
|
|
1224
|
-
throw new Error("
|
|
1220
|
+
throw new Error("pi-workflows credential config schema is invalid");
|
|
1225
1221
|
}
|
|
1226
1222
|
const telegram: DecisionCredentialConfig["telegram"] = {};
|
|
1227
1223
|
for (const [name, raw] of Object.entries(record(config.telegram, "Telegram credentials"))) {
|
|
@@ -1234,29 +1230,26 @@ function parseCredentialConfig(value: unknown): DecisionCredentialConfig {
|
|
|
1234
1230
|
return { schema: "pi-workflows.credentials.v1", telegram };
|
|
1235
1231
|
}
|
|
1236
1232
|
|
|
1237
|
-
type
|
|
1238
|
-
HumanDecisionDeliveryRecord,
|
|
1239
|
-
{ schema: "pi-workflows.human-decision-delivery.v2" }
|
|
1240
|
-
>;
|
|
1233
|
+
type DeliveryRecord = HumanDecisionDeliveryRecord;
|
|
1241
1234
|
|
|
1242
|
-
type
|
|
1235
|
+
type DeliveryRecordExtra = Partial<
|
|
1243
1236
|
Pick<
|
|
1244
|
-
|
|
1237
|
+
DeliveryRecord,
|
|
1245
1238
|
"recipientIndex" | "partIndex" | "partCount" | "contentDigest" | "messageCount" | "errorCode"
|
|
1246
1239
|
>
|
|
1247
1240
|
>;
|
|
1248
1241
|
|
|
1249
|
-
function
|
|
1242
|
+
function deliveryRecord(
|
|
1250
1243
|
request: HumanDecisionChannelRequest,
|
|
1251
1244
|
channel: string,
|
|
1252
1245
|
attemptId: string,
|
|
1253
|
-
phase:
|
|
1254
|
-
state:
|
|
1246
|
+
phase: DeliveryRecord["phase"],
|
|
1247
|
+
state: DeliveryRecord["state"],
|
|
1255
1248
|
createdAt: string,
|
|
1256
|
-
extra:
|
|
1257
|
-
):
|
|
1249
|
+
extra: DeliveryRecordExtra = {},
|
|
1250
|
+
): DeliveryRecord {
|
|
1258
1251
|
return {
|
|
1259
|
-
schema: "pi-workflows.human-decision-delivery.
|
|
1252
|
+
schema: "pi-workflows.human-decision-delivery.v1",
|
|
1260
1253
|
attemptId,
|
|
1261
1254
|
decisionId: request.decisionId,
|
|
1262
1255
|
requestDigest: request.requestDigest,
|
|
@@ -1271,37 +1264,14 @@ function v2DeliveryRecord(
|
|
|
1271
1264
|
}
|
|
1272
1265
|
|
|
1273
1266
|
function isCompleteDelivery(record: HumanDecisionDeliveryRecord): boolean {
|
|
1274
|
-
return
|
|
1275
|
-
record.state === "confirmed" &&
|
|
1276
|
-
(record.schema === "pi-workflows.human-decision-delivery.v1" || record.phase === "complete")
|
|
1277
|
-
);
|
|
1267
|
+
return record.state === "confirmed" && record.phase === "complete";
|
|
1278
1268
|
}
|
|
1279
1269
|
|
|
1280
1270
|
function hasUnsettledDeliveryIntent(records: HumanDecisionDeliveryRecord[]): boolean {
|
|
1281
|
-
const hasLegacyIntent = records.some(
|
|
1282
|
-
(record) =>
|
|
1283
|
-
record.schema === "pi-workflows.human-decision-delivery.v1" && record.state === "intent",
|
|
1284
|
-
);
|
|
1285
|
-
if (
|
|
1286
|
-
hasLegacyIntent &&
|
|
1287
|
-
!records.some(
|
|
1288
|
-
(record) =>
|
|
1289
|
-
record.schema === "pi-workflows.human-decision-delivery.v1" && record.state !== "intent",
|
|
1290
|
-
)
|
|
1291
|
-
) {
|
|
1292
|
-
return true;
|
|
1293
|
-
}
|
|
1294
1271
|
return records.some((record) => {
|
|
1295
|
-
if (
|
|
1296
|
-
record.schema !== "pi-workflows.human-decision-delivery.v2" ||
|
|
1297
|
-
record.phase !== "part" ||
|
|
1298
|
-
record.state !== "intent"
|
|
1299
|
-
) {
|
|
1300
|
-
return false;
|
|
1301
|
-
}
|
|
1272
|
+
if (record.phase !== "part" || record.state !== "intent") return false;
|
|
1302
1273
|
return !records.some(
|
|
1303
1274
|
(other) =>
|
|
1304
|
-
other.schema === "pi-workflows.human-decision-delivery.v2" &&
|
|
1305
1275
|
other.phase === "part" &&
|
|
1306
1276
|
other.state !== "intent" &&
|
|
1307
1277
|
other.recipientIndex === record.recipientIndex &&
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
SqliteControllerStore,
|
|
4
|
+
WorkflowTurnIntentRecord,
|
|
5
|
+
WorkflowTurnIntentResolution,
|
|
6
|
+
} from "../controllers/sqlite.js";
|
|
7
|
+
import { deferredTurnMessageId } from "./deferred-turn.js";
|
|
8
|
+
|
|
9
|
+
export type BranchIntentResolution = {
|
|
10
|
+
resolution: WorkflowTurnIntentResolution;
|
|
11
|
+
messageId: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type NaturalDelivery = {
|
|
15
|
+
runId: string;
|
|
16
|
+
targetSessionId: string;
|
|
17
|
+
resolution: Exclude<WorkflowTurnIntentResolution, "fallback">;
|
|
18
|
+
send: (turnIntentId?: string) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type FallbackDelivery = {
|
|
22
|
+
targetSessionId: string;
|
|
23
|
+
send: (intent: WorkflowTurnIntentRecord) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class DeferredTurnCoordinator {
|
|
27
|
+
private readonly store: () => SqliteControllerStore | null;
|
|
28
|
+
private readonly branchResolution: (intentId: string) => BranchIntentResolution | null;
|
|
29
|
+
private readonly leaseMs: number;
|
|
30
|
+
private readonly deferredNatural = new Map<string, NaturalDelivery>();
|
|
31
|
+
|
|
32
|
+
constructor(options: {
|
|
33
|
+
store: () => SqliteControllerStore | null;
|
|
34
|
+
branchResolution: (intentId: string) => BranchIntentResolution | null;
|
|
35
|
+
leaseMs: number;
|
|
36
|
+
}) {
|
|
37
|
+
this.store = options.store;
|
|
38
|
+
this.branchResolution = options.branchResolution;
|
|
39
|
+
this.leaseMs = options.leaseMs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
sendNatural(delivery: NaturalDelivery, idle: boolean): "sent" | "deferred" | "suppressed" {
|
|
43
|
+
const store = this.store();
|
|
44
|
+
const intent = store?.findPendingWorkflowTurnIntent({
|
|
45
|
+
runId: delivery.runId,
|
|
46
|
+
targetSessionId: delivery.targetSessionId,
|
|
47
|
+
});
|
|
48
|
+
if (store === null || store === undefined || intent === undefined) {
|
|
49
|
+
delivery.send();
|
|
50
|
+
return "sent";
|
|
51
|
+
}
|
|
52
|
+
if (!idle) {
|
|
53
|
+
this.deferredNatural.set(intent.intentId, delivery);
|
|
54
|
+
return "deferred";
|
|
55
|
+
}
|
|
56
|
+
const result = this.deliverNatural(intent.intentId, delivery);
|
|
57
|
+
if (result === "deferred") this.deferredNatural.set(intent.intentId, delivery);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
flushNatural(idle: boolean): number {
|
|
62
|
+
if (!idle) return 0;
|
|
63
|
+
let sent = 0;
|
|
64
|
+
for (const [intentId, delivery] of this.deferredNatural) {
|
|
65
|
+
const result = this.deliverNatural(intentId, delivery);
|
|
66
|
+
if (result !== "deferred") {
|
|
67
|
+
this.deferredNatural.delete(intentId);
|
|
68
|
+
}
|
|
69
|
+
if (result === "sent") sent += 1;
|
|
70
|
+
}
|
|
71
|
+
return sent;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
deliverFallbacks(delivery: FallbackDelivery, idle: boolean): number {
|
|
75
|
+
const store = this.store();
|
|
76
|
+
if (!idle || store === null) return 0;
|
|
77
|
+
const claimToken = randomUUID();
|
|
78
|
+
const intents = store.claimEligibleWorkflowTurnIntents({
|
|
79
|
+
targetSessionId: delivery.targetSessionId,
|
|
80
|
+
claimToken,
|
|
81
|
+
leaseMs: this.leaseMs,
|
|
82
|
+
});
|
|
83
|
+
let sent = 0;
|
|
84
|
+
for (const [index, intent] of intents.entries()) {
|
|
85
|
+
const branch = this.branchResolution(intent.intentId);
|
|
86
|
+
if (branch !== null) {
|
|
87
|
+
store.resolveWorkflowTurnIntent({
|
|
88
|
+
intentId: intent.intentId,
|
|
89
|
+
targetSessionId: delivery.targetSessionId,
|
|
90
|
+
claimToken,
|
|
91
|
+
resolution: branch.resolution,
|
|
92
|
+
messageId: branch.messageId,
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
delivery.send(intent);
|
|
98
|
+
store.resolveWorkflowTurnIntent({
|
|
99
|
+
intentId: intent.intentId,
|
|
100
|
+
targetSessionId: delivery.targetSessionId,
|
|
101
|
+
claimToken,
|
|
102
|
+
resolution: "fallback",
|
|
103
|
+
messageId: deferredTurnMessageId(intent.intentId, "fallback"),
|
|
104
|
+
});
|
|
105
|
+
sent += 1;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
for (const pending of intents.slice(index)) {
|
|
108
|
+
store.releaseWorkflowTurnIntentClaim({
|
|
109
|
+
intentId: pending.intentId,
|
|
110
|
+
targetSessionId: delivery.targetSessionId,
|
|
111
|
+
claimToken,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return sent;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
clearDeferred(): void {
|
|
121
|
+
this.deferredNatural.clear();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private deliverNatural(
|
|
125
|
+
intentId: string,
|
|
126
|
+
delivery: NaturalDelivery,
|
|
127
|
+
): "sent" | "deferred" | "suppressed" {
|
|
128
|
+
const store = this.store();
|
|
129
|
+
if (store === null) return "deferred";
|
|
130
|
+
const claimToken = randomUUID();
|
|
131
|
+
const intent = store.claimWorkflowTurnIntent({
|
|
132
|
+
intentId,
|
|
133
|
+
targetSessionId: delivery.targetSessionId,
|
|
134
|
+
claimToken,
|
|
135
|
+
leaseMs: this.leaseMs,
|
|
136
|
+
});
|
|
137
|
+
if (intent === undefined) {
|
|
138
|
+
const current = store.getWorkflowTurnIntent(intentId);
|
|
139
|
+
return current?.resolvedAt === null ? "deferred" : "suppressed";
|
|
140
|
+
}
|
|
141
|
+
const branch = this.branchResolution(intent.intentId);
|
|
142
|
+
if (branch !== null) {
|
|
143
|
+
store.resolveWorkflowTurnIntent({
|
|
144
|
+
intentId: intent.intentId,
|
|
145
|
+
targetSessionId: delivery.targetSessionId,
|
|
146
|
+
claimToken,
|
|
147
|
+
resolution: branch.resolution,
|
|
148
|
+
messageId: branch.messageId,
|
|
149
|
+
});
|
|
150
|
+
return "suppressed";
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
delivery.send(intent.intentId);
|
|
154
|
+
store.resolveWorkflowTurnIntent({
|
|
155
|
+
intentId: intent.intentId,
|
|
156
|
+
targetSessionId: delivery.targetSessionId,
|
|
157
|
+
claimToken,
|
|
158
|
+
resolution: delivery.resolution,
|
|
159
|
+
messageId: deferredTurnMessageId(intent.intentId, delivery.resolution),
|
|
160
|
+
});
|
|
161
|
+
return "sent";
|
|
162
|
+
} catch (error) {
|
|
163
|
+
store.releaseWorkflowTurnIntentClaim({
|
|
164
|
+
intentId: intent.intentId,
|
|
165
|
+
targetSessionId: delivery.targetSessionId,
|
|
166
|
+
claimToken,
|
|
167
|
+
});
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
WorkflowTurnIntentCause,
|
|
4
|
+
WorkflowTurnIntentFacts,
|
|
5
|
+
WorkflowTurnIntentRecord,
|
|
6
|
+
WorkflowTurnIntentResolution,
|
|
7
|
+
} from "../controllers/sqlite.js";
|
|
8
|
+
|
|
9
|
+
export const DEFERRED_TURN_MESSAGE_TYPE = "pi-workflows-deferred-turn";
|
|
10
|
+
export const DEFERRED_TURN_MESSAGE_SCHEMA = "pi-workflows.deferred-turn-message.v1";
|
|
11
|
+
const FACTS_SCHEMA = "pi-workflows.deferred-turn-facts.v1";
|
|
12
|
+
const MAX_REASON_CHARS = 8_192;
|
|
13
|
+
|
|
14
|
+
export type DeferredTurnSource = {
|
|
15
|
+
runId: string;
|
|
16
|
+
workflowName: string;
|
|
17
|
+
targetSessionId: string;
|
|
18
|
+
cause: WorkflowTurnIntentCause;
|
|
19
|
+
sourceEventId: string;
|
|
20
|
+
observedState: string;
|
|
21
|
+
nodeId?: string | null;
|
|
22
|
+
attemptId?: string | null;
|
|
23
|
+
reason?: string | null;
|
|
24
|
+
handoff?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type DeferredTurnDescriptor = {
|
|
28
|
+
intentId: string;
|
|
29
|
+
sourceEventId: string;
|
|
30
|
+
runId: string;
|
|
31
|
+
workflowRef: string;
|
|
32
|
+
targetSessionId: string;
|
|
33
|
+
cause: WorkflowTurnIntentCause;
|
|
34
|
+
nodeId: string | null;
|
|
35
|
+
attemptId: string | null;
|
|
36
|
+
fallbackFacts: WorkflowTurnIntentFacts;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type DeferredTurnMessageDetails = {
|
|
40
|
+
schema: typeof DEFERRED_TURN_MESSAGE_SCHEMA;
|
|
41
|
+
turnIntentId: string;
|
|
42
|
+
runId: string;
|
|
43
|
+
cause: WorkflowTurnIntentCause;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function deferredTurnSourceEventId(options: {
|
|
47
|
+
runId: string;
|
|
48
|
+
cause: WorkflowTurnIntentCause;
|
|
49
|
+
nodeId?: string | null;
|
|
50
|
+
attemptId?: string | null;
|
|
51
|
+
source?: string;
|
|
52
|
+
}): string {
|
|
53
|
+
const parts = [
|
|
54
|
+
options.runId,
|
|
55
|
+
options.source ?? "run",
|
|
56
|
+
options.nodeId ?? "$run",
|
|
57
|
+
options.attemptId ?? "$none",
|
|
58
|
+
options.cause,
|
|
59
|
+
];
|
|
60
|
+
return `turn-source:${digest(parts)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createDeferredTurnDescriptor(source: DeferredTurnSource): DeferredTurnDescriptor {
|
|
64
|
+
const nodeId = source.nodeId ?? null;
|
|
65
|
+
const attemptId = source.attemptId ?? null;
|
|
66
|
+
const fallbackFacts = createDeferredTurnFacts(source);
|
|
67
|
+
return {
|
|
68
|
+
intentId: `deferred-turn:${digest([
|
|
69
|
+
source.targetSessionId,
|
|
70
|
+
source.runId,
|
|
71
|
+
source.sourceEventId,
|
|
72
|
+
nodeId ?? "$run",
|
|
73
|
+
attemptId ?? "$none",
|
|
74
|
+
source.cause,
|
|
75
|
+
])}`,
|
|
76
|
+
sourceEventId: source.sourceEventId,
|
|
77
|
+
runId: source.runId,
|
|
78
|
+
workflowRef: source.workflowName,
|
|
79
|
+
targetSessionId: source.targetSessionId,
|
|
80
|
+
cause: source.cause,
|
|
81
|
+
nodeId,
|
|
82
|
+
attemptId,
|
|
83
|
+
fallbackFacts,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createDeferredTurnFacts(source: DeferredTurnSource): WorkflowTurnIntentFacts {
|
|
88
|
+
return {
|
|
89
|
+
schema: FACTS_SCHEMA,
|
|
90
|
+
workflowName: source.workflowName,
|
|
91
|
+
runId: source.runId,
|
|
92
|
+
observedState: source.observedState,
|
|
93
|
+
cause: source.cause,
|
|
94
|
+
nodeId: source.nodeId ?? null,
|
|
95
|
+
attemptId: source.attemptId ?? null,
|
|
96
|
+
reason: boundedReason(source.reason),
|
|
97
|
+
handoff: source.handoff ?? false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function deferredTurnMessageId(
|
|
102
|
+
intentId: string,
|
|
103
|
+
resolution: WorkflowTurnIntentResolution,
|
|
104
|
+
): string {
|
|
105
|
+
return `turn-message:${digest([intentId, resolution])}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function deferredTurnMessageDetails(
|
|
109
|
+
intent: WorkflowTurnIntentRecord,
|
|
110
|
+
): DeferredTurnMessageDetails {
|
|
111
|
+
return {
|
|
112
|
+
schema: DEFERRED_TURN_MESSAGE_SCHEMA,
|
|
113
|
+
turnIntentId: intent.intentId,
|
|
114
|
+
runId: intent.runId,
|
|
115
|
+
cause: intent.cause,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildDeferredTurnContent(intent: WorkflowTurnIntentRecord): string {
|
|
120
|
+
const facts = intent.fallbackFacts;
|
|
121
|
+
const source = [
|
|
122
|
+
facts.nodeId === null ? null : `node ${facts.nodeId}`,
|
|
123
|
+
facts.attemptId === null ? null : `attempt ${facts.attemptId}`,
|
|
124
|
+
]
|
|
125
|
+
.filter((part): part is string => part !== null)
|
|
126
|
+
.join(", ");
|
|
127
|
+
const reason =
|
|
128
|
+
facts.reason === null
|
|
129
|
+
? ""
|
|
130
|
+
: ` Reason: ${facts.reason}${/[.!?]$/.test(facts.reason) ? "" : "."}`;
|
|
131
|
+
const sourceText = source.length === 0 ? "" : ` Source: ${source}.`;
|
|
132
|
+
if (facts.cause === "launchFailed") {
|
|
133
|
+
return [
|
|
134
|
+
`Workflow ${facts.workflowName} failed to start (run ${facts.runId}).`,
|
|
135
|
+
`${sourceText}${reason}`.trim(),
|
|
136
|
+
"Inspect the durable workflow state before you explain the outcome or start a corrected workflow.",
|
|
137
|
+
]
|
|
138
|
+
.filter((part) => part.length > 0)
|
|
139
|
+
.join(" ");
|
|
140
|
+
}
|
|
141
|
+
if (facts.handoff) {
|
|
142
|
+
return [
|
|
143
|
+
`Workflow ${facts.workflowName} was handed to another runner (run ${facts.runId}).`,
|
|
144
|
+
`Observed state: ${facts.observedState}.${sourceText}${reason}`,
|
|
145
|
+
"Inspect the durable workflow state before you explain the outcome or take any authorized corrective action.",
|
|
146
|
+
].join(" ");
|
|
147
|
+
}
|
|
148
|
+
return [
|
|
149
|
+
`Workflow ${facts.workflowName} ended with state ${facts.observedState} (run ${facts.runId}).`,
|
|
150
|
+
`${sourceText}${reason}`.trim(),
|
|
151
|
+
"Inspect the durable workflow state before you explain the outcome or take any authorized corrective action.",
|
|
152
|
+
]
|
|
153
|
+
.filter((part) => part.length > 0)
|
|
154
|
+
.join(" ");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function boundedReason(value: string | null | undefined): string | null {
|
|
158
|
+
if (value === null || value === undefined) return null;
|
|
159
|
+
const trimmed = value.trim();
|
|
160
|
+
if (trimmed.length === 0) return null;
|
|
161
|
+
return trimmed.length <= MAX_REASON_CHARS ? trimmed : trimmed.slice(0, MAX_REASON_CHARS);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function digest(parts: readonly string[]): string {
|
|
165
|
+
return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
166
|
+
}
|