@caupulican/pi-adaptative 0.86.3 → 0.86.5
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/CHANGELOG.md +32 -0
- package/dist/core/agent-paths.d.ts +13 -0
- package/dist/core/agent-paths.d.ts.map +1 -1
- package/dist/core/agent-paths.js +23 -0
- package/dist/core/agent-paths.js.map +1 -1
- package/dist/core/autonomy/bounded-completion.d.ts +7 -0
- package/dist/core/autonomy/bounded-completion.d.ts.map +1 -1
- package/dist/core/autonomy/bounded-completion.js +12 -4
- package/dist/core/autonomy/bounded-completion.js.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.d.ts +2 -0
- package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.js +6 -3
- package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
- package/dist/core/autonomy/subagent-prompt.js +1 -1
- package/dist/core/autonomy/subagent-prompt.js.map +1 -1
- package/dist/core/background-lane-controller.d.ts +25 -2
- package/dist/core/background-lane-controller.d.ts.map +1 -1
- package/dist/core/background-lane-controller.js +67 -0
- package/dist/core/background-lane-controller.js.map +1 -1
- package/dist/core/delegation/replay-safe-mailbox-bounds.d.ts +4 -0
- package/dist/core/delegation/replay-safe-mailbox-bounds.d.ts.map +1 -0
- package/dist/core/delegation/replay-safe-mailbox-bounds.js +20 -0
- package/dist/core/delegation/replay-safe-mailbox-bounds.js.map +1 -0
- package/dist/core/delegation/sanitized-context-fork.d.ts +25 -0
- package/dist/core/delegation/sanitized-context-fork.d.ts.map +1 -0
- package/dist/core/delegation/sanitized-context-fork.js +299 -0
- package/dist/core/delegation/sanitized-context-fork.js.map +1 -0
- package/dist/core/delegation/session-root-mailbox.d.ts +75 -0
- package/dist/core/delegation/session-root-mailbox.d.ts.map +1 -0
- package/dist/core/delegation/session-root-mailbox.js +627 -0
- package/dist/core/delegation/session-root-mailbox.js.map +1 -0
- package/dist/core/delegation/worker-agent-control-coordinator.d.ts +102 -9
- package/dist/core/delegation/worker-agent-control-coordinator.d.ts.map +1 -1
- package/dist/core/delegation/worker-agent-control-coordinator.js +1307 -125
- package/dist/core/delegation/worker-agent-control-coordinator.js.map +1 -1
- package/dist/core/delegation/worker-agent-control.d.ts +183 -8
- package/dist/core/delegation/worker-agent-control.d.ts.map +1 -1
- package/dist/core/delegation/worker-agent-control.js +778 -48
- package/dist/core/delegation/worker-agent-control.js.map +1 -1
- package/dist/core/delegation/worker-attempt-executor.d.ts +9 -0
- package/dist/core/delegation/worker-attempt-executor.d.ts.map +1 -1
- package/dist/core/delegation/worker-attempt-executor.js +479 -214
- package/dist/core/delegation/worker-attempt-executor.js.map +1 -1
- package/dist/core/delegation/worker-authority-resolver.d.ts +9 -0
- package/dist/core/delegation/worker-authority-resolver.d.ts.map +1 -1
- package/dist/core/delegation/worker-authority-resolver.js +21 -2
- package/dist/core/delegation/worker-authority-resolver.js.map +1 -1
- package/dist/core/delegation/worker-context-fork-store.d.ts +71 -0
- package/dist/core/delegation/worker-context-fork-store.d.ts.map +1 -0
- package/dist/core/delegation/worker-context-fork-store.js +437 -0
- package/dist/core/delegation/worker-context-fork-store.js.map +1 -0
- package/dist/core/delegation/worker-context-inheritance-policy.d.ts +18 -0
- package/dist/core/delegation/worker-context-inheritance-policy.d.ts.map +1 -0
- package/dist/core/delegation/worker-context-inheritance-policy.js +28 -0
- package/dist/core/delegation/worker-context-inheritance-policy.js.map +1 -0
- package/dist/core/delegation/worker-conversation-revision.d.ts +31 -0
- package/dist/core/delegation/worker-conversation-revision.d.ts.map +1 -0
- package/dist/core/delegation/worker-conversation-revision.js +203 -0
- package/dist/core/delegation/worker-conversation-revision.js.map +1 -0
- package/dist/core/delegation/worker-conversation-store.d.ts +130 -18
- package/dist/core/delegation/worker-conversation-store.d.ts.map +1 -1
- package/dist/core/delegation/worker-conversation-store.js +1263 -79
- package/dist/core/delegation/worker-conversation-store.js.map +1 -1
- package/dist/core/delegation/worker-delegation-controller.d.ts +29 -2
- package/dist/core/delegation/worker-delegation-controller.d.ts.map +1 -1
- package/dist/core/delegation/worker-delegation-controller.js +625 -111
- package/dist/core/delegation/worker-delegation-controller.js.map +1 -1
- package/dist/core/delegation/worker-delegation-request.d.ts +2 -0
- package/dist/core/delegation/worker-delegation-request.d.ts.map +1 -1
- package/dist/core/delegation/worker-delegation-request.js.map +1 -1
- package/dist/core/delegation/worker-dispatch-scheduler.d.ts +20 -1
- package/dist/core/delegation/worker-dispatch-scheduler.d.ts.map +1 -1
- package/dist/core/delegation/worker-dispatch-scheduler.js +210 -37
- package/dist/core/delegation/worker-dispatch-scheduler.js.map +1 -1
- package/dist/core/delegation/worker-fleet-limits.d.ts +38 -0
- package/dist/core/delegation/worker-fleet-limits.d.ts.map +1 -0
- package/dist/core/delegation/worker-fleet-limits.js +113 -0
- package/dist/core/delegation/worker-fleet-limits.js.map +1 -0
- package/dist/core/delegation/worker-lane-projection.d.ts +1 -0
- package/dist/core/delegation/worker-lane-projection.d.ts.map +1 -1
- package/dist/core/delegation/worker-lane-projection.js +6 -2
- package/dist/core/delegation/worker-lane-projection.js.map +1 -1
- package/dist/core/delegation/worker-lifecycle.d.ts +20 -3
- package/dist/core/delegation/worker-lifecycle.d.ts.map +1 -1
- package/dist/core/delegation/worker-lifecycle.js +42 -18
- package/dist/core/delegation/worker-lifecycle.js.map +1 -1
- package/dist/core/delegation/worker-notification-coordinator.d.ts +4 -0
- package/dist/core/delegation/worker-notification-coordinator.d.ts.map +1 -1
- package/dist/core/delegation/worker-notification-coordinator.js +45 -4
- package/dist/core/delegation/worker-notification-coordinator.js.map +1 -1
- package/dist/core/delegation/worker-provider-turn-protocol.d.ts +50 -0
- package/dist/core/delegation/worker-provider-turn-protocol.d.ts.map +1 -0
- package/dist/core/delegation/worker-provider-turn-protocol.js +185 -0
- package/dist/core/delegation/worker-provider-turn-protocol.js.map +1 -0
- package/dist/core/delegation/worker-recovery-coordinator.d.ts +45 -4
- package/dist/core/delegation/worker-recovery-coordinator.d.ts.map +1 -1
- package/dist/core/delegation/worker-recovery-coordinator.js +223 -17
- package/dist/core/delegation/worker-recovery-coordinator.js.map +1 -1
- package/dist/core/delegation/worker-retry-policy.d.ts +36 -0
- package/dist/core/delegation/worker-retry-policy.d.ts.map +1 -0
- package/dist/core/delegation/worker-retry-policy.js +41 -0
- package/dist/core/delegation/worker-retry-policy.js.map +1 -0
- package/dist/core/delegation/worker-runner.d.ts +3 -1
- package/dist/core/delegation/worker-runner.d.ts.map +1 -1
- package/dist/core/delegation/worker-runner.js +4 -2
- package/dist/core/delegation/worker-runner.js.map +1 -1
- package/dist/core/delegation/worker-task-view.d.ts +43 -0
- package/dist/core/delegation/worker-task-view.d.ts.map +1 -0
- package/dist/core/delegation/worker-task-view.js +194 -0
- package/dist/core/delegation/worker-task-view.js.map +1 -0
- package/dist/core/delegation/worker-terminal-handoff-coordinator.d.ts +55 -0
- package/dist/core/delegation/worker-terminal-handoff-coordinator.d.ts.map +1 -0
- package/dist/core/delegation/worker-terminal-handoff-coordinator.js +179 -0
- package/dist/core/delegation/worker-terminal-handoff-coordinator.js.map +1 -0
- package/dist/core/delegation/worker-tree-budget-coordinator.d.ts +2 -2
- package/dist/core/delegation/worker-tree-budget-coordinator.d.ts.map +1 -1
- package/dist/core/delegation/worker-tree-budget-coordinator.js +12 -46
- package/dist/core/delegation/worker-tree-budget-coordinator.js.map +1 -1
- package/dist/core/keybindings.d.ts +10 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +2 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/orchestration/attempt-ordering.d.ts +9 -0
- package/dist/core/orchestration/attempt-ordering.d.ts.map +1 -0
- package/dist/core/orchestration/attempt-ordering.js +29 -0
- package/dist/core/orchestration/attempt-ordering.js.map +1 -0
- package/dist/core/orchestration/capability-gateway.d.ts +19 -0
- package/dist/core/orchestration/capability-gateway.d.ts.map +1 -1
- package/dist/core/orchestration/capability-gateway.js +19 -2
- package/dist/core/orchestration/capability-gateway.js.map +1 -1
- package/dist/core/orchestration/contracts.d.ts +43 -3
- package/dist/core/orchestration/contracts.d.ts.map +1 -1
- package/dist/core/orchestration/contracts.js +24 -0
- package/dist/core/orchestration/contracts.js.map +1 -1
- package/dist/core/orchestration/delegation-ledger.d.ts +8 -3
- package/dist/core/orchestration/delegation-ledger.d.ts.map +1 -1
- package/dist/core/orchestration/delegation-ledger.js +148 -55
- package/dist/core/orchestration/delegation-ledger.js.map +1 -1
- package/dist/core/orchestration/event-store.d.ts +6 -3
- package/dist/core/orchestration/event-store.d.ts.map +1 -1
- package/dist/core/orchestration/event-store.js +228 -30
- package/dist/core/orchestration/event-store.js.map +1 -1
- package/dist/core/orchestration/task-runtime-codecs.d.ts +31 -0
- package/dist/core/orchestration/task-runtime-codecs.d.ts.map +1 -0
- package/dist/core/orchestration/task-runtime-codecs.js +1471 -0
- package/dist/core/orchestration/task-runtime-codecs.js.map +1 -0
- package/dist/core/orchestration/task-runtime-projection.d.ts +30 -0
- package/dist/core/orchestration/task-runtime-projection.d.ts.map +1 -0
- package/dist/core/orchestration/task-runtime-projection.js +340 -0
- package/dist/core/orchestration/task-runtime-projection.js.map +1 -0
- package/dist/core/orchestration/task-runtime-reducer.d.ts +38 -0
- package/dist/core/orchestration/task-runtime-reducer.d.ts.map +1 -0
- package/dist/core/orchestration/task-runtime-reducer.js +1270 -0
- package/dist/core/orchestration/task-runtime-reducer.js.map +1 -0
- package/dist/core/orchestration/task-runtime-state.d.ts +142 -0
- package/dist/core/orchestration/task-runtime-state.d.ts.map +1 -0
- package/dist/core/orchestration/task-runtime-state.js +14 -0
- package/dist/core/orchestration/task-runtime-state.js.map +1 -0
- package/dist/core/orchestration/task-runtime.d.ts +44 -98
- package/dist/core/orchestration/task-runtime.d.ts.map +1 -1
- package/dist/core/orchestration/task-runtime.js +357 -1113
- package/dist/core/orchestration/task-runtime.js.map +1 -1
- package/dist/core/orchestration/worker-context-fork-reference.d.ts +20 -0
- package/dist/core/orchestration/worker-context-fork-reference.d.ts.map +1 -0
- package/dist/core/orchestration/worker-context-fork-reference.js +53 -0
- package/dist/core/orchestration/worker-context-fork-reference.js.map +1 -0
- package/dist/core/research/research-runner.d.ts +2 -0
- package/dist/core/research/research-runner.d.ts.map +1 -1
- package/dist/core/research/research-runner.js +1 -0
- package/dist/core/research/research-runner.js.map +1 -1
- package/dist/core/runtime-builder.d.ts.map +1 -1
- package/dist/core/runtime-builder.js +8 -0
- package/dist/core/runtime-builder.js.map +1 -1
- package/dist/core/secrets/credential-project.d.ts.map +1 -1
- package/dist/core/secrets/credential-project.js +24 -2
- package/dist/core/secrets/credential-project.js.map +1 -1
- package/dist/core/security/secret-text.d.ts +3 -0
- package/dist/core/security/secret-text.d.ts.map +1 -1
- package/dist/core/security/secret-text.js +15 -0
- package/dist/core/security/secret-text.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +160 -8
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +31 -4
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +749 -103
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/activity-lane.d.ts +5 -0
- package/dist/modes/interactive/components/activity-lane.d.ts.map +1 -1
- package/dist/modes/interactive/components/activity-lane.js +133 -49
- package/dist/modes/interactive/components/activity-lane.js.map +1 -1
- package/dist/modes/interactive/components/agents-overlay.d.ts +40 -0
- package/dist/modes/interactive/components/agents-overlay.d.ts.map +1 -0
- package/dist/modes/interactive/components/agents-overlay.js +153 -0
- package/dist/modes/interactive/components/agents-overlay.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +18 -13
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-event-controller.d.ts +1 -1
- package/dist/modes/interactive/interactive-event-controller.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-event-controller.js +6 -3
- package/dist/modes/interactive/interactive-event-controller.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +5 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +51 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/key-handlers.d.ts +1 -0
- package/dist/modes/interactive/key-handlers.d.ts.map +1 -1
- package/dist/modes/interactive/key-handlers.js +1 -0
- package/dist/modes/interactive/key-handlers.js.map +1 -1
- package/docs/harness-architecture.md +61 -17
- package/docs/settings.md +17 -9
- package/docs/worker-profiles.md +6 -2
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -1,863 +1,19 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
3
|
import { createAgentIdentity } from "./agent-resume.js";
|
|
4
|
-
import {
|
|
5
|
-
import { isHarnessCapability, MAX_ORCHESTRATION_COLLECTION_LENGTH, MAX_ORCHESTRATION_DISPATCH_INSTRUCTIONS_LENGTH, MAX_ORCHESTRATION_IDENTIFIER_LENGTH, MAX_ORCHESTRATION_MODEL_PROVIDER_LENGTH, ORCHESTRATION_SCHEMA_VERSION, toJsonObject, } from "./contracts.js";
|
|
4
|
+
import { MAX_ORCHESTRATION_AGENT_BINDINGS, MAX_ORCHESTRATION_APPROVALS, MAX_ORCHESTRATION_ATTEMPTS, MAX_ORCHESTRATION_CHECKPOINTS, MAX_ORCHESTRATION_NOTIFICATIONS, MAX_ORCHESTRATION_OBJECTIVE_EVIDENCE, MAX_ORCHESTRATION_OBJECTIVES, MAX_ORCHESTRATION_TASKS, ORCHESTRATION_SCHEMA_VERSION, toJsonObject, } from "./contracts.js";
|
|
6
5
|
import { OrchestrationSnapshotRequiredError } from "./event-store.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
function emptyProjection() {
|
|
17
|
-
return {
|
|
18
|
-
lastOrdinal: 0,
|
|
19
|
-
agents: {},
|
|
20
|
-
objectives: {},
|
|
21
|
-
tasks: {},
|
|
22
|
-
attempts: {},
|
|
23
|
-
checkpoints: {},
|
|
24
|
-
approvals: {},
|
|
25
|
-
notifications: {},
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
function cloneProjection(state) {
|
|
29
|
-
return structuredClone(state);
|
|
30
|
-
}
|
|
31
|
-
function projectionFromSnapshot(projection, throughOrdinal) {
|
|
32
|
-
const candidate = record(projection, "orchestration projection snapshot");
|
|
33
|
-
if (candidate.lastOrdinal !== throughOrdinal) {
|
|
34
|
-
throw new DurableTaskRuntimeError("Orchestration projection snapshot ordinal does not match its baseline.");
|
|
35
|
-
}
|
|
36
|
-
for (const field of [
|
|
37
|
-
"agents",
|
|
38
|
-
"objectives",
|
|
39
|
-
"tasks",
|
|
40
|
-
"attempts",
|
|
41
|
-
"checkpoints",
|
|
42
|
-
"approvals",
|
|
43
|
-
"notifications",
|
|
44
|
-
]) {
|
|
45
|
-
record(candidate[field], `orchestration projection snapshot.${field}`);
|
|
46
|
-
}
|
|
47
|
-
const normalized = structuredClone(candidate);
|
|
48
|
-
const agents = normalized.agents;
|
|
49
|
-
for (const [agentId, agent] of Object.entries(agents)) {
|
|
50
|
-
agents[agentId] = normalizeAgentBinding(agent, `orchestration projection snapshot.agents.${agentId}`);
|
|
51
|
-
}
|
|
52
|
-
return normalized;
|
|
53
|
-
}
|
|
54
|
-
function record(value, label) {
|
|
55
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
56
|
-
throw new DurableTaskRuntimeError(`${label} is not an object.`);
|
|
57
|
-
}
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
function string(value, label) {
|
|
61
|
-
if (typeof value !== "string" || value.length === 0)
|
|
62
|
-
throw new DurableTaskRuntimeError(`${label} is required.`);
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
65
|
-
function number(value, label) {
|
|
66
|
-
if (typeof value !== "number" || !Number.isFinite(value))
|
|
67
|
-
throw new DurableTaskRuntimeError(`${label} is invalid.`);
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
function boolean(value, label) {
|
|
71
|
-
if (typeof value !== "boolean")
|
|
72
|
-
throw new DurableTaskRuntimeError(`${label} is invalid.`);
|
|
73
|
-
return value;
|
|
74
|
-
}
|
|
75
|
-
function assertRiskBudget(budget, label) {
|
|
76
|
-
try {
|
|
77
|
-
validateRiskBudget(budget ?? {}, label);
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
throw new DurableTaskRuntimeError(error instanceof Error ? error.message : String(error));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
function stringArray(value, label) {
|
|
84
|
-
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
|
|
85
|
-
throw new DurableTaskRuntimeError(`${label} must be a string array.`);
|
|
86
|
-
}
|
|
87
|
-
return [...value];
|
|
88
|
-
}
|
|
89
|
-
function dispatchIdentifier(value, label) {
|
|
90
|
-
const identifier = string(value, label);
|
|
91
|
-
if (identifier.length > MAX_ORCHESTRATION_IDENTIFIER_LENGTH) {
|
|
92
|
-
throw new DurableTaskRuntimeError(`${label} exceeds its durable size bound.`);
|
|
93
|
-
}
|
|
94
|
-
return identifier;
|
|
95
|
-
}
|
|
96
|
-
function optionalDispatchIdentifier(value, label, maxLength = MAX_ORCHESTRATION_IDENTIFIER_LENGTH) {
|
|
97
|
-
if (value === undefined)
|
|
98
|
-
return undefined;
|
|
99
|
-
const identifier = string(value, label);
|
|
100
|
-
if (identifier.length > maxLength)
|
|
101
|
-
throw new DurableTaskRuntimeError(`${label} exceeds its durable size bound.`);
|
|
102
|
-
return identifier;
|
|
103
|
-
}
|
|
104
|
-
function dispatchInstructions(value, label) {
|
|
105
|
-
const instructions = string(value, label);
|
|
106
|
-
if (instructions.length > MAX_ORCHESTRATION_DISPATCH_INSTRUCTIONS_LENGTH) {
|
|
107
|
-
throw new DurableTaskRuntimeError(`${label} exceeds its durable size bound.`);
|
|
108
|
-
}
|
|
109
|
-
return instructions;
|
|
110
|
-
}
|
|
111
|
-
function dispatchIdentifierArray(value, label) {
|
|
112
|
-
if (!Array.isArray(value) ||
|
|
113
|
-
value.length > MAX_ORCHESTRATION_COLLECTION_LENGTH ||
|
|
114
|
-
!value.every((entry) => typeof entry === "string" && entry.length > 0 && entry.length <= MAX_ORCHESTRATION_IDENTIFIER_LENGTH)) {
|
|
115
|
-
throw new DurableTaskRuntimeError(`${label} must be a bounded identifier array.`);
|
|
116
|
-
}
|
|
117
|
-
if (new Set(value).size !== value.length) {
|
|
118
|
-
throw new DurableTaskRuntimeError(`${label} contains duplicates.`);
|
|
119
|
-
}
|
|
120
|
-
return [...value];
|
|
121
|
-
}
|
|
122
|
-
const DISPATCH_FIELDS = new Set([
|
|
123
|
-
"taskId",
|
|
124
|
-
"profileId",
|
|
125
|
-
"instructions",
|
|
126
|
-
"resourcePointerIds",
|
|
127
|
-
"parentAgentId",
|
|
128
|
-
"requirementIds",
|
|
129
|
-
"executionKind",
|
|
130
|
-
"logicalLaneId",
|
|
131
|
-
"dispatchSequence",
|
|
132
|
-
"provider",
|
|
133
|
-
"authorizationId",
|
|
134
|
-
"worktreeLaneKey",
|
|
135
|
-
"executionContract",
|
|
136
|
-
]);
|
|
137
|
-
function dispatchFromValue(value, label) {
|
|
138
|
-
const dispatch = record(value, label);
|
|
139
|
-
const unsupportedField = Object.keys(dispatch).find((field) => !DISPATCH_FIELDS.has(field));
|
|
140
|
-
if (unsupportedField)
|
|
141
|
-
throw new DurableTaskRuntimeError(`${label}.${unsupportedField} is unsupported.`);
|
|
142
|
-
const executionKind = dispatch.executionKind;
|
|
143
|
-
if (executionKind !== undefined && executionKind !== "in-process" && executionKind !== "managed-process") {
|
|
144
|
-
throw new DurableTaskRuntimeError(`${label}.executionKind is invalid.`);
|
|
145
|
-
}
|
|
146
|
-
const logicalLaneId = optionalDispatchIdentifier(dispatch.logicalLaneId, `${label}.logicalLaneId`);
|
|
147
|
-
const parentAgentId = optionalDispatchIdentifier(dispatch.parentAgentId, `${label}.parentAgentId`);
|
|
148
|
-
const provider = optionalDispatchIdentifier(dispatch.provider, `${label}.provider`, MAX_ORCHESTRATION_MODEL_PROVIDER_LENGTH);
|
|
149
|
-
const authorizationId = optionalDispatchIdentifier(dispatch.authorizationId, `${label}.authorizationId`);
|
|
150
|
-
const worktreeLaneKey = optionalDispatchIdentifier(dispatch.worktreeLaneKey, `${label}.worktreeLaneKey`);
|
|
151
|
-
if (dispatch.dispatchSequence !== undefined &&
|
|
152
|
-
(!Number.isSafeInteger(dispatch.dispatchSequence) || Number(dispatch.dispatchSequence) < 1)) {
|
|
153
|
-
throw new DurableTaskRuntimeError(`${label}.dispatchSequence is invalid.`);
|
|
154
|
-
}
|
|
155
|
-
let executionContract;
|
|
156
|
-
try {
|
|
157
|
-
executionContract = dispatch.executionContract
|
|
158
|
-
? parseWorkerExecutionContract(dispatch.executionContract)
|
|
159
|
-
: undefined;
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
throw new DurableTaskRuntimeError(error instanceof Error ? error.message : String(error));
|
|
163
|
-
}
|
|
164
|
-
if (executionContract && executionKind === "managed-process") {
|
|
165
|
-
throw new DurableTaskRuntimeError(`${label} cannot combine a worker execution contract with managed execution.`);
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
taskId: dispatchIdentifier(dispatch.taskId, `${label}.taskId`),
|
|
169
|
-
profileId: dispatchIdentifier(dispatch.profileId, `${label}.profileId`),
|
|
170
|
-
instructions: dispatchInstructions(dispatch.instructions, `${label}.instructions`),
|
|
171
|
-
resourcePointerIds: dispatchIdentifierArray(dispatch.resourcePointerIds, `${label}.resourcePointerIds`),
|
|
172
|
-
...(parentAgentId ? { parentAgentId } : {}),
|
|
173
|
-
requirementIds: dispatch.requirementIds === undefined
|
|
174
|
-
? []
|
|
175
|
-
: dispatchIdentifierArray(dispatch.requirementIds, `${label}.requirementIds`),
|
|
176
|
-
...(executionKind ? { executionKind } : {}),
|
|
177
|
-
...(logicalLaneId ? { logicalLaneId } : {}),
|
|
178
|
-
...(typeof dispatch.dispatchSequence === "number" ? { dispatchSequence: dispatch.dispatchSequence } : {}),
|
|
179
|
-
...(provider ? { provider } : {}),
|
|
180
|
-
...(authorizationId ? { authorizationId } : {}),
|
|
181
|
-
...(worktreeLaneKey ? { worktreeLaneKey } : {}),
|
|
182
|
-
...(executionContract ? { executionContract } : {}),
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
function objectiveFromPayload(payload) {
|
|
186
|
-
return structuredClone(record(payload.objective, "objective"));
|
|
187
|
-
}
|
|
188
|
-
function evidenceFromPayload(payload) {
|
|
189
|
-
return structuredClone(record(payload.evidence, "evidence"));
|
|
190
|
-
}
|
|
191
|
-
function assertAcceptanceCriteria(criteria) {
|
|
192
|
-
const criterionIds = criteria.map((criterion) => criterion.id);
|
|
193
|
-
if (criterionIds.some((id) => !id.trim()) ||
|
|
194
|
-
new Set(criterionIds).size !== criterionIds.length ||
|
|
195
|
-
criteria.some((criterion) => !criterion.description.trim())) {
|
|
196
|
-
throw new DurableTaskRuntimeError("Objective acceptance criteria require unique ids and descriptions.");
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
function taskFromPayload(payload) {
|
|
200
|
-
return structuredClone(record(payload.task, "task"));
|
|
201
|
-
}
|
|
202
|
-
function leaseFromPayload(payload) {
|
|
203
|
-
const lease = record(payload.lease, "lease");
|
|
204
|
-
return {
|
|
205
|
-
leaseId: string(lease.leaseId, "lease.leaseId"),
|
|
206
|
-
attemptId: string(lease.attemptId, "lease.attemptId"),
|
|
207
|
-
ownerId: string(lease.ownerId, "lease.ownerId"),
|
|
208
|
-
fencingToken: number(lease.fencingToken, "lease.fencingToken"),
|
|
209
|
-
issuedAt: string(lease.issuedAt, "lease.issuedAt"),
|
|
210
|
-
expiresAt: string(lease.expiresAt, "lease.expiresAt"),
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
function checkpointFromPayload(payload) {
|
|
214
|
-
const checkpoint = record(payload.checkpoint, "checkpoint");
|
|
215
|
-
const usage = checkpoint.usage === undefined ? undefined : usageFromPayload(checkpoint.usage, "checkpoint.usage");
|
|
216
|
-
return {
|
|
217
|
-
checkpointId: string(checkpoint.checkpointId, "checkpoint.checkpointId"),
|
|
218
|
-
attemptId: string(checkpoint.attemptId, "checkpoint.attemptId"),
|
|
219
|
-
fencingToken: number(checkpoint.fencingToken, "checkpoint.fencingToken"),
|
|
220
|
-
summary: string(checkpoint.summary, "checkpoint.summary"),
|
|
221
|
-
artifactIds: stringArray(checkpoint.artifactIds, "checkpoint.artifactIds"),
|
|
222
|
-
evidenceIds: stringArray(checkpoint.evidenceIds, "checkpoint.evidenceIds"),
|
|
223
|
-
...(usage ? { usage } : {}),
|
|
224
|
-
createdAt: string(checkpoint.createdAt, "checkpoint.createdAt"),
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
function usageFromPayload(value, label) {
|
|
228
|
-
const usage = record(value, label);
|
|
229
|
-
const expectedFields = [
|
|
230
|
-
"toolCalls",
|
|
231
|
-
"inputTokens",
|
|
232
|
-
"outputTokens",
|
|
233
|
-
"cacheReadTokens",
|
|
234
|
-
"cacheWriteTokens",
|
|
235
|
-
"totalTokens",
|
|
236
|
-
"costUsd",
|
|
237
|
-
"activeWallClockMs",
|
|
238
|
-
];
|
|
239
|
-
const unexpected = Object.keys(usage).find((field) => !expectedFields.includes(field));
|
|
240
|
-
if (unexpected)
|
|
241
|
-
throw new DurableTaskRuntimeError(`${label}.${unexpected} is unsupported.`);
|
|
242
|
-
try {
|
|
243
|
-
return validateAttemptUsageSnapshot({
|
|
244
|
-
toolCalls: number(usage.toolCalls, `${label}.toolCalls`),
|
|
245
|
-
inputTokens: number(usage.inputTokens, `${label}.inputTokens`),
|
|
246
|
-
outputTokens: number(usage.outputTokens, `${label}.outputTokens`),
|
|
247
|
-
cacheReadTokens: number(usage.cacheReadTokens, `${label}.cacheReadTokens`),
|
|
248
|
-
cacheWriteTokens: number(usage.cacheWriteTokens, `${label}.cacheWriteTokens`),
|
|
249
|
-
totalTokens: number(usage.totalTokens, `${label}.totalTokens`),
|
|
250
|
-
costUsd: number(usage.costUsd, `${label}.costUsd`),
|
|
251
|
-
activeWallClockMs: number(usage.activeWallClockMs, `${label}.activeWallClockMs`),
|
|
252
|
-
}, label);
|
|
253
|
-
}
|
|
254
|
-
catch (error) {
|
|
255
|
-
throw new DurableTaskRuntimeError(error instanceof Error ? error.message : String(error));
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
function resultFromPayload(payload) {
|
|
259
|
-
return structuredClone(record(payload.result, "result"));
|
|
260
|
-
}
|
|
261
|
-
function normalizeAgentBinding(value, label) {
|
|
262
|
-
const agent = structuredClone(record(value, label));
|
|
263
|
-
if (!agent.agentId?.trim())
|
|
264
|
-
throw new DurableTaskRuntimeError(`${label}.agentId is required.`);
|
|
265
|
-
const parentAgentId = agent.parentAgentId?.trim();
|
|
266
|
-
const rootAgentId = agent.rootAgentId?.trim() || agent.agentId;
|
|
267
|
-
const depth = agent.depth ?? 0;
|
|
268
|
-
if (!rootAgentId || !Number.isSafeInteger(depth) || depth < 0) {
|
|
269
|
-
throw new DurableTaskRuntimeError(`${label} lineage is invalid.`);
|
|
270
|
-
}
|
|
271
|
-
if (parentAgentId && depth === 0)
|
|
272
|
-
throw new DurableTaskRuntimeError(`${label} child lineage depth is invalid.`);
|
|
273
|
-
return {
|
|
274
|
-
...agent,
|
|
275
|
-
...(parentAgentId ? { parentAgentId } : {}),
|
|
276
|
-
rootAgentId,
|
|
277
|
-
depth,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
function agentFromPayload(payload) {
|
|
281
|
-
return normalizeAgentBinding(payload.agent, "agent");
|
|
282
|
-
}
|
|
283
|
-
function approvalFromPayload(payload) {
|
|
284
|
-
const approval = record(payload.approval, "approval");
|
|
285
|
-
const capabilities = stringArray(approval.requestedCapabilities, "approval.requestedCapabilities");
|
|
286
|
-
if (!capabilities.every(isHarnessCapability)) {
|
|
287
|
-
throw new DurableTaskRuntimeError("Approval contains an unknown capability.");
|
|
288
|
-
}
|
|
289
|
-
const requestedBudget = approval.requestedBudget
|
|
290
|
-
? structuredClone(record(approval.requestedBudget, "approval.requestedBudget"))
|
|
291
|
-
: undefined;
|
|
292
|
-
assertRiskBudget(requestedBudget, "approval.requestedBudget");
|
|
293
|
-
if (capabilities.length === 0 && !requestedBudget) {
|
|
294
|
-
throw new DurableTaskRuntimeError("Approval must request capabilities or budget.");
|
|
295
|
-
}
|
|
296
|
-
if (approval.schemaVersion !== ORCHESTRATION_SCHEMA_VERSION) {
|
|
297
|
-
throw new DurableTaskRuntimeError("Approval schema version is invalid.");
|
|
298
|
-
}
|
|
299
|
-
return {
|
|
300
|
-
schemaVersion: ORCHESTRATION_SCHEMA_VERSION,
|
|
301
|
-
approvalId: string(approval.approvalId, "approval.approvalId"),
|
|
302
|
-
objectiveId: string(approval.objectiveId, "approval.objectiveId"),
|
|
303
|
-
...(typeof approval.taskId === "string" ? { taskId: string(approval.taskId, "approval.taskId") } : {}),
|
|
304
|
-
...(typeof approval.attemptId === "string"
|
|
305
|
-
? { attemptId: string(approval.attemptId, "approval.attemptId") }
|
|
306
|
-
: {}),
|
|
307
|
-
reasonCode: string(approval.reasonCode, "approval.reasonCode"),
|
|
308
|
-
summary: string(approval.summary, "approval.summary"),
|
|
309
|
-
requestedCapabilities: capabilities,
|
|
310
|
-
...(requestedBudget ? { requestedBudget } : {}),
|
|
311
|
-
reversible: boolean(approval.reversible, "approval.reversible"),
|
|
312
|
-
createdAt: string(approval.createdAt, "approval.createdAt"),
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
function approvalResolutionFromPayload(payload) {
|
|
316
|
-
const resolution = record(payload.resolution, "resolution");
|
|
317
|
-
if (resolution.schemaVersion !== ORCHESTRATION_SCHEMA_VERSION) {
|
|
318
|
-
throw new DurableTaskRuntimeError("Approval resolution schema version is invalid.");
|
|
319
|
-
}
|
|
320
|
-
const outcome = string(resolution.outcome, "resolution.outcome");
|
|
321
|
-
if (outcome !== "approved" && outcome !== "rejected") {
|
|
322
|
-
throw new DurableTaskRuntimeError(`Unknown approval outcome '${outcome}'.`);
|
|
323
|
-
}
|
|
324
|
-
return {
|
|
325
|
-
schemaVersion: ORCHESTRATION_SCHEMA_VERSION,
|
|
326
|
-
approvalId: string(resolution.approvalId, "resolution.approvalId"),
|
|
327
|
-
outcome,
|
|
328
|
-
reasonCode: string(resolution.reasonCode, "resolution.reasonCode"),
|
|
329
|
-
resolvedAt: string(resolution.resolvedAt, "resolution.resolvedAt"),
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
function updateObjectiveStatus(state, objectiveId, status, at) {
|
|
333
|
-
const current = state.objectives[objectiveId];
|
|
334
|
-
if (!current)
|
|
335
|
-
throw new DurableTaskRuntimeError(`Unknown objective '${objectiveId}'.`);
|
|
336
|
-
state.objectives[objectiveId] = {
|
|
337
|
-
...current,
|
|
338
|
-
objective: { ...current.objective, status, updatedAt: at },
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
function terminalAttemptStatus(status) {
|
|
342
|
-
return ["completed", "partial", "blocked", "failed", "cancelled", "expired"].includes(status);
|
|
343
|
-
}
|
|
344
|
-
function cancelOpenObjectiveWork(state, objectiveId, at, reasonCode) {
|
|
345
|
-
const objective = state.objectives[objectiveId];
|
|
346
|
-
const tasks = state.tasks;
|
|
347
|
-
const attempts = state.attempts;
|
|
348
|
-
for (const taskId of objective?.taskIds ?? []) {
|
|
349
|
-
const taskState = tasks[taskId];
|
|
350
|
-
if (taskState && !["completed", "failed", "cancelled"].includes(taskState.task.status)) {
|
|
351
|
-
tasks[taskId] = {
|
|
352
|
-
...taskState,
|
|
353
|
-
task: { ...taskState.task, status: "cancelled", updatedAt: at },
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
for (const attemptId of taskState?.attemptIds ?? []) {
|
|
357
|
-
const attempt = attempts[attemptId];
|
|
358
|
-
if (attempt && !terminalAttemptStatus(attempt.status)) {
|
|
359
|
-
attempts[attemptId] = { ...attempt, status: "cancelled", reasonCode, updatedAt: at };
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
function taskStatusForResult(status) {
|
|
365
|
-
if (status === "completed")
|
|
366
|
-
return "completed";
|
|
367
|
-
if (status === "failed")
|
|
368
|
-
return "failed";
|
|
369
|
-
if (status === "cancelled")
|
|
370
|
-
return "cancelled";
|
|
371
|
-
return "blocked";
|
|
372
|
-
}
|
|
373
|
-
function releaseAttemptAgent(agents, attempt, at) {
|
|
374
|
-
if (!attempt.agentId)
|
|
375
|
-
return;
|
|
376
|
-
const agent = agents[attempt.agentId];
|
|
377
|
-
if (!agent)
|
|
378
|
-
return;
|
|
379
|
-
const next = { ...agent, status: "registered", updatedAt: at };
|
|
380
|
-
delete next.activeAttemptId;
|
|
381
|
-
agents[attempt.agentId] = next;
|
|
382
|
-
}
|
|
383
|
-
function missingTrustedCriteria(result, criterionIds) {
|
|
384
|
-
const proven = new Set(result.evidence.flatMap((evidence) => (evidence.trusted && evidence.criterionId ? [evidence.criterionId] : [])));
|
|
385
|
-
return criterionIds.filter((criterionId) => !proven.has(criterionId));
|
|
386
|
-
}
|
|
387
|
-
function refreshReadyTasks(state, objectiveId, at) {
|
|
388
|
-
const objective = state.objectives[objectiveId];
|
|
389
|
-
if (!objective || objective.objective.status !== "active")
|
|
390
|
-
return;
|
|
391
|
-
const mutableTasks = state.tasks;
|
|
392
|
-
for (const taskId of objective.taskIds) {
|
|
393
|
-
const current = mutableTasks[taskId];
|
|
394
|
-
if (!current || current.task.status !== "pending")
|
|
395
|
-
continue;
|
|
396
|
-
if (current.task.dependsOn.every((dependencyId) => mutableTasks[dependencyId]?.task.status === "completed")) {
|
|
397
|
-
mutableTasks[taskId] = { ...current, task: { ...current.task, status: "ready", updatedAt: at } };
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
export function reduceOrchestrationEvent(projection, event) {
|
|
402
|
-
if (event.ordinal <= projection.lastOrdinal)
|
|
403
|
-
return projection;
|
|
404
|
-
const state = cloneProjection(projection);
|
|
405
|
-
const agents = state.agents;
|
|
406
|
-
const objectives = state.objectives;
|
|
407
|
-
const tasks = state.tasks;
|
|
408
|
-
const attempts = state.attempts;
|
|
409
|
-
const checkpoints = state.checkpoints;
|
|
410
|
-
const approvals = state.approvals;
|
|
411
|
-
const notifications = state.notifications;
|
|
412
|
-
switch (event.type) {
|
|
413
|
-
case "objective.created": {
|
|
414
|
-
const objective = objectiveFromPayload(event.payload);
|
|
415
|
-
if (objectives[objective.objectiveId]) {
|
|
416
|
-
throw new DurableTaskRuntimeError(`Objective '${objective.objectiveId}' was created more than once.`);
|
|
417
|
-
}
|
|
418
|
-
objectives[objective.objectiveId] = { objective, taskIds: [], evidence: [] };
|
|
419
|
-
break;
|
|
420
|
-
}
|
|
421
|
-
case "objective.updated": {
|
|
422
|
-
const objective = objectiveFromPayload(event.payload);
|
|
423
|
-
const current = objectives[event.aggregateId];
|
|
424
|
-
if (!current)
|
|
425
|
-
throw new DurableTaskRuntimeError(`Unknown objective '${event.aggregateId}'.`);
|
|
426
|
-
if (objective.objectiveId !== event.aggregateId) {
|
|
427
|
-
throw new DurableTaskRuntimeError(`Updated objective id '${objective.objectiveId}' does not match aggregate.`);
|
|
428
|
-
}
|
|
429
|
-
objectives[event.aggregateId] = { ...current, objective };
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
case "objective.evidence_recorded": {
|
|
433
|
-
const evidence = evidenceFromPayload(event.payload);
|
|
434
|
-
const current = objectives[event.aggregateId];
|
|
435
|
-
if (!current)
|
|
436
|
-
throw new DurableTaskRuntimeError(`Unknown objective '${event.aggregateId}'.`);
|
|
437
|
-
const existing = current.evidence.find((candidate) => candidate.evidenceId === evidence.evidenceId);
|
|
438
|
-
if (existing && !isDeepStrictEqual(existing, evidence)) {
|
|
439
|
-
throw new DurableTaskRuntimeError(`Objective evidence '${evidence.evidenceId}' has conflicting content.`);
|
|
440
|
-
}
|
|
441
|
-
if (!existing)
|
|
442
|
-
objectives[event.aggregateId] = { ...current, evidence: [...current.evidence, evidence] };
|
|
443
|
-
break;
|
|
444
|
-
}
|
|
445
|
-
case "objective.paused":
|
|
446
|
-
updateObjectiveStatus(state, event.aggregateId, "paused", event.occurredAt);
|
|
447
|
-
break;
|
|
448
|
-
case "objective.resumed":
|
|
449
|
-
updateObjectiveStatus(state, event.aggregateId, "active", event.occurredAt);
|
|
450
|
-
refreshReadyTasks(state, event.aggregateId, event.occurredAt);
|
|
451
|
-
break;
|
|
452
|
-
case "objective.completed":
|
|
453
|
-
updateObjectiveStatus(state, event.aggregateId, "completed", event.occurredAt);
|
|
454
|
-
cancelOpenObjectiveWork(state, event.aggregateId, event.occurredAt, "objective_completed");
|
|
455
|
-
break;
|
|
456
|
-
case "objective.cancelled": {
|
|
457
|
-
updateObjectiveStatus(state, event.aggregateId, "cancelled", event.occurredAt);
|
|
458
|
-
cancelOpenObjectiveWork(state, event.aggregateId, event.occurredAt, "objective_cancelled");
|
|
459
|
-
break;
|
|
460
|
-
}
|
|
461
|
-
case "task.created": {
|
|
462
|
-
const task = taskFromPayload(event.payload);
|
|
463
|
-
const objective = objectives[task.objectiveId];
|
|
464
|
-
if (!objective)
|
|
465
|
-
throw new DurableTaskRuntimeError(`Task '${task.taskId}' references an unknown objective.`);
|
|
466
|
-
if (tasks[task.taskId])
|
|
467
|
-
throw new DurableTaskRuntimeError(`Task '${task.taskId}' was created more than once.`);
|
|
468
|
-
tasks[task.taskId] = { task, attemptIds: [] };
|
|
469
|
-
objectives[task.objectiveId] = { ...objective, taskIds: [...objective.taskIds, task.taskId] };
|
|
470
|
-
refreshReadyTasks(state, task.objectiveId, event.occurredAt);
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
case "task.ready": {
|
|
474
|
-
const taskId = string(event.payload.taskId, "task.ready.taskId");
|
|
475
|
-
const current = tasks[taskId];
|
|
476
|
-
if (!current)
|
|
477
|
-
throw new DurableTaskRuntimeError(`Unknown task '${taskId}'.`);
|
|
478
|
-
tasks[taskId] = { ...current, task: { ...current.task, status: "ready", updatedAt: event.occurredAt } };
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
481
|
-
case "task.failed": {
|
|
482
|
-
const taskId = string(event.payload.taskId, "task.failed.taskId");
|
|
483
|
-
const current = tasks[taskId];
|
|
484
|
-
if (!current)
|
|
485
|
-
throw new DurableTaskRuntimeError(`Unknown task '${taskId}'.`);
|
|
486
|
-
tasks[taskId] = { ...current, task: { ...current.task, status: "failed", updatedAt: event.occurredAt } };
|
|
487
|
-
break;
|
|
488
|
-
}
|
|
489
|
-
case "task.verification_finished": {
|
|
490
|
-
const taskId = string(event.payload.taskId, "task.verification_finished.taskId");
|
|
491
|
-
const verifierTaskId = string(event.payload.verifierTaskId, "task.verification_finished.verifierTaskId");
|
|
492
|
-
const verifierAttemptId = string(event.payload.verifierAttemptId, "task.verification_finished.verifierAttemptId");
|
|
493
|
-
const verdict = string(event.payload.verdict, "task.verification_finished.verdict");
|
|
494
|
-
if (verdict !== "accepted" && verdict !== "rejected" && verdict !== "inconclusive") {
|
|
495
|
-
throw new DurableTaskRuntimeError(`Unknown verification verdict '${verdict}'.`);
|
|
496
|
-
}
|
|
497
|
-
const current = tasks[taskId];
|
|
498
|
-
if (!current)
|
|
499
|
-
throw new DurableTaskRuntimeError(`Unknown task '${taskId}'.`);
|
|
500
|
-
const reasonCode = string(event.payload.reasonCode, "task.verification_finished.reasonCode");
|
|
501
|
-
tasks[taskId] = {
|
|
502
|
-
...current,
|
|
503
|
-
task: {
|
|
504
|
-
...current.task,
|
|
505
|
-
status: verdict === "accepted" ? "completed" : "blocked",
|
|
506
|
-
updatedAt: event.occurredAt,
|
|
507
|
-
},
|
|
508
|
-
verification: {
|
|
509
|
-
verifierTaskId,
|
|
510
|
-
verifierAttemptId,
|
|
511
|
-
verdict,
|
|
512
|
-
reasonCode,
|
|
513
|
-
completedAt: event.occurredAt,
|
|
514
|
-
},
|
|
515
|
-
};
|
|
516
|
-
refreshReadyTasks(state, current.task.objectiveId, event.occurredAt);
|
|
517
|
-
break;
|
|
518
|
-
}
|
|
519
|
-
case "agent.registered": {
|
|
520
|
-
const agent = agentFromPayload(event.payload);
|
|
521
|
-
if (agents[agent.agentId])
|
|
522
|
-
throw new DurableTaskRuntimeError(`Agent '${agent.agentId}' was registered more than once.`);
|
|
523
|
-
if (agent.parentAgentId) {
|
|
524
|
-
const parent = agents[agent.parentAgentId];
|
|
525
|
-
if (!parent)
|
|
526
|
-
throw new DurableTaskRuntimeError(`Unknown parent agent '${agent.parentAgentId}'.`);
|
|
527
|
-
if (agent.rootAgentId !== parent.rootAgentId || agent.depth !== parent.depth + 1) {
|
|
528
|
-
throw new DurableTaskRuntimeError(`Agent '${agent.agentId}' lineage conflicts with its parent.`);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
else if (agent.rootAgentId !== agent.agentId || agent.depth !== 0) {
|
|
532
|
-
throw new DurableTaskRuntimeError(`Root agent '${agent.agentId}' lineage is invalid.`);
|
|
533
|
-
}
|
|
534
|
-
agents[agent.agentId] = agent;
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
case "agent.suspended": {
|
|
538
|
-
const agentId = string(event.payload.agentId, "agent.suspended.agentId");
|
|
539
|
-
const agent = agents[agentId];
|
|
540
|
-
if (!agent)
|
|
541
|
-
throw new DurableTaskRuntimeError(`Unknown agent '${agentId}'.`);
|
|
542
|
-
agents[agentId] = { ...agent, status: "suspended", updatedAt: event.occurredAt };
|
|
543
|
-
break;
|
|
544
|
-
}
|
|
545
|
-
case "agent.resume_requested": {
|
|
546
|
-
const agentId = string(event.payload.agentId, "agent.resume_requested.agentId");
|
|
547
|
-
const agent = agents[agentId];
|
|
548
|
-
if (!agent)
|
|
549
|
-
throw new DurableTaskRuntimeError(`Unknown agent '${agentId}'.`);
|
|
550
|
-
agents[agentId] = { ...agent, status: "resuming", updatedAt: event.occurredAt };
|
|
551
|
-
break;
|
|
552
|
-
}
|
|
553
|
-
case "agent.resumed": {
|
|
554
|
-
const agentId = string(event.payload.agentId, "agent.resumed.agentId");
|
|
555
|
-
const attemptId = string(event.payload.attemptId, "agent.resumed.attemptId");
|
|
556
|
-
const agent = agents[agentId];
|
|
557
|
-
if (!agent)
|
|
558
|
-
throw new DurableTaskRuntimeError(`Unknown agent '${agentId}'.`);
|
|
559
|
-
agents[agentId] = { ...agent, status: "active", activeAttemptId: attemptId, updatedAt: event.occurredAt };
|
|
560
|
-
break;
|
|
561
|
-
}
|
|
562
|
-
case "attempt.queued": {
|
|
563
|
-
const attemptId = string(event.payload.attemptId, "attempt.queued.attemptId");
|
|
564
|
-
const taskId = string(event.payload.taskId, "attempt.queued.taskId");
|
|
565
|
-
const task = tasks[taskId];
|
|
566
|
-
if (!task)
|
|
567
|
-
throw new DurableTaskRuntimeError(`Attempt '${attemptId}' references an unknown task.`);
|
|
568
|
-
if (attempts[attemptId])
|
|
569
|
-
throw new DurableTaskRuntimeError(`Attempt '${attemptId}' was queued more than once.`);
|
|
570
|
-
const dispatch = dispatchFromValue(event.payload.dispatch, "attempt.queued.dispatch");
|
|
571
|
-
if (dispatch.taskId !== taskId) {
|
|
572
|
-
throw new DurableTaskRuntimeError(`Attempt '${attemptId}' dispatch task does not match its task.`);
|
|
573
|
-
}
|
|
574
|
-
if (dispatch.executionContract &&
|
|
575
|
-
(dispatch.executionContract.worker.profile.profileId !== dispatch.profileId ||
|
|
576
|
-
dispatch.executionContract.worker.profile.role !== task.task.role)) {
|
|
577
|
-
throw new DurableTaskRuntimeError(`Attempt '${attemptId}' execution contract does not match its dispatch.`);
|
|
578
|
-
}
|
|
579
|
-
attempts[attemptId] = {
|
|
580
|
-
attemptId,
|
|
581
|
-
taskId,
|
|
582
|
-
dispatch,
|
|
583
|
-
status: "queued",
|
|
584
|
-
...(typeof event.payload.grantId === "string" ? { grantId: event.payload.grantId } : {}),
|
|
585
|
-
checkpointIds: [],
|
|
586
|
-
createdAt: event.occurredAt,
|
|
587
|
-
updatedAt: event.occurredAt,
|
|
588
|
-
};
|
|
589
|
-
tasks[taskId] = {
|
|
590
|
-
...task,
|
|
591
|
-
task: { ...task.task, status: "running", updatedAt: event.occurredAt },
|
|
592
|
-
attemptIds: [...task.attemptIds, attemptId],
|
|
593
|
-
};
|
|
594
|
-
break;
|
|
595
|
-
}
|
|
596
|
-
case "attempt.grant_bound": {
|
|
597
|
-
const attemptId = string(event.payload.attemptId, "attempt.grant_bound.attemptId");
|
|
598
|
-
const attempt = attempts[attemptId];
|
|
599
|
-
if (!attempt)
|
|
600
|
-
throw new DurableTaskRuntimeError(`Unknown attempt '${attemptId}'.`);
|
|
601
|
-
const grant = event.payload.grant
|
|
602
|
-
? structuredClone(record(event.payload.grant, "attempt.grant_bound.grant"))
|
|
603
|
-
: undefined;
|
|
604
|
-
attempts[attemptId] = {
|
|
605
|
-
...attempt,
|
|
606
|
-
grantId: grant?.grantId ?? string(event.payload.grantId, "attempt.grant_bound.grantId"),
|
|
607
|
-
...(grant ? { grant } : {}),
|
|
608
|
-
updatedAt: event.occurredAt,
|
|
609
|
-
};
|
|
610
|
-
break;
|
|
611
|
-
}
|
|
612
|
-
case "attempt.leased": {
|
|
613
|
-
const lease = leaseFromPayload(event.payload);
|
|
614
|
-
const attempt = attempts[lease.attemptId];
|
|
615
|
-
if (!attempt)
|
|
616
|
-
throw new DurableTaskRuntimeError(`Unknown attempt '${lease.attemptId}'.`);
|
|
617
|
-
const agentId = typeof event.payload.agentId === "string" ? event.payload.agentId : undefined;
|
|
618
|
-
if (agentId) {
|
|
619
|
-
const agent = agents[agentId];
|
|
620
|
-
if (!agent)
|
|
621
|
-
throw new DurableTaskRuntimeError(`Unknown agent '${agentId}'.`);
|
|
622
|
-
agents[agentId] = {
|
|
623
|
-
...agent,
|
|
624
|
-
status: "active",
|
|
625
|
-
activeAttemptId: lease.attemptId,
|
|
626
|
-
updatedAt: event.occurredAt,
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
attempts[lease.attemptId] = {
|
|
630
|
-
...attempt,
|
|
631
|
-
status: "leased",
|
|
632
|
-
lease,
|
|
633
|
-
...(agentId ? { agentId } : {}),
|
|
634
|
-
updatedAt: event.occurredAt,
|
|
635
|
-
};
|
|
636
|
-
break;
|
|
637
|
-
}
|
|
638
|
-
case "attempt.started": {
|
|
639
|
-
const attemptId = string(event.payload.attemptId, "attempt.started.attemptId");
|
|
640
|
-
const attempt = attempts[attemptId];
|
|
641
|
-
if (!attempt)
|
|
642
|
-
throw new DurableTaskRuntimeError(`Unknown attempt '${attemptId}'.`);
|
|
643
|
-
attempts[attemptId] = { ...attempt, status: "running", updatedAt: event.occurredAt };
|
|
644
|
-
break;
|
|
645
|
-
}
|
|
646
|
-
case "attempt.checkpointed": {
|
|
647
|
-
const checkpoint = checkpointFromPayload(event.payload);
|
|
648
|
-
const attempt = attempts[checkpoint.attemptId];
|
|
649
|
-
if (!attempt)
|
|
650
|
-
throw new DurableTaskRuntimeError(`Unknown attempt '${checkpoint.attemptId}'.`);
|
|
651
|
-
checkpoints[checkpoint.checkpointId] = checkpoint;
|
|
652
|
-
attempts[checkpoint.attemptId] = {
|
|
653
|
-
...attempt,
|
|
654
|
-
checkpointIds: [...attempt.checkpointIds, checkpoint.checkpointId],
|
|
655
|
-
updatedAt: event.occurredAt,
|
|
656
|
-
};
|
|
657
|
-
if (attempt.agentId) {
|
|
658
|
-
const agent = agents[attempt.agentId];
|
|
659
|
-
if (agent) {
|
|
660
|
-
agents[attempt.agentId] = {
|
|
661
|
-
...agent,
|
|
662
|
-
resumeContext: { ...agent.resumeContext, latestCheckpointId: checkpoint.checkpointId },
|
|
663
|
-
updatedAt: event.occurredAt,
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
break;
|
|
668
|
-
}
|
|
669
|
-
case "attempt.suspended": {
|
|
670
|
-
const attemptId = string(event.payload.attemptId, "attempt.suspended.attemptId");
|
|
671
|
-
const attempt = attempts[attemptId];
|
|
672
|
-
if (!attempt)
|
|
673
|
-
throw new DurableTaskRuntimeError(`Unknown attempt '${attemptId}'.`);
|
|
674
|
-
const leaseId = string(event.payload.leaseId, "attempt.suspended.leaseId");
|
|
675
|
-
const fencingToken = number(event.payload.fencingToken, "attempt.suspended.fencingToken");
|
|
676
|
-
if (!attempt.lease || attempt.lease.leaseId !== leaseId || attempt.lease.fencingToken !== fencingToken) {
|
|
677
|
-
throw new DurableTaskRuntimeError(`Attempt '${attemptId}' lease or fencing token is stale.`);
|
|
678
|
-
}
|
|
679
|
-
if (attempt.status !== "leased" && attempt.status !== "running") {
|
|
680
|
-
throw new DurableTaskRuntimeError(`Attempt '${attemptId}' cannot suspend from '${attempt.status}'.`);
|
|
681
|
-
}
|
|
682
|
-
attempts[attemptId] = {
|
|
683
|
-
...attempt,
|
|
684
|
-
status: "suspended",
|
|
685
|
-
...(typeof event.payload.reasonCode === "string" ? { reasonCode: event.payload.reasonCode } : {}),
|
|
686
|
-
updatedAt: event.occurredAt,
|
|
687
|
-
};
|
|
688
|
-
if (attempt.agentId) {
|
|
689
|
-
const agent = agents[attempt.agentId];
|
|
690
|
-
if (agent)
|
|
691
|
-
agents[attempt.agentId] = { ...agent, status: "suspended", updatedAt: event.occurredAt };
|
|
692
|
-
}
|
|
693
|
-
break;
|
|
694
|
-
}
|
|
695
|
-
case "attempt.resumed": {
|
|
696
|
-
const lease = leaseFromPayload(event.payload);
|
|
697
|
-
const agentId = string(event.payload.agentId, "attempt.resumed.agentId");
|
|
698
|
-
const attempt = attempts[lease.attemptId];
|
|
699
|
-
const agent = agents[agentId];
|
|
700
|
-
if (!attempt)
|
|
701
|
-
throw new DurableTaskRuntimeError(`Unknown attempt '${lease.attemptId}'.`);
|
|
702
|
-
if (!agent)
|
|
703
|
-
throw new DurableTaskRuntimeError(`Unknown agent '${agentId}'.`);
|
|
704
|
-
attempts[lease.attemptId] = {
|
|
705
|
-
...attempt,
|
|
706
|
-
status: "leased",
|
|
707
|
-
agentId,
|
|
708
|
-
lease,
|
|
709
|
-
updatedAt: event.occurredAt,
|
|
710
|
-
};
|
|
711
|
-
agents[agentId] = {
|
|
712
|
-
...agent,
|
|
713
|
-
status: "active",
|
|
714
|
-
activeAttemptId: lease.attemptId,
|
|
715
|
-
updatedAt: event.occurredAt,
|
|
716
|
-
};
|
|
717
|
-
break;
|
|
718
|
-
}
|
|
719
|
-
case "attempt.cancelled": {
|
|
720
|
-
const attemptId = string(event.payload.attemptId, "attempt.cancelled.attemptId");
|
|
721
|
-
const attempt = attempts[attemptId];
|
|
722
|
-
if (!attempt)
|
|
723
|
-
throw new DurableTaskRuntimeError(`Unknown attempt '${attemptId}'.`);
|
|
724
|
-
attempts[attemptId] = {
|
|
725
|
-
...attempt,
|
|
726
|
-
status: "cancelled",
|
|
727
|
-
reasonCode: string(event.payload.reasonCode, "attempt.cancelled.reasonCode"),
|
|
728
|
-
updatedAt: event.occurredAt,
|
|
729
|
-
};
|
|
730
|
-
const task = tasks[attempt.taskId];
|
|
731
|
-
if (task) {
|
|
732
|
-
tasks[attempt.taskId] = {
|
|
733
|
-
...task,
|
|
734
|
-
task: { ...task.task, status: "cancelled", updatedAt: event.occurredAt },
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
releaseAttemptAgent(agents, attempt, event.occurredAt);
|
|
738
|
-
break;
|
|
739
|
-
}
|
|
740
|
-
case "attempt.finished": {
|
|
741
|
-
const result = resultFromPayload(event.payload);
|
|
742
|
-
const attempt = attempts[result.attemptId];
|
|
743
|
-
if (!attempt)
|
|
744
|
-
throw new DurableTaskRuntimeError(`Unknown attempt '${result.attemptId}'.`);
|
|
745
|
-
attempts[result.attemptId] = { ...attempt, status: result.status, result, updatedAt: event.occurredAt };
|
|
746
|
-
const task = tasks[result.taskId];
|
|
747
|
-
if (!task)
|
|
748
|
-
throw new DurableTaskRuntimeError(`Unknown task '${result.taskId}'.`);
|
|
749
|
-
tasks[result.taskId] = {
|
|
750
|
-
...task,
|
|
751
|
-
task: { ...task.task, status: taskStatusForResult(result.status), updatedAt: event.occurredAt },
|
|
752
|
-
};
|
|
753
|
-
releaseAttemptAgent(agents, attempt, event.occurredAt);
|
|
754
|
-
refreshReadyTasks(state, task.task.objectiveId, event.occurredAt);
|
|
755
|
-
break;
|
|
756
|
-
}
|
|
757
|
-
case "attempt.lease_expired": {
|
|
758
|
-
const attemptId = string(event.payload.attemptId, "attempt.lease_expired.attemptId");
|
|
759
|
-
const attempt = attempts[attemptId];
|
|
760
|
-
if (!attempt)
|
|
761
|
-
throw new DurableTaskRuntimeError(`Unknown attempt '${attemptId}'.`);
|
|
762
|
-
attempts[attemptId] = {
|
|
763
|
-
...attempt,
|
|
764
|
-
status: "expired",
|
|
765
|
-
...(typeof event.payload.reasonCode === "string" ? { reasonCode: event.payload.reasonCode } : {}),
|
|
766
|
-
updatedAt: event.occurredAt,
|
|
767
|
-
};
|
|
768
|
-
const task = tasks[attempt.taskId];
|
|
769
|
-
if (task)
|
|
770
|
-
tasks[attempt.taskId] = { ...task, task: { ...task.task, status: "ready", updatedAt: event.occurredAt } };
|
|
771
|
-
break;
|
|
772
|
-
}
|
|
773
|
-
case "approval.requested": {
|
|
774
|
-
const approval = approvalFromPayload(event.payload);
|
|
775
|
-
if (approvals[approval.approvalId]) {
|
|
776
|
-
throw new DurableTaskRuntimeError(`Approval '${approval.approvalId}' was requested more than once.`);
|
|
777
|
-
}
|
|
778
|
-
approvals[approval.approvalId] = { request: approval, status: "pending" };
|
|
779
|
-
const notificationId = `approval-requested:${approval.approvalId}`;
|
|
780
|
-
if (notifications[notificationId]) {
|
|
781
|
-
throw new DurableTaskRuntimeError(`Notification '${notificationId}' already exists.`);
|
|
782
|
-
}
|
|
783
|
-
notifications[notificationId] = {
|
|
784
|
-
notificationId,
|
|
785
|
-
objectiveId: approval.objectiveId,
|
|
786
|
-
...(approval.attemptId ? { attemptId: approval.attemptId } : {}),
|
|
787
|
-
status: "pending",
|
|
788
|
-
message: approval.summary,
|
|
789
|
-
createdAt: event.occurredAt,
|
|
790
|
-
};
|
|
791
|
-
break;
|
|
792
|
-
}
|
|
793
|
-
case "approval.resolved": {
|
|
794
|
-
const resolution = approvalResolutionFromPayload(event.payload);
|
|
795
|
-
const approval = approvals[resolution.approvalId];
|
|
796
|
-
if (!approval)
|
|
797
|
-
throw new DurableTaskRuntimeError(`Unknown approval '${resolution.approvalId}'.`);
|
|
798
|
-
approvals[resolution.approvalId] = {
|
|
799
|
-
...approval,
|
|
800
|
-
status: resolution.outcome,
|
|
801
|
-
resolution,
|
|
802
|
-
};
|
|
803
|
-
const approvalNotification = notifications[`approval-requested:${resolution.approvalId}`];
|
|
804
|
-
if (approvalNotification?.status === "pending") {
|
|
805
|
-
notifications[approvalNotification.notificationId] = {
|
|
806
|
-
...approvalNotification,
|
|
807
|
-
status: "delivered",
|
|
808
|
-
deliveredAt: event.occurredAt,
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
if (resolution.outcome === "rejected" && approval.request.attemptId) {
|
|
812
|
-
const attempt = attempts[approval.request.attemptId];
|
|
813
|
-
if (attempt && !terminalAttemptStatus(attempt.status)) {
|
|
814
|
-
attempts[attempt.attemptId] = {
|
|
815
|
-
...attempt,
|
|
816
|
-
status: "blocked",
|
|
817
|
-
reasonCode: "approval_rejected",
|
|
818
|
-
updatedAt: event.occurredAt,
|
|
819
|
-
};
|
|
820
|
-
const task = tasks[attempt.taskId];
|
|
821
|
-
if (task) {
|
|
822
|
-
tasks[attempt.taskId] = {
|
|
823
|
-
...task,
|
|
824
|
-
task: { ...task.task, status: "blocked", updatedAt: event.occurredAt },
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
break;
|
|
830
|
-
}
|
|
831
|
-
case "notification.enqueued": {
|
|
832
|
-
const notificationId = string(event.payload.notificationId, "notification.enqueued.notificationId");
|
|
833
|
-
notifications[notificationId] = {
|
|
834
|
-
notificationId,
|
|
835
|
-
objectiveId: string(event.payload.objectiveId, "notification.enqueued.objectiveId"),
|
|
836
|
-
...(typeof event.payload.attemptId === "string" ? { attemptId: event.payload.attemptId } : {}),
|
|
837
|
-
status: "pending",
|
|
838
|
-
message: string(event.payload.message, "notification.enqueued.message"),
|
|
839
|
-
createdAt: event.occurredAt,
|
|
840
|
-
};
|
|
841
|
-
break;
|
|
842
|
-
}
|
|
843
|
-
case "notification.delivered": {
|
|
844
|
-
const notificationId = string(event.payload.notificationId, "notification.delivered.notificationId");
|
|
845
|
-
const notification = notifications[notificationId];
|
|
846
|
-
if (!notification)
|
|
847
|
-
throw new DurableTaskRuntimeError(`Unknown notification '${notificationId}'.`);
|
|
848
|
-
notifications[notificationId] = { ...notification, status: "delivered", deliveredAt: event.occurredAt };
|
|
849
|
-
break;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
return { ...state, lastOrdinal: event.ordinal };
|
|
853
|
-
}
|
|
854
|
-
export function projectOrchestrationEvents(events) {
|
|
855
|
-
return events.reduce(reduceOrchestrationEvent, emptyProjection());
|
|
856
|
-
}
|
|
6
|
+
import { approvalFromPayload, assertAcceptanceCriteria, assertReferencedAcceptanceCriteriaRetained, assertRiskBudget, checkpointSummary, dispatchFromValue, dispatchIdentifier, dispatchIdentifierArray, executionGrantFromValue, MAX_DATE_EPOCH_MS, projectionFromSnapshot, retryStateFromValue, taskFromValue, usageFromPayload, validateTaskContractForState, validateTaskDependencyIds, } from "./task-runtime-codecs.js";
|
|
7
|
+
import { assertBoundedIdentifierList, assertIdentifierListHasCapacity, assertObjectiveEvidenceHasCapacity, assertProjectionRecordReplacementWithinLimits, assertRecordHasCapacity, cloneProjection, projectionCapacity, requestedProjectionSlots, } from "./task-runtime-projection.js";
|
|
8
|
+
import { activeTaskAttempt, assertAgentNotRetired, assertAgentRetirementEligible, assertApprovalRequestTransition, assertApprovalResolutionTransition, assertAttemptCheckpointTransition, assertAttemptFinishTransition, assertAttemptGrantTransition, assertAttemptLeaseTransition, assertAttemptStartTransition, assertNotificationTarget, assertObjectiveStatusTransition, assertRetryBackoffElapsedAt, assertTaskAttemptBudgetForState, assertTaskFailureTransition, assertVerificationTransition, attemptDispatchReadiness, projectOrchestrationEvents, reduceOrchestrationEvent, requireActiveObjectiveForAttemptInProjection, } from "./task-runtime-reducer.js";
|
|
9
|
+
import { DurableTaskRuntimeError, terminalAttemptStatus, } from "./task-runtime-state.js";
|
|
10
|
+
export { validateTaskDependencyIds } from "./task-runtime-codecs.js";
|
|
11
|
+
export { projectOrchestrationEvents, reduceOrchestrationEvent, } from "./task-runtime-reducer.js";
|
|
12
|
+
export { DurableTaskRuntimeError, } from "./task-runtime-state.js";
|
|
857
13
|
export class DurableTaskRuntime {
|
|
858
14
|
constructor(options) {
|
|
859
15
|
this.store = options.store;
|
|
860
|
-
this.now = options.now ?? Date.now;
|
|
16
|
+
this.now = options.now ?? (() => Date.now());
|
|
861
17
|
this.createId = options.createId ?? randomUUID;
|
|
862
18
|
const snapshot = this.store.readProjectionSnapshot();
|
|
863
19
|
if (snapshot) {
|
|
@@ -881,6 +37,146 @@ export class DurableTaskRuntime {
|
|
|
881
37
|
this.refresh();
|
|
882
38
|
return cloneProjection(this.state);
|
|
883
39
|
}
|
|
40
|
+
/** Read-only retained-record counts and remaining lifetime slots for orchestration admission. */
|
|
41
|
+
getProjectionCapacity() {
|
|
42
|
+
this.refresh();
|
|
43
|
+
return projectionCapacity(this.state);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Preflight every record needed by a multi-event operation before its first append. Callers use
|
|
47
|
+
* this to reserve implementation and mandatory-verifier task/attempt pairs atomically.
|
|
48
|
+
*/
|
|
49
|
+
assertProjectionHeadroom(required) {
|
|
50
|
+
this.refresh();
|
|
51
|
+
const capacity = projectionCapacity(this.state);
|
|
52
|
+
const agents = requestedProjectionSlots(required.agents, "Agent binding");
|
|
53
|
+
const objectives = requestedProjectionSlots(required.objectives, "Objective");
|
|
54
|
+
const tasks = requestedProjectionSlots(required.tasks, "Task");
|
|
55
|
+
const attempts = requestedProjectionSlots(required.attempts, "Attempt");
|
|
56
|
+
const checkpoints = requestedProjectionSlots(required.checkpoints, "Checkpoint");
|
|
57
|
+
const approvals = requestedProjectionSlots(required.approvals, "Approval");
|
|
58
|
+
const notifications = requestedProjectionSlots(required.notifications, "Notification");
|
|
59
|
+
const evidence = requestedProjectionSlots(required.evidence, "Evidence");
|
|
60
|
+
if (agents > capacity.headroom.agents) {
|
|
61
|
+
throw new DurableTaskRuntimeError(`Orchestration agent binding limit (${capacity.limits.agents}) lacks ${agents} required slots.`);
|
|
62
|
+
}
|
|
63
|
+
if (tasks > capacity.headroom.tasks) {
|
|
64
|
+
throw new DurableTaskRuntimeError(`Orchestration task limit (${capacity.limits.tasks}) lacks ${tasks} required slots.`);
|
|
65
|
+
}
|
|
66
|
+
if (attempts > capacity.headroom.attempts) {
|
|
67
|
+
throw new DurableTaskRuntimeError(`Orchestration attempt limit (${capacity.limits.attempts}) lacks ${attempts} required slots.`);
|
|
68
|
+
}
|
|
69
|
+
if (objectives > capacity.headroom.objectives) {
|
|
70
|
+
throw new DurableTaskRuntimeError(`Orchestration objective limit (${capacity.limits.objectives}) lacks ${objectives} required slots.`);
|
|
71
|
+
}
|
|
72
|
+
if (checkpoints > capacity.headroom.checkpoints) {
|
|
73
|
+
throw new DurableTaskRuntimeError(`Orchestration checkpoint limit (${capacity.limits.checkpoints}) lacks ${checkpoints} required slots.`);
|
|
74
|
+
}
|
|
75
|
+
if (approvals > capacity.headroom.approvals) {
|
|
76
|
+
throw new DurableTaskRuntimeError(`Orchestration approval limit (${capacity.limits.approvals}) lacks ${approvals} required slots.`);
|
|
77
|
+
}
|
|
78
|
+
if (notifications > capacity.headroom.notifications) {
|
|
79
|
+
throw new DurableTaskRuntimeError(`Orchestration notification limit (${capacity.limits.notifications}) lacks ${notifications} required slots.`);
|
|
80
|
+
}
|
|
81
|
+
if (evidence > capacity.headroom.evidence) {
|
|
82
|
+
throw new DurableTaskRuntimeError(`Orchestration evidence limit (${capacity.limits.evidence}) lacks ${evidence} required slots.`);
|
|
83
|
+
}
|
|
84
|
+
return capacity;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Preflight the idempotent evidence batch for one objective before synchronizing any member.
|
|
88
|
+
* The objective may be absent when its creation is part of the same compound operation.
|
|
89
|
+
*/
|
|
90
|
+
assertObjectiveEvidenceHeadroom(objectiveId, evidenceIds) {
|
|
91
|
+
this.refresh();
|
|
92
|
+
assertBoundedIdentifierList(evidenceIds, MAX_ORCHESTRATION_OBJECTIVE_EVIDENCE, "Objective evidence ids");
|
|
93
|
+
const objective = this.state.objectives[objectiveId];
|
|
94
|
+
const existingIds = new Set(objective?.evidence.map((evidence) => evidence.evidenceId) ?? []);
|
|
95
|
+
let required = 0;
|
|
96
|
+
for (const evidenceId of evidenceIds) {
|
|
97
|
+
if (!existingIds.has(evidenceId))
|
|
98
|
+
required += 1;
|
|
99
|
+
}
|
|
100
|
+
if ((objective?.evidence.length ?? 0) + required > MAX_ORCHESTRATION_OBJECTIVE_EVIDENCE) {
|
|
101
|
+
throw new DurableTaskRuntimeError(`Orchestration objective evidence limit (${MAX_ORCHESTRATION_OBJECTIVE_EVIDENCE}) lacks ${required} required slots.`);
|
|
102
|
+
}
|
|
103
|
+
const capacity = projectionCapacity(this.state);
|
|
104
|
+
if (required > capacity.headroom.evidence) {
|
|
105
|
+
throw new DurableTaskRuntimeError(`Orchestration evidence limit (${capacity.limits.evidence}) lacks ${required} required slots.`);
|
|
106
|
+
}
|
|
107
|
+
return required;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Preflight the final retained objective record for a multi-event owner synchronization. This
|
|
111
|
+
* validates the objective metadata and the complete idempotently merged evidence batch before
|
|
112
|
+
* ensureObjective can append the first event, so a later byte rejection cannot leave a prefix.
|
|
113
|
+
*/
|
|
114
|
+
assertObjectiveSynchronizationHeadroom(input, evidence) {
|
|
115
|
+
this.refresh();
|
|
116
|
+
const objectiveId = dispatchIdentifier(input.objectiveId, "objective.objectiveId");
|
|
117
|
+
const current = this.state.objectives[objectiveId];
|
|
118
|
+
const requiredEvidence = this.assertObjectiveEvidenceHeadroom(objectiveId, evidence.map((item) => item.evidenceId));
|
|
119
|
+
this.assertProjectionHeadroom({ objectives: current ? 0 : 1, evidence: requiredEvidence });
|
|
120
|
+
assertRiskBudget(input.riskBudget, "objective.riskBudget");
|
|
121
|
+
const acceptanceCriteria = structuredClone(input.acceptanceCriteria ?? []);
|
|
122
|
+
assertAcceptanceCriteria(acceptanceCriteria);
|
|
123
|
+
const title = input.title.trim();
|
|
124
|
+
const description = input.description.trim();
|
|
125
|
+
if (!title || !description)
|
|
126
|
+
throw new DurableTaskRuntimeError("Objective title and description are required.");
|
|
127
|
+
const now = this.nowIso();
|
|
128
|
+
const objective = current
|
|
129
|
+
? {
|
|
130
|
+
...current.objective,
|
|
131
|
+
title,
|
|
132
|
+
description,
|
|
133
|
+
constraints: [...(input.constraints ?? [])],
|
|
134
|
+
acceptanceCriteria,
|
|
135
|
+
riskBudget: { ...(input.riskBudget ?? {}) },
|
|
136
|
+
updatedAt: now,
|
|
137
|
+
}
|
|
138
|
+
: {
|
|
139
|
+
schemaVersion: ORCHESTRATION_SCHEMA_VERSION,
|
|
140
|
+
objectiveId,
|
|
141
|
+
title,
|
|
142
|
+
description,
|
|
143
|
+
status: "active",
|
|
144
|
+
constraints: [...(input.constraints ?? [])],
|
|
145
|
+
acceptanceCriteria,
|
|
146
|
+
riskBudget: { ...(input.riskBudget ?? {}) },
|
|
147
|
+
createdAt: now,
|
|
148
|
+
updatedAt: now,
|
|
149
|
+
};
|
|
150
|
+
if (current) {
|
|
151
|
+
assertReferencedAcceptanceCriteriaRetained(this.state.tasks, current, acceptanceCriteria);
|
|
152
|
+
}
|
|
153
|
+
const mergedEvidence = current?.evidence.map((item) => structuredClone(item)) ?? [];
|
|
154
|
+
const evidenceById = new Map(mergedEvidence.map((item) => [item.evidenceId, item]));
|
|
155
|
+
for (const item of evidence) {
|
|
156
|
+
const evidenceId = dispatchIdentifier(item.evidenceId, "objective evidence.evidenceId");
|
|
157
|
+
if (!item.summary.trim() || !item.createdAt.trim()) {
|
|
158
|
+
throw new DurableTaskRuntimeError("Objective evidence requires an id, summary, and creation time.");
|
|
159
|
+
}
|
|
160
|
+
if (item.criterionId && !objective.acceptanceCriteria.some((criterion) => criterion.id === item.criterionId)) {
|
|
161
|
+
throw new DurableTaskRuntimeError(`Objective evidence references unknown acceptance criterion '${item.criterionId}'.`);
|
|
162
|
+
}
|
|
163
|
+
const existing = evidenceById.get(evidenceId);
|
|
164
|
+
if (existing) {
|
|
165
|
+
if (!isDeepStrictEqual(existing, item)) {
|
|
166
|
+
throw new DurableTaskRuntimeError(`Objective evidence '${evidenceId}' has conflicting content.`);
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const retained = structuredClone(item);
|
|
171
|
+
mergedEvidence.push(retained);
|
|
172
|
+
evidenceById.set(evidenceId, retained);
|
|
173
|
+
}
|
|
174
|
+
assertProjectionRecordReplacementWithinLimits(this.state, "objectives", objectiveId, {
|
|
175
|
+
objective,
|
|
176
|
+
taskIds: current?.taskIds ?? [],
|
|
177
|
+
evidence: mergedEvidence,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
884
180
|
registerAgent(input) {
|
|
885
181
|
this.refresh();
|
|
886
182
|
const now = this.nowIso();
|
|
@@ -916,6 +212,7 @@ export class DurableTaskRuntime {
|
|
|
916
212
|
};
|
|
917
213
|
if (this.state.agents[agent.agentId])
|
|
918
214
|
throw new DurableTaskRuntimeError(`Agent '${agent.agentId}' already exists.`);
|
|
215
|
+
assertRecordHasCapacity(this.state.agents, MAX_ORCHESTRATION_AGENT_BINDINGS, "agent binding");
|
|
919
216
|
this.commit({
|
|
920
217
|
type: "agent.registered",
|
|
921
218
|
aggregateId: agent.agentId,
|
|
@@ -925,6 +222,25 @@ export class DurableTaskRuntime {
|
|
|
925
222
|
});
|
|
926
223
|
return structuredClone(agent);
|
|
927
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Retire one idle persistent identity without deleting its binding, lineage, transcript pointers,
|
|
227
|
+
* or attempt history. A repeated call is inert and returns the durable retired binding.
|
|
228
|
+
*/
|
|
229
|
+
retireAgent(agentId) {
|
|
230
|
+
this.refresh();
|
|
231
|
+
const normalizedAgentId = dispatchIdentifier(agentId.trim(), "agentId");
|
|
232
|
+
const agent = assertAgentRetirementEligible(this.state, normalizedAgentId);
|
|
233
|
+
if (agent.status === "retired")
|
|
234
|
+
return structuredClone(agent);
|
|
235
|
+
this.commit({
|
|
236
|
+
type: "agent.retired",
|
|
237
|
+
aggregateId: normalizedAgentId,
|
|
238
|
+
actor: "runtime",
|
|
239
|
+
idempotencyKey: `agent-retired:${normalizedAgentId}`,
|
|
240
|
+
payload: toJsonObject({ agentId: normalizedAgentId }),
|
|
241
|
+
});
|
|
242
|
+
return structuredClone(this.state.agents[normalizedAgentId]);
|
|
243
|
+
}
|
|
928
244
|
createObjective(input) {
|
|
929
245
|
this.refresh();
|
|
930
246
|
assertRiskBudget(input.riskBudget, "objective.riskBudget");
|
|
@@ -947,6 +263,7 @@ export class DurableTaskRuntime {
|
|
|
947
263
|
if (this.state.objectives[objective.objectiveId]) {
|
|
948
264
|
throw new DurableTaskRuntimeError(`Objective '${objective.objectiveId}' already exists.`);
|
|
949
265
|
}
|
|
266
|
+
assertRecordHasCapacity(this.state.objectives, MAX_ORCHESTRATION_OBJECTIVES, "objective");
|
|
950
267
|
this.commit({
|
|
951
268
|
type: "objective.created",
|
|
952
269
|
aggregateId: objective.objectiveId,
|
|
@@ -984,11 +301,7 @@ export class DurableTaskRuntime {
|
|
|
984
301
|
};
|
|
985
302
|
if (isDeepStrictEqual(currentFields, nextFields))
|
|
986
303
|
return structuredClone(current.objective);
|
|
987
|
-
|
|
988
|
-
const referencedRemovedIds = current.taskIds.flatMap((taskId) => (this.state.tasks[taskId]?.task.acceptanceCriterionIds ?? []).filter((criterionId) => !retainedCriterionIds.has(criterionId)));
|
|
989
|
-
if (referencedRemovedIds.length > 0) {
|
|
990
|
-
throw new DurableTaskRuntimeError(`Cannot remove acceptance criteria referenced by tasks: ${[...new Set(referencedRemovedIds)].join(", ")}.`);
|
|
991
|
-
}
|
|
304
|
+
assertReferencedAcceptanceCriteriaRetained(this.state.tasks, current, acceptanceCriteria);
|
|
992
305
|
const objective = {
|
|
993
306
|
...current.objective,
|
|
994
307
|
...nextFields,
|
|
@@ -1019,6 +332,7 @@ export class DurableTaskRuntime {
|
|
|
1019
332
|
!objective.objective.acceptanceCriteria.some((criterion) => criterion.id === evidence.criterionId)) {
|
|
1020
333
|
throw new DurableTaskRuntimeError(`Objective evidence references unknown acceptance criterion '${evidence.criterionId}'.`);
|
|
1021
334
|
}
|
|
335
|
+
assertObjectiveEvidenceHasCapacity(this.state.objectives, objective);
|
|
1022
336
|
this.commit({
|
|
1023
337
|
type: "objective.evidence_recorded",
|
|
1024
338
|
aggregateId: objectiveId,
|
|
@@ -1030,59 +344,9 @@ export class DurableTaskRuntime {
|
|
|
1030
344
|
}
|
|
1031
345
|
createTask(input) {
|
|
1032
346
|
this.refresh();
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
throw new DurableTaskRuntimeError(`Unknown objective '${input.objectiveId}'.`);
|
|
1037
|
-
if (objectiveState.objective.status !== "active") {
|
|
1038
|
-
throw new DurableTaskRuntimeError(`Objective '${input.objectiveId}' is not active.`);
|
|
1039
|
-
}
|
|
1040
|
-
const dependsOn = [...new Set(input.dependsOn ?? [])];
|
|
1041
|
-
const acceptanceCriterionIds = [...new Set(input.acceptanceCriterionIds ?? [])];
|
|
1042
|
-
const objectiveCriterionIds = new Set(objectiveState.objective.acceptanceCriteria.map((criterion) => criterion.id));
|
|
1043
|
-
const unknownCriterionIds = acceptanceCriterionIds.filter((criterionId) => !objectiveCriterionIds.has(criterionId));
|
|
1044
|
-
if (unknownCriterionIds.length > 0) {
|
|
1045
|
-
throw new DurableTaskRuntimeError(`Task references unknown acceptance criteria: ${unknownCriterionIds.join(", ")}.`);
|
|
1046
|
-
}
|
|
1047
|
-
for (const dependencyId of dependsOn) {
|
|
1048
|
-
const dependency = this.state.tasks[dependencyId];
|
|
1049
|
-
if (!dependency || dependency.task.objectiveId !== input.objectiveId) {
|
|
1050
|
-
throw new DurableTaskRuntimeError(`Task dependency '${dependencyId}' is not in objective '${input.objectiveId}'.`);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
if (input.verificationOfTaskId) {
|
|
1054
|
-
const subject = this.state.tasks[input.verificationOfTaskId];
|
|
1055
|
-
if (!subject || subject.task.objectiveId !== input.objectiveId) {
|
|
1056
|
-
throw new DurableTaskRuntimeError(`Verification subject '${input.verificationOfTaskId}' is not in objective '${input.objectiveId}'.`);
|
|
1057
|
-
}
|
|
1058
|
-
if (input.role !== "verifier") {
|
|
1059
|
-
throw new DurableTaskRuntimeError("Only verifier tasks may declare verificationOfTaskId.");
|
|
1060
|
-
}
|
|
1061
|
-
if (subject.task.role === "verifier") {
|
|
1062
|
-
throw new DurableTaskRuntimeError("A verifier task cannot independently verify another verifier task.");
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
const now = this.nowIso();
|
|
1066
|
-
const task = {
|
|
1067
|
-
schemaVersion: ORCHESTRATION_SCHEMA_VERSION,
|
|
1068
|
-
taskId: input.taskId ?? `task-${this.createId()}`,
|
|
1069
|
-
objectiveId: input.objectiveId,
|
|
1070
|
-
title: input.title.trim(),
|
|
1071
|
-
description: input.description.trim(),
|
|
1072
|
-
role: input.role,
|
|
1073
|
-
status: dependsOn.length === 0 ? "ready" : "pending",
|
|
1074
|
-
dependsOn,
|
|
1075
|
-
requiredCapabilities: [...new Set(input.requiredCapabilities ?? [])],
|
|
1076
|
-
acceptanceCriterionIds,
|
|
1077
|
-
...(input.verificationOfTaskId ? { verificationOfTaskId: input.verificationOfTaskId } : {}),
|
|
1078
|
-
riskBudget: { ...(input.riskBudget ?? {}) },
|
|
1079
|
-
createdAt: now,
|
|
1080
|
-
updatedAt: now,
|
|
1081
|
-
};
|
|
1082
|
-
if (!task.title || !task.description)
|
|
1083
|
-
throw new DurableTaskRuntimeError("Task title and description are required.");
|
|
1084
|
-
if (this.state.tasks[task.taskId])
|
|
1085
|
-
throw new DurableTaskRuntimeError(`Task '${task.taskId}' already exists.`);
|
|
347
|
+
const { task, objectiveState } = this.buildTask(input);
|
|
348
|
+
assertRecordHasCapacity(this.state.tasks, MAX_ORCHESTRATION_TASKS, "task");
|
|
349
|
+
assertIdentifierListHasCapacity(objectiveState.taskIds, MAX_ORCHESTRATION_TASKS, "objective task list");
|
|
1086
350
|
this.commit({
|
|
1087
351
|
type: "task.created",
|
|
1088
352
|
aggregateId: task.objectiveId,
|
|
@@ -1092,6 +356,35 @@ export class DurableTaskRuntime {
|
|
|
1092
356
|
});
|
|
1093
357
|
return structuredClone(this.state.tasks[task.taskId].task);
|
|
1094
358
|
}
|
|
359
|
+
/** Atomically create a new task and its first queued attempt in one durable transition. */
|
|
360
|
+
prepareTaskAttempt(input, dispatch, grantId) {
|
|
361
|
+
this.refresh();
|
|
362
|
+
const { task, objectiveState } = this.buildTask(input);
|
|
363
|
+
const taskState = { task, attemptIds: [] };
|
|
364
|
+
const normalizedDispatch = this.normalizeDispatchForTask(taskState, dispatch);
|
|
365
|
+
this.assertTaskAttemptBudget(taskState, objectiveState.objective);
|
|
366
|
+
assertRecordHasCapacity(this.state.tasks, MAX_ORCHESTRATION_TASKS, "task");
|
|
367
|
+
assertIdentifierListHasCapacity(objectiveState.taskIds, MAX_ORCHESTRATION_TASKS, "objective task list");
|
|
368
|
+
assertRecordHasCapacity(this.state.attempts, MAX_ORCHESTRATION_ATTEMPTS, "attempt");
|
|
369
|
+
const attemptId = `attempt-${this.createId()}`;
|
|
370
|
+
this.commit({
|
|
371
|
+
type: "task.attempt_prepared",
|
|
372
|
+
aggregateId: task.taskId,
|
|
373
|
+
actor: "runtime",
|
|
374
|
+
idempotencyKey: `task-attempt-prepared:${task.taskId}:${attemptId}`,
|
|
375
|
+
payload: toJsonObject({
|
|
376
|
+
task,
|
|
377
|
+
attemptId,
|
|
378
|
+
taskId: task.taskId,
|
|
379
|
+
dispatch: normalizedDispatch,
|
|
380
|
+
...(grantId ? { grantId } : {}),
|
|
381
|
+
}),
|
|
382
|
+
});
|
|
383
|
+
return {
|
|
384
|
+
task: structuredClone(this.state.tasks[task.taskId].task),
|
|
385
|
+
attempt: structuredClone(this.state.attempts[attemptId]),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
1095
388
|
queueAttempt(taskId, dispatch, grantId) {
|
|
1096
389
|
this.refresh();
|
|
1097
390
|
const task = this.requireDispatchableTask(taskId);
|
|
@@ -1099,21 +392,10 @@ export class DurableTaskRuntime {
|
|
|
1099
392
|
if (pendingApproval) {
|
|
1100
393
|
throw new DurableTaskRuntimeError(`Task '${taskId}' is awaiting approval '${pendingApproval.request.approvalId}'.`);
|
|
1101
394
|
}
|
|
1102
|
-
const normalizedDispatch =
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
if (normalizedDispatch.executionContract &&
|
|
1107
|
-
(normalizedDispatch.executionContract.worker.profile.profileId !== normalizedDispatch.profileId ||
|
|
1108
|
-
normalizedDispatch.executionContract.worker.profile.role !== task.task.role)) {
|
|
1109
|
-
throw new DurableTaskRuntimeError("Execution contract does not match the dispatch profile and task role.");
|
|
1110
|
-
}
|
|
1111
|
-
const objective = this.state.objectives[task.task.objectiveId].objective;
|
|
1112
|
-
const attemptCeilings = [task.task.riskBudget.maxAttempts, objective.riskBudget.maxAttempts].filter((value) => value !== undefined);
|
|
1113
|
-
const maxAttempts = attemptCeilings.length > 0 ? Math.min(...attemptCeilings) : undefined;
|
|
1114
|
-
if (maxAttempts !== undefined && task.attemptIds.length >= maxAttempts) {
|
|
1115
|
-
throw new DurableTaskRuntimeError(`Task '${taskId}' exhausted its ${maxAttempts} attempt budget.`);
|
|
1116
|
-
}
|
|
395
|
+
const normalizedDispatch = this.normalizeDispatchForTask(task, dispatch);
|
|
396
|
+
this.assertTaskAttemptBudget(task, this.state.objectives[task.task.objectiveId].objective);
|
|
397
|
+
assertRecordHasCapacity(this.state.attempts, MAX_ORCHESTRATION_ATTEMPTS, "attempt");
|
|
398
|
+
assertIdentifierListHasCapacity(task.attemptIds, MAX_ORCHESTRATION_ATTEMPTS, "task attempt list");
|
|
1117
399
|
const attemptId = `attempt-${this.createId()}`;
|
|
1118
400
|
this.commit({
|
|
1119
401
|
type: "attempt.queued",
|
|
@@ -1124,32 +406,17 @@ export class DurableTaskRuntime {
|
|
|
1124
406
|
});
|
|
1125
407
|
return structuredClone(this.state.attempts[attemptId]);
|
|
1126
408
|
}
|
|
409
|
+
/** Read-only controller seam for deciding whether one durable queued or suspended attempt may run. */
|
|
410
|
+
getAttemptDispatchReadiness(attemptId) {
|
|
411
|
+
this.refresh();
|
|
412
|
+
return structuredClone(attemptDispatchReadiness(this.state, this.requireAttempt(attemptId)));
|
|
413
|
+
}
|
|
1127
414
|
bindAttemptGrant(attemptId, grant) {
|
|
1128
415
|
this.refresh();
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
if (!grant.grantId.trim())
|
|
1134
|
-
throw new DurableTaskRuntimeError("Grant id is required.");
|
|
1135
|
-
const task = this.state.tasks[attempt.taskId];
|
|
1136
|
-
if (!task ||
|
|
1137
|
-
grant.schemaVersion !== ORCHESTRATION_SCHEMA_VERSION ||
|
|
1138
|
-
grant.attemptId !== attemptId ||
|
|
1139
|
-
grant.taskId !== attempt.taskId ||
|
|
1140
|
-
grant.objectiveId !== task.task.objectiveId ||
|
|
1141
|
-
grant.role !== task.task.role) {
|
|
1142
|
-
throw new DurableTaskRuntimeError("Execution grant target does not match the attempt.");
|
|
1143
|
-
}
|
|
1144
|
-
const approval = this.approvalForAttempt(attemptId);
|
|
1145
|
-
if (approval?.status === "pending") {
|
|
1146
|
-
throw new DurableTaskRuntimeError(`Attempt '${attemptId}' is awaiting approval '${approval.request.approvalId}'.`);
|
|
1147
|
-
}
|
|
1148
|
-
if (approval?.status === "rejected") {
|
|
1149
|
-
throw new DurableTaskRuntimeError(`Approval '${approval.request.approvalId}' was rejected.`);
|
|
1150
|
-
}
|
|
1151
|
-
if (attempt.grantId === grant.grantId) {
|
|
1152
|
-
if (attempt.grant && !isDeepStrictEqual(attempt.grant, grant)) {
|
|
416
|
+
const normalizedGrant = executionGrantFromValue(grant, "grant");
|
|
417
|
+
const attempt = assertAttemptGrantTransition(this.state, attemptId, attemptId, normalizedGrant);
|
|
418
|
+
if (attempt.grantId === normalizedGrant.grantId) {
|
|
419
|
+
if (attempt.grant && !isDeepStrictEqual(attempt.grant, normalizedGrant)) {
|
|
1153
420
|
throw new DurableTaskRuntimeError(`Attempt '${attemptId}' grant has conflicting content.`);
|
|
1154
421
|
}
|
|
1155
422
|
return structuredClone(attempt);
|
|
@@ -1160,35 +427,16 @@ export class DurableTaskRuntime {
|
|
|
1160
427
|
type: "attempt.grant_bound",
|
|
1161
428
|
aggregateId: attemptId,
|
|
1162
429
|
actor: "policy",
|
|
1163
|
-
idempotencyKey: `attempt-grant-bound:${attemptId}:${
|
|
1164
|
-
payload: toJsonObject({ attemptId, grant }),
|
|
430
|
+
idempotencyKey: `attempt-grant-bound:${attemptId}:${normalizedGrant.grantId}`,
|
|
431
|
+
payload: toJsonObject({ attemptId, grant: normalizedGrant }),
|
|
1165
432
|
});
|
|
1166
433
|
return structuredClone(this.state.attempts[attemptId]);
|
|
1167
434
|
}
|
|
1168
435
|
leaseAttempt(attemptId, ownerId, ttlMs, agentId) {
|
|
1169
436
|
this.refresh();
|
|
1170
437
|
const attempt = this.requireAttempt(attemptId);
|
|
1171
|
-
if (attempt.status !== "queued")
|
|
1172
|
-
throw new DurableTaskRuntimeError(`Attempt '${attemptId}' is not queued.`);
|
|
1173
|
-
this.requireActiveObjectiveForAttempt(attempt);
|
|
1174
|
-
const approval = this.approvalForAttempt(attemptId);
|
|
1175
|
-
if (approval?.status === "pending") {
|
|
1176
|
-
throw new DurableTaskRuntimeError(`Attempt '${attemptId}' is awaiting approval '${approval.request.approvalId}'.`);
|
|
1177
|
-
}
|
|
1178
|
-
if (!attempt.grantId) {
|
|
1179
|
-
throw new DurableTaskRuntimeError(`Attempt '${attemptId}' requires an execution grant before leasing.`);
|
|
1180
|
-
}
|
|
1181
|
-
if (agentId) {
|
|
1182
|
-
const agent = this.state.agents[agentId];
|
|
1183
|
-
if (!agent)
|
|
1184
|
-
throw new DurableTaskRuntimeError(`Unknown agent '${agentId}'.`);
|
|
1185
|
-
if (agent.status !== "registered")
|
|
1186
|
-
throw new DurableTaskRuntimeError(`Agent '${agentId}' is not idle.`);
|
|
1187
|
-
const task = this.state.tasks[attempt.taskId];
|
|
1188
|
-
if (!task || task.task.role !== agent.role)
|
|
1189
|
-
throw new DurableTaskRuntimeError(`Agent '${agentId}' role does not match task.`);
|
|
1190
|
-
}
|
|
1191
438
|
const lease = this.issueLease(attempt, ownerId, ttlMs);
|
|
439
|
+
assertAttemptLeaseTransition(this.state, attemptId, lease, agentId, this.nowIso());
|
|
1192
440
|
this.commit({
|
|
1193
441
|
type: "attempt.leased",
|
|
1194
442
|
aggregateId: attemptId,
|
|
@@ -1200,10 +448,7 @@ export class DurableTaskRuntime {
|
|
|
1200
448
|
}
|
|
1201
449
|
startAttempt(attemptId, leaseId, fencingToken) {
|
|
1202
450
|
this.refresh();
|
|
1203
|
-
|
|
1204
|
-
this.requireActiveObjectiveForAttempt(attempt);
|
|
1205
|
-
if (attempt.status !== "leased")
|
|
1206
|
-
throw new DurableTaskRuntimeError(`Attempt '${attemptId}' is not leased.`);
|
|
451
|
+
assertAttemptStartTransition(this.state, attemptId, attemptId, leaseId, fencingToken, this.nowIso());
|
|
1207
452
|
this.commit({
|
|
1208
453
|
type: "attempt.started",
|
|
1209
454
|
aggregateId: attemptId,
|
|
@@ -1218,18 +463,22 @@ export class DurableTaskRuntime {
|
|
|
1218
463
|
const attempt = this.requireLiveLease(args.attemptId, args.leaseId, args.fencingToken);
|
|
1219
464
|
if (attempt.status !== "running")
|
|
1220
465
|
throw new DurableTaskRuntimeError(`Attempt '${args.attemptId}' is not running.`);
|
|
466
|
+
const summary = checkpointSummary(args.summary, "checkpoint.summary");
|
|
467
|
+
const artifactIds = dispatchIdentifierArray(args.artifactIds ?? [], "checkpoint.artifactIds");
|
|
468
|
+
const evidenceIds = dispatchIdentifierArray(args.evidenceIds ?? [], "checkpoint.evidenceIds");
|
|
469
|
+
assertIdentifierListHasCapacity(attempt.checkpointIds, MAX_ORCHESTRATION_CHECKPOINTS, "attempt checkpoint list");
|
|
470
|
+
assertRecordHasCapacity(this.state.checkpoints, MAX_ORCHESTRATION_CHECKPOINTS, "checkpoint");
|
|
1221
471
|
const checkpoint = {
|
|
1222
472
|
checkpointId: `checkpoint-${this.createId()}`,
|
|
1223
473
|
attemptId: args.attemptId,
|
|
1224
474
|
fencingToken: args.fencingToken,
|
|
1225
|
-
summary
|
|
1226
|
-
artifactIds
|
|
1227
|
-
evidenceIds
|
|
475
|
+
summary,
|
|
476
|
+
artifactIds,
|
|
477
|
+
evidenceIds,
|
|
1228
478
|
...(args.usage ? { usage: usageFromPayload(args.usage, "checkpoint.usage") } : {}),
|
|
1229
479
|
createdAt: this.nowIso(),
|
|
1230
480
|
};
|
|
1231
|
-
|
|
1232
|
-
throw new DurableTaskRuntimeError("Checkpoint summary is required.");
|
|
481
|
+
assertAttemptCheckpointTransition(this.state, args.attemptId, checkpoint, args.leaseId, this.nowIso());
|
|
1233
482
|
this.commit({
|
|
1234
483
|
type: "attempt.checkpointed",
|
|
1235
484
|
aggregateId: args.attemptId,
|
|
@@ -1241,22 +490,7 @@ export class DurableTaskRuntime {
|
|
|
1241
490
|
}
|
|
1242
491
|
finishAttempt(result) {
|
|
1243
492
|
this.refresh();
|
|
1244
|
-
|
|
1245
|
-
if (attempt.status !== "running" && attempt.status !== "leased") {
|
|
1246
|
-
throw new DurableTaskRuntimeError(`Attempt '${result.attemptId}' cannot finish from '${attempt.status}'.`);
|
|
1247
|
-
}
|
|
1248
|
-
if (attempt.taskId !== result.taskId)
|
|
1249
|
-
throw new DurableTaskRuntimeError("Worker result taskId does not match attempt.");
|
|
1250
|
-
const task = this.state.tasks[attempt.taskId];
|
|
1251
|
-
if (!task || task.task.objectiveId !== result.objectiveId) {
|
|
1252
|
-
throw new DurableTaskRuntimeError("Worker result objectiveId does not match attempt.");
|
|
1253
|
-
}
|
|
1254
|
-
if (result.status === "completed") {
|
|
1255
|
-
const missingCriteria = missingTrustedCriteria(result, task.task.acceptanceCriterionIds);
|
|
1256
|
-
if (missingCriteria.length > 0) {
|
|
1257
|
-
throw new DurableTaskRuntimeError(`Completed result lacks trusted evidence for acceptance criteria: ${missingCriteria.join(", ")}.`);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
493
|
+
assertAttemptFinishTransition(this.state, result.attemptId, result, this.nowIso());
|
|
1260
494
|
this.commit({
|
|
1261
495
|
type: "attempt.finished",
|
|
1262
496
|
aggregateId: result.attemptId,
|
|
@@ -1268,36 +502,7 @@ export class DurableTaskRuntime {
|
|
|
1268
502
|
}
|
|
1269
503
|
finishVerification(args) {
|
|
1270
504
|
this.refresh();
|
|
1271
|
-
|
|
1272
|
-
const verifierTask = this.state.tasks[args.verifierTaskId];
|
|
1273
|
-
const verifierAttempt = this.state.attempts[args.verifierAttemptId];
|
|
1274
|
-
if (!subject)
|
|
1275
|
-
throw new DurableTaskRuntimeError(`Unknown verification subject '${args.taskId}'.`);
|
|
1276
|
-
if (!verifierTask || verifierTask.task.verificationOfTaskId !== args.taskId) {
|
|
1277
|
-
throw new DurableTaskRuntimeError(`Task '${args.verifierTaskId}' is not the verifier for '${args.taskId}'.`);
|
|
1278
|
-
}
|
|
1279
|
-
if (!verifierAttempt || verifierAttempt.taskId !== args.verifierTaskId) {
|
|
1280
|
-
throw new DurableTaskRuntimeError(`Unknown verifier attempt '${args.verifierAttemptId}'.`);
|
|
1281
|
-
}
|
|
1282
|
-
if (args.verdict === "inconclusive") {
|
|
1283
|
-
if (verifierAttempt.status === "queued" ||
|
|
1284
|
-
verifierAttempt.status === "leased" ||
|
|
1285
|
-
verifierAttempt.status === "running") {
|
|
1286
|
-
throw new DurableTaskRuntimeError(`Verifier attempt '${args.verifierAttemptId}' is not terminal.`);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
else if (verifierAttempt.status !== "completed" || !verifierAttempt.result) {
|
|
1290
|
-
throw new DurableTaskRuntimeError(`Verifier attempt '${args.verifierAttemptId}' is not completed.`);
|
|
1291
|
-
}
|
|
1292
|
-
if (subject.task.status !== "blocked") {
|
|
1293
|
-
throw new DurableTaskRuntimeError(`Verification subject '${args.taskId}' cannot reconcile from '${subject.task.status}'.`);
|
|
1294
|
-
}
|
|
1295
|
-
if (!args.reasonCode.trim())
|
|
1296
|
-
throw new DurableTaskRuntimeError("Verification reason code is required.");
|
|
1297
|
-
if (args.verdict === "accepted" &&
|
|
1298
|
-
!verifierAttempt.result?.evidence.some((evidence) => evidence.trusted && evidence.kind === "review" && evidence.metadata?.subjectTaskId === args.taskId)) {
|
|
1299
|
-
throw new DurableTaskRuntimeError("Accepted verification requires trusted review evidence for the subject task.");
|
|
1300
|
-
}
|
|
505
|
+
assertVerificationTransition(this.state, args.taskId, args);
|
|
1301
506
|
this.commit({
|
|
1302
507
|
type: "task.verification_finished",
|
|
1303
508
|
aggregateId: args.taskId,
|
|
@@ -1341,6 +546,15 @@ export class DurableTaskRuntime {
|
|
|
1341
546
|
}
|
|
1342
547
|
if (!args.reasonCode.trim())
|
|
1343
548
|
throw new DurableTaskRuntimeError("Suspension reason is required.");
|
|
549
|
+
const retry = args.retry ? retryStateFromValue(args.retry, "attempt.suspended.retry") : undefined;
|
|
550
|
+
if (retry) {
|
|
551
|
+
if (retry.retriesUsed !== (attempt.retry?.retriesUsed ?? 0) + 1) {
|
|
552
|
+
throw new DurableTaskRuntimeError(`Attempt '${args.attemptId}' retry count is not monotonic.`);
|
|
553
|
+
}
|
|
554
|
+
if (Date.parse(retry.notBefore) <= this.now()) {
|
|
555
|
+
throw new DurableTaskRuntimeError("Retry not-before deadline must be in the future.");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
1344
558
|
this.commit({
|
|
1345
559
|
type: "attempt.suspended",
|
|
1346
560
|
aggregateId: args.attemptId,
|
|
@@ -1351,6 +565,7 @@ export class DurableTaskRuntime {
|
|
|
1351
565
|
leaseId: args.leaseId,
|
|
1352
566
|
fencingToken: args.fencingToken,
|
|
1353
567
|
reasonCode: args.reasonCode.trim(),
|
|
568
|
+
...(retry ? { retry } : {}),
|
|
1354
569
|
}),
|
|
1355
570
|
});
|
|
1356
571
|
return structuredClone(this.state.attempts[args.attemptId]);
|
|
@@ -1362,8 +577,7 @@ export class DurableTaskRuntime {
|
|
|
1362
577
|
throw new DurableTaskRuntimeError(`Unknown task '${taskId}'.`);
|
|
1363
578
|
if (["completed", "failed", "cancelled"].includes(task.task.status))
|
|
1364
579
|
return structuredClone(task.task);
|
|
1365
|
-
|
|
1366
|
-
throw new DurableTaskRuntimeError("Task failure reason is required.");
|
|
580
|
+
assertTaskFailureTransition(this.state, taskId, taskId, reasonCode);
|
|
1367
581
|
this.commit({
|
|
1368
582
|
type: "task.failed",
|
|
1369
583
|
aggregateId: taskId,
|
|
@@ -1425,11 +639,20 @@ export class DurableTaskRuntime {
|
|
|
1425
639
|
}
|
|
1426
640
|
return expired;
|
|
1427
641
|
}
|
|
1428
|
-
requestAgentResume(agentId) {
|
|
642
|
+
requestAgentResume(agentId, attemptId) {
|
|
1429
643
|
this.refresh();
|
|
1430
644
|
const agent = this.state.agents[agentId];
|
|
1431
645
|
if (!agent)
|
|
1432
646
|
throw new DurableTaskRuntimeError(`Unknown agent '${agentId}'.`);
|
|
647
|
+
assertAgentNotRetired(agent, "request resume");
|
|
648
|
+
const attempt = this.requireAttempt(attemptId);
|
|
649
|
+
if (attempt.status !== "suspended" || attempt.agentId !== agentId || agent.activeAttemptId !== attemptId) {
|
|
650
|
+
throw new DurableTaskRuntimeError(`Agent '${agentId}' cannot resume suspended attempt '${attemptId}'.`);
|
|
651
|
+
}
|
|
652
|
+
this.assertRetryBackoffElapsed(attempt);
|
|
653
|
+
this.requireActiveObjectiveForAttempt(attempt);
|
|
654
|
+
if (agent.status === "resuming")
|
|
655
|
+
return structuredClone(agent);
|
|
1433
656
|
if (agent.status !== "suspended")
|
|
1434
657
|
throw new DurableTaskRuntimeError(`Agent '${agentId}' is not suspended.`);
|
|
1435
658
|
this.commit({
|
|
@@ -1437,10 +660,15 @@ export class DurableTaskRuntime {
|
|
|
1437
660
|
aggregateId: agentId,
|
|
1438
661
|
actor: "runtime",
|
|
1439
662
|
idempotencyKey: `agent-resume-requested:${agentId}:${this.state.lastOrdinal}`,
|
|
1440
|
-
payload: toJsonObject({ agentId }),
|
|
663
|
+
payload: toJsonObject({ agentId, attemptId }),
|
|
1441
664
|
});
|
|
1442
665
|
return structuredClone(this.state.agents[agentId]);
|
|
1443
666
|
}
|
|
667
|
+
/** Read-only guard used before changing agent state; resumeAttempt repeats it authoritatively. */
|
|
668
|
+
assertAttemptReadyForResume(attemptId) {
|
|
669
|
+
this.refresh();
|
|
670
|
+
this.assertRetryBackoffElapsed(this.requireAttempt(attemptId));
|
|
671
|
+
}
|
|
1444
672
|
resumeAttempt(attemptId, agentId, ttlMs, ownerId = agentId) {
|
|
1445
673
|
this.refresh();
|
|
1446
674
|
const attempt = this.requireAttempt(attemptId);
|
|
@@ -1450,6 +678,7 @@ export class DurableTaskRuntime {
|
|
|
1450
678
|
}
|
|
1451
679
|
if (!agent || agent.status !== "resuming")
|
|
1452
680
|
throw new DurableTaskRuntimeError(`Agent '${agentId}' is not resuming.`);
|
|
681
|
+
this.assertRetryBackoffElapsed(attempt);
|
|
1453
682
|
this.requireActiveObjectiveForAttempt(attempt);
|
|
1454
683
|
const lease = this.issueLease(attempt, ownerId, ttlMs);
|
|
1455
684
|
this.commit({
|
|
@@ -1471,28 +700,9 @@ export class DurableTaskRuntime {
|
|
|
1471
700
|
this.transitionObjective(objectiveId, "objective.cancelled", "cancelled");
|
|
1472
701
|
}
|
|
1473
702
|
completeObjective(objectiveId) {
|
|
1474
|
-
this.
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
if (incomplete.length > 0) {
|
|
1478
|
-
throw new DurableTaskRuntimeError(`Objective '${objectiveId}' has incomplete tasks: ${incomplete.join(", ")}.`);
|
|
1479
|
-
}
|
|
1480
|
-
const requiredCriterionIds = objective.objective.acceptanceCriteria
|
|
1481
|
-
.filter((criterion) => criterion.required)
|
|
1482
|
-
.map((criterion) => criterion.id);
|
|
1483
|
-
const evidence = [
|
|
1484
|
-
...objective.evidence,
|
|
1485
|
-
...objective.taskIds.flatMap((taskId) => {
|
|
1486
|
-
const task = this.state.tasks[taskId];
|
|
1487
|
-
return (task?.attemptIds.flatMap((attemptId) => this.state.attempts[attemptId]?.result?.evidence ?? []) ?? []);
|
|
1488
|
-
}),
|
|
1489
|
-
];
|
|
1490
|
-
const provenCriterionIds = new Set(evidence.flatMap((item) => (item.trusted && item.criterionId ? [item.criterionId] : [])));
|
|
1491
|
-
const unproven = requiredCriterionIds.filter((criterionId) => !provenCriterionIds.has(criterionId));
|
|
1492
|
-
if (unproven.length > 0) {
|
|
1493
|
-
throw new DurableTaskRuntimeError(`Objective '${objectiveId}' lacks trusted evidence for required criteria: ${unproven.join(", ")}.`);
|
|
1494
|
-
}
|
|
1495
|
-
this.transitionObjective(objectiveId, "objective.completed", "completed");
|
|
703
|
+
this.transitionObjective(objectiveId, "objective.completed", "completed", {
|
|
704
|
+
completionPolicy: "task_evidence",
|
|
705
|
+
});
|
|
1496
706
|
}
|
|
1497
707
|
completeObjectiveFromOwner(objectiveId, acceptanceOverride) {
|
|
1498
708
|
this.refresh();
|
|
@@ -1502,16 +712,9 @@ export class DurableTaskRuntime {
|
|
|
1502
712
|
if (objective.objective.status === "cancelled") {
|
|
1503
713
|
throw new DurableTaskRuntimeError(`Objective '${objectiveId}' is terminal.`);
|
|
1504
714
|
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
.filter((criterion) => criterion.required && !provenCriterionIds.has(criterion.id))
|
|
1509
|
-
.map((criterion) => criterion.id);
|
|
1510
|
-
if (unproven.length > 0) {
|
|
1511
|
-
throw new DurableTaskRuntimeError(`Objective '${objectiveId}' lacks trusted owner evidence for required criteria: ${unproven.join(", ")}.`);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
this.transitionObjective(objectiveId, "objective.completed", "completed");
|
|
715
|
+
this.transitionObjective(objectiveId, "objective.completed", "completed", {
|
|
716
|
+
completionPolicy: acceptanceOverride ? "owner_override" : "owner_evidence",
|
|
717
|
+
});
|
|
1515
718
|
}
|
|
1516
719
|
requestApproval(approval) {
|
|
1517
720
|
this.refresh();
|
|
@@ -1523,14 +726,9 @@ export class DurableTaskRuntime {
|
|
|
1523
726
|
}
|
|
1524
727
|
return structuredClone(existing);
|
|
1525
728
|
}
|
|
1526
|
-
this.
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
candidate.request.taskId === normalized.taskId &&
|
|
1530
|
-
candidate.request.attemptId === normalized.attemptId);
|
|
1531
|
-
if (pendingForTarget) {
|
|
1532
|
-
throw new DurableTaskRuntimeError(`Target already awaits approval '${pendingForTarget.request.approvalId}'.`);
|
|
1533
|
-
}
|
|
729
|
+
assertApprovalRequestTransition(this.state, normalized.attemptId ?? normalized.taskId ?? normalized.objectiveId, normalized);
|
|
730
|
+
assertRecordHasCapacity(this.state.approvals, MAX_ORCHESTRATION_APPROVALS, "approval");
|
|
731
|
+
assertRecordHasCapacity(this.state.notifications, MAX_ORCHESTRATION_NOTIFICATIONS, "notification");
|
|
1534
732
|
this.commit({
|
|
1535
733
|
type: "approval.requested",
|
|
1536
734
|
aggregateId: normalized.attemptId ?? normalized.taskId ?? normalized.objectiveId,
|
|
@@ -1562,6 +760,7 @@ export class DurableTaskRuntime {
|
|
|
1562
760
|
reasonCode: reasonCode.trim(),
|
|
1563
761
|
resolvedAt: this.nowIso(),
|
|
1564
762
|
};
|
|
763
|
+
assertApprovalResolutionTransition(this.state, approval.request.attemptId ?? approval.request.taskId ?? approval.request.objectiveId, resolution);
|
|
1565
764
|
this.commit({
|
|
1566
765
|
type: "approval.resolved",
|
|
1567
766
|
aggregateId: approval.request.attemptId ?? approval.request.taskId ?? approval.request.objectiveId,
|
|
@@ -1573,11 +772,18 @@ export class DurableTaskRuntime {
|
|
|
1573
772
|
}
|
|
1574
773
|
enqueueNotification(args) {
|
|
1575
774
|
this.refresh();
|
|
1576
|
-
this.
|
|
775
|
+
assertNotificationTarget(this.state, args.objectiveId, args.attemptId);
|
|
1577
776
|
const notificationId = args.notificationId ?? `notification-${this.createId()}`;
|
|
1578
777
|
const existing = this.state.notifications[notificationId];
|
|
1579
|
-
if (existing)
|
|
778
|
+
if (existing) {
|
|
779
|
+
if (existing.objectiveId !== args.objectiveId ||
|
|
780
|
+
existing.attemptId !== args.attemptId ||
|
|
781
|
+
existing.message !== args.message) {
|
|
782
|
+
throw new DurableTaskRuntimeError(`Notification id '${notificationId}' has conflicting content.`);
|
|
783
|
+
}
|
|
1580
784
|
return structuredClone(existing);
|
|
785
|
+
}
|
|
786
|
+
assertRecordHasCapacity(this.state.notifications, MAX_ORCHESTRATION_NOTIFICATIONS, "notification");
|
|
1581
787
|
this.commit({
|
|
1582
788
|
type: "notification.enqueued",
|
|
1583
789
|
aggregateId: args.objectiveId,
|
|
@@ -1603,20 +809,67 @@ export class DurableTaskRuntime {
|
|
|
1603
809
|
});
|
|
1604
810
|
return structuredClone(this.state.notifications[notificationId]);
|
|
1605
811
|
}
|
|
1606
|
-
|
|
812
|
+
buildTask(input) {
|
|
813
|
+
assertRiskBudget(input.riskBudget, "task.riskBudget");
|
|
814
|
+
const objectiveState = this.state.objectives[input.objectiveId];
|
|
815
|
+
if (!objectiveState)
|
|
816
|
+
throw new DurableTaskRuntimeError(`Unknown objective '${input.objectiveId}'.`);
|
|
817
|
+
if (objectiveState.objective.status !== "active") {
|
|
818
|
+
throw new DurableTaskRuntimeError(`Objective '${input.objectiveId}' is not active.`);
|
|
819
|
+
}
|
|
820
|
+
const dependsOn = validateTaskDependencyIds(this.state.tasks, input.objectiveId, input.dependsOn);
|
|
821
|
+
const acceptanceCriterionIds = [...new Set(input.acceptanceCriterionIds ?? [])];
|
|
822
|
+
const now = this.nowIso();
|
|
823
|
+
const task = taskFromValue({
|
|
824
|
+
schemaVersion: ORCHESTRATION_SCHEMA_VERSION,
|
|
825
|
+
taskId: input.taskId ?? `task-${this.createId()}`,
|
|
826
|
+
objectiveId: input.objectiveId,
|
|
827
|
+
title: input.title.trim(),
|
|
828
|
+
description: input.description.trim(),
|
|
829
|
+
role: input.role,
|
|
830
|
+
status: dependsOn.length === 0 ? "ready" : "pending",
|
|
831
|
+
dependsOn,
|
|
832
|
+
requiredCapabilities: [...new Set(input.requiredCapabilities ?? [])],
|
|
833
|
+
acceptanceCriterionIds,
|
|
834
|
+
...(input.verificationOfTaskId ? { verificationOfTaskId: input.verificationOfTaskId } : {}),
|
|
835
|
+
riskBudget: { ...(input.riskBudget ?? {}) },
|
|
836
|
+
createdAt: now,
|
|
837
|
+
updatedAt: now,
|
|
838
|
+
}, "task");
|
|
839
|
+
if (this.state.tasks[task.taskId])
|
|
840
|
+
throw new DurableTaskRuntimeError(`Task '${task.taskId}' already exists.`);
|
|
841
|
+
validateTaskContractForState(this.state, task, `Task '${task.taskId}'`);
|
|
842
|
+
return { task, objectiveState };
|
|
843
|
+
}
|
|
844
|
+
normalizeDispatchForTask(task, dispatch) {
|
|
845
|
+
const normalizedDispatch = dispatchFromValue(dispatch, "dispatch");
|
|
846
|
+
if (normalizedDispatch.taskId !== task.task.taskId) {
|
|
847
|
+
throw new DurableTaskRuntimeError("Dispatch taskId does not match the queued task.");
|
|
848
|
+
}
|
|
849
|
+
if (normalizedDispatch.executionContract &&
|
|
850
|
+
(normalizedDispatch.executionContract.worker.profile.profileId !== normalizedDispatch.profileId ||
|
|
851
|
+
normalizedDispatch.executionContract.worker.profile.role !== task.task.role)) {
|
|
852
|
+
throw new DurableTaskRuntimeError("Execution contract does not match the dispatch profile and task role.");
|
|
853
|
+
}
|
|
854
|
+
return normalizedDispatch;
|
|
855
|
+
}
|
|
856
|
+
assertTaskAttemptBudget(task, objective) {
|
|
857
|
+
assertTaskAttemptBudgetForState(task, objective);
|
|
858
|
+
}
|
|
859
|
+
transitionObjective(objectiveId, type, target, payload = {}) {
|
|
1607
860
|
this.refresh();
|
|
1608
861
|
const objective = this.requireObjective(objectiveId);
|
|
1609
862
|
if (objective.objective.status === target)
|
|
1610
863
|
return;
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
864
|
+
assertObjectiveStatusTransition(this.state, objectiveId, target, type === "objective.completed"
|
|
865
|
+
? payload.completionPolicy
|
|
866
|
+
: undefined);
|
|
1614
867
|
this.commit({
|
|
1615
868
|
type,
|
|
1616
869
|
aggregateId: objectiveId,
|
|
1617
870
|
actor: "human",
|
|
1618
871
|
idempotencyKey: `${type}:${objectiveId}:${this.state.lastOrdinal}`,
|
|
1619
|
-
payload
|
|
872
|
+
payload,
|
|
1620
873
|
});
|
|
1621
874
|
}
|
|
1622
875
|
requireObjective(objectiveId) {
|
|
@@ -1633,9 +886,13 @@ export class DurableTaskRuntime {
|
|
|
1633
886
|
if (objective.objective.status !== "active") {
|
|
1634
887
|
throw new DurableTaskRuntimeError(`Objective '${objective.objective.objectiveId}' is not active.`);
|
|
1635
888
|
}
|
|
1636
|
-
if (!["ready", "blocked", "failed"].includes(task.task.status)) {
|
|
889
|
+
if (!["pending", "ready", "blocked", "failed"].includes(task.task.status)) {
|
|
1637
890
|
throw new DurableTaskRuntimeError(`Task '${taskId}' is not dispatchable from '${task.task.status}'.`);
|
|
1638
891
|
}
|
|
892
|
+
const active = activeTaskAttempt(this.state, task);
|
|
893
|
+
if (active) {
|
|
894
|
+
throw new DurableTaskRuntimeError(`Task '${taskId}' already owns active attempt '${active.attemptId}'.`);
|
|
895
|
+
}
|
|
1639
896
|
return task;
|
|
1640
897
|
}
|
|
1641
898
|
requireAttempt(attemptId) {
|
|
@@ -1658,9 +915,6 @@ export class DurableTaskRuntime {
|
|
|
1658
915
|
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
1659
916
|
};
|
|
1660
917
|
}
|
|
1661
|
-
approvalForAttempt(attemptId) {
|
|
1662
|
-
return Object.values(this.state.approvals).find((approval) => approval.request.attemptId === attemptId);
|
|
1663
|
-
}
|
|
1664
918
|
pendingApprovalForTask(taskId) {
|
|
1665
919
|
const task = this.state.tasks[taskId];
|
|
1666
920
|
if (!task)
|
|
@@ -1671,37 +925,10 @@ export class DurableTaskRuntime {
|
|
|
1671
925
|
(approval.request.taskId === undefined || approval.request.taskId === taskId));
|
|
1672
926
|
}
|
|
1673
927
|
requireActiveObjectiveForAttempt(attempt) {
|
|
1674
|
-
|
|
1675
|
-
if (!task)
|
|
1676
|
-
throw new DurableTaskRuntimeError(`Unknown task '${attempt.taskId}'.`);
|
|
1677
|
-
const objective = this.requireObjective(task.task.objectiveId).objective;
|
|
1678
|
-
if (objective.status !== "active") {
|
|
1679
|
-
throw new DurableTaskRuntimeError(`Objective '${objective.objectiveId}' is not active.`);
|
|
1680
|
-
}
|
|
928
|
+
requireActiveObjectiveForAttemptInProjection(this.state, attempt);
|
|
1681
929
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
if (objective.status !== "active") {
|
|
1685
|
-
throw new DurableTaskRuntimeError(`Objective '${approval.objectiveId}' is not active.`);
|
|
1686
|
-
}
|
|
1687
|
-
if (approval.attemptId && !approval.taskId) {
|
|
1688
|
-
throw new DurableTaskRuntimeError("Attempt-scoped approval requires a taskId.");
|
|
1689
|
-
}
|
|
1690
|
-
if (approval.taskId) {
|
|
1691
|
-
const task = this.state.tasks[approval.taskId];
|
|
1692
|
-
if (!task || task.task.objectiveId !== approval.objectiveId) {
|
|
1693
|
-
throw new DurableTaskRuntimeError(`Approval task '${approval.taskId}' does not belong to its objective.`);
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
if (approval.attemptId) {
|
|
1697
|
-
const attempt = this.state.attempts[approval.attemptId];
|
|
1698
|
-
if (!attempt || attempt.taskId !== approval.taskId) {
|
|
1699
|
-
throw new DurableTaskRuntimeError(`Approval attempt '${approval.attemptId}' does not belong to its task.`);
|
|
1700
|
-
}
|
|
1701
|
-
if (attempt.status !== "queued" || attempt.grantId) {
|
|
1702
|
-
throw new DurableTaskRuntimeError(`Approval attempt '${approval.attemptId}' is not awaiting policy.`);
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
930
|
+
assertRetryBackoffElapsed(attempt) {
|
|
931
|
+
assertRetryBackoffElapsedAt(attempt, this.now());
|
|
1705
932
|
}
|
|
1706
933
|
requireLiveLease(attemptId, leaseId, fencingToken) {
|
|
1707
934
|
const attempt = this.requireAttempt(attemptId);
|
|
@@ -1735,7 +962,24 @@ export class DurableTaskRuntime {
|
|
|
1735
962
|
}
|
|
1736
963
|
commit(input) {
|
|
1737
964
|
this.compactCurrentProjection();
|
|
1738
|
-
|
|
965
|
+
let admittedProjection;
|
|
966
|
+
const event = this.store.append(input, {
|
|
967
|
+
expectedLastOrdinal: this.state.lastOrdinal,
|
|
968
|
+
validateBeforeCommit: (candidate) => {
|
|
969
|
+
if (candidate.ordinal > this.state.lastOrdinal) {
|
|
970
|
+
admittedProjection = reduceOrchestrationEvent(this.state, candidate);
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
});
|
|
974
|
+
if (admittedProjection) {
|
|
975
|
+
this.state = admittedProjection;
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
// Exact idempotent replay can return an older event after another writer committed a
|
|
979
|
+
// contiguous suffix. Adopt through readAfter so no intervening ordinal is skipped. Test stores
|
|
980
|
+
// that do not execute append admission still fall back to reducing their returned next event.
|
|
981
|
+
if (event.ordinal > this.state.lastOrdinal)
|
|
982
|
+
this.refresh();
|
|
1739
983
|
if (event.ordinal > this.state.lastOrdinal)
|
|
1740
984
|
this.state = reduceOrchestrationEvent(this.state, event);
|
|
1741
985
|
}
|