@osolmaz/pi-workflows 0.16.4 → 0.16.6
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 +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +5 -1
- package/dist/builtins/autoimplement.workflow.js +29 -10
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/client/client.js +3 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/protocol.d.ts +1 -1
- package/dist/client/protocol.js +18 -1
- package/dist/client/protocol.js.map +1 -1
- package/dist/client/view.d.ts +7 -5
- package/dist/extension/index.d.ts +2 -1
- package/dist/extension/index.js +115 -165
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +1 -1
- package/dist/extension/recorder.js +2 -2
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -1
- package/dist/extension/step-message.js +4 -4
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +12 -1
- package/dist/extension/workflow-message-coordinator.js +65 -54
- package/dist/extension/workflow-message-coordinator.js.map +1 -1
- package/dist/resource-managers/index.d.ts +1 -2
- package/dist/resource-managers/index.js +1 -2
- package/dist/resource-managers/index.js.map +1 -1
- package/dist/resource-managers/sqlite.d.ts +2 -295
- package/dist/resource-managers/sqlite.js +71 -1528
- package/dist/resource-managers/sqlite.js.map +1 -1
- package/dist/resource-managers/types.d.ts +2 -7
- package/dist/server/resource-runner-entry.js +2 -6
- package/dist/server/resource-runner-entry.js.map +1 -1
- package/dist/server/rpc-bridge.js +1 -1
- package/dist/server/rpc-bridge.js.map +1 -1
- package/dist/server/rpc-executor.js +2 -4
- package/dist/server/rpc-executor.js.map +1 -1
- package/dist/server/server-entry.js +3 -0
- package/dist/server/server-entry.js.map +1 -1
- package/dist/server/server.d.ts +22 -8
- package/dist/server/server.js +498 -543
- package/dist/server/server.js.map +1 -1
- package/dist/server/state.d.ts +16 -29
- package/dist/server/state.js +105 -319
- package/dist/server/state.js.map +1 -1
- package/dist/server/view.d.ts +4 -5
- package/dist/server/view.js +51 -51
- package/dist/server/view.js.map +1 -1
- package/dist/server/workflow-runner-entry.d.ts +1 -1
- package/dist/server/workflow-runner-entry.js +1 -49
- package/dist/server/workflow-runner-entry.js.map +1 -1
- package/dist/server/workflow-runner-protocol.d.ts +2 -7
- package/dist/server/workflow-runner-protocol.js +5 -10
- package/dist/server/workflow-runner-protocol.js.map +1 -1
- package/dist/server/workflow-runner-store.d.ts +5 -6
- package/dist/server/workflow-runner-store.js +7 -14
- package/dist/server/workflow-runner-store.js.map +1 -1
- package/dist/state/attempt-time.d.ts +21 -0
- package/dist/state/attempt-time.js +95 -0
- package/dist/state/attempt-time.js.map +1 -0
- package/dist/state/json.d.ts +2 -1
- package/dist/state/json.js.map +1 -1
- package/dist/state/project-store.d.ts +39 -0
- package/dist/state/project-store.js +154 -0
- package/dist/state/project-store.js.map +1 -0
- package/dist/state/prune.d.ts +28 -0
- package/dist/state/prune.js +278 -83
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +23 -23
- package/dist/state/workflow-messages.d.ts +1 -1
- package/dist/state/workflow-messages.js +3 -1
- package/dist/state/workflow-messages.js.map +1 -1
- package/dist/viewer/backup.js +5 -0
- package/dist/viewer/backup.js.map +1 -1
- package/dist/workflows/diagnostics.d.ts +8 -0
- package/dist/workflows/diagnostics.js +61 -0
- package/dist/workflows/diagnostics.js.map +1 -0
- package/dist/workflows/engine.d.ts +2 -15
- package/dist/workflows/engine.js +125 -245
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +3 -7
- package/dist/workflows/human-decision.js +2 -56
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +2 -1
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/queue.d.ts +267 -0
- package/dist/workflows/queue.js +1248 -0
- package/dist/workflows/queue.js.map +1 -0
- package/dist/workflows/requests.d.ts +47 -0
- package/dist/workflows/requests.js +158 -0
- package/dist/workflows/requests.js.map +1 -0
- package/dist/workflows/schema.js +2 -4
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +11 -16
- package/dist/workflows/store.js +174 -169
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +6 -9
- package/dist/workflows/tool-input.js +15 -12
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/transitions.d.ts +38 -0
- package/dist/workflows/transitions.js +276 -0
- package/dist/workflows/transitions.js.map +1 -0
- package/dist/workflows/types.d.ts +9 -42
- package/dist/workflows/workflow-message-content.d.ts +7 -1
- package/dist/workflows/workflow-message-content.js +22 -4
- package/dist/workflows/workflow-message-content.js.map +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +5 -5
- package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
- package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
- package/docs/2026-09-06-durable-execution-plan.md +176 -0
- package/docs/DEFERRED_TURNS.md +84 -105
- package/docs/HUMAN_DECISIONS.md +17 -19
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
- package/docs/SQLITE_STATE.md +87 -30
- package/docs/WORKFLOW_SERVER.md +50 -11
- package/docs/WORKFLOW_STEP_MESSAGES.md +72 -54
- package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
- package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
- package/docs/plans/2026-09-04-automatic-state-retention-plan.md +283 -0
- package/docs/workflows.md +109 -44
- package/examples/workflows/autoresearch.workflow.ts +0 -2
- package/examples/workflows/branch.workflow.ts +0 -4
- package/examples/workflows/echo.workflow.ts +0 -2
- package/examples/workflows/live-settings.workflow.ts +0 -2
- package/examples/workflows/two-turn.workflow.ts +0 -2
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/protocol/fixtures/client-v1.json +1 -1
- package/skills/pi-workflows/SKILL.md +8 -7
- package/src/builtins/autoimplement.workflow.ts +31 -10
- package/src/client/client.ts +3 -1
- package/src/client/protocol.ts +21 -1
- package/src/client/view.ts +9 -5
- package/src/extension/index.ts +117 -189
- package/src/extension/recorder.ts +2 -2
- package/src/extension/step-message.ts +4 -4
- package/src/extension/workflow-message-coordinator.ts +78 -50
- package/src/resource-managers/index.ts +1 -9
- package/src/resource-managers/sqlite.ts +330 -2655
- package/src/resource-managers/types.ts +2 -3
- package/src/server/resource-runner-entry.ts +2 -7
- package/src/server/rpc-bridge.ts +1 -1
- package/src/server/rpc-executor.ts +4 -9
- package/src/server/server-entry.ts +3 -0
- package/src/server/server.ts +568 -644
- package/src/server/state.ts +122 -466
- package/src/server/view.ts +56 -60
- package/src/server/workflow-runner-entry.ts +2 -55
- package/src/server/workflow-runner-protocol.ts +5 -15
- package/src/server/workflow-runner-store.ts +12 -27
- package/src/state/attempt-time.ts +122 -0
- package/src/state/json.ts +2 -1
- package/src/state/project-store.ts +218 -0
- package/src/state/prune.ts +408 -95
- package/src/state/schema.ts +23 -23
- package/src/state/workflow-messages.ts +4 -2
- package/src/viewer/backup.ts +7 -0
- package/src/workflows/diagnostics.ts +78 -0
- package/src/workflows/engine.ts +133 -291
- package/src/workflows/human-decision.ts +3 -81
- package/src/workflows/index.ts +8 -2
- package/src/workflows/queue.ts +2091 -0
- package/src/workflows/requests.ts +283 -0
- package/src/workflows/schema.ts +2 -6
- package/src/workflows/store.ts +223 -231
- package/src/workflows/tool-input.ts +20 -12
- package/src/workflows/transitions.ts +330 -0
- package/src/workflows/types.ts +9 -46
- package/src/workflows/workflow-message-content.ts +28 -4
- package/dist/resource-managers/runtime.d.ts +0 -59
- package/dist/resource-managers/runtime.js +0 -423
- package/dist/resource-managers/runtime.js.map +0 -1
- package/src/resource-managers/runtime.ts +0 -576
|
@@ -13,13 +13,12 @@ const inputSchema = Type.Unknown({
|
|
|
13
13
|
});
|
|
14
14
|
const runIdSchema = Type.String({
|
|
15
15
|
description:
|
|
16
|
-
"Run id; required for restart and optional for status, cancel,
|
|
16
|
+
"Run id; required for restart and optional for status, cancel, settings, and follow-ups",
|
|
17
17
|
});
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
description: "Workflow attempt id; required when action is update or submit",
|
|
18
|
+
const requestIdSchema = Type.String({
|
|
19
|
+
minLength: 1,
|
|
20
|
+
description:
|
|
21
|
+
"Exact durable request id from the step contract or checkpoint; required for answer, update, and submit",
|
|
23
22
|
});
|
|
24
23
|
const updateSchema = Type.Object(
|
|
25
24
|
{
|
|
@@ -45,7 +44,18 @@ export const WorkflowActionSchemas = {
|
|
|
45
44
|
},
|
|
46
45
|
noExtraProperties,
|
|
47
46
|
),
|
|
48
|
-
restart: Type.Object(
|
|
47
|
+
restart: Type.Object(
|
|
48
|
+
{
|
|
49
|
+
action: Type.Literal("restart"),
|
|
50
|
+
runId: runIdSchema,
|
|
51
|
+
expectedRevision: Type.Integer({
|
|
52
|
+
minimum: 0,
|
|
53
|
+
description:
|
|
54
|
+
"Use runRevision from the terminal run view, not its presentation revision. Restart only when the user explicitly requests a fresh run.",
|
|
55
|
+
}),
|
|
56
|
+
},
|
|
57
|
+
noExtraProperties,
|
|
58
|
+
),
|
|
49
59
|
"change-settings": Type.Object(
|
|
50
60
|
{
|
|
51
61
|
action: Type.Literal("change-settings"),
|
|
@@ -85,16 +95,15 @@ export const WorkflowActionSchemas = {
|
|
|
85
95
|
answer: Type.Object(
|
|
86
96
|
{
|
|
87
97
|
action: Type.Literal("answer"),
|
|
98
|
+
requestId: requestIdSchema,
|
|
88
99
|
input: inputSchema,
|
|
89
|
-
runId: Type.Optional(runIdSchema),
|
|
90
100
|
},
|
|
91
101
|
noExtraProperties,
|
|
92
102
|
),
|
|
93
103
|
update: Type.Object(
|
|
94
104
|
{
|
|
95
105
|
action: Type.Literal("update"),
|
|
96
|
-
|
|
97
|
-
attempt: attemptSchema,
|
|
106
|
+
requestId: requestIdSchema,
|
|
98
107
|
update: updateSchema,
|
|
99
108
|
},
|
|
100
109
|
noExtraProperties,
|
|
@@ -102,8 +111,7 @@ export const WorkflowActionSchemas = {
|
|
|
102
111
|
submit: Type.Object(
|
|
103
112
|
{
|
|
104
113
|
action: Type.Literal("submit"),
|
|
105
|
-
|
|
106
|
-
attempt: attemptSchema,
|
|
114
|
+
requestId: requestIdSchema,
|
|
107
115
|
output: outputSchema,
|
|
108
116
|
},
|
|
109
117
|
noExtraProperties,
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import type { JsonValue } from "../state/json.js";
|
|
3
|
+
import { resolveNext, resolveNextForOutcome } from "./graph.js";
|
|
4
|
+
import type {
|
|
5
|
+
WorkflowDefinitionSnapshot,
|
|
6
|
+
WorkflowRunState,
|
|
7
|
+
WorkflowStepRecord,
|
|
8
|
+
WorkflowTraceEventDraft,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
/** Worker proposals carry one change, never a replacement run projection. */
|
|
12
|
+
export type WorkflowTransition = {
|
|
13
|
+
event: WorkflowTraceEventDraft;
|
|
14
|
+
} & (
|
|
15
|
+
| { kind: "startAttempt"; startedAt: string }
|
|
16
|
+
| { kind: "finishAttempt"; step: WorkflowStepRecord }
|
|
17
|
+
| { kind: "setTimeout"; timeoutMs: number | null }
|
|
18
|
+
| { kind: "resume"; attemptId?: string }
|
|
19
|
+
| { kind: "pause" }
|
|
20
|
+
| { kind: "resumeInteraction" }
|
|
21
|
+
| {
|
|
22
|
+
kind: "waitForInput";
|
|
23
|
+
requestId: string;
|
|
24
|
+
requestKind: "checkpoint" | "decision";
|
|
25
|
+
contract: JsonValue;
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
kind: "finish";
|
|
29
|
+
status: "completed" | "failed" | "timed_out" | "cancelled";
|
|
30
|
+
error?: string;
|
|
31
|
+
finalOutput?: unknown;
|
|
32
|
+
}
|
|
33
|
+
| { kind: "record" }
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
/** Translate engine-local state into the narrow proposal for this event. */
|
|
37
|
+
export function executionTransition(
|
|
38
|
+
state: WorkflowRunState,
|
|
39
|
+
event: WorkflowTraceEventDraft,
|
|
40
|
+
): WorkflowTransition {
|
|
41
|
+
switch (event.type) {
|
|
42
|
+
case "node_started":
|
|
43
|
+
if (state.currentNodeStartedAt === undefined)
|
|
44
|
+
throw new Error("Attempt start time is missing");
|
|
45
|
+
return { kind: "startAttempt", startedAt: state.currentNodeStartedAt, event };
|
|
46
|
+
case "node_finished":
|
|
47
|
+
case "node_failed": {
|
|
48
|
+
const step = state.steps.at(-1);
|
|
49
|
+
if (step === undefined || step.attemptId !== event.attemptId) {
|
|
50
|
+
throw new Error("Finished attempt is missing from engine state");
|
|
51
|
+
}
|
|
52
|
+
return { kind: "finishAttempt", step, event };
|
|
53
|
+
}
|
|
54
|
+
case "node_timeout_set":
|
|
55
|
+
return { kind: "setTimeout", timeoutMs: state.currentNodeTimeoutMs ?? null, event };
|
|
56
|
+
case "run_resumed":
|
|
57
|
+
return {
|
|
58
|
+
kind: "resume",
|
|
59
|
+
...(state.currentAttemptId === undefined ? {} : { attemptId: state.currentAttemptId }),
|
|
60
|
+
event,
|
|
61
|
+
};
|
|
62
|
+
case "run_paused":
|
|
63
|
+
return { kind: "pause", event };
|
|
64
|
+
case "interaction_validation_started":
|
|
65
|
+
case "interaction_accepted":
|
|
66
|
+
return { kind: "resumeInteraction", event };
|
|
67
|
+
case "run_completed":
|
|
68
|
+
case "run_failed":
|
|
69
|
+
case "run_interrupted":
|
|
70
|
+
case "run_timed_out":
|
|
71
|
+
case "run_cancelled":
|
|
72
|
+
if (state.status === "running" || state.status === "waiting")
|
|
73
|
+
throw new Error("Terminal transition has unfinished execution");
|
|
74
|
+
return {
|
|
75
|
+
kind: "finish",
|
|
76
|
+
status: state.status,
|
|
77
|
+
...(state.error === undefined ? {} : { error: state.error }),
|
|
78
|
+
...(state.finalOutput === undefined ? {} : { finalOutput: state.finalOutput }),
|
|
79
|
+
event,
|
|
80
|
+
};
|
|
81
|
+
default:
|
|
82
|
+
return { kind: "record", event };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const recordEvents = new Set([
|
|
87
|
+
"run_started",
|
|
88
|
+
"include_entered",
|
|
89
|
+
"include_exited",
|
|
90
|
+
"settings_route_retried",
|
|
91
|
+
"agent_session_required",
|
|
92
|
+
"agent_prompt_sent",
|
|
93
|
+
"interaction_resume_prepared",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/** Apply a validated transition to a projection read by the durable owner. */
|
|
97
|
+
export function applyExecutionTransition(
|
|
98
|
+
current: WorkflowRunState,
|
|
99
|
+
definition: WorkflowDefinitionSnapshot,
|
|
100
|
+
transition: WorkflowTransition,
|
|
101
|
+
now: string,
|
|
102
|
+
): WorkflowRunState {
|
|
103
|
+
if (
|
|
104
|
+
current.status !== "running" &&
|
|
105
|
+
!(current.status === "waiting" && transition.kind === "resumeInteraction")
|
|
106
|
+
) {
|
|
107
|
+
throw new Error(`Run ${current.runId} cannot execute with status ${current.status}`);
|
|
108
|
+
}
|
|
109
|
+
const state = structuredClone(current);
|
|
110
|
+
const event = transition.event;
|
|
111
|
+
if (typeof event !== "object" || event === null || typeof event.type !== "string") {
|
|
112
|
+
throw new Error("Execution transition event is invalid");
|
|
113
|
+
}
|
|
114
|
+
switch (transition.kind) {
|
|
115
|
+
case "startAttempt": {
|
|
116
|
+
requireEvent(event, "node_started");
|
|
117
|
+
const nodeId = requireIdentity(event.nodeId, "node");
|
|
118
|
+
const attemptId = requireIdentity(event.attemptId, "attempt");
|
|
119
|
+
const node = definition.nodes[nodeId];
|
|
120
|
+
if (node === undefined || node.nodeType !== event.payload.nodeType) {
|
|
121
|
+
throw new Error("Attempt node does not match the workflow definition");
|
|
122
|
+
}
|
|
123
|
+
if (state.steps.some((step) => step.attemptId === attemptId)) {
|
|
124
|
+
throw new Error("A completed attempt cannot start again");
|
|
125
|
+
}
|
|
126
|
+
if (state.currentAttemptId !== undefined) {
|
|
127
|
+
throw new Error("Another attempt is already active");
|
|
128
|
+
}
|
|
129
|
+
if (nextNode(definition, state) !== nodeId) {
|
|
130
|
+
throw new Error("Attempt does not follow the accepted graph route");
|
|
131
|
+
}
|
|
132
|
+
if (!Number.isFinite(Date.parse(transition.startedAt)))
|
|
133
|
+
throw new Error("Invalid attempt time");
|
|
134
|
+
state.currentNode = nodeId;
|
|
135
|
+
state.currentAttemptId = attemptId;
|
|
136
|
+
state.currentNodeStartedAt = transition.startedAt;
|
|
137
|
+
setSettings(state, event.payload);
|
|
138
|
+
if (node.statusDetail !== undefined) state.statusDetail = node.statusDetail;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "finishAttempt": {
|
|
142
|
+
const step = transition.step;
|
|
143
|
+
requireEvent(event, step.outcome === "ok" ? "node_finished" : "node_failed");
|
|
144
|
+
requireAttempt(state, event);
|
|
145
|
+
if (
|
|
146
|
+
step.attemptId !== event.attemptId ||
|
|
147
|
+
step.nodeId !== event.nodeId ||
|
|
148
|
+
step.nodeType !== definition.nodes[step.nodeId]?.nodeType ||
|
|
149
|
+
step.startedAt !== state.currentNodeStartedAt ||
|
|
150
|
+
step.outcome !== event.payload.outcome ||
|
|
151
|
+
!isDeepStrictEqual(step.output, event.payload.output ?? null)
|
|
152
|
+
) {
|
|
153
|
+
throw new Error("Attempt result does not match the active request");
|
|
154
|
+
}
|
|
155
|
+
if (state.steps.some((prior) => prior.attemptId === step.attemptId)) {
|
|
156
|
+
throw new Error("Attempt result was already committed");
|
|
157
|
+
}
|
|
158
|
+
state.steps.push(structuredClone(step));
|
|
159
|
+
state.results[step.nodeId] = {
|
|
160
|
+
...step,
|
|
161
|
+
durationMs: Date.parse(step.finishedAt) - Date.parse(step.startedAt),
|
|
162
|
+
};
|
|
163
|
+
if (step.outcome === "ok") state.outputs[step.nodeId] = step.output;
|
|
164
|
+
clearAttempt(state);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case "setTimeout":
|
|
168
|
+
requireEvent(event, "node_timeout_set");
|
|
169
|
+
requireAttempt(state, event);
|
|
170
|
+
if (
|
|
171
|
+
transition.timeoutMs !== null &&
|
|
172
|
+
(!Number.isFinite(transition.timeoutMs) || transition.timeoutMs <= 0)
|
|
173
|
+
) {
|
|
174
|
+
throw new Error("Invalid attempt timeout");
|
|
175
|
+
}
|
|
176
|
+
if (state.currentNodeElapsedMs !== undefined)
|
|
177
|
+
throw new Error("Attempt timeout was already configured");
|
|
178
|
+
state.currentNodeTimeoutMs = transition.timeoutMs;
|
|
179
|
+
state.currentNodeElapsedMs = 0;
|
|
180
|
+
break;
|
|
181
|
+
case "resume":
|
|
182
|
+
requireEvent(event, "run_resumed");
|
|
183
|
+
if (transition.attemptId !== undefined && transition.attemptId !== state.currentAttemptId) {
|
|
184
|
+
throw new Error("Resume does not name the active attempt");
|
|
185
|
+
}
|
|
186
|
+
if (transition.attemptId === undefined) clearAttempt(state);
|
|
187
|
+
delete state.paused;
|
|
188
|
+
break;
|
|
189
|
+
case "waitForInput":
|
|
190
|
+
requireEvent(event, "checkpoint_requested");
|
|
191
|
+
requireAttempt(state, event);
|
|
192
|
+
if (definition.nodes[state.currentNode ?? ""]?.nodeType !== "checkpoint") {
|
|
193
|
+
throw new Error("Only a checkpoint can request checkpoint input");
|
|
194
|
+
}
|
|
195
|
+
state.status = "waiting";
|
|
196
|
+
state.waitingOn = requireIdentity(event.nodeId, "node");
|
|
197
|
+
state.statusDetail =
|
|
198
|
+
transition.requestKind === "decision"
|
|
199
|
+
? "waiting for a human decision"
|
|
200
|
+
: "waiting for checkpoint input";
|
|
201
|
+
delete state.finishedAt;
|
|
202
|
+
break;
|
|
203
|
+
case "resumeInteraction":
|
|
204
|
+
if (
|
|
205
|
+
event.type !== "interaction_validation_started" &&
|
|
206
|
+
event.type !== "interaction_accepted"
|
|
207
|
+
) {
|
|
208
|
+
throw new Error("Invalid interaction resume event");
|
|
209
|
+
}
|
|
210
|
+
if (state.waitingOn !== event.nodeId || state.currentAttemptId !== event.attemptId) {
|
|
211
|
+
throw new Error("Interaction does not match the waiting attempt");
|
|
212
|
+
}
|
|
213
|
+
state.status = "running";
|
|
214
|
+
state.currentNode = requireIdentity(event.nodeId, "node");
|
|
215
|
+
delete state.waitingOn;
|
|
216
|
+
delete state.finishedAt;
|
|
217
|
+
delete state.statusDetail;
|
|
218
|
+
break;
|
|
219
|
+
case "pause":
|
|
220
|
+
requireEvent(event, "run_paused");
|
|
221
|
+
state.paused = true;
|
|
222
|
+
break;
|
|
223
|
+
case "finish":
|
|
224
|
+
if (
|
|
225
|
+
!["completed", "failed", "timed_out", "cancelled"].includes(transition.status) ||
|
|
226
|
+
(event.type !== `run_${transition.status}` &&
|
|
227
|
+
!(event.type === "run_interrupted" && transition.status === "failed"))
|
|
228
|
+
) {
|
|
229
|
+
throw new Error("Run outcome does not match the transition");
|
|
230
|
+
}
|
|
231
|
+
if (transition.status === "completed" && state.currentAttemptId !== undefined) {
|
|
232
|
+
throw new Error("A run with an unfinished attempt cannot complete");
|
|
233
|
+
}
|
|
234
|
+
if (transition.status === "completed") {
|
|
235
|
+
const last = state.steps.at(-1);
|
|
236
|
+
if (
|
|
237
|
+
last?.outcome !== "ok" ||
|
|
238
|
+
nextNode(definition, state) !== null ||
|
|
239
|
+
!isDeepStrictEqual(transition.finalOutput, last.output)
|
|
240
|
+
) {
|
|
241
|
+
throw new Error("Completion does not match the accepted graph result");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
state.status = transition.status;
|
|
245
|
+
state.finishedAt = now;
|
|
246
|
+
if (transition.error !== undefined) state.error = transition.error;
|
|
247
|
+
if (transition.finalOutput !== undefined) state.finalOutput = transition.finalOutput;
|
|
248
|
+
clearAttempt(state);
|
|
249
|
+
break;
|
|
250
|
+
case "record":
|
|
251
|
+
if (!recordEvents.has(event.type)) throw new Error(`Unknown execution event: ${event.type}`);
|
|
252
|
+
if (event.type === "agent_prompt_sent" || event.type === "agent_session_required") {
|
|
253
|
+
requireAttempt(state, event);
|
|
254
|
+
}
|
|
255
|
+
if (event.type === "agent_session_required")
|
|
256
|
+
state.statusDetail = "waiting for origin Pi session";
|
|
257
|
+
if (event.type === "settings_route_retried") {
|
|
258
|
+
const payload = event.payload;
|
|
259
|
+
if (typeof payload.scopeId !== "string" || typeof payload.changeNumber !== "number") {
|
|
260
|
+
throw new Error("Settings retry binding is invalid");
|
|
261
|
+
}
|
|
262
|
+
state.currentSettingsScopeId = payload.scopeId;
|
|
263
|
+
state.currentSettingsChangeNumber = payload.changeNumber;
|
|
264
|
+
if (typeof payload.settingsHash !== "string")
|
|
265
|
+
throw new Error("Settings retry hash is missing");
|
|
266
|
+
state.currentSettingsHash = payload.settingsHash;
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
default:
|
|
270
|
+
throw new Error("Unknown execution transition");
|
|
271
|
+
}
|
|
272
|
+
return state;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function nextNode(definition: WorkflowDefinitionSnapshot, state: WorkflowRunState): string | null {
|
|
276
|
+
const last = state.steps.at(-1);
|
|
277
|
+
if (last === undefined) return definition.startAt;
|
|
278
|
+
const result = state.results[last.nodeId];
|
|
279
|
+
if (result === undefined) throw new Error("Accepted graph result is missing");
|
|
280
|
+
return result.outcome === "ok"
|
|
281
|
+
? resolveNext(definition.edges, last.nodeId, result.output, result)
|
|
282
|
+
: resolveNextForOutcome(definition.edges, last.nodeId, result);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function requireEvent(event: WorkflowTraceEventDraft, type: string): void {
|
|
286
|
+
if (event.type !== type) throw new Error(`Transition requires ${type}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function requireIdentity(value: string | undefined, name: string): string {
|
|
290
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`Missing ${name} identity`);
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function requireAttempt(state: WorkflowRunState, event: WorkflowTraceEventDraft): void {
|
|
295
|
+
if (
|
|
296
|
+
state.currentNode !== requireIdentity(event.nodeId, "node") ||
|
|
297
|
+
state.currentAttemptId !== requireIdentity(event.attemptId, "attempt")
|
|
298
|
+
) {
|
|
299
|
+
throw new Error("Execution transition does not match the active attempt");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function clearAttempt(state: WorkflowRunState): void {
|
|
304
|
+
delete state.currentNode;
|
|
305
|
+
delete state.currentAttemptId;
|
|
306
|
+
delete state.currentNodeStartedAt;
|
|
307
|
+
delete state.currentNodeTimeoutMs;
|
|
308
|
+
delete state.currentNodeElapsedMs;
|
|
309
|
+
delete state.currentSettingsScopeId;
|
|
310
|
+
delete state.currentSettingsChangeNumber;
|
|
311
|
+
delete state.currentSettingsHash;
|
|
312
|
+
delete state.statusDetail;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function setSettings(state: WorkflowRunState, payload: WorkflowTraceEventDraft["payload"]): void {
|
|
316
|
+
delete state.currentSettingsScopeId;
|
|
317
|
+
delete state.currentSettingsChangeNumber;
|
|
318
|
+
delete state.currentSettingsHash;
|
|
319
|
+
if (typeof payload.settingsScopeId === "string") {
|
|
320
|
+
if (
|
|
321
|
+
typeof payload.settingsChangeNumber !== "number" ||
|
|
322
|
+
typeof payload.settingsHash !== "string"
|
|
323
|
+
) {
|
|
324
|
+
throw new Error("Attempt settings binding is invalid");
|
|
325
|
+
}
|
|
326
|
+
state.currentSettingsScopeId = payload.settingsScopeId;
|
|
327
|
+
state.currentSettingsChangeNumber = payload.settingsChangeNumber;
|
|
328
|
+
state.currentSettingsHash = payload.settingsHash;
|
|
329
|
+
}
|
|
330
|
+
}
|
package/src/workflows/types.ts
CHANGED
|
@@ -448,20 +448,9 @@ export type HumanDecisionCancellationRecord = {
|
|
|
448
448
|
reason: "cancelled" | "expired";
|
|
449
449
|
};
|
|
450
450
|
|
|
451
|
-
export type HumanDecisionContinuationRecord = {
|
|
452
|
-
schema: "pi-workflows.human-decision-continuation.v1";
|
|
453
|
-
decisionId: string;
|
|
454
|
-
requestDigest: string;
|
|
455
|
-
provenance: "human" | "timeout";
|
|
456
|
-
parentRunId: string;
|
|
457
|
-
runId: string;
|
|
458
|
-
createdAt: string;
|
|
459
|
-
};
|
|
460
|
-
|
|
461
451
|
/**
|
|
462
|
-
* A
|
|
463
|
-
*
|
|
464
|
-
* produces the checkpoint's output before the run pauses.
|
|
452
|
+
* A same-run input request. The optional `run` callback produces its prompt.
|
|
453
|
+
* The accepted answer becomes the node output; requesting input does not finish the node.
|
|
465
454
|
*/
|
|
466
455
|
export type CheckpointNodeDefinition = WorkflowNodeCommon & {
|
|
467
456
|
nodeType: "checkpoint";
|
|
@@ -478,15 +467,6 @@ export type WorkflowNodeDefinition =
|
|
|
478
467
|
| ActionNodeDefinition
|
|
479
468
|
| CheckpointNodeDefinition;
|
|
480
469
|
|
|
481
|
-
export type WorkflowPresentationContext = {
|
|
482
|
-
/** Final persisted state of the workflow run. */
|
|
483
|
-
state: WorkflowRunState;
|
|
484
|
-
/** Convenience alias for `state.finalOutput`. */
|
|
485
|
-
finalOutput: unknown;
|
|
486
|
-
/** Aborted if a new run starts, the session closes, or prompt generation times out. */
|
|
487
|
-
signal: AbortSignal;
|
|
488
|
-
};
|
|
489
|
-
|
|
490
470
|
/** Runtime parser that also carries its normalized TypeScript result type. */
|
|
491
471
|
export type WorkflowValueParser<T> = (value: unknown) => MaybePromise<T>;
|
|
492
472
|
|
|
@@ -577,14 +557,6 @@ export type WorkflowDefinition<
|
|
|
577
557
|
title?:
|
|
578
558
|
| string
|
|
579
559
|
| ((context: { input: TInput; workflowName: string }) => MaybePromise<string | undefined>);
|
|
580
|
-
/**
|
|
581
|
-
* Optional instructions for a normal assistant response after the run ends.
|
|
582
|
-
* The Pi extension resolves this only after the final state is persisted;
|
|
583
|
-
* the engine and durable store remain presentation-agnostic.
|
|
584
|
-
*/
|
|
585
|
-
presentationPrompt?:
|
|
586
|
-
| string
|
|
587
|
-
| ((context: WorkflowPresentationContext) => MaybePromise<string | undefined>);
|
|
588
560
|
startAt: string;
|
|
589
561
|
nodes: Record<string, WorkflowNodeDefinition>;
|
|
590
562
|
includes?: TIncludes;
|
|
@@ -686,14 +658,8 @@ export type WorkflowRunState = {
|
|
|
686
658
|
traceSeq: number;
|
|
687
659
|
runId: string;
|
|
688
660
|
workflowName: string;
|
|
689
|
-
/** Set on
|
|
661
|
+
/** Set on explicit fresh restarts. Checkpoint answers keep the same run. */
|
|
690
662
|
parentRunId?: string;
|
|
691
|
-
/**
|
|
692
|
-
* Steps carried from the parent at continuation start. Steps beyond this
|
|
693
|
-
* count were recorded by this run itself; resume uses it to tell a
|
|
694
|
-
* carried checkpoint from this run's own.
|
|
695
|
-
*/
|
|
696
|
-
carriedStepCount?: number;
|
|
697
663
|
runTitle?: string;
|
|
698
664
|
/** Stable built-in identity or immutable file source used by this run. */
|
|
699
665
|
workflowSource?: WorkflowSource;
|
|
@@ -718,12 +684,13 @@ export type WorkflowRunState = {
|
|
|
718
684
|
currentAttemptId?: string;
|
|
719
685
|
currentNodeStartedAt?: string;
|
|
720
686
|
/** Durable active-execution deadline for the current attempt. Null disables the deadline. */
|
|
721
|
-
|
|
687
|
+
currentNodeTimeoutMs?: number | null;
|
|
688
|
+
currentNodeElapsedMs?: number;
|
|
722
689
|
currentSettingsScopeId?: string;
|
|
723
690
|
currentSettingsChangeNumber?: number;
|
|
724
691
|
currentSettingsHash?: string;
|
|
725
692
|
statusDetail?: string;
|
|
726
|
-
/** Redacted
|
|
693
|
+
/** Redacted receipt for the latest accepted human decision in this run. */
|
|
727
694
|
humanDecision?: HumanDecisionReceipt;
|
|
728
695
|
/** True while the run is held at a step boundary by a pause request. */
|
|
729
696
|
paused?: boolean;
|
|
@@ -878,6 +845,7 @@ export type AssistantMessageReceipt = {
|
|
|
878
845
|
|
|
879
846
|
/** The step contract handed to the executor alongside the prompt. */
|
|
880
847
|
export type AgentStepContract = {
|
|
848
|
+
requestId: string;
|
|
881
849
|
runId: string;
|
|
882
850
|
workflowName: string;
|
|
883
851
|
nodeId: string;
|
|
@@ -931,8 +899,8 @@ export interface AgentStepExecutor {
|
|
|
931
899
|
* leave the run claimable for an origin session. Omission is unsupported.
|
|
932
900
|
*/
|
|
933
901
|
readonly assistantMessageMode?: "visible" | "park" | "unsupported";
|
|
934
|
-
/** True when the
|
|
935
|
-
readonly
|
|
902
|
+
/** True when the host records active time while this executor parks and resumes an attempt. */
|
|
903
|
+
readonly preservesActiveTimeBudget?: boolean;
|
|
936
904
|
runAgentStep(request: AgentStepRequest, signal: AbortSignal): Promise<AgentStepSubmission>;
|
|
937
905
|
}
|
|
938
906
|
|
|
@@ -968,11 +936,6 @@ export type WorkflowEngineOptions = {
|
|
|
968
936
|
* Awaited after `run_started` is persisted, before any node executes.
|
|
969
937
|
*/
|
|
970
938
|
onRunStarted?: (runId: string, state: WorkflowRunState) => MaybePromise<void>;
|
|
971
|
-
/**
|
|
972
|
-
* Awaited before the terminal snapshot is persisted. This is where a
|
|
973
|
-
* session recorder stops and drains. Errors are swallowed: finishing the run wins.
|
|
974
|
-
*/
|
|
975
|
-
onRunFinishing?: (runId: string, state: WorkflowRunState) => MaybePromise<void>;
|
|
976
939
|
/** Default per-node timeout. Defaults to 15 minutes. */
|
|
977
940
|
defaultNodeTimeoutMs?: number;
|
|
978
941
|
/** Guard against unbounded graph loops. Defaults to 100 executed steps. */
|
|
@@ -9,7 +9,7 @@ import type { AgentStepContract } from "./types.js";
|
|
|
9
9
|
export const WORKFLOW_STEP_MESSAGE_TYPE = "pi-workflows-step";
|
|
10
10
|
export const WORKFLOW_DECISION_MESSAGE_TYPE = "pi-workflows-interaction";
|
|
11
11
|
export const WORKFLOW_NOTIFICATION_MESSAGE_TYPE = "pi-workflows-notification";
|
|
12
|
-
export const WORKFLOW_TERMINAL_MESSAGE_TYPE = "pi-workflows-
|
|
12
|
+
export const WORKFLOW_TERMINAL_MESSAGE_TYPE = "pi-workflows-terminal";
|
|
13
13
|
export const WORKFLOW_FOLLOW_UP_MESSAGE_TYPE = "pi-workflows-follow-up";
|
|
14
14
|
|
|
15
15
|
export function stepWorkflowMessageContent(options: {
|
|
@@ -20,6 +20,8 @@ export function stepWorkflowMessageContent(options: {
|
|
|
20
20
|
}): WorkflowMessageContent {
|
|
21
21
|
const outer = requireRecord(options.contract, "Stored workflow step contract");
|
|
22
22
|
const agent = requireAgentContract(outer.contract);
|
|
23
|
+
if (agent.requestId !== options.requestId)
|
|
24
|
+
throw new Error("Workflow message request identity conflicts with its contract");
|
|
23
25
|
const details: Record<string, JsonValue> = {
|
|
24
26
|
schema: "pi-workflows.agent-step-message.v1",
|
|
25
27
|
kind: "step",
|
|
@@ -61,6 +63,27 @@ export function decisionWorkflowMessageContent(options: {
|
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
export function checkpointWorkflowMessageContent(options: {
|
|
67
|
+
workflowMessageId: string;
|
|
68
|
+
requestId: string;
|
|
69
|
+
runId: string;
|
|
70
|
+
contract: JsonValue;
|
|
71
|
+
}): WorkflowMessageContent {
|
|
72
|
+
return {
|
|
73
|
+
schema: WORKFLOW_MESSAGE_CONTENT_SCHEMA,
|
|
74
|
+
customType: WORKFLOW_DECISION_MESSAGE_TYPE,
|
|
75
|
+
content: `${JSON.stringify(options.contract, null, 2)}\n\nAnswer checkpoint ${options.requestId} with the workflow answer action.`,
|
|
76
|
+
display: true,
|
|
77
|
+
details: {
|
|
78
|
+
workflowMessageId: options.workflowMessageId,
|
|
79
|
+
requestId: options.requestId,
|
|
80
|
+
runId: options.runId,
|
|
81
|
+
kind: "checkpoint",
|
|
82
|
+
},
|
|
83
|
+
triggerTurn: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
64
87
|
export function notificationWorkflowMessageContent(options: {
|
|
65
88
|
workflowMessageId: string;
|
|
66
89
|
notificationId: string;
|
|
@@ -93,14 +116,14 @@ export function terminalWorkflowMessageContent(options: {
|
|
|
93
116
|
schema: WORKFLOW_MESSAGE_CONTENT_SCHEMA,
|
|
94
117
|
customType: WORKFLOW_TERMINAL_MESSAGE_TYPE,
|
|
95
118
|
content: options.content,
|
|
96
|
-
display:
|
|
119
|
+
display: true,
|
|
97
120
|
details: {
|
|
98
121
|
workflowMessageId: options.workflowMessageId,
|
|
99
122
|
runId: options.runId,
|
|
100
123
|
kind: "terminal",
|
|
101
124
|
terminal: options.details,
|
|
102
125
|
},
|
|
103
|
-
triggerTurn:
|
|
126
|
+
triggerTurn: false,
|
|
104
127
|
};
|
|
105
128
|
}
|
|
106
129
|
|
|
@@ -128,6 +151,7 @@ export function followUpWorkflowMessageContent(options: {
|
|
|
128
151
|
function requireAgentContract(value: unknown): AgentStepContract {
|
|
129
152
|
if (
|
|
130
153
|
!isRecord(value) ||
|
|
154
|
+
typeof value.requestId !== "string" ||
|
|
131
155
|
typeof value.runId !== "string" ||
|
|
132
156
|
typeof value.workflowName !== "string" ||
|
|
133
157
|
typeof value.nodeId !== "string" ||
|
|
@@ -159,7 +183,7 @@ function decisionPrompt(contract: Record<string, unknown>): string {
|
|
|
159
183
|
typeof presentation.summary === "string" ? presentation.summary : "",
|
|
160
184
|
...blocks,
|
|
161
185
|
choices.length === 0 ? "" : `Choices:\n${choices.join("\n")}`,
|
|
162
|
-
|
|
186
|
+
`A human must answer this protected decision with \`/workflow answer ${String(contract.decisionId)} <response>\`.`,
|
|
163
187
|
]
|
|
164
188
|
.filter(Boolean)
|
|
165
189
|
.join("\n\n");
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type { ResourceManagerStore } from "./store.js";
|
|
2
|
-
import type { AnyResourceManagerDefinition, ResourceManagerDefinition, ManagedResource, ManagedResourceRef } from "./types.js";
|
|
3
|
-
import { ResourceManagerWorkflowCoordinator, type ResourceManagerWorkflowScheduler } from "./workflows.js";
|
|
4
|
-
export type ResourceManagerRuntimeOptions = {
|
|
5
|
-
store: ResourceManagerStore;
|
|
6
|
-
resourceManagers: AnyResourceManagerDefinition[];
|
|
7
|
-
workflowScheduler?: ResourceManagerWorkflowScheduler;
|
|
8
|
-
maxConcurrent?: number;
|
|
9
|
-
resourceManagerConcurrency?: Record<string, number>;
|
|
10
|
-
defaultTimeoutMs?: number;
|
|
11
|
-
leaseMs?: number;
|
|
12
|
-
pollIntervalMs?: number;
|
|
13
|
-
baseBackoffMs?: number;
|
|
14
|
-
maxBackoffMs?: number;
|
|
15
|
-
jitterRatio?: number;
|
|
16
|
-
now?: () => Date;
|
|
17
|
-
random?: () => number;
|
|
18
|
-
};
|
|
19
|
-
export declare class ResourceManagerRuntime {
|
|
20
|
-
readonly store: ResourceManagerStore;
|
|
21
|
-
private readonly ownerId;
|
|
22
|
-
private readonly definitions;
|
|
23
|
-
private readonly workflowCoordinator;
|
|
24
|
-
private readonly maxConcurrent;
|
|
25
|
-
private readonly resourceManagerConcurrency;
|
|
26
|
-
private readonly defaultTimeoutMs;
|
|
27
|
-
private readonly leaseMs;
|
|
28
|
-
private readonly pollIntervalMs;
|
|
29
|
-
private readonly baseBackoffMs;
|
|
30
|
-
private readonly maxBackoffMs;
|
|
31
|
-
private readonly jitterRatio;
|
|
32
|
-
private readonly now;
|
|
33
|
-
private readonly random;
|
|
34
|
-
private readonly activeByResourceManager;
|
|
35
|
-
private readonly activeReconciles;
|
|
36
|
-
private runAbort;
|
|
37
|
-
private runnerTasks;
|
|
38
|
-
constructor(options: ResourceManagerRuntimeOptions);
|
|
39
|
-
putResource<TSpec, TStatus>(definition: ResourceManagerDefinition<TSpec, TStatus>, key: string, spec: TSpec): ManagedResource<TSpec, TStatus>;
|
|
40
|
-
putResourceByName(resourceManager: string, key: string, spec: unknown): ManagedResource;
|
|
41
|
-
requestDeletion(ref: ManagedResourceRef): ManagedResource;
|
|
42
|
-
enqueue(ref: ManagedResourceRef, afterMs?: number): void;
|
|
43
|
-
start(): void;
|
|
44
|
-
stop(): Promise<void>;
|
|
45
|
-
runOne(): Promise<boolean>;
|
|
46
|
-
runUntilIdle(maxReconciles?: number): Promise<number>;
|
|
47
|
-
get workflowRequests(): ResourceManagerWorkflowCoordinator;
|
|
48
|
-
private runnerLoop;
|
|
49
|
-
private claimNext;
|
|
50
|
-
private eligibleResourceManagers;
|
|
51
|
-
private processClaim;
|
|
52
|
-
private reconcileClaim;
|
|
53
|
-
private callReconciler;
|
|
54
|
-
private finishSuccessfulClaim;
|
|
55
|
-
private finishFailedClaim;
|
|
56
|
-
private backoffMs;
|
|
57
|
-
private readyQueueExists;
|
|
58
|
-
private recordEvent;
|
|
59
|
-
}
|