@osolmaz/pi-workflows 0.11.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
- package/dist/builtins/autoimplement-command-batches.js +258 -0
- package/dist/builtins/autoimplement-command-batches.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +29 -3
- package/dist/builtins/autoimplement.workflow.js +876 -231
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +3 -1
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +2 -0
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/sanity-check-session.d.ts +17 -0
- package/dist/builtins/sanity-check-session.js +168 -0
- package/dist/builtins/sanity-check-session.js.map +1 -0
- package/dist/builtins/sanity-check.workflow.d.ts +83 -0
- package/dist/builtins/sanity-check.workflow.js +398 -0
- package/dist/builtins/sanity-check.workflow.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +90 -2
- package/dist/controllers/sqlite.js +380 -5
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.js +7 -7
- package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
- package/dist/extension/deferred-turn-coordinator.js +143 -0
- package/dist/extension/deferred-turn-coordinator.js.map +1 -0
- package/dist/extension/deferred-turn.d.ts +44 -0
- package/dist/extension/deferred-turn.js +110 -0
- package/dist/extension/deferred-turn.js.map +1 -0
- package/dist/extension/index.js +286 -47
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -0
- package/dist/extension/step-message.js.map +1 -1
- package/dist/herdr/setup.js +4 -4
- package/dist/workflows/command-batch.d.ts +38 -0
- package/dist/workflows/command-batch.js +176 -0
- package/dist/workflows/command-batch.js.map +1 -0
- package/dist/workflows/engine.js +8 -4
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -0
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +2 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/shell.d.ts +4 -0
- package/dist/workflows/shell.js +6 -0
- package/dist/workflows/shell.js.map +1 -1
- package/dist/workflows/store.js +3 -1
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +4 -3
- package/docs/2026-08-18-herdr-piw-plan.md +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +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 +7 -7
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +3 -3
- package/docs/MONITOR.md +2 -2
- package/docs/WORKFLOW_COMPOSITION.md +2 -2
- package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
- package/docs/WORKFLOW_UPDATES.md +28 -5
- package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
- package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
- package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
- package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
- package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
- package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
- package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +175 -0
- package/docs/run-bundles.md +3 -1
- package/docs/workflows.md +62 -19
- package/examples/workflows/command-batch.workflow.ts +50 -0
- package/examples/workflows/sanity-check.workflow.ts +1 -0
- package/herdr-plugin.toml +3 -3
- package/package.json +1 -1
- package/schemas/decision-presentation-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v2.schema.json +1 -1
- package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
- package/schemas/human-decision-cancellation-v1.schema.json +1 -1
- package/schemas/human-decision-continuation-v1.schema.json +1 -1
- package/schemas/human-decision-delivery-v1.schema.json +1 -1
- package/schemas/human-decision-delivery-v2.schema.json +1 -1
- package/schemas/human-decision-receipt-v1.schema.json +1 -1
- package/schemas/human-decision-receipt-v2.schema.json +1 -1
- package/schemas/human-decision-request-v1.schema.json +1 -1
- package/schemas/human-decision-request-v2.schema.json +1 -1
- package/schemas/human-decision-resolution-v1.schema.json +1 -1
- package/schemas/human-decision-resolution-v2.schema.json +1 -1
- package/schemas/human-decision-settlement-v1.schema.json +1 -1
- package/skills/autodoc/SKILL.md +33 -2
- package/skills/autoimplement/SKILL.md +66 -12
- package/skills/autoplan/SKILL.md +26 -2
- package/skills/monitor/SKILL.md +35 -21
- package/skills/pi-workflows/SKILL.md +19 -5
- package/src/builtins/autoimplement-command-batches.ts +364 -0
- package/src/builtins/autoimplement.workflow.ts +1065 -261
- package/src/builtins/catalog.ts +3 -1
- package/src/builtins/index.ts +13 -0
- package/src/builtins/sanity-check-session.ts +205 -0
- package/src/builtins/sanity-check.workflow.ts +624 -0
- package/src/controllers/sqlite.ts +580 -7
- package/src/extension/decision-channels.ts +7 -7
- package/src/extension/deferred-turn-coordinator.ts +171 -0
- package/src/extension/deferred-turn.ts +166 -0
- package/src/extension/index.ts +359 -49
- package/src/extension/step-message.ts +1 -0
- package/src/herdr/setup.ts +4 -4
- package/src/workflows/command-batch.ts +254 -0
- package/src/workflows/engine.ts +8 -5
- package/src/workflows/index.ts +15 -0
- package/src/workflows/schema.ts +2 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +3 -1
- package/src/workflows/types.ts +4 -3
|
@@ -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
|
);
|
|
@@ -1163,9 +1163,9 @@ export async function writeDecisionChannelProfile(options: {
|
|
|
1163
1163
|
}
|
|
1164
1164
|
|
|
1165
1165
|
function parseChannelConfig(value: unknown): DecisionChannelConfig {
|
|
1166
|
-
const config = record(value, "
|
|
1166
|
+
const config = record(value, "pi-workflows channel config");
|
|
1167
1167
|
if (config.schema !== "pi-workflows.channels.v1") {
|
|
1168
|
-
throw new Error("
|
|
1168
|
+
throw new Error("pi-workflows channel config schema is invalid");
|
|
1169
1169
|
}
|
|
1170
1170
|
const telegramProfiles: NonNullable<DecisionChannelConfig["telegramProfiles"]> = {};
|
|
1171
1171
|
if (config.telegramProfiles !== undefined) {
|
|
@@ -1191,7 +1191,7 @@ function parseChannelConfig(value: unknown): DecisionChannelConfig {
|
|
|
1191
1191
|
};
|
|
1192
1192
|
}
|
|
1193
1193
|
}
|
|
1194
|
-
const audiencesRaw = record(config.audiences, "
|
|
1194
|
+
const audiencesRaw = record(config.audiences, "pi-workflows audiences");
|
|
1195
1195
|
const audiences: DecisionChannelConfig["audiences"] = {};
|
|
1196
1196
|
for (const [name, raw] of Object.entries(audiencesRaw)) {
|
|
1197
1197
|
requireSimpleId(name, "Decision audience");
|
|
@@ -1219,9 +1219,9 @@ function parseChannelConfig(value: unknown): DecisionChannelConfig {
|
|
|
1219
1219
|
}
|
|
1220
1220
|
|
|
1221
1221
|
function parseCredentialConfig(value: unknown): DecisionCredentialConfig {
|
|
1222
|
-
const config = record(value, "
|
|
1222
|
+
const config = record(value, "pi-workflows credential config");
|
|
1223
1223
|
if (config.schema !== "pi-workflows.credentials.v1") {
|
|
1224
|
-
throw new Error("
|
|
1224
|
+
throw new Error("pi-workflows credential config schema is invalid");
|
|
1225
1225
|
}
|
|
1226
1226
|
const telegram: DecisionCredentialConfig["telegram"] = {};
|
|
1227
1227
|
for (const [name, raw] of Object.entries(record(config.telegram, "Telegram credentials"))) {
|
|
@@ -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
|
+
}
|