ai-workflow-logic 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-workflow-logic",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "AI workflow domain projections and pure runtime helpers with an explicit Halfcode filesystem adapter",
6
6
  "type": "module",
@@ -8,12 +8,15 @@
8
8
  "exports": {
9
9
  ".": "./src/index.ts",
10
10
  "./browser": "./src/browser.ts",
11
+ "./checkpoint-filesystem": "./src/checkpoint-filesystem.ts",
11
12
  "./filesystem": "./src/filesystem.ts",
12
13
  "./run-freeze": "./src/run-freeze.ts"
13
14
  },
14
15
  "dependencies": {
15
- "ai-workflow-contract": "0.1.3",
16
- "halfcode-compiler.xnl": "0.2.2"
16
+ "ai-workflow-contract": "0.1.4",
17
+ "halfcode-compiler.xnl": "0.2.2",
18
+ "work-ctrl-flow-contract": "0.1.1",
19
+ "work-ctrl-flow-logic": "0.1.1"
17
20
  },
18
21
  "files": [
19
22
  "src"
@@ -0,0 +1,282 @@
1
+ import type {
2
+ AIAgentEffectConfig,
3
+ AIAgentHostRunResult,
4
+ AIAgentInstanceReference,
5
+ AIAgentInvocation,
6
+ AIAgentInvocationReceipt,
7
+ AIAgentProcessorRuntime,
8
+ AIAgentRunResult,
9
+ AIAgentSelector,
10
+ AIWorkflowAuthoredEffects,
11
+ AIWorkflowFlowRunCheckpoint,
12
+ AIWorkflowProfileDurableState,
13
+ FlowClosedValue,
14
+ } from 'ai-workflow-contract';
15
+ import {
16
+ AIAgentInvocationError,
17
+ normalizeAIAgentEffectConfig,
18
+ normalizeAIAgentInvocation,
19
+ normalizeAIAgentSelector,
20
+ normalizeCanonicalAIAgentState,
21
+ assertAIAgentNodeTaskBinding,
22
+ } from './agent-state.js';
23
+ import { normalizeFlowClosedObject, normalizeFlowClosedValue } from './run-checkpoint.js';
24
+
25
+ export { AIAgentInvocationError, normalizeAIAgentEffectConfig, normalizeAIAgentInvocation, normalizeAIAgentSelector } from './agent-state.js';
26
+
27
+ function fail(code: string, location: string, message: string): never {
28
+ throw new AIAgentInvocationError(code, location, message);
29
+ }
30
+
31
+ function profileAI(checkpoint: AIWorkflowFlowRunCheckpoint): AIWorkflowProfileDurableState {
32
+ if (checkpoint.profile.kind !== 'AICtrlWorkflow' && checkpoint.profile.kind !== 'AIDataWorkflow') {
33
+ return fail('AI_AGENT_PROFILE_REQUIRED', 'checkpoint.profile.kind', 'Agent invocation requires an AI workflow profile.');
34
+ }
35
+ return normalizeCanonicalAIAgentState(checkpoint.profile.ai, 'checkpoint.profile.ai');
36
+ }
37
+
38
+ async function loadCheckpoint(runtime: AIAgentProcessorRuntime): Promise<AIWorkflowFlowRunCheckpoint> {
39
+ const checkpoint = await runtime.checkpointRuntime.checkpointStore.load(runtime.checkpointKey) as AIWorkflowFlowRunCheckpoint | undefined;
40
+ if (!checkpoint) fail('AI_AGENT_CHECKPOINT_NOT_FOUND', 'runtime.checkpointKey', 'Canonical Flow checkpoint does not exist.');
41
+ if (checkpoint.instanceId !== runtime.checkpointKey.instanceId || checkpoint.runId !== runtime.checkpointKey.runId) {
42
+ fail('AI_AGENT_CHECKPOINT_IDENTITY_MISMATCH', 'runtime.checkpointKey', 'Loaded checkpoint identity differs from the requested run.');
43
+ }
44
+ return checkpoint;
45
+ }
46
+
47
+ function withAI(checkpoint: AIWorkflowFlowRunCheckpoint, ai: AIWorkflowProfileDurableState): AIWorkflowFlowRunCheckpoint {
48
+ if (checkpoint.profile.kind === 'AICtrlWorkflow') return { ...checkpoint, version: checkpoint.version + 1, profile: { ...checkpoint.profile, ai } };
49
+ if (checkpoint.profile.kind === 'AIDataWorkflow') return { ...checkpoint, version: checkpoint.version + 1, profile: { ...checkpoint.profile, ai } };
50
+ return fail('AI_AGENT_PROFILE_REQUIRED', 'checkpoint.profile.kind', 'Agent invocation requires an AI workflow profile.');
51
+ }
52
+
53
+ async function commitAI(runtime: AIAgentProcessorRuntime, checkpoint: AIWorkflowFlowRunCheckpoint, ai: AIWorkflowProfileDurableState): Promise<AIWorkflowFlowRunCheckpoint> {
54
+ return runtime.checkpointRuntime.checkpointStore.commit({ expectedVersion: checkpoint.version, checkpoint: withAI(checkpoint, normalizeCanonicalAIAgentState(ai)) }) as Promise<AIWorkflowFlowRunCheckpoint>;
55
+ }
56
+
57
+ function same(left: unknown, right: unknown): boolean {
58
+ return JSON.stringify(left) === JSON.stringify(right);
59
+ }
60
+
61
+ function semanticFacts(config: AIAgentEffectConfig, invocationMetadata?: Readonly<Record<string, FlowClosedValue>>): Readonly<Record<string, FlowClosedValue>> {
62
+ return normalizeFlowClosedObject({
63
+ materialRefs: config.materialRefs ?? [],
64
+ effectPolicy: config.effectPolicy ?? null,
65
+ invocationMetadata: invocationMetadata ?? null,
66
+ }, 'semanticFacts');
67
+ }
68
+
69
+ function assertTaskAuthority(runtime: AIAgentProcessorRuntime, config: AIAgentEffectConfig): void {
70
+ if (!runtime.taskBinding) fail('AI_AGENT_TASK_BINDING_REQUIRED', 'runtime.taskBinding', 'A frozen Agent task binding is required.');
71
+ assertAIAgentNodeTaskBinding(runtime.taskBinding, {
72
+ workflowKind: runtime.workflowKind,
73
+ workflowRef: runtime.workflowRef,
74
+ nodeId: runtime.nodeId,
75
+ }, config);
76
+ }
77
+
78
+ function assertSameInvocation(receipt: AIAgentInvocationReceipt, runtime: AIAgentProcessorRuntime, mode: 'new' | 'targeted', input: FlowClosedValue, config: AIAgentEffectConfig, invocationMetadata?: Readonly<Record<string, FlowClosedValue>>): void {
79
+ if (receipt.nodeId !== runtime.nodeId || receipt.mode !== mode || receipt.agentDefinitionRef !== config.agentDefinitionRef || !same(receipt.input, input) || receipt.generation !== runtime.generation || !same(receipt.semanticFacts, semanticFacts(config, invocationMetadata))) {
80
+ fail('AI_AGENT_INVOCATION_KEY_CONFLICT', `checkpoint.profile.ai.invocationsByKey.${runtime.invocationKey}`, 'Invocation key is already bound to different facts.');
81
+ }
82
+ if (mode === 'new' && receipt.instanceName !== config.instanceName) fail('AI_AGENT_INVOCATION_KEY_CONFLICT', `checkpoint.profile.ai.invocationsByKey.${runtime.invocationKey}.instanceName`, 'Invocation key has another name intent.');
83
+ }
84
+
85
+ function resultFromReceipt(ai: AIWorkflowProfileDurableState, receipt: AIAgentInvocationReceipt): AIAgentRunResult {
86
+ if (receipt.status !== 'completed' || receipt.instanceId === undefined || receipt.output === undefined) fail('AI_AGENT_COMPLETED_INVOCATION_INCOMPLETE', 'receipt', 'Completed receipt is incomplete.');
87
+ const instance = ai.instancesById[receipt.instanceId];
88
+ if (!instance) fail('AI_AGENT_INSTANCE_ID_NOT_FOUND', 'receipt.instanceId', 'Completed receipt instance is missing.');
89
+ return Object.freeze({
90
+ output: receipt.output,
91
+ instance,
92
+ receipt: Object.freeze({ invocationKey: receipt.invocationKey, nodeId: receipt.nodeId, status: 'completed' as const }),
93
+ });
94
+ }
95
+
96
+ function pendingReceipt(runtime: AIAgentProcessorRuntime, mode: 'new' | 'targeted', input: FlowClosedValue, config: AIAgentEffectConfig, instance?: AIAgentInstanceReference, invocationMetadata?: Readonly<Record<string, FlowClosedValue>>): AIAgentInvocationReceipt {
97
+ const instanceName = mode === 'targeted' ? instance?.instanceName : config.instanceName;
98
+ return Object.freeze({
99
+ invocationKey: runtime.invocationKey,
100
+ nodeId: runtime.nodeId,
101
+ status: 'pending',
102
+ mode,
103
+ agentDefinitionRef: config.agentDefinitionRef,
104
+ input,
105
+ semanticFacts: semanticFacts(config, invocationMetadata),
106
+ ...(instanceName === undefined ? {} : { instanceName }),
107
+ ...(instance === undefined ? {} : { instanceId: instance.instanceId }),
108
+ ...(runtime.generation === undefined ? {} : { generation: runtime.generation }),
109
+ ...(runtime.metadata === undefined ? {} : { metadata: normalizeFlowClosedObject(runtime.metadata, 'runtime.metadata') }),
110
+ });
111
+ }
112
+
113
+ function reserveName(ai: AIWorkflowProfileDurableState, receipt: AIAgentInvocationReceipt): void {
114
+ if (receipt.instanceName === undefined) return;
115
+ if (ai.instanceIdByName[receipt.instanceName] !== undefined) fail('AI_AGENT_INSTANCE_NAME_ALREADY_BOUND', 'config.instanceName', 'Instance name is already bound.');
116
+ for (const existing of Object.values(ai.invocationsByKey)) {
117
+ if (existing.invocationKey !== receipt.invocationKey && existing.status === 'pending' && existing.instanceName === receipt.instanceName) {
118
+ fail('AI_AGENT_INSTANCE_NAME_RESERVED', 'config.instanceName', 'Instance name is already reserved by another invocation.');
119
+ }
120
+ }
121
+ }
122
+
123
+ function resolveSelector(ai: AIWorkflowProfileDurableState, selector: AIAgentSelector, config: AIAgentEffectConfig): AIAgentInstanceReference {
124
+ const instanceId = 'byInstanceId' in selector ? selector.byInstanceId : ai.instanceIdByName[selector.byInstanceName];
125
+ if (instanceId === undefined) fail('AI_AGENT_INSTANCE_NAME_NOT_FOUND', 'selector.byInstanceName', 'Named Agent instance does not exist.');
126
+ const instance = ai.instancesById[instanceId];
127
+ if (!instance) fail('AI_AGENT_INSTANCE_ID_NOT_FOUND', 'selector', 'Agent instance does not exist.');
128
+ if (instance.instanceName !== undefined && ai.instanceIdByName[instance.instanceName] !== instance.instanceId) fail('AI_AGENT_INSTANCE_INDEX_CONFLICT', 'checkpoint.profile.ai', 'Agent instance indexes are not reciprocal.');
129
+ if (instance.agentDefinitionRef !== config.agentDefinitionRef) fail('AI_AGENT_DEFINITION_CONFLICT', 'config.agentDefinitionRef', 'Target instance is bound to another Agent definition.');
130
+ return instance;
131
+ }
132
+
133
+ function normalizeHostResult(result: AIAgentHostRunResult, config: AIAgentEffectConfig, expected?: AIAgentInstanceReference): { output: FlowClosedValue; instance: AIAgentInstanceReference; hostReceipt?: Record<string, FlowClosedValue> } {
134
+ const output = normalizeFlowClosedValue(result.output, 'hostResult.output');
135
+ const source = result.instance;
136
+ if (!source || typeof source !== 'object') fail('AI_AGENT_HOST_INSTANCE_INVALID', 'hostResult.instance', 'Host result must contain an instance reference.');
137
+ const candidate = normalizeCanonicalAIAgentState({
138
+ schemaVersion: 'depa.ai-agent-state/v1',
139
+ instancesById: {
140
+ [source.instanceId]: {
141
+ authority: source.authority,
142
+ instanceId: source.instanceId,
143
+ ...(source.instanceName === undefined ? {} : { instanceName: source.instanceName }),
144
+ ...(source.sessionId === undefined ? {} : { sessionId: source.sessionId }),
145
+ ...(source.metadata === undefined ? {} : { metadata: source.metadata }),
146
+ agentDefinitionRef: source.agentDefinitionRef ?? config.agentDefinitionRef,
147
+ },
148
+ },
149
+ instanceIdByName: source.instanceName === undefined ? {} : { [source.instanceName]: source.instanceId },
150
+ invocationsByKey: {},
151
+ }, 'hostResult.state');
152
+ const instance = candidate.instancesById[source.instanceId];
153
+ if (!instance || instance.agentDefinitionRef !== config.agentDefinitionRef) fail('AI_AGENT_HOST_DEFINITION_CONFLICT', 'hostResult.instance.agentDefinitionRef', 'Host instance uses another Agent definition.');
154
+ if (config.instanceName !== undefined && instance.instanceName !== config.instanceName) fail('AI_AGENT_HOST_NAME_CONFLICT', 'hostResult.instance.instanceName', 'Host instance did not preserve the reserved name.');
155
+ if (expected && (instance.instanceId !== expected.instanceId || instance.authority !== expected.authority)) fail('AI_AGENT_HOST_TARGET_CONFLICT', 'hostResult.instance', 'Host targeted result differs from the selected instance.');
156
+ return { output, instance, ...(result.hostReceipt === undefined ? {} : { hostReceipt: normalizeFlowClosedObject(result.hostReceipt, 'hostResult.hostReceipt') }) };
157
+ }
158
+
159
+ async function complete(runtime: AIAgentProcessorRuntime, checkpoint: AIWorkflowFlowRunCheckpoint, pending: AIAgentInvocationReceipt, host: AIAgentHostRunResult, config: AIAgentEffectConfig, expected?: AIAgentInstanceReference): Promise<AIAgentRunResult> {
160
+ const normalized = normalizeHostResult(host, config, expected);
161
+ const latest = await loadCheckpoint(runtime);
162
+ const ai = profileAI(latest);
163
+ const accepted = ai.invocationsByKey[runtime.invocationKey];
164
+ if (accepted?.status === 'completed') return resultFromReceipt(ai, accepted);
165
+ if (!accepted || !same(accepted, pending)) fail('AI_AGENT_INVOCATION_RESERVATION_CHANGED', 'checkpoint.profile.ai', 'Invocation reservation changed before completion.');
166
+ const prior = ai.instancesById[normalized.instance.instanceId];
167
+ if (prior && !same(prior, normalized.instance)) fail('AI_AGENT_INSTANCE_ID_CONFLICT', 'hostResult.instance.instanceId', 'Instance id is already bound to different facts.');
168
+ if (normalized.instance.instanceName !== undefined) {
169
+ const priorId = ai.instanceIdByName[normalized.instance.instanceName];
170
+ if (priorId !== undefined && priorId !== normalized.instance.instanceId) fail('AI_AGENT_INSTANCE_NAME_ALREADY_BOUND', 'hostResult.instance.instanceName', 'Instance name is already bound to another id.');
171
+ }
172
+ const completed: AIAgentInvocationReceipt = Object.freeze({ ...pending, status: 'completed', instanceId: normalized.instance.instanceId, output: normalized.output, ...(normalized.hostReceipt === undefined ? {} : { hostReceipt: normalized.hostReceipt }) });
173
+ const nextAI = normalizeCanonicalAIAgentState({
174
+ ...ai,
175
+ instancesById: { ...ai.instancesById, [normalized.instance.instanceId]: normalized.instance },
176
+ instanceIdByName: normalized.instance.instanceName === undefined ? ai.instanceIdByName : { ...ai.instanceIdByName, [normalized.instance.instanceName]: normalized.instance.instanceId },
177
+ invocationsByKey: { ...ai.invocationsByKey, [runtime.invocationKey]: completed },
178
+ });
179
+ try {
180
+ const committed = await commitAI(runtime, latest, nextAI);
181
+ return resultFromReceipt(profileAI(committed), completed);
182
+ } catch (error) {
183
+ const converged = await loadCheckpoint(runtime);
184
+ if (converged.version <= latest.version) throw error;
185
+ const convergedAI = profileAI(converged);
186
+ const acceptedCompletion = convergedAI.invocationsByKey[pending.invocationKey];
187
+ if (!acceptedCompletion || acceptedCompletion.status !== 'completed'
188
+ || acceptedCompletion.nodeId !== pending.nodeId
189
+ || acceptedCompletion.mode !== pending.mode
190
+ || acceptedCompletion.agentDefinitionRef !== pending.agentDefinitionRef
191
+ || acceptedCompletion.instanceId !== normalized.instance.instanceId
192
+ || acceptedCompletion.instanceName !== normalized.instance.instanceName
193
+ || acceptedCompletion.generation !== pending.generation
194
+ || !same(acceptedCompletion.input, pending.input)
195
+ || !same(acceptedCompletion.semanticFacts, pending.semanticFacts)) throw error;
196
+ const acceptedInstance = convergedAI.instancesById[normalized.instance.instanceId];
197
+ if (!acceptedInstance || !same(acceptedInstance, normalized.instance)) throw error;
198
+ return resultFromReceipt(convergedAI, acceptedCompletion);
199
+ }
200
+ }
201
+
202
+ export async function runAgent<Input extends FlowClosedValue = FlowClosedValue, Output extends FlowClosedValue = FlowClosedValue>(runtime: AIAgentProcessorRuntime, input: Input, configInput: AIAgentEffectConfig): Promise<AIAgentRunResult<Output>> {
203
+ const inputValue = normalizeFlowClosedValue(input, 'input') as Input;
204
+ const config = normalizeAIAgentEffectConfig(configInput);
205
+ assertTaskAuthority(runtime, config);
206
+ let checkpoint = await loadCheckpoint(runtime);
207
+ let ai = profileAI(checkpoint);
208
+ let receipt = ai.invocationsByKey[runtime.invocationKey];
209
+ if (receipt) {
210
+ assertSameInvocation(receipt, runtime, 'new', inputValue, config);
211
+ if (receipt.status === 'completed') return resultFromReceipt(ai, receipt) as AIAgentRunResult<Output>;
212
+ } else {
213
+ receipt = pendingReceipt(runtime, 'new', inputValue, config);
214
+ reserveName(ai, receipt);
215
+ try {
216
+ checkpoint = await commitAI(runtime, checkpoint, { ...ai, invocationsByKey: { ...ai.invocationsByKey, [runtime.invocationKey]: receipt } });
217
+ } catch (error) {
218
+ const latest = await loadCheckpoint(runtime);
219
+ if (latest.version <= checkpoint.version) throw error;
220
+ const latestAI = profileAI(latest);
221
+ const accepted = latestAI.invocationsByKey[runtime.invocationKey];
222
+ if (accepted) {
223
+ assertSameInvocation(accepted, runtime, 'new', inputValue, config);
224
+ checkpoint = latest;
225
+ } else {
226
+ reserveName(latestAI, receipt);
227
+ checkpoint = await commitAI(runtime, latest, { ...latestAI, invocationsByKey: { ...latestAI.invocationsByKey, [runtime.invocationKey]: receipt } });
228
+ }
229
+ }
230
+ ai = profileAI(checkpoint);
231
+ receipt = ai.invocationsByKey[runtime.invocationKey]!;
232
+ }
233
+ if (receipt.status === 'completed') return resultFromReceipt(ai, receipt) as AIAgentRunResult<Output>;
234
+ const host = await runtime.effects.runAgent({ invocationKey: runtime.invocationKey, flowInstanceId: runtime.checkpointKey.instanceId, runId: runtime.checkpointKey.runId, nodeId: runtime.nodeId, ...(runtime.generation === undefined ? {} : { generation: runtime.generation }), input: inputValue, agentDefinitionRef: config.agentDefinitionRef, ...(config.instanceName === undefined ? {} : { instanceName: config.instanceName }), ...(config.materialRefs === undefined ? {} : { materialRefs: config.materialRefs }), ...(config.effectPolicy === undefined ? {} : { effectPolicy: config.effectPolicy }), ...(runtime.signal === undefined ? {} : { signal: runtime.signal }), ...(runtime.metadata === undefined ? {} : { metadata: runtime.metadata }) });
235
+ return complete(runtime, checkpoint, receipt, host, config) as Promise<AIAgentRunResult<Output>>;
236
+ }
237
+
238
+ export async function runTargetedAgent<Input extends FlowClosedValue = FlowClosedValue, Output extends FlowClosedValue = FlowClosedValue>(runtime: AIAgentProcessorRuntime, selectorInput: AIAgentSelector, invocationInput: AIAgentInvocation<Input>, configInput: AIAgentEffectConfig): Promise<AIAgentRunResult<Output>> {
239
+ const selector = normalizeAIAgentSelector(selectorInput);
240
+ const invocation = normalizeAIAgentInvocation<Input>(invocationInput);
241
+ const config = normalizeAIAgentEffectConfig(configInput);
242
+ assertTaskAuthority(runtime, config);
243
+ let checkpoint = await loadCheckpoint(runtime);
244
+ let ai = profileAI(checkpoint);
245
+ const instance = resolveSelector(ai, selector, config);
246
+ let receipt = ai.invocationsByKey[runtime.invocationKey];
247
+ if (receipt) {
248
+ assertSameInvocation(receipt, runtime, 'targeted', invocation.payload, config, invocation.metadata);
249
+ if (receipt.instanceId !== instance.instanceId) fail('AI_AGENT_INVOCATION_TARGET_CONFLICT', 'selector', 'Invocation key is reserved for another instance.');
250
+ if (receipt.status === 'completed') return resultFromReceipt(ai, receipt) as AIAgentRunResult<Output>;
251
+ } else {
252
+ receipt = pendingReceipt(runtime, 'targeted', invocation.payload, config, instance, invocation.metadata);
253
+ try {
254
+ checkpoint = await commitAI(runtime, checkpoint, { ...ai, invocationsByKey: { ...ai.invocationsByKey, [runtime.invocationKey]: receipt } });
255
+ } catch (error) {
256
+ const latest = await loadCheckpoint(runtime);
257
+ if (latest.version <= checkpoint.version) throw error;
258
+ const latestAI = profileAI(latest);
259
+ const resolved = resolveSelector(latestAI, selector, config);
260
+ if (resolved.instanceId !== instance.instanceId) fail('AI_AGENT_SELECTOR_CHANGED', 'selector', 'Selector resolved to another instance after checkpoint conflict.');
261
+ const accepted = latestAI.invocationsByKey[runtime.invocationKey];
262
+ if (accepted) {
263
+ assertSameInvocation(accepted, runtime, 'targeted', invocation.payload, config, invocation.metadata);
264
+ checkpoint = latest;
265
+ } else {
266
+ checkpoint = await commitAI(runtime, latest, { ...latestAI, invocationsByKey: { ...latestAI.invocationsByKey, [runtime.invocationKey]: receipt } });
267
+ }
268
+ }
269
+ ai = profileAI(checkpoint);
270
+ receipt = ai.invocationsByKey[runtime.invocationKey]!;
271
+ }
272
+ if (receipt.status === 'completed') return resultFromReceipt(ai, receipt) as AIAgentRunResult<Output>;
273
+ const host = await runtime.effects.runTargetedAgent({ invocationKey: runtime.invocationKey, flowInstanceId: runtime.checkpointKey.instanceId, runId: runtime.checkpointKey.runId, nodeId: runtime.nodeId, ...(runtime.generation === undefined ? {} : { generation: runtime.generation }), input: invocation.payload, instance, agentDefinitionRef: config.agentDefinitionRef, ...(config.materialRefs === undefined ? {} : { materialRefs: config.materialRefs }), ...(config.effectPolicy === undefined ? {} : { effectPolicy: config.effectPolicy }), ...(runtime.signal === undefined ? {} : { signal: runtime.signal }), metadata: { ...(runtime.metadata ?? {}), ...(invocation.metadata ?? {}) } });
274
+ return complete(runtime, checkpoint, receipt, host, config, instance) as Promise<AIAgentRunResult<Output>>;
275
+ }
276
+
277
+ export function bindAIAgentProcessors(runtime: AIAgentProcessorRuntime): AIWorkflowAuthoredEffects {
278
+ return Object.freeze({
279
+ runAgent: <Input extends FlowClosedValue, Output extends FlowClosedValue>(input: Input, config: AIAgentEffectConfig) => runAgent<Input, Output>(runtime, input, config),
280
+ runTargetedAgent: <Input extends FlowClosedValue, Output extends FlowClosedValue>(selector: AIAgentSelector, invocation: AIAgentInvocation<Input>, config: AIAgentEffectConfig) => runTargetedAgent<Input, Output>(runtime, selector, invocation, config),
281
+ });
282
+ }
@@ -0,0 +1,293 @@
1
+ import type {
2
+ AIAgentEffectConfig,
3
+ AIAgentInstanceReference,
4
+ AIAgentInvocation,
5
+ AIAgentInvocationReceipt,
6
+ AIAgentSelector,
7
+ AIWorkflowProfileDurableState,
8
+ AIWorkflowAgentTaskRef,
9
+ AIWorkflowKind,
10
+ FlowClosedObject,
11
+ FlowClosedValue,
12
+ LegacyAIWorkflowProfileDurableState,
13
+ } from 'ai-workflow-contract';
14
+ import { normalizeFlowClosedObject, normalizeFlowClosedValue } from './run-checkpoint.js';
15
+
16
+ export class AIAgentInvocationError extends Error {
17
+ constructor(public readonly code: string, public readonly location: string, message: string) {
18
+ super(`${location}: ${message}`);
19
+ this.name = 'AIAgentInvocationError';
20
+ }
21
+ }
22
+
23
+ function fail(code: string, location: string, message: string): never {
24
+ throw new AIAgentInvocationError(code, location, message);
25
+ }
26
+
27
+ function record(value: unknown, required: readonly string[], optional: readonly string[], location: string): Record<string, unknown> {
28
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) fail('AI_AGENT_OBJECT_REQUIRED', location, 'Expected a plain data object.');
29
+ const prototype = Object.getPrototypeOf(value);
30
+ if (prototype !== Object.prototype && prototype !== null) fail('AI_AGENT_OBJECT_REQUIRED', location, 'Expected a plain data object.');
31
+ const descriptors = Object.getOwnPropertyDescriptors(value);
32
+ const allowed = new Set([...required, ...optional]);
33
+ for (const key of Reflect.ownKeys(descriptors)) {
34
+ if (typeof key !== 'string') fail('AI_AGENT_SYMBOL_UNSUPPORTED', location, 'Symbol fields are not supported.');
35
+ const descriptor = descriptors[key]!;
36
+ if (!('value' in descriptor)) fail('AI_AGENT_ACCESSOR_UNSUPPORTED', `${location}.${key}`, 'Accessor fields are not supported.');
37
+ if (!allowed.has(key)) fail('AI_AGENT_FIELD_UNSUPPORTED', `${location}.${key}`, 'Unexpected field.');
38
+ }
39
+ for (const key of required) if (!Object.prototype.hasOwnProperty.call(descriptors, key)) fail('AI_AGENT_FIELD_REQUIRED', `${location}.${key}`, 'Required field is missing.');
40
+ return value as Record<string, unknown>;
41
+ }
42
+
43
+ function objectMap(value: unknown, location: string): Record<string, unknown> {
44
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) fail('AI_AGENT_INDEX_REQUIRED', location, 'Expected a plain data index.');
45
+ const prototype = Object.getPrototypeOf(value);
46
+ if (prototype !== Object.prototype && prototype !== null) fail('AI_AGENT_INDEX_REQUIRED', location, 'Expected a plain data index.');
47
+ const descriptors = Object.getOwnPropertyDescriptors(value);
48
+ const output: Record<string, unknown> = {};
49
+ for (const key of Object.keys(descriptors).sort(compareCodeUnits)) {
50
+ const descriptor = descriptors[key]!;
51
+ if (!('value' in descriptor)) fail('AI_AGENT_ACCESSOR_UNSUPPORTED', `${location}.${key}`, 'Accessor fields are not supported.');
52
+ output[key] = descriptor.value;
53
+ }
54
+ if (Reflect.ownKeys(descriptors).some((key) => typeof key !== 'string')) fail('AI_AGENT_SYMBOL_UNSUPPORTED', location, 'Symbol fields are not supported.');
55
+ return output;
56
+ }
57
+
58
+ function compareCodeUnits(left: string, right: string): number {
59
+ const length = Math.min(left.length, right.length);
60
+ for (let index = 0; index < length; index += 1) {
61
+ const difference = left.charCodeAt(index) - right.charCodeAt(index);
62
+ if (difference !== 0) return difference;
63
+ }
64
+ return left.length - right.length;
65
+ }
66
+
67
+ function identity(value: unknown, location: string): string {
68
+ if (typeof value !== 'string' || value.length === 0 || value.trim() !== value || value.normalize('NFC') !== value) {
69
+ fail('AI_AGENT_IDENTITY_INVALID', location, 'Expected one non-empty NFC identity without surrounding whitespace.');
70
+ }
71
+ return value;
72
+ }
73
+
74
+ function resourceRef(value: unknown, location: string): `resource://${string}` {
75
+ const result = identity(value, location);
76
+ if (!result.startsWith('resource://') || result.length === 11) fail('AI_AGENT_DEFINITION_REF_INVALID', location, 'Expected an exact resource:// identity.');
77
+ return result as `resource://${string}`;
78
+ }
79
+
80
+ function optionalClosedObject(value: unknown, location: string): FlowClosedObject {
81
+ return normalizeFlowClosedObject(value, location);
82
+ }
83
+
84
+ const FORBIDDEN_RUNTIME_STATE_KEYS = new Set([
85
+ 'actor', 'actorstate', 'compaction', 'conversation', 'history', 'providertranscript', 'transcript',
86
+ ]);
87
+
88
+ function safeOpaqueObject(value: unknown, location: string): FlowClosedObject {
89
+ const normalized = optionalClosedObject(value, location);
90
+ const visit = (item: FlowClosedValue, itemLocation: string): void => {
91
+ if (Array.isArray(item)) {
92
+ item.forEach((nested, index) => visit(nested, `${itemLocation}[${index}]`));
93
+ return;
94
+ }
95
+ if (item === null || typeof item !== 'object') return;
96
+ for (const [key, nested] of Object.entries(item)) {
97
+ const semanticKey = key.normalize('NFC').toLowerCase().replace(/[^a-z0-9]/g, '');
98
+ if (FORBIDDEN_RUNTIME_STATE_KEYS.has(semanticKey)) {
99
+ fail('AI_AGENT_RUNTIME_STATE_FIELD_FORBIDDEN', `${itemLocation}.${key}`, 'Runtime-owned Agent state cannot enter the Flow checkpoint.');
100
+ }
101
+ visit(nested, `${itemLocation}.${key}`);
102
+ }
103
+ };
104
+ visit(normalized, location);
105
+ return normalized;
106
+ }
107
+
108
+ function optionalStringArray(value: unknown, location: string): readonly string[] {
109
+ if (!Array.isArray(value)) fail('AI_AGENT_ARRAY_REQUIRED', location, 'Expected an array.');
110
+ const descriptors = Object.getOwnPropertyDescriptors(value);
111
+ for (let index = 0; index < value.length; index += 1) {
112
+ if (!Object.prototype.hasOwnProperty.call(descriptors, String(index))) fail('AI_AGENT_ARRAY_SPARSE', `${location}[${index}]`, 'Sparse arrays are not supported.');
113
+ }
114
+ return Object.freeze(value.map((item, index) => identity(item, `${location}[${index}]`)));
115
+ }
116
+
117
+ export function normalizeAIAgentSelector(value: unknown, location = 'selector'): AIAgentSelector {
118
+ const input = record(value, [], ['byInstanceId', 'byInstanceName'], location);
119
+ const hasId = Object.prototype.hasOwnProperty.call(input, 'byInstanceId');
120
+ const hasName = Object.prototype.hasOwnProperty.call(input, 'byInstanceName');
121
+ if (hasId === hasName) fail('AI_AGENT_SELECTOR_EXACTLY_ONE', location, 'Selector must contain exactly one supported key.');
122
+ return Object.freeze(hasId
123
+ ? { byInstanceId: identity(input.byInstanceId, `${location}.byInstanceId`) }
124
+ : { byInstanceName: identity(input.byInstanceName, `${location}.byInstanceName`) });
125
+ }
126
+
127
+ export function normalizeAIAgentEffectConfig(value: unknown, location = 'config'): AIAgentEffectConfig {
128
+ const input = record(value, ['agentDefinitionRef'], ['instanceName', 'materialRefs', 'effectPolicy'], location);
129
+ return Object.freeze({
130
+ agentDefinitionRef: resourceRef(input.agentDefinitionRef, `${location}.agentDefinitionRef`),
131
+ ...(Object.prototype.hasOwnProperty.call(input, 'instanceName') ? { instanceName: identity(input.instanceName, `${location}.instanceName`) } : {}),
132
+ ...(Object.prototype.hasOwnProperty.call(input, 'materialRefs') ? { materialRefs: optionalStringArray(input.materialRefs, `${location}.materialRefs`) } : {}),
133
+ ...(Object.prototype.hasOwnProperty.call(input, 'effectPolicy') ? { effectPolicy: safeOpaqueObject(input.effectPolicy, `${location}.effectPolicy`) } : {}),
134
+ });
135
+ }
136
+
137
+ export interface AIAgentNodeBindingIdentity {
138
+ readonly workflowKind: AIWorkflowKind;
139
+ readonly workflowRef: `resource://${string}`;
140
+ readonly nodeId: string;
141
+ }
142
+
143
+ /** Exact frozen task proof used by Ctrl/Data loaders before constructing a node runtime. */
144
+ export function assertAIAgentNodeTaskBinding(
145
+ task: AIWorkflowAgentTaskRef,
146
+ identityInput: AIAgentNodeBindingIdentity,
147
+ configInput: AIAgentEffectConfig,
148
+ ): AIAgentEffectConfig {
149
+ const taskValue = record(task, ['workflowKind', 'workflowRef', 'nodeId', 'agentDefinitionRef'], [], 'task');
150
+ const identityValue = record(identityInput, ['workflowKind', 'workflowRef', 'nodeId'], [], 'identity');
151
+ if (identityValue.workflowKind !== 'AICtrlWorkflow' && identityValue.workflowKind !== 'AIDataWorkflow') fail('AI_AGENT_TASK_WORKFLOW_KIND_INVALID', 'identity.workflowKind', 'Unsupported AI workflow kind.');
152
+ const identity = {
153
+ workflowKind: identityValue.workflowKind,
154
+ workflowRef: resourceRef(identityValue.workflowRef, 'identity.workflowRef'),
155
+ nodeId: identityValue.nodeId,
156
+ } as const;
157
+ const config = normalizeAIAgentEffectConfig(configInput);
158
+ if (taskValue.workflowKind !== identity.workflowKind) fail('AI_AGENT_TASK_WORKFLOW_KIND_MISMATCH', 'task.workflowKind', 'Frozen task workflow kind differs from the node.');
159
+ if (resourceRef(taskValue.workflowRef, 'task.workflowRef') !== identity.workflowRef) fail('AI_AGENT_TASK_WORKFLOW_REF_MISMATCH', 'task.workflowRef', 'Frozen task workflow identity differs from the node.');
160
+ if (identityValue.nodeId !== taskValue.nodeId) fail('AI_AGENT_TASK_NODE_ID_MISMATCH', 'task.nodeId', 'Frozen task node identity differs from the node.');
161
+ if (resourceRef(taskValue.agentDefinitionRef, 'task.agentDefinitionRef') !== config.agentDefinitionRef) fail('AI_AGENT_TASK_DEFINITION_MISMATCH', 'config.agentDefinitionRef', 'Frozen task Agent definition differs from config.');
162
+ return config;
163
+ }
164
+
165
+ export function normalizeAIAgentInvocation<Input extends FlowClosedValue = FlowClosedValue>(value: unknown, location = 'invocation'): AIAgentInvocation<Input> {
166
+ const input = record(value, ['kind', 'payload'], ['metadata'], location);
167
+ if (input.kind !== 'ai.agent') fail('AI_AGENT_INVOCATION_KIND_INVALID', `${location}.kind`, 'Invocation kind must be ai.agent.');
168
+ return Object.freeze({
169
+ kind: 'ai.agent',
170
+ payload: normalizeFlowClosedValue(input.payload, `${location}.payload`) as Input,
171
+ ...(Object.prototype.hasOwnProperty.call(input, 'metadata') ? { metadata: safeOpaqueObject(input.metadata, `${location}.metadata`) } : {}),
172
+ });
173
+ }
174
+
175
+ function normalizeInstance(value: unknown, id: string, location: string): AIAgentInstanceReference {
176
+ const input = record(value, ['authority', 'instanceId', 'agentDefinitionRef'], ['instanceName', 'sessionId', 'metadata'], location);
177
+ const instanceId = identity(input.instanceId, `${location}.instanceId`);
178
+ if (instanceId !== id) fail('AI_AGENT_INSTANCE_ID_INDEX_MISMATCH', `${location}.instanceId`, 'Instance id must equal its index key.');
179
+ return Object.freeze({
180
+ authority: identity(input.authority, `${location}.authority`),
181
+ instanceId,
182
+ agentDefinitionRef: resourceRef(input.agentDefinitionRef, `${location}.agentDefinitionRef`),
183
+ ...(Object.prototype.hasOwnProperty.call(input, 'instanceName') ? { instanceName: identity(input.instanceName, `${location}.instanceName`) } : {}),
184
+ ...(Object.prototype.hasOwnProperty.call(input, 'sessionId') ? { sessionId: identity(input.sessionId, `${location}.sessionId`) } : {}),
185
+ ...(Object.prototype.hasOwnProperty.call(input, 'metadata') ? { metadata: safeOpaqueObject(input.metadata, `${location}.metadata`) } : {}),
186
+ });
187
+ }
188
+
189
+ function normalizeReceipt(value: unknown, key: string, instances: Readonly<Record<string, AIAgentInstanceReference>>, location: string): AIAgentInvocationReceipt {
190
+ const input = record(value, ['invocationKey', 'nodeId', 'status', 'mode', 'agentDefinitionRef', 'input', 'semanticFacts'], ['instanceName', 'instanceId', 'generation', 'metadata', 'output', 'hostReceipt'], location);
191
+ const invocationKey = identity(input.invocationKey, `${location}.invocationKey`);
192
+ if (invocationKey !== key) fail('AI_AGENT_INVOCATION_KEY_INDEX_MISMATCH', `${location}.invocationKey`, 'Invocation key must equal its index key.');
193
+ if (input.status !== 'pending' && input.status !== 'completed') fail('AI_AGENT_INVOCATION_STATUS_INVALID', `${location}.status`, 'Status must be pending or completed.');
194
+ if (input.mode !== 'new' && input.mode !== 'targeted') fail('AI_AGENT_INVOCATION_MODE_INVALID', `${location}.mode`, 'Mode must be new or targeted.');
195
+ const instanceId = Object.prototype.hasOwnProperty.call(input, 'instanceId') ? identity(input.instanceId, `${location}.instanceId`) : undefined;
196
+ if (input.mode === 'targeted' && instanceId === undefined) fail('AI_AGENT_TARGETED_INSTANCE_REQUIRED', location, 'Targeted invocation requires an instance id.');
197
+ if (input.status === 'completed' && (instanceId === undefined || instances[instanceId] === undefined || !Object.prototype.hasOwnProperty.call(input, 'output'))) {
198
+ fail('AI_AGENT_COMPLETED_INVOCATION_INCOMPLETE', location, 'Completed invocation requires an indexed instance and output.');
199
+ }
200
+ const agentDefinitionRef = resourceRef(input.agentDefinitionRef, `${location}.agentDefinitionRef`);
201
+ if (instanceId !== undefined && instances[instanceId] !== undefined
202
+ && instances[instanceId].agentDefinitionRef !== agentDefinitionRef) {
203
+ fail('AI_AGENT_RECEIPT_DEFINITION_MISMATCH', `${location}.agentDefinitionRef`, 'Invocation receipt definition must equal its indexed instance definition.');
204
+ }
205
+ if (instanceId !== undefined && instances[instanceId] !== undefined) {
206
+ const receiptName = Object.prototype.hasOwnProperty.call(input, 'instanceName')
207
+ ? identity(input.instanceName, `${location}.instanceName`)
208
+ : undefined;
209
+ if (receiptName !== instances[instanceId].instanceName) {
210
+ fail('AI_AGENT_RECEIPT_INSTANCE_NAME_MISMATCH', `${location}.instanceName`, 'Invocation receipt name must equal its indexed instance name.');
211
+ }
212
+ }
213
+ if (Object.prototype.hasOwnProperty.call(input, 'generation') && (typeof input.generation !== 'number' || !Number.isSafeInteger(input.generation) || input.generation < 0)) {
214
+ fail('AI_AGENT_GENERATION_INVALID', `${location}.generation`, 'Generation must be a non-negative safe integer.');
215
+ }
216
+ return Object.freeze({
217
+ invocationKey,
218
+ nodeId: identity(input.nodeId, `${location}.nodeId`),
219
+ status: input.status,
220
+ mode: input.mode,
221
+ agentDefinitionRef,
222
+ input: normalizeFlowClosedValue(input.input, `${location}.input`),
223
+ semanticFacts: safeOpaqueObject(input.semanticFacts, `${location}.semanticFacts`),
224
+ ...(Object.prototype.hasOwnProperty.call(input, 'instanceName') ? { instanceName: identity(input.instanceName, `${location}.instanceName`) } : {}),
225
+ ...(instanceId === undefined ? {} : { instanceId }),
226
+ ...(Object.prototype.hasOwnProperty.call(input, 'generation') ? { generation: input.generation as number } : {}),
227
+ ...(Object.prototype.hasOwnProperty.call(input, 'metadata') ? { metadata: safeOpaqueObject(input.metadata, `${location}.metadata`) } : {}),
228
+ ...(Object.prototype.hasOwnProperty.call(input, 'output') ? { output: normalizeFlowClosedValue(input.output, `${location}.output`) } : {}),
229
+ ...(Object.prototype.hasOwnProperty.call(input, 'hostReceipt') ? { hostReceipt: safeOpaqueObject(input.hostReceipt, `${location}.hostReceipt`) } : {}),
230
+ });
231
+ }
232
+
233
+ export function normalizeCanonicalAIAgentState(value: unknown, location = 'ai'): AIWorkflowProfileDurableState {
234
+ const input = record(value, ['schemaVersion', 'instancesById', 'instanceIdByName', 'invocationsByKey'], [], location);
235
+ if (input.schemaVersion !== 'depa.ai-agent-state/v1') fail('AI_AGENT_STATE_SCHEMA_UNSUPPORTED', `${location}.schemaVersion`, 'Unsupported Agent state schema.');
236
+ const instanceValues = objectMap(input.instancesById, `${location}.instancesById`);
237
+ const instancesById: Record<string, AIAgentInstanceReference> = {};
238
+ for (const id of Object.keys(instanceValues).sort(compareCodeUnits)) instancesById[identity(id, `${location}.instancesById`)] = normalizeInstance(instanceValues[id], id, `${location}.instancesById.${id}`);
239
+ const nameValues = objectMap(input.instanceIdByName, `${location}.instanceIdByName`);
240
+ const instanceIdByName: Record<string, string> = {};
241
+ const seenIds = new Set<string>();
242
+ for (const rawName of Object.keys(nameValues).sort(compareCodeUnits)) {
243
+ const name = identity(rawName, `${location}.instanceIdByName`);
244
+ const id = identity(nameValues[name], `${location}.instanceIdByName.${name}`);
245
+ if (!instancesById[id] || instancesById[id].instanceName !== name) fail('AI_AGENT_INSTANCE_NAME_INDEX_MISMATCH', `${location}.instanceIdByName.${name}`, 'Name index must be reciprocal.');
246
+ if (seenIds.has(id)) fail('AI_AGENT_INSTANCE_NAME_DUPLICATE', `${location}.instanceIdByName.${name}`, 'One instance cannot have multiple names.');
247
+ seenIds.add(id);
248
+ instanceIdByName[name] = id;
249
+ }
250
+ for (const instance of Object.values(instancesById)) if (instance.instanceName !== undefined && instanceIdByName[instance.instanceName] !== instance.instanceId) fail('AI_AGENT_INSTANCE_NAME_INDEX_MISSING', `${location}.instancesById.${instance.instanceId}.instanceName`, 'Named instance requires reciprocal index.');
251
+ const receiptValues = objectMap(input.invocationsByKey, `${location}.invocationsByKey`);
252
+ const invocationsByKey: Record<string, AIAgentInvocationReceipt> = {};
253
+ const reservations = new Set<string>();
254
+ for (const key of Object.keys(receiptValues).sort(compareCodeUnits)) {
255
+ const receipt = normalizeReceipt(receiptValues[key], identity(key, `${location}.invocationsByKey`), instancesById, `${location}.invocationsByKey.${key}`);
256
+ if (receipt.status === 'pending' && receipt.mode === 'new' && receipt.instanceName !== undefined) {
257
+ if (reservations.has(receipt.instanceName) || instanceIdByName[receipt.instanceName] !== undefined) fail('AI_AGENT_INSTANCE_NAME_RESERVED', `${location}.invocationsByKey.${key}.instanceName`, 'Name has another accepted binding or reservation.');
258
+ reservations.add(receipt.instanceName);
259
+ }
260
+ invocationsByKey[key] = receipt;
261
+ }
262
+ return Object.freeze({ schemaVersion: 'depa.ai-agent-state/v1', instancesById: Object.freeze(instancesById), instanceIdByName: Object.freeze(instanceIdByName), invocationsByKey: Object.freeze(invocationsByKey) });
263
+ }
264
+
265
+ export function upgradeLegacyAIWorkflowProfileDurableState(value: LegacyAIWorkflowProfileDurableState, agentDefinitionRefs: Readonly<Record<string, `resource://${string}`>>, location = 'legacyAI'): AIWorkflowProfileDurableState {
266
+ const input = record(value, ['externalRuntimeRefs', 'receipts'], [], location);
267
+ const refs = objectMap(input.externalRuntimeRefs, `${location}.externalRuntimeRefs`);
268
+ const instancesById: Record<string, AIAgentInstanceReference> = {};
269
+ const instanceIdByName: Record<string, string> = {};
270
+ for (const name of Object.keys(refs).sort(compareCodeUnits)) {
271
+ const refLocation = `${location}.externalRuntimeRefs.${name}`;
272
+ const ref = record(refs[name], ['authority', 'instanceId'], ['sessionId', 'metadata'], refLocation);
273
+ const instanceId = identity(ref.instanceId, `${refLocation}.instanceId`);
274
+ if (instancesById[instanceId]) fail('AI_AGENT_LEGACY_INSTANCE_DUPLICATE', refLocation, 'Legacy instance id is not unique.');
275
+ const agentDefinitionRef = agentDefinitionRefs[name];
276
+ if (!agentDefinitionRef) fail('AI_AGENT_LEGACY_DEFINITION_REQUIRED', refLocation, 'Explicit Agent definition is required for legacy upgrade.');
277
+ instanceIdByName[name] = instanceId;
278
+ instancesById[instanceId] = { authority: identity(ref.authority, `${refLocation}.authority`), instanceId, instanceName: identity(name, refLocation), agentDefinitionRef: resourceRef(agentDefinitionRef, `${refLocation}.agentDefinitionRef`), ...(Object.prototype.hasOwnProperty.call(ref, 'sessionId') ? { sessionId: identity(ref.sessionId, `${refLocation}.sessionId`) } : {}), ...(Object.prototype.hasOwnProperty.call(ref, 'metadata') ? { metadata: optionalClosedObject(ref.metadata, `${refLocation}.metadata`) } : {}) };
279
+ }
280
+ if (!Array.isArray(input.receipts)) fail('AI_AGENT_ARRAY_REQUIRED', `${location}.receipts`, 'Expected receipts array.');
281
+ const invocationsByKey: Record<string, AIAgentInvocationReceipt> = {};
282
+ input.receipts.forEach((item, index) => {
283
+ const receiptLocation = `${location}.receipts[${index}]`;
284
+ const receipt = record(item, ['nodeId', 'externalRuntimeRef'], ['metadata'], receiptLocation);
285
+ const nodeId = identity(receipt.nodeId, `${receiptLocation}.nodeId`);
286
+ const name = identity(receipt.externalRuntimeRef, `${receiptLocation}.externalRuntimeRef`);
287
+ const instanceId = instanceIdByName[name];
288
+ if (!instanceId) fail('AI_WORKFLOW_EXTERNAL_RUNTIME_REF_UNKNOWN', `${receiptLocation}.externalRuntimeRef`, 'Legacy receipt reference is unknown.');
289
+ const key = `${nodeId}#legacy-${index + 1}`;
290
+ invocationsByKey[key] = { invocationKey: key, nodeId, status: 'completed', mode: 'targeted', agentDefinitionRef: instancesById[instanceId]!.agentDefinitionRef, instanceId, instanceName: name, input: null, semanticFacts: {}, output: null, ...(Object.prototype.hasOwnProperty.call(receipt, 'metadata') ? { metadata: safeOpaqueObject(receipt.metadata, `${receiptLocation}.metadata`) } : {}) };
291
+ });
292
+ return normalizeCanonicalAIAgentState({ schemaVersion: 'depa.ai-agent-state/v1', instancesById, instanceIdByName, invocationsByKey }, location);
293
+ }