ai-workflow-logic 0.2.0 → 0.2.2

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.
@@ -0,0 +1,387 @@
1
+ import type {
2
+ AIAgentProcessorRuntime, AIWorkflowFlowRunCheckpoint, FlowClosedValue,
3
+ WorkflowTaskDelegation, WorkflowDelegationRequest, WorkflowDelegationObservation,
4
+ WorkflowDelegationRecord, WorkflowDelegationEffectPort, WorkflowDelegationOwnerTarget,
5
+ } from 'ai-workflow-contract';
6
+ import { normalizeFlowClosedValue } from './run-checkpoint.js';
7
+ import { assertAIAgentNodeTaskBinding } from './agent-state.js';
8
+
9
+ export class WorkflowDelegationError extends Error {
10
+ constructor(readonly code: string) { super(code); this.name = 'WorkflowDelegationError'; }
11
+ }
12
+
13
+ export class WorkflowDelegationSuspendedError extends Error {
14
+ constructor(readonly invocationKey: string, readonly status: string, readonly cause?: unknown) {
15
+ super(`Workflow delegation ${invocationKey} is ${status}; observe the original invocation.`);
16
+ this.name = 'WorkflowDelegationSuspendedError';
17
+ }
18
+ }
19
+
20
+ function assert(condition: unknown, code: string): asserts condition {
21
+ if (!condition) throw new WorkflowDelegationError(code);
22
+ }
23
+ function dataRecord(value: unknown): Record<string, FlowClosedValue> {
24
+ // Normalize before accessing properties: getters, symbols and non-data values are rejected.
25
+ const data = normalizeFlowClosedValue(value, 'delegation');
26
+ assert(data !== null && typeof data === 'object' && !Array.isArray(data), 'DELEGATION_OBJECT_REQUIRED');
27
+ return data as Record<string, FlowClosedValue>;
28
+ }
29
+ function object(value: unknown, required: string[], optional: string[] = []): Record<string, FlowClosedValue> {
30
+ const row = dataRecord(value);
31
+ assert(required.every(key => Object.hasOwn(row, key)) && Object.keys(row).every(key => [...required, ...optional].includes(key)), 'DELEGATION_FIELDS_INVALID');
32
+ return row;
33
+ }
34
+ function identity(value: unknown): asserts value is string {
35
+ assert(typeof value === 'string' && value.length > 0 && value.trim() === value && value.normalize('NFC') === value, 'DELEGATION_IDENTITY_INVALID');
36
+ }
37
+ function identityValue(value: unknown): string { identity(value); return value; }
38
+ function integer(value: unknown): asserts value is number {
39
+ assert(Number.isSafeInteger(value) && (value as number) >= 0, 'DELEGATION_INTEGER_INVALID');
40
+ }
41
+ function same(a: unknown, b: unknown): boolean {
42
+ function sorted(value: unknown): unknown {
43
+ if (Array.isArray(value)) return value.map(sorted);
44
+ if (!value || typeof value !== 'object') return value;
45
+ const row = value as Record<string, unknown>;
46
+ return Object.fromEntries(Object.keys(row).sort().map(key => [key, sorted(row[key])]));
47
+ }
48
+ return JSON.stringify(sorted(a)) === JSON.stringify(sorted(b));
49
+ }
50
+ function target(value: unknown): WorkflowDelegationOwnerTarget {
51
+ const row = object(value, ['owner', 'providerScope', 'targetId']);
52
+ return Object.freeze({ owner: identityValue(row.owner), providerScope: identityValue(row.providerScope), targetId: identityValue(row.targetId) });
53
+ }
54
+
55
+ export function normalizeWorkflowDelegation(value: unknown): WorkflowTaskDelegation {
56
+ const row = object(value, ['kind', 'task', 'context', 'sessionPolicy', 'materials'], ['execution']);
57
+ assert(row.kind === 'workflow.task-delegation/v1', 'DELEGATION_KIND_INVALID');
58
+ const task = object(row.task, ['nodeId', 'agentDefinitionRef', 'input']);
59
+ identity(task.nodeId); identity(task.agentDefinitionRef);
60
+ assert(task.agentDefinitionRef.startsWith('resource://') && task.agentDefinitionRef.length > 11, 'DELEGATION_TASK_REF_INVALID');
61
+ const contextInput = object(row.context, ['mode'], ['owner', 'providerScope', 'targetId', 'nodeId']);
62
+ let context: WorkflowTaskDelegation['context'];
63
+ if (contextInput.mode === 'fresh') {
64
+ object(contextInput, ['mode', 'owner', 'providerScope']);
65
+ context = { mode: 'fresh', owner: identityValue(contextInput.owner), providerScope: identityValue(contextInput.providerScope) };
66
+ } else if (contextInput.mode === 'reuse') {
67
+ object(contextInput, ['mode', 'owner', 'providerScope', 'targetId']);
68
+ context = { mode: 'reuse', ...target({ owner: contextInput.owner, providerScope: contextInput.providerScope, targetId: contextInput.targetId }) };
69
+ } else {
70
+ object(contextInput, ['mode', 'nodeId']); assert(contextInput.mode === 'from-node', 'DELEGATION_CONTEXT_INVALID');
71
+ context = { mode: 'from-node', nodeId: identityValue(contextInput.nodeId) };
72
+ }
73
+ assert(row.sessionPolicy === 'shared' || row.sessionPolicy === 'independent-per-generation', 'DELEGATION_SESSION_POLICY_INVALID');
74
+ assert(row.sessionPolicy !== 'independent-per-generation' || context.mode === 'fresh', 'DELEGATION_INDEPENDENCE_REQUIRED');
75
+ assert(Array.isArray(row.materials) && row.materials.length <= 17, 'DELEGATION_MATERIALS_INVALID');
76
+ let total = 0;
77
+ const ids = new Set<string>();
78
+ let instructions = 0;
79
+ const materials = row.materials.map((item: unknown) => {
80
+ const material = object(item, ['id', 'role', 'ref', 'sha256', 'bytes']);
81
+ identity(material.id); identity(material.ref); identity(material.sha256); integer(material.bytes);
82
+ assert((material.role === 'instruction' || material.role === 'auxiliary') && /^[a-f0-9]{64}$/.test(material.sha256), 'DELEGATION_MATERIAL_INVALID');
83
+ assert(!ids.has(material.id) && material.bytes <= 1048576, 'DELEGATION_MATERIAL_LIMIT');
84
+ ids.add(material.id); total += material.bytes;
85
+ if (material.role === 'instruction') instructions++;
86
+ return Object.freeze({ id: material.id, ref: material.ref, sha256: material.sha256, bytes: material.bytes, role: material.role });
87
+ });
88
+ assert(instructions <= 1 && materials.length - instructions <= 16 && total <= 8388608, 'DELEGATION_MATERIAL_LIMIT');
89
+ const execution=Object.hasOwn(row,'execution')?normalizeFlowClosedValue(row.execution,'delegation.execution'):undefined;
90
+ if(execution!==undefined)assert(new TextEncoder().encode(JSON.stringify(execution)).length<=16384,'DELEGATION_EXECUTION_LIMIT');
91
+ return Object.freeze({ kind: row.kind, task: Object.freeze({ nodeId: task.nodeId, agentDefinitionRef: task.agentDefinitionRef as `resource://${string}`, input: task.input }), context: Object.freeze(context), sessionPolicy: row.sessionPolicy, materials: Object.freeze(materials),...(execution===undefined?{}:{execution}) });
92
+ }
93
+
94
+ function normalizeRequest(value: unknown): WorkflowDelegationRequest {
95
+ const row = object(value, ['delegation', 'lineage', 'attempt'], ['target']);
96
+ const lineage = object(row.lineage, ['instanceId', 'runId', 'nodeId', 'invocationKey', 'visit', 'generation'], ['sourceInvocationKey']);
97
+ for (const [key, value] of Object.entries(lineage)) key === 'generation' ? integer(value) : identity(value);
98
+ integer(lineage.generation);
99
+ integer(row.attempt);
100
+ const delegation = normalizeWorkflowDelegation(row.delegation);
101
+ assert(delegation.task.nodeId === lineage.nodeId, 'DELEGATION_LINEAGE_MISMATCH');
102
+ assert((delegation.context.mode === 'from-node') === Object.hasOwn(lineage, 'sourceInvocationKey'), 'DELEGATION_SOURCE_MISMATCH');
103
+ assert((delegation.context.mode !== 'fresh') === Object.hasOwn(row, 'target'), 'DELEGATION_TARGET_MISMATCH');
104
+ return { delegation, lineage: Object.freeze({
105
+ instanceId: identityValue(lineage.instanceId), runId: identityValue(lineage.runId), nodeId: identityValue(lineage.nodeId),
106
+ invocationKey: identityValue(lineage.invocationKey), visit: identityValue(lineage.visit), generation: lineage.generation,
107
+ ...(lineage.sourceInvocationKey === undefined ? {} : { sourceInvocationKey: identityValue(lineage.sourceInvocationKey) }),
108
+ }), attempt: row.attempt, ...(row.target === undefined ? {} : { target: target(row.target) }) };
109
+ }
110
+
111
+ export function normalizeWorkflowDelegationObservation(value: unknown, request: WorkflowDelegationRequest): WorkflowDelegationObservation {
112
+ const row = object(value, ['invocationKey', 'attempt', 'receiptId', 'status'], ['target', 'output', 'settled', 'reason']);
113
+ identity(row.receiptId);
114
+ assert(row.invocationKey === request.lineage.invocationKey && row.attempt === request.attempt, 'DELEGATION_RECEIPT_IDENTITY_MISMATCH');
115
+ if (row.status === 'completed') {
116
+ object(row, ['invocationKey', 'attempt', 'receiptId', 'status', 'target', 'output']);
117
+ const actual = target(row.target);
118
+ assertObservationTarget(actual, request);
119
+ } else if (row.status === 'failed') {
120
+ object(row, ['invocationKey', 'attempt', 'receiptId', 'status', 'target', 'settled', 'reason']);
121
+ assert(row.settled === true, 'DELEGATION_FAILURE_UNSETTLED'); identity(row.reason);
122
+ assertObservationTarget(target(row.target), request);
123
+ } else if (row.status === 'rejected' || row.status === 'not-dispatched') {
124
+ object(row, ['invocationKey', 'attempt', 'receiptId', 'status', 'settled', 'reason']);
125
+ assert(row.settled === true, 'DELEGATION_REJECTION_UNSETTLED'); identity(row.reason);
126
+ } else {
127
+ object(row, ['invocationKey', 'attempt', 'receiptId', 'status'], ['target']);
128
+ assert(['pending', 'busy', 'awaiting-user', 'unknown'].includes(identityValue(row.status)), 'DELEGATION_STATUS_INVALID');
129
+ if (row.target !== undefined) assertObservationTarget(target(row.target), request);
130
+ }
131
+ return Object.freeze(row) as WorkflowDelegationObservation;
132
+ }
133
+
134
+ function assertObservationTarget(actual: WorkflowDelegationOwnerTarget, request: WorkflowDelegationRequest): void {
135
+ if (request.target) assert(same(actual, request.target), 'DELEGATION_OWNER_TARGET_MISMATCH');
136
+ const context = request.delegation.context;
137
+ if (context.mode === 'fresh') assert(actual.owner === context.owner && actual.providerScope === context.providerScope, 'DELEGATION_OWNER_TARGET_MISMATCH');
138
+ }
139
+
140
+ export function normalizeWorkflowDelegations(value: unknown): Readonly<Record<string, WorkflowDelegationRecord>> {
141
+ const normalized = normalizeFlowClosedValue(value, 'delegationsByKey');
142
+ assert(normalized && typeof normalized === 'object' && !Array.isArray(normalized), 'DELEGATION_INDEX_INVALID');
143
+ const result: Record<string, WorkflowDelegationRecord> = {};
144
+ for (const key of Object.keys(normalized).sort()) {
145
+ identity(key);
146
+ const row = object(dataRecord(normalized)[key], ['attempts']);
147
+ assert(Array.isArray(row.attempts) && row.attempts.length > 0, 'DELEGATION_ATTEMPTS_REQUIRED');
148
+ const attempts = row.attempts.map((item: unknown, index: number) => {
149
+ const attempt = object(item, ['request', 'dispatchClaimed'], ['observation']);
150
+ assert(typeof attempt.dispatchClaimed === 'boolean' && (attempt.observation === undefined || attempt.dispatchClaimed), 'DELEGATION_DISPATCH_CLAIM_INVALID');
151
+ const request = normalizeRequest(attempt.request);
152
+ assert(request.attempt === index && request.lineage.invocationKey === key, 'DELEGATION_ATTEMPT_IDENTITY_MISMATCH');
153
+ return Object.freeze({ request, dispatchClaimed: attempt.dispatchClaimed, ...(attempt.observation === undefined ? {} : { observation: normalizeWorkflowDelegationObservation(attempt.observation, request) }) });
154
+ });
155
+ for (let index = 1; index < attempts.length; index++) {
156
+ const previous = attempts[index - 1];
157
+ assert(previous.observation?.status === 'rejected' || previous.observation?.status === 'not-dispatched', 'DELEGATION_RETRY_FORBIDDEN');
158
+ assert(same({ ...previous.request, attempt: index }, attempts[index].request), 'DELEGATION_RETRY_INTENT_CHANGED');
159
+ }
160
+ result[key] = Object.freeze({ attempts: Object.freeze(attempts) });
161
+ }
162
+ return Object.freeze(result);
163
+ }
164
+
165
+ export function validateWorkflowDelegationTransition(before: Readonly<Record<string, WorkflowDelegationRecord>>, after: Readonly<Record<string, WorkflowDelegationRecord>>): void {
166
+ for (const [key, record] of Object.entries(before)) {
167
+ const next = after[key];
168
+ assert(next && next.attempts.length >= record.attempts.length && next.attempts.length <= record.attempts.length + 1, 'DELEGATION_HISTORY_IMMUTABLE');
169
+ record.attempts.forEach((attempt, index) => {
170
+ const candidate = next.attempts[index]!;
171
+ assert(same(attempt.request, candidate.request), 'DELEGATION_RESERVATION_IMMUTABLE');
172
+ assert(!attempt.dispatchClaimed || candidate.dispatchClaimed, 'DELEGATION_DISPATCH_CLAIM_IMMUTABLE');
173
+ if (attempt.observation && ['completed', 'failed', 'rejected', 'not-dispatched'].includes(attempt.observation.status)) {
174
+ assert(same(attempt, candidate), 'DELEGATION_TERMINAL_IMMUTABLE');
175
+ }
176
+ });
177
+ if (next.attempts.length > record.attempts.length) assert(!next.attempts.at(-1)!.observation, 'DELEGATION_RESERVATION_REQUIRED');
178
+ }
179
+ for (const [key, record] of Object.entries(after)) if (!before[key]) {
180
+ assert(record.attempts.length === 1 && !record.attempts[0]!.observation, 'DELEGATION_RESERVATION_REQUIRED');
181
+ }
182
+ }
183
+
184
+ export interface WorkflowDelegationRuntime extends Omit<AIAgentProcessorRuntime, 'effects'> {
185
+ readonly effects: WorkflowDelegationEffectPort;
186
+ readonly visit: string;
187
+ readonly generation: number;
188
+ /** Native scheduler/admission projection, never supplied by authored task input. */
189
+ readonly dependencyInvocations: Readonly<Record<string, string>>;
190
+ }
191
+
192
+ async function load(runtime: WorkflowDelegationRuntime): Promise<AIWorkflowFlowRunCheckpoint> {
193
+ const checkpoint = await runtime.checkpointRuntime.checkpointStore.load(runtime.checkpointKey);
194
+ assert(checkpoint && checkpoint.instanceId === runtime.checkpointKey.instanceId && checkpoint.runId === runtime.checkpointKey.runId, 'DELEGATION_CHECKPOINT_MISSING');
195
+ assert(checkpoint.profile.kind === runtime.workflowKind && ['AICtrlWorkflow', 'AIDataWorkflow'].includes(checkpoint.profile.kind), 'DELEGATION_PROFILE_MISMATCH');
196
+ return checkpoint as AIWorkflowFlowRunCheckpoint;
197
+ }
198
+ function records(checkpoint: AIWorkflowFlowRunCheckpoint): Readonly<Record<string, WorkflowDelegationRecord>> {
199
+ assert(checkpoint.profile.kind === 'AICtrlWorkflow' || checkpoint.profile.kind === 'AIDataWorkflow', 'DELEGATION_PROFILE_MISMATCH');
200
+ return checkpoint.profile.ai.delegationsByKey ?? {};
201
+ }
202
+
203
+ /** Native loop frames and accepted data revisions establish serial order across visits.
204
+ * These identities come from the scheduler/checkpoint, never the authored task input.
205
+ * Same-visit sharing still requires the ordinary explicit predecessor relation.
206
+ */
207
+ function isNativeSerialPredecessor(runtime: WorkflowDelegationRuntime, checkpoint: AIWorkflowFlowRunCheckpoint, previous: WorkflowDelegationRequest): boolean {
208
+ const before = previous.lineage;
209
+ if (before.instanceId !== checkpoint.instanceId || before.runId !== checkpoint.runId) return false;
210
+ if (checkpoint.profile.kind === 'AICtrlWorkflow') {
211
+ const frames = (visit: string, generation: number): { scope: string; loops: { key: string; iteration: number }[] } | undefined => {
212
+ let value: unknown;
213
+ try { value = JSON.parse(visit); } catch { return; }
214
+ if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== 'string' || !Array.isArray(value[1]) || !value[1].length) return;
215
+ const loops: { key: string; iteration: number }[] = [];
216
+ for (const item of value[1]) {
217
+ if (item === null || typeof item !== 'object' || Array.isArray(item)) return;
218
+ const row = dataRecord(item);
219
+ if (Object.keys(row).length !== 2 || typeof row.key !== 'string' || !row.key || typeof row.iteration !== 'number' || !Number.isSafeInteger(row.iteration) || row.iteration < 0) return;
220
+ loops.push({ key: row.key, iteration: row.iteration });
221
+ }
222
+ if (loops.at(-1)!.iteration !== generation) return;
223
+ return { scope: value[0], loops };
224
+ };
225
+ const oldVisit = frames(before.visit, before.generation), currentVisit = frames(runtime.visit, runtime.generation);
226
+ if (!oldVisit || !currentVisit || oldVisit.scope !== currentVisit.scope || oldVisit.loops.length !== currentVisit.loops.length) return false;
227
+ if (oldVisit.loops.some((frame, index) => frame.key !== currentVisit.loops[index]!.key)) return false;
228
+ // An outer iteration also orders all of its inner iterations, even after an inner reset.
229
+ const changed = oldVisit.loops.findIndex((frame, index) => frame.iteration !== currentVisit.loops[index]!.iteration);
230
+ return changed >= 0 && oldVisit.loops[changed]!.iteration < currentVisit.loops[changed]!.iteration;
231
+ }
232
+ if (checkpoint.profile.kind === 'AIDataWorkflow') {
233
+ if (before.nodeId !== runtime.nodeId || before.generation >= runtime.generation || before.visit !== `generation:${before.generation}` || runtime.visit !== `generation:${runtime.generation}`) return false;
234
+ const graph = dataRecord(checkpoint.profile.runGraph), nodes = dataRecord(graph.nodes), node = dataRecord(nodes[runtime.nodeId]);
235
+ if (graph.currentGeneration !== runtime.generation || node.generation !== runtime.generation) return false;
236
+ // A number supplied by a caller is insufficient: require the persisted native revision
237
+ // which actually reopened this node, or its still-active reciprocal invalidation fact.
238
+ const invalidations = Array.isArray(graph.invalidations) ? graph.invalidations : [];
239
+ if (invalidations.some(value => { const fact = dataRecord(value); return fact.nodeId === runtime.nodeId && fact.generation === runtime.generation; })) return true;
240
+ const patches = Array.isArray(graph.patchHistory) ? graph.patchHistory : [];
241
+ return patches.some(value => {
242
+ const patch = dataRecord(value);
243
+ return patch.generation === runtime.generation && Array.isArray(patch.operations) && patch.operations.some(value => {
244
+ const operation = dataRecord(value);
245
+ return operation.op === 'update-node' && operation.nodeId === runtime.nodeId;
246
+ });
247
+ });
248
+ }
249
+ return false;
250
+ }
251
+ async function commit(runtime: WorkflowDelegationRuntime, checkpoint: AIWorkflowFlowRunCheckpoint, key: string, record: WorkflowDelegationRecord): Promise<void> {
252
+ assert(checkpoint.profile.kind === 'AICtrlWorkflow' || checkpoint.profile.kind === 'AIDataWorkflow', 'DELEGATION_PROFILE_MISMATCH');
253
+ await runtime.checkpointRuntime.checkpointStore.commit({ expectedVersion: checkpoint.version, checkpoint: {
254
+ ...checkpoint, version: checkpoint.version + 1,
255
+ profile: { ...checkpoint.profile, ai: { ...checkpoint.profile.ai, delegationsByKey: { ...records(checkpoint), [key]: record } } },
256
+ } });
257
+ }
258
+
259
+ /** A typed result is returned while pending; native adapters suspend rather than inventing output. */
260
+ export async function delegateWorkflowTask(runtime: WorkflowDelegationRuntime, input: WorkflowTaskDelegation): Promise<WorkflowDelegationObservation> {
261
+ const delegation = normalizeWorkflowDelegation(input);
262
+ assert(delegation.task.nodeId === runtime.nodeId, 'DELEGATION_NODE_MISMATCH');
263
+ assertAIAgentNodeTaskBinding(runtime.taskBinding, { workflowKind: runtime.workflowKind, workflowRef: runtime.workflowRef, nodeId: runtime.nodeId }, { agentDefinitionRef: delegation.task.agentDefinitionRef });
264
+ identity(runtime.visit); integer(runtime.generation);
265
+ let checkpoint = await load(runtime);
266
+ // Target, policy, and materials are admitted with the immutable run config, not task input.
267
+ const binding = { ...delegation, task: { ...delegation.task, input: null } };
268
+ const bindings = dataRecord(dataRecord(checkpoint.config).workflowDelegations);
269
+ assert(same(bindings[runtime.nodeId], binding), 'DELEGATION_FROZEN_BINDING_MISMATCH');
270
+ const key = runtime.invocationKey;
271
+ let record = records(checkpoint)[key];
272
+ let request: WorkflowDelegationRequest;
273
+ let winner = false;
274
+ if (record) {
275
+ request = record.attempts.at(-1)!.request;
276
+ assert(same(request.delegation, delegation) && request.lineage.visit === runtime.visit && request.lineage.generation === runtime.generation && request.lineage.instanceId === checkpoint.instanceId && request.lineage.runId === checkpoint.runId && request.lineage.nodeId === runtime.nodeId, 'DELEGATION_INVOCATION_CONFLICT');
277
+ } else {
278
+ let sourceInvocationKey: string | undefined;
279
+ let selected: WorkflowDelegationOwnerTarget | undefined;
280
+ const context = delegation.context;
281
+ if (context.mode === 'reuse') selected = target({ owner: context.owner, providerScope: context.providerScope, targetId: context.targetId });
282
+ if (context.mode === 'from-node') {
283
+ sourceInvocationKey = runtime.dependencyInvocations[context.nodeId];
284
+ assert(sourceInvocationKey && context.nodeId !== runtime.nodeId, 'DELEGATION_DEPENDENCY_REQUIRED');
285
+ const source = records(checkpoint)[sourceInvocationKey]?.attempts.at(-1);
286
+ assert(source?.observation?.status === 'completed' && source.request.lineage.nodeId === context.nodeId, 'DELEGATION_SOURCE_UNFINISHED');
287
+ assert(source.request.lineage.instanceId === checkpoint.instanceId && source.request.lineage.runId === checkpoint.runId, 'DELEGATION_SOURCE_SCOPE_MISMATCH');
288
+ assert(source.request.lineage.generation === runtime.generation && source.request.lineage.visit === runtime.visit, 'DELEGATION_SOURCE_STALE');
289
+ assert(source.request.delegation.sessionPolicy !== 'independent-per-generation', 'DELEGATION_INDEPENDENCE_REQUIRED');
290
+ selected = source.observation.target;
291
+ }
292
+ for (const existing of Object.values(records(checkpoint))) {
293
+ const last = existing.attempts.at(-1)!;
294
+ const terminal = last.observation && ['completed', 'failed', 'rejected', 'not-dispatched'].includes(last.observation.status);
295
+ const actual = last.observation && 'target' in last.observation ? last.observation.target ?? last.request.target : last.request.target;
296
+ if (selected && actual && same(selected, actual)) {
297
+ assert(terminal && (Object.values(runtime.dependencyInvocations).includes(last.request.lineage.invocationKey) || isNativeSerialPredecessor(runtime, checkpoint, last.request)), 'DELEGATION_SHARED_TARGET_CONCURRENT');
298
+ }
299
+ }
300
+ request = normalizeRequest({ delegation, lineage: { ...runtime.checkpointKey, nodeId: runtime.nodeId, invocationKey: key, visit: runtime.visit, generation: runtime.generation, ...(sourceInvocationKey ? { sourceInvocationKey } : {}) }, attempt: 0, ...(selected ? { target: selected } : {}) });
301
+ record = { attempts: [{ request, dispatchClaimed: false }] };
302
+ await runtime.effects.verifyMaterials(delegation.materials);
303
+ try { await commit(runtime, checkpoint, key, record); }
304
+ catch (error) {
305
+ const latest = await load(runtime);
306
+ const accepted = records(latest)[key];
307
+ if (!accepted || !same(accepted.attempts.at(-1)!.request, request)) throw error;
308
+ record = accepted;
309
+ }
310
+ }
311
+ const last = record.attempts.at(-1)!;
312
+ if (last.observation && ['completed', 'failed', 'rejected', 'not-dispatched'].includes(last.observation.status)) return last.observation;
313
+ await runtime.effects.verifyMaterials(delegation.materials);
314
+ if (!last.dispatchClaimed) {
315
+ checkpoint = await load(runtime);
316
+ record = records(checkpoint)[key]!;
317
+ if (!record.attempts.at(-1)!.dispatchClaimed) {
318
+ const claimed = { attempts: [...record.attempts.slice(0, -1), { ...record.attempts.at(-1)!, dispatchClaimed: true }] };
319
+ try { await commit(runtime, checkpoint, key, claimed); winner = true; }
320
+ catch (error) {
321
+ const accepted = records(await load(runtime))[key]?.attempts.at(-1);
322
+ if (!accepted?.dispatchClaimed || !same(accepted.request, request)) throw error;
323
+ }
324
+ }
325
+ }
326
+ // A lost submit receipt is uncertain. Never call submit again on recovery.
327
+ let response: WorkflowDelegationObservation;
328
+ try { response = await (winner ? runtime.effects.submit(request) : runtime.effects.observe(request)); }
329
+ catch (cause) { throw new WorkflowDelegationSuspendedError(key, 'unknown', cause); }
330
+ const observation = normalizeWorkflowDelegationObservation(response, request);
331
+ checkpoint = await load(runtime);
332
+ record = records(checkpoint)[key]!;
333
+ const latest = record.attempts.at(-1)!;
334
+ if (latest.observation && ['completed', 'failed', 'rejected', 'not-dispatched'].includes(latest.observation.status)) return latest.observation;
335
+ assert(same(latest.request, request), 'DELEGATION_ATTEMPT_CHANGED');
336
+ if (observation.status === 'completed' && delegation.context.mode === 'fresh') {
337
+ for (const [otherKey, other] of Object.entries(records(checkpoint))) {
338
+ if (otherKey === key) continue;
339
+ for (const attempt of other.attempts) {
340
+ assert(attempt.observation?.status !== 'completed' || !same(attempt.observation.target, observation.target), 'DELEGATION_FRESH_CONTEXT_REUSED');
341
+ }
342
+ }
343
+ }
344
+ const updated = { attempts: [...record.attempts.slice(0, -1), { request, dispatchClaimed: true, observation }] };
345
+ try { await commit(runtime, checkpoint, key, updated); }
346
+ catch (error) {
347
+ const accepted = records(await load(runtime))[key]?.attempts.at(-1)?.observation;
348
+ if (!accepted || !same(accepted, observation)) throw error;
349
+ }
350
+ return observation;
351
+ }
352
+
353
+ /** Explicit audited retry reservation. It never dispatches and never rewrites the failed attempt. */
354
+ export async function prepareWorkflowDelegationRetry(runtime: WorkflowDelegationRuntime, input: { expectedVersion: number; invocationKey: string; receiptId: string }): Promise<void> {
355
+ const checkpoint = await load(runtime);
356
+ assert(checkpoint.version === input.expectedVersion && input.invocationKey === runtime.invocationKey, 'DELEGATION_RETRY_CAS_MISMATCH');
357
+ const record = records(checkpoint)[input.invocationKey];
358
+ const last = record?.attempts.at(-1);
359
+ assert(last && last.observation && ['rejected', 'not-dispatched'].includes(last.observation.status) && last.observation.receiptId === input.receiptId, 'DELEGATION_RETRY_FORBIDDEN');
360
+ await commit(runtime, checkpoint, input.invocationKey, { attempts: [...record!.attempts, { request: { ...last.request, attempt: last.request.attempt + 1 }, dispatchClaimed: false }] });
361
+ }
362
+
363
+ /** Bound by native node adapters. Authored code supplies only this task's business input. */
364
+ export function bindWorkflowTaskDelegation(runtime: Omit<WorkflowDelegationRuntime, 'dependencyInvocations'>): { delegateTask(input: FlowClosedValue): Promise<FlowClosedValue> } {
365
+ return Object.freeze({ async delegateTask(input: FlowClosedValue): Promise<FlowClosedValue> {
366
+ const checkpoint = await load({ ...runtime, dependencyInvocations: {} });
367
+ const config = dataRecord(checkpoint.config);
368
+ const binding = dataRecord(config.workflowDelegations)[runtime.nodeId];
369
+ assert(binding, 'DELEGATION_FROZEN_BINDING_MISSING');
370
+ const dependencies = (config.workflowDelegationDependencies === undefined ? {} : dataRecord(config.workflowDelegationDependencies))[runtime.nodeId] ?? [];
371
+ assert(Array.isArray(dependencies) && dependencies.every((id: unknown) => typeof id === 'string'), 'DELEGATION_DEPENDENCIES_INVALID');
372
+ const dependencyInvocations: Record<string, string> = {};
373
+ for (const nodeId of dependencies.map((id: unknown) => identityValue(id))) {
374
+ const matches = Object.values(records(checkpoint)).map(record => record.attempts.at(-1)!).filter(attempt => attempt.request.lineage.nodeId === nodeId && attempt.request.lineage.generation === runtime.generation && attempt.request.lineage.visit === runtime.visit);
375
+ assert(matches.length <= 1, 'DELEGATION_SOURCE_AMBIGUOUS');
376
+ if (matches.length === 1) dependencyInvocations[nodeId] = matches[0]!.request.lineage.invocationKey;
377
+ }
378
+ const normalizedBinding = normalizeWorkflowDelegation(binding);
379
+ // 已存在的调用沿保存的原lineage观察;新的循环寻址不能改写旧恢复身份。
380
+ const original = records(checkpoint)[runtime.invocationKey]?.attempts.at(-1)?.request;
381
+ const admittedRuntime = original ? { ...runtime, visit: original.lineage.visit, generation: original.lineage.generation } : runtime;
382
+ const observation = await delegateWorkflowTask({ ...admittedRuntime, dependencyInvocations }, { ...normalizedBinding, task: { ...normalizedBinding.task, input } });
383
+ if(observation.status==='failed')throw new WorkflowDelegationError(`DELEGATION_OWNER_FAILED:${observation.reason}`);
384
+ if (observation.status !== 'completed') throw new WorkflowDelegationSuspendedError(runtime.invocationKey, observation.status);
385
+ return observation.output;
386
+ } });
387
+ }