@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.
Files changed (226) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/core/agent-paths.d.ts +13 -0
  3. package/dist/core/agent-paths.d.ts.map +1 -1
  4. package/dist/core/agent-paths.js +23 -0
  5. package/dist/core/agent-paths.js.map +1 -1
  6. package/dist/core/autonomy/bounded-completion.d.ts +7 -0
  7. package/dist/core/autonomy/bounded-completion.d.ts.map +1 -1
  8. package/dist/core/autonomy/bounded-completion.js +12 -4
  9. package/dist/core/autonomy/bounded-completion.js.map +1 -1
  10. package/dist/core/autonomy/lane-tool-surface.d.ts +2 -0
  11. package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
  12. package/dist/core/autonomy/lane-tool-surface.js +6 -3
  13. package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
  14. package/dist/core/autonomy/subagent-prompt.js +1 -1
  15. package/dist/core/autonomy/subagent-prompt.js.map +1 -1
  16. package/dist/core/background-lane-controller.d.ts +25 -2
  17. package/dist/core/background-lane-controller.d.ts.map +1 -1
  18. package/dist/core/background-lane-controller.js +67 -0
  19. package/dist/core/background-lane-controller.js.map +1 -1
  20. package/dist/core/delegation/replay-safe-mailbox-bounds.d.ts +4 -0
  21. package/dist/core/delegation/replay-safe-mailbox-bounds.d.ts.map +1 -0
  22. package/dist/core/delegation/replay-safe-mailbox-bounds.js +20 -0
  23. package/dist/core/delegation/replay-safe-mailbox-bounds.js.map +1 -0
  24. package/dist/core/delegation/sanitized-context-fork.d.ts +25 -0
  25. package/dist/core/delegation/sanitized-context-fork.d.ts.map +1 -0
  26. package/dist/core/delegation/sanitized-context-fork.js +299 -0
  27. package/dist/core/delegation/sanitized-context-fork.js.map +1 -0
  28. package/dist/core/delegation/session-root-mailbox.d.ts +75 -0
  29. package/dist/core/delegation/session-root-mailbox.d.ts.map +1 -0
  30. package/dist/core/delegation/session-root-mailbox.js +627 -0
  31. package/dist/core/delegation/session-root-mailbox.js.map +1 -0
  32. package/dist/core/delegation/worker-agent-control-coordinator.d.ts +102 -9
  33. package/dist/core/delegation/worker-agent-control-coordinator.d.ts.map +1 -1
  34. package/dist/core/delegation/worker-agent-control-coordinator.js +1307 -125
  35. package/dist/core/delegation/worker-agent-control-coordinator.js.map +1 -1
  36. package/dist/core/delegation/worker-agent-control.d.ts +183 -8
  37. package/dist/core/delegation/worker-agent-control.d.ts.map +1 -1
  38. package/dist/core/delegation/worker-agent-control.js +778 -48
  39. package/dist/core/delegation/worker-agent-control.js.map +1 -1
  40. package/dist/core/delegation/worker-attempt-executor.d.ts +9 -0
  41. package/dist/core/delegation/worker-attempt-executor.d.ts.map +1 -1
  42. package/dist/core/delegation/worker-attempt-executor.js +479 -214
  43. package/dist/core/delegation/worker-attempt-executor.js.map +1 -1
  44. package/dist/core/delegation/worker-authority-resolver.d.ts +9 -0
  45. package/dist/core/delegation/worker-authority-resolver.d.ts.map +1 -1
  46. package/dist/core/delegation/worker-authority-resolver.js +21 -2
  47. package/dist/core/delegation/worker-authority-resolver.js.map +1 -1
  48. package/dist/core/delegation/worker-context-fork-store.d.ts +71 -0
  49. package/dist/core/delegation/worker-context-fork-store.d.ts.map +1 -0
  50. package/dist/core/delegation/worker-context-fork-store.js +437 -0
  51. package/dist/core/delegation/worker-context-fork-store.js.map +1 -0
  52. package/dist/core/delegation/worker-context-inheritance-policy.d.ts +18 -0
  53. package/dist/core/delegation/worker-context-inheritance-policy.d.ts.map +1 -0
  54. package/dist/core/delegation/worker-context-inheritance-policy.js +28 -0
  55. package/dist/core/delegation/worker-context-inheritance-policy.js.map +1 -0
  56. package/dist/core/delegation/worker-conversation-revision.d.ts +31 -0
  57. package/dist/core/delegation/worker-conversation-revision.d.ts.map +1 -0
  58. package/dist/core/delegation/worker-conversation-revision.js +203 -0
  59. package/dist/core/delegation/worker-conversation-revision.js.map +1 -0
  60. package/dist/core/delegation/worker-conversation-store.d.ts +130 -18
  61. package/dist/core/delegation/worker-conversation-store.d.ts.map +1 -1
  62. package/dist/core/delegation/worker-conversation-store.js +1263 -79
  63. package/dist/core/delegation/worker-conversation-store.js.map +1 -1
  64. package/dist/core/delegation/worker-delegation-controller.d.ts +29 -2
  65. package/dist/core/delegation/worker-delegation-controller.d.ts.map +1 -1
  66. package/dist/core/delegation/worker-delegation-controller.js +625 -111
  67. package/dist/core/delegation/worker-delegation-controller.js.map +1 -1
  68. package/dist/core/delegation/worker-delegation-request.d.ts +2 -0
  69. package/dist/core/delegation/worker-delegation-request.d.ts.map +1 -1
  70. package/dist/core/delegation/worker-delegation-request.js.map +1 -1
  71. package/dist/core/delegation/worker-dispatch-scheduler.d.ts +20 -1
  72. package/dist/core/delegation/worker-dispatch-scheduler.d.ts.map +1 -1
  73. package/dist/core/delegation/worker-dispatch-scheduler.js +210 -37
  74. package/dist/core/delegation/worker-dispatch-scheduler.js.map +1 -1
  75. package/dist/core/delegation/worker-fleet-limits.d.ts +38 -0
  76. package/dist/core/delegation/worker-fleet-limits.d.ts.map +1 -0
  77. package/dist/core/delegation/worker-fleet-limits.js +113 -0
  78. package/dist/core/delegation/worker-fleet-limits.js.map +1 -0
  79. package/dist/core/delegation/worker-lane-projection.d.ts +1 -0
  80. package/dist/core/delegation/worker-lane-projection.d.ts.map +1 -1
  81. package/dist/core/delegation/worker-lane-projection.js +6 -2
  82. package/dist/core/delegation/worker-lane-projection.js.map +1 -1
  83. package/dist/core/delegation/worker-lifecycle.d.ts +20 -3
  84. package/dist/core/delegation/worker-lifecycle.d.ts.map +1 -1
  85. package/dist/core/delegation/worker-lifecycle.js +42 -18
  86. package/dist/core/delegation/worker-lifecycle.js.map +1 -1
  87. package/dist/core/delegation/worker-notification-coordinator.d.ts +4 -0
  88. package/dist/core/delegation/worker-notification-coordinator.d.ts.map +1 -1
  89. package/dist/core/delegation/worker-notification-coordinator.js +45 -4
  90. package/dist/core/delegation/worker-notification-coordinator.js.map +1 -1
  91. package/dist/core/delegation/worker-provider-turn-protocol.d.ts +50 -0
  92. package/dist/core/delegation/worker-provider-turn-protocol.d.ts.map +1 -0
  93. package/dist/core/delegation/worker-provider-turn-protocol.js +185 -0
  94. package/dist/core/delegation/worker-provider-turn-protocol.js.map +1 -0
  95. package/dist/core/delegation/worker-recovery-coordinator.d.ts +45 -4
  96. package/dist/core/delegation/worker-recovery-coordinator.d.ts.map +1 -1
  97. package/dist/core/delegation/worker-recovery-coordinator.js +223 -17
  98. package/dist/core/delegation/worker-recovery-coordinator.js.map +1 -1
  99. package/dist/core/delegation/worker-retry-policy.d.ts +36 -0
  100. package/dist/core/delegation/worker-retry-policy.d.ts.map +1 -0
  101. package/dist/core/delegation/worker-retry-policy.js +41 -0
  102. package/dist/core/delegation/worker-retry-policy.js.map +1 -0
  103. package/dist/core/delegation/worker-runner.d.ts +3 -1
  104. package/dist/core/delegation/worker-runner.d.ts.map +1 -1
  105. package/dist/core/delegation/worker-runner.js +4 -2
  106. package/dist/core/delegation/worker-runner.js.map +1 -1
  107. package/dist/core/delegation/worker-task-view.d.ts +43 -0
  108. package/dist/core/delegation/worker-task-view.d.ts.map +1 -0
  109. package/dist/core/delegation/worker-task-view.js +194 -0
  110. package/dist/core/delegation/worker-task-view.js.map +1 -0
  111. package/dist/core/delegation/worker-terminal-handoff-coordinator.d.ts +55 -0
  112. package/dist/core/delegation/worker-terminal-handoff-coordinator.d.ts.map +1 -0
  113. package/dist/core/delegation/worker-terminal-handoff-coordinator.js +179 -0
  114. package/dist/core/delegation/worker-terminal-handoff-coordinator.js.map +1 -0
  115. package/dist/core/delegation/worker-tree-budget-coordinator.d.ts +2 -2
  116. package/dist/core/delegation/worker-tree-budget-coordinator.d.ts.map +1 -1
  117. package/dist/core/delegation/worker-tree-budget-coordinator.js +12 -46
  118. package/dist/core/delegation/worker-tree-budget-coordinator.js.map +1 -1
  119. package/dist/core/keybindings.d.ts +10 -0
  120. package/dist/core/keybindings.d.ts.map +1 -1
  121. package/dist/core/keybindings.js +2 -0
  122. package/dist/core/keybindings.js.map +1 -1
  123. package/dist/core/orchestration/attempt-ordering.d.ts +9 -0
  124. package/dist/core/orchestration/attempt-ordering.d.ts.map +1 -0
  125. package/dist/core/orchestration/attempt-ordering.js +29 -0
  126. package/dist/core/orchestration/attempt-ordering.js.map +1 -0
  127. package/dist/core/orchestration/capability-gateway.d.ts +19 -0
  128. package/dist/core/orchestration/capability-gateway.d.ts.map +1 -1
  129. package/dist/core/orchestration/capability-gateway.js +19 -2
  130. package/dist/core/orchestration/capability-gateway.js.map +1 -1
  131. package/dist/core/orchestration/contracts.d.ts +43 -3
  132. package/dist/core/orchestration/contracts.d.ts.map +1 -1
  133. package/dist/core/orchestration/contracts.js +24 -0
  134. package/dist/core/orchestration/contracts.js.map +1 -1
  135. package/dist/core/orchestration/delegation-ledger.d.ts +8 -3
  136. package/dist/core/orchestration/delegation-ledger.d.ts.map +1 -1
  137. package/dist/core/orchestration/delegation-ledger.js +148 -55
  138. package/dist/core/orchestration/delegation-ledger.js.map +1 -1
  139. package/dist/core/orchestration/event-store.d.ts +6 -3
  140. package/dist/core/orchestration/event-store.d.ts.map +1 -1
  141. package/dist/core/orchestration/event-store.js +228 -30
  142. package/dist/core/orchestration/event-store.js.map +1 -1
  143. package/dist/core/orchestration/task-runtime-codecs.d.ts +31 -0
  144. package/dist/core/orchestration/task-runtime-codecs.d.ts.map +1 -0
  145. package/dist/core/orchestration/task-runtime-codecs.js +1471 -0
  146. package/dist/core/orchestration/task-runtime-codecs.js.map +1 -0
  147. package/dist/core/orchestration/task-runtime-projection.d.ts +30 -0
  148. package/dist/core/orchestration/task-runtime-projection.d.ts.map +1 -0
  149. package/dist/core/orchestration/task-runtime-projection.js +340 -0
  150. package/dist/core/orchestration/task-runtime-projection.js.map +1 -0
  151. package/dist/core/orchestration/task-runtime-reducer.d.ts +38 -0
  152. package/dist/core/orchestration/task-runtime-reducer.d.ts.map +1 -0
  153. package/dist/core/orchestration/task-runtime-reducer.js +1270 -0
  154. package/dist/core/orchestration/task-runtime-reducer.js.map +1 -0
  155. package/dist/core/orchestration/task-runtime-state.d.ts +142 -0
  156. package/dist/core/orchestration/task-runtime-state.d.ts.map +1 -0
  157. package/dist/core/orchestration/task-runtime-state.js +14 -0
  158. package/dist/core/orchestration/task-runtime-state.js.map +1 -0
  159. package/dist/core/orchestration/task-runtime.d.ts +44 -98
  160. package/dist/core/orchestration/task-runtime.d.ts.map +1 -1
  161. package/dist/core/orchestration/task-runtime.js +357 -1113
  162. package/dist/core/orchestration/task-runtime.js.map +1 -1
  163. package/dist/core/orchestration/worker-context-fork-reference.d.ts +20 -0
  164. package/dist/core/orchestration/worker-context-fork-reference.d.ts.map +1 -0
  165. package/dist/core/orchestration/worker-context-fork-reference.js +53 -0
  166. package/dist/core/orchestration/worker-context-fork-reference.js.map +1 -0
  167. package/dist/core/research/research-runner.d.ts +2 -0
  168. package/dist/core/research/research-runner.d.ts.map +1 -1
  169. package/dist/core/research/research-runner.js +1 -0
  170. package/dist/core/research/research-runner.js.map +1 -1
  171. package/dist/core/runtime-builder.d.ts.map +1 -1
  172. package/dist/core/runtime-builder.js +8 -0
  173. package/dist/core/runtime-builder.js.map +1 -1
  174. package/dist/core/secrets/credential-project.d.ts.map +1 -1
  175. package/dist/core/secrets/credential-project.js +24 -2
  176. package/dist/core/secrets/credential-project.js.map +1 -1
  177. package/dist/core/security/secret-text.d.ts +3 -0
  178. package/dist/core/security/secret-text.d.ts.map +1 -1
  179. package/dist/core/security/secret-text.js +15 -0
  180. package/dist/core/security/secret-text.js.map +1 -1
  181. package/dist/core/settings-manager.d.ts +3 -0
  182. package/dist/core/settings-manager.d.ts.map +1 -1
  183. package/dist/core/settings-manager.js +160 -8
  184. package/dist/core/settings-manager.js.map +1 -1
  185. package/dist/core/tools/delegate.d.ts +31 -4
  186. package/dist/core/tools/delegate.d.ts.map +1 -1
  187. package/dist/core/tools/delegate.js +749 -103
  188. package/dist/core/tools/delegate.js.map +1 -1
  189. package/dist/main.d.ts.map +1 -1
  190. package/dist/main.js +1 -0
  191. package/dist/main.js.map +1 -1
  192. package/dist/modes/interactive/components/activity-lane.d.ts +5 -0
  193. package/dist/modes/interactive/components/activity-lane.d.ts.map +1 -1
  194. package/dist/modes/interactive/components/activity-lane.js +133 -49
  195. package/dist/modes/interactive/components/activity-lane.js.map +1 -1
  196. package/dist/modes/interactive/components/agents-overlay.d.ts +40 -0
  197. package/dist/modes/interactive/components/agents-overlay.d.ts.map +1 -0
  198. package/dist/modes/interactive/components/agents-overlay.js +153 -0
  199. package/dist/modes/interactive/components/agents-overlay.js.map +1 -0
  200. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  201. package/dist/modes/interactive/components/settings-selector.js +18 -13
  202. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  203. package/dist/modes/interactive/interactive-event-controller.d.ts +1 -1
  204. package/dist/modes/interactive/interactive-event-controller.d.ts.map +1 -1
  205. package/dist/modes/interactive/interactive-event-controller.js +6 -3
  206. package/dist/modes/interactive/interactive-event-controller.js.map +1 -1
  207. package/dist/modes/interactive/interactive-mode.d.ts +5 -0
  208. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  209. package/dist/modes/interactive/interactive-mode.js +51 -0
  210. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  211. package/dist/modes/interactive/key-handlers.d.ts +1 -0
  212. package/dist/modes/interactive/key-handlers.d.ts.map +1 -1
  213. package/dist/modes/interactive/key-handlers.js +1 -0
  214. package/dist/modes/interactive/key-handlers.js.map +1 -1
  215. package/docs/harness-architecture.md +61 -17
  216. package/docs/settings.md +17 -9
  217. package/docs/worker-profiles.md +6 -2
  218. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  219. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  220. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  221. package/examples/extensions/sandbox/package-lock.json +2 -2
  222. package/examples/extensions/sandbox/package.json +1 -1
  223. package/examples/extensions/with-deps/package-lock.json +2 -2
  224. package/examples/extensions/with-deps/package.json +1 -1
  225. package/npm-shrinkwrap.json +12 -12
  226. 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 { validateAttemptUsageSnapshot } from "./attempt-usage.js";
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 { validateRiskBudget } from "./risk-budget.js";
8
- import { parseWorkerExecutionContract } from "./worker-execution-contract.js";
9
- const MAX_DATE_EPOCH_MS = 8_640_000_000_000_000;
10
- export class DurableTaskRuntimeError extends Error {
11
- constructor(message) {
12
- super(message);
13
- this.name = "DurableTaskRuntimeError";
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
- const retainedCriterionIds = new Set(acceptanceCriteria.map((criterion) => criterion.id));
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
- assertRiskBudget(input.riskBudget, "task.riskBudget");
1034
- const objectiveState = this.state.objectives[input.objectiveId];
1035
- if (!objectiveState)
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 = dispatchFromValue(dispatch, "dispatch");
1103
- if (normalizedDispatch.taskId !== taskId) {
1104
- throw new DurableTaskRuntimeError("Dispatch taskId does not match the queued task.");
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 attempt = this.requireAttempt(attemptId);
1130
- if (!["queued", "leased", "running"].includes(attempt.status)) {
1131
- throw new DurableTaskRuntimeError(`Attempt '${attemptId}' cannot bind a grant from '${attempt.status}'.`);
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}:${grant.grantId}`,
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
- const attempt = this.requireLiveLease(attemptId, leaseId, fencingToken);
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: args.summary.trim(),
1226
- artifactIds: [...(args.artifactIds ?? [])],
1227
- evidenceIds: [...(args.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
- if (!checkpoint.summary)
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
- const attempt = this.requireLiveLease(result.attemptId, result.leaseId, result.fencingToken);
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
- const subject = this.state.tasks[args.taskId];
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
- if (!reasonCode.trim())
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.refresh();
1475
- const objective = this.requireObjective(objectiveId);
1476
- const incomplete = objective.taskIds.filter((taskId) => this.state.tasks[taskId]?.task.status !== "completed");
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
- if (!acceptanceOverride) {
1506
- const provenCriterionIds = new Set(objective.evidence.flatMap((evidence) => evidence.trusted && evidence.criterionId ? [evidence.criterionId] : []));
1507
- const unproven = objective.objective.acceptanceCriteria
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.validateApprovalRequest(normalized);
1527
- const pendingForTarget = Object.values(this.state.approvals).find((candidate) => candidate.status === "pending" &&
1528
- candidate.request.objectiveId === normalized.objectiveId &&
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.requireObjective(args.objectiveId);
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
- transitionObjective(objectiveId, type, target) {
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
- if (["completed", "cancelled"].includes(objective.objective.status)) {
1612
- throw new DurableTaskRuntimeError(`Objective '${objectiveId}' is terminal.`);
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
- const task = this.state.tasks[attempt.taskId];
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
- validateApprovalRequest(approval) {
1683
- const objective = this.requireObjective(approval.objectiveId).objective;
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
- const event = this.store.append(input, { expectedLastOrdinal: this.state.lastOrdinal });
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
  }