ai-workflow-logic 0.1.9 → 0.2.0
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 +6 -5
- package/src/agent-definition-selection.ts +766 -0
- package/src/agent-invocation.ts +2 -2
- package/src/agent-resources.ts +134 -29
- package/src/agent-state.ts +5 -5
- package/src/checkpoint-store.ts +3 -1
- package/src/filesystem.ts +22 -2
- package/src/index.ts +15 -0
- package/src/resource-resolution.ts +121 -0
- package/src/run-checkpoint.ts +225 -3
- package/src/run-freeze.ts +30 -15
package/src/agent-invocation.ts
CHANGED
|
@@ -121,8 +121,8 @@ function reserveName(ai: AIWorkflowProfileDurableState, receipt: AIAgentInvocati
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
function resolveSelector(ai: AIWorkflowProfileDurableState, selector: AIAgentSelector, config: AIAgentEffectConfig): AIAgentInstanceReference {
|
|
124
|
-
const instanceId = '
|
|
125
|
-
if (instanceId === undefined) fail('AI_AGENT_INSTANCE_NAME_NOT_FOUND', 'selector.
|
|
124
|
+
const instanceId = 'byId' in selector ? selector.byId : ai.instanceIdByName[selector.byName];
|
|
125
|
+
if (instanceId === undefined) fail('AI_AGENT_INSTANCE_NAME_NOT_FOUND', 'selector.byName', 'Named Agent instance does not exist.');
|
|
126
126
|
const instance = ai.instancesById[instanceId];
|
|
127
127
|
if (!instance) fail('AI_AGENT_INSTANCE_ID_NOT_FOUND', 'selector', 'Agent instance does not exist.');
|
|
128
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.');
|
package/src/agent-resources.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AIAgentDefinitionProjection,
|
|
3
|
+
AIAgentMessagePrefixItemProjection,
|
|
3
4
|
AIAgentMessageProjection,
|
|
4
5
|
AIAgentMessageRole,
|
|
5
6
|
AIWorkflowEffectPolicyProjection,
|
|
@@ -27,6 +28,8 @@ import { AIWorkflowResourceProjectionError } from './errors.js';
|
|
|
27
28
|
|
|
28
29
|
const AGENT_SUBDOMAINS = Object.freeze([
|
|
29
30
|
'Messages',
|
|
31
|
+
'MessagePrefix',
|
|
32
|
+
'ContextPipeline',
|
|
30
33
|
'InputSchemaRef',
|
|
31
34
|
'OutputSchemaRef',
|
|
32
35
|
'ToolRefs',
|
|
@@ -144,12 +147,44 @@ function projectAgentDefinition(
|
|
|
144
147
|
diagnostics: AIWorkflowResourceProjectionDiagnostic[],
|
|
145
148
|
): AIAgentDefinitionProjection | undefined {
|
|
146
149
|
validateAllowedSubdomains(resource, AGENT_SUBDOMAINS, diagnostics);
|
|
147
|
-
const messagesNode =
|
|
150
|
+
const messagesNode = exactOptionalSubdomain(resource, 'Messages', diagnostics);
|
|
151
|
+
const messagePrefixNode = exactOptionalSubdomain(resource, 'MessagePrefix', diagnostics);
|
|
152
|
+
if (messagesNode && messagePrefixNode) {
|
|
153
|
+
diagnostics.push(diagnostic(
|
|
154
|
+
'AI_AGENT_MESSAGE_PREFIX_CONFLICT',
|
|
155
|
+
resource.documentUri,
|
|
156
|
+
'AIAgentDefinition must declare exactly one of legacy Messages or MessagePrefix.',
|
|
157
|
+
));
|
|
158
|
+
} else if (!messagesNode && !messagePrefixNode) {
|
|
159
|
+
diagnostics.push(diagnostic(
|
|
160
|
+
'AI_AGENT_MESSAGE_PREFIX_MISSING',
|
|
161
|
+
resource.documentUri,
|
|
162
|
+
'AIAgentDefinition must declare legacy Messages or MessagePrefix.',
|
|
163
|
+
));
|
|
164
|
+
}
|
|
148
165
|
const toolRefsNode = requiredSubdomain(resource, 'ToolRefs', diagnostics);
|
|
149
166
|
const materialPortRefsNode = requiredSubdomain(resource, 'MaterialPortRefs', diagnostics);
|
|
167
|
+
const projectedMessagePrefix = messagePrefixNode
|
|
168
|
+
? projectMessagePrefix(resource, messagePrefixNode, resources, edges, diagnostics)
|
|
169
|
+
: undefined;
|
|
150
170
|
const messages = messagesNode
|
|
151
171
|
? projectMessages(resource, messagesNode, resources, edges, diagnostics)
|
|
152
|
-
:
|
|
172
|
+
: projectedMessagePrefix
|
|
173
|
+
? projectedMessagePrefix
|
|
174
|
+
.filter((item): item is Extract<AIAgentMessagePrefixItemProjection, { type: 'message' }> => item.type === 'message')
|
|
175
|
+
.map(({ type: _type, ...message }) => Object.freeze(message))
|
|
176
|
+
: [];
|
|
177
|
+
const messagePrefix = projectedMessagePrefix
|
|
178
|
+
?? messages.map((message) => Object.freeze({ ...message, type: 'message' as const }));
|
|
179
|
+
const contextPipeline = projectOptionalTypedSubdomain(
|
|
180
|
+
resource,
|
|
181
|
+
'ContextPipeline',
|
|
182
|
+
resources,
|
|
183
|
+
'agent-context-pipeline',
|
|
184
|
+
edges,
|
|
185
|
+
diagnostics,
|
|
186
|
+
'AgentContextPipeline',
|
|
187
|
+
);
|
|
153
188
|
const inputSchema = projectOptionalSchemaSubdomain(resource, 'InputSchemaRef', resources, 'agent-input-schema', edges, diagnostics);
|
|
154
189
|
const outputSchema = projectOptionalSchemaSubdomain(resource, 'OutputSchemaRef', resources, 'agent-output-schema', edges, diagnostics);
|
|
155
190
|
const tools = toolRefsNode
|
|
@@ -160,12 +195,14 @@ function projectAgentDefinition(
|
|
|
160
195
|
? projectTypedRefList(resource, materialPortRefsNode, 'MaterialPortRef', resources, 'agent-material-port', edges, diagnostics, 'MaterialPort')
|
|
161
196
|
: [];
|
|
162
197
|
|
|
163
|
-
if (!messagesNode || !toolRefsNode || !materialPortRefsNode) return undefined;
|
|
198
|
+
if ((!messagesNode && !messagePrefixNode) || (messagesNode && messagePrefixNode) || !toolRefsNode || !materialPortRefsNode) return undefined;
|
|
164
199
|
return Object.freeze({
|
|
165
200
|
kind: 'AIAgentDefinition',
|
|
166
201
|
fqn: resource.fqn ?? resource.resourceId,
|
|
167
202
|
resource,
|
|
203
|
+
messagePrefix: Object.freeze(messagePrefix),
|
|
168
204
|
messages: Object.freeze(messages),
|
|
205
|
+
...(contextPipeline === undefined ? {} : { contextPipeline }),
|
|
169
206
|
...(inputSchema === undefined ? {} : { inputSchema }),
|
|
170
207
|
...(outputSchema === undefined ? {} : { outputSchema }),
|
|
171
208
|
tools: Object.freeze(tools),
|
|
@@ -174,6 +211,67 @@ function projectAgentDefinition(
|
|
|
174
211
|
});
|
|
175
212
|
}
|
|
176
213
|
|
|
214
|
+
function exactOptionalSubdomain(
|
|
215
|
+
resource: ResourceRecord,
|
|
216
|
+
name: string,
|
|
217
|
+
diagnostics: AIWorkflowResourceProjectionDiagnostic[],
|
|
218
|
+
): ResourceNode | undefined {
|
|
219
|
+
const value = resource.node.subdomains[name];
|
|
220
|
+
if (!value) return undefined;
|
|
221
|
+
if (value.tag !== name) {
|
|
222
|
+
diagnostics.push(diagnostic(
|
|
223
|
+
'AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH',
|
|
224
|
+
`${resource.documentUri}#${name}`,
|
|
225
|
+
`Subdomain ${name} must contain a ${name} node, got '${value.tag}'.`,
|
|
226
|
+
));
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function projectMessagePrefix(
|
|
233
|
+
resource: ResourceRecord,
|
|
234
|
+
prefixNode: ResourceNode,
|
|
235
|
+
resources: ReadonlyMap<string, ResourceRecord>,
|
|
236
|
+
edges: ResourceDependencyEdge[],
|
|
237
|
+
diagnostics: AIWorkflowResourceProjectionDiagnostic[],
|
|
238
|
+
): AIAgentMessagePrefixItemProjection[] {
|
|
239
|
+
const output: AIAgentMessagePrefixItemProjection[] = [];
|
|
240
|
+
const ids = new Set<string>();
|
|
241
|
+
prefixNode.body.forEach((value, index) => {
|
|
242
|
+
const location = `${resource.documentUri}#MessagePrefix:${index}`;
|
|
243
|
+
if (!isResourceNode(value) || (value.tag !== 'Message' && value.tag !== 'MessageSource')) {
|
|
244
|
+
diagnostics.push(diagnostic(
|
|
245
|
+
'AI_AGENT_MESSAGE_PREFIX_NODE_INVALID',
|
|
246
|
+
location,
|
|
247
|
+
'MessagePrefix may contain only Message or MessageSource nodes.',
|
|
248
|
+
));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const id = canonicalString(value.resourceId);
|
|
252
|
+
if (!id) {
|
|
253
|
+
diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_MISSING', location, `${value.tag} must declare an explicit #id.`));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (ids.has(id)) {
|
|
257
|
+
diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_DUPLICATE', location, `MessagePrefix id '${id}' is declared more than once.`));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
ids.add(id);
|
|
261
|
+
if (value.tag === 'Message') {
|
|
262
|
+
const message = projectMessageNode(resource, value, resources, edges, diagnostics, location);
|
|
263
|
+
if (message) output.push(Object.freeze({ ...message, type: 'message' }));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
validateAllowedNodeSubdomains(value, [], location, diagnostics);
|
|
267
|
+
const source = projectTypedValues(value.properties.kind, value.properties.ref, resources, location, diagnostics, 'AgentMessageSource');
|
|
268
|
+
if (!source) return;
|
|
269
|
+
edges.push(edge(resource.resourceId, source.resource.resourceId, 'agent-message-source'));
|
|
270
|
+
output.push(Object.freeze({ type: 'message-source', id, source }));
|
|
271
|
+
});
|
|
272
|
+
return output;
|
|
273
|
+
}
|
|
274
|
+
|
|
177
275
|
function projectMessages(
|
|
178
276
|
resource: ResourceRecord,
|
|
179
277
|
messagesNode: ResourceNode,
|
|
@@ -189,39 +287,45 @@ function projectMessages(
|
|
|
189
287
|
diagnostics.push(diagnostic('AI_AGENT_MESSAGE_NODE_INVALID', location, 'Messages may contain only Message nodes.'));
|
|
190
288
|
return;
|
|
191
289
|
}
|
|
192
|
-
validateAllowedNodeSubdomains(value, ['SchemaRef'], location, diagnostics);
|
|
193
290
|
const id = canonicalString(value.resourceId);
|
|
194
|
-
|
|
195
|
-
if (!id) {
|
|
196
|
-
diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_MISSING', location, 'Message must declare an explicit #id.'));
|
|
197
|
-
} else if (ids.has(id)) {
|
|
291
|
+
if (id && ids.has(id)) {
|
|
198
292
|
diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_DUPLICATE', location, `Message id '${id}' is declared more than once.`));
|
|
199
|
-
|
|
200
|
-
ids.add(id);
|
|
293
|
+
return;
|
|
201
294
|
}
|
|
202
|
-
if (
|
|
203
|
-
|
|
295
|
+
if (id) {
|
|
296
|
+
ids.add(id);
|
|
204
297
|
}
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
value.properties.promptRef,
|
|
208
|
-
resources,
|
|
209
|
-
location,
|
|
210
|
-
diagnostics,
|
|
211
|
-
);
|
|
212
|
-
if (prompt) edges.push(edge(resource.resourceId, prompt.resource.resourceId, 'agent-message-prompt'));
|
|
213
|
-
const schemaNode = value.subdomains.SchemaRef;
|
|
214
|
-
const schema = schemaNode?.tag === 'SchemaRef'
|
|
215
|
-
? projectMessageSchemaRefNode(schemaNode, resources, `${location}#SchemaRef`, diagnostics)
|
|
216
|
-
: undefined;
|
|
217
|
-
if (schemaNode && schemaNode.tag !== 'SchemaRef') diagnostics.push(diagnostic('AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH', `${location}#SchemaRef`, `Subdomain SchemaRef must contain a SchemaRef node, got '${schemaNode.tag}'.`));
|
|
218
|
-
if (schema) edges.push(edge(resource.resourceId, schema.resource.resourceId, 'agent-message-schema'));
|
|
219
|
-
if (!id || !isMessageRole(role) || !prompt) return;
|
|
220
|
-
output.push(Object.freeze({ id, role, prompt, ...(schema === undefined ? {} : { schema }) }));
|
|
298
|
+
const message = projectMessageNode(resource, value, resources, edges, diagnostics, location);
|
|
299
|
+
if (message) output.push(message);
|
|
221
300
|
});
|
|
222
301
|
return output;
|
|
223
302
|
}
|
|
224
303
|
|
|
304
|
+
function projectMessageNode(
|
|
305
|
+
resource: ResourceRecord,
|
|
306
|
+
value: ResourceNode,
|
|
307
|
+
resources: ReadonlyMap<string, ResourceRecord>,
|
|
308
|
+
edges: ResourceDependencyEdge[],
|
|
309
|
+
diagnostics: AIWorkflowResourceProjectionDiagnostic[],
|
|
310
|
+
location: string,
|
|
311
|
+
): AIAgentMessageProjection | undefined {
|
|
312
|
+
validateAllowedNodeSubdomains(value, ['SchemaRef'], location, diagnostics);
|
|
313
|
+
const id = canonicalString(value.resourceId);
|
|
314
|
+
const role = value.properties.role;
|
|
315
|
+
if (!id) diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_MISSING', location, 'Message must declare an explicit #id.'));
|
|
316
|
+
if (!isMessageRole(role)) diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ROLE_INVALID', location, 'Message role must be system, developer, user, or assistant.'));
|
|
317
|
+
const prompt = projectTypedValues(value.properties.promptKind, value.properties.promptRef, resources, location, diagnostics);
|
|
318
|
+
if (prompt) edges.push(edge(resource.resourceId, prompt.resource.resourceId, 'agent-message-prompt'));
|
|
319
|
+
const schemaNode = value.subdomains.SchemaRef;
|
|
320
|
+
const schema = schemaNode?.tag === 'SchemaRef'
|
|
321
|
+
? projectMessageSchemaRefNode(schemaNode, resources, `${location}#SchemaRef`, diagnostics)
|
|
322
|
+
: undefined;
|
|
323
|
+
if (schemaNode && schemaNode.tag !== 'SchemaRef') diagnostics.push(diagnostic('AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH', `${location}#SchemaRef`, `Subdomain SchemaRef must contain a SchemaRef node, got '${schemaNode.tag}'.`));
|
|
324
|
+
if (schema) edges.push(edge(resource.resourceId, schema.resource.resourceId, 'agent-message-schema'));
|
|
325
|
+
if (!id || !isMessageRole(role) || !prompt) return undefined;
|
|
326
|
+
return Object.freeze({ id, role, prompt, ...(schema === undefined ? {} : { schema }) });
|
|
327
|
+
}
|
|
328
|
+
|
|
225
329
|
function projectTypedRefList(
|
|
226
330
|
owner: ResourceRecord,
|
|
227
331
|
list: ResourceNode,
|
|
@@ -265,6 +369,7 @@ function projectOptionalTypedSubdomain(
|
|
|
265
369
|
relation: AIWorkflowAgentResourceRelation,
|
|
266
370
|
edges: ResourceDependencyEdge[],
|
|
267
371
|
diagnostics: AIWorkflowResourceProjectionDiagnostic[],
|
|
372
|
+
expectedKind?: string,
|
|
268
373
|
): AIWorkflowTypedResourceRef | undefined {
|
|
269
374
|
const node = owner.node.subdomains[name];
|
|
270
375
|
if (!node) return undefined;
|
|
@@ -272,7 +377,7 @@ function projectOptionalTypedSubdomain(
|
|
|
272
377
|
diagnostics.push(diagnostic('AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH', `${owner.documentUri}#${name}`, `Subdomain ${name} must contain a ${name} node, got '${node.tag}'.`));
|
|
273
378
|
return undefined;
|
|
274
379
|
}
|
|
275
|
-
const typedRef = projectTypedRefNode(node, resources, `${owner.documentUri}#${name}`, diagnostics);
|
|
380
|
+
const typedRef = projectTypedRefNode(node, resources, `${owner.documentUri}#${name}`, diagnostics, expectedKind);
|
|
276
381
|
if (typedRef) edges.push(edge(owner.resourceId, typedRef.resource.resourceId, relation));
|
|
277
382
|
return typedRef;
|
|
278
383
|
}
|
package/src/agent-state.ts
CHANGED
|
@@ -115,13 +115,13 @@ function optionalStringArray(value: unknown, location: string): readonly string[
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
export function normalizeAIAgentSelector(value: unknown, location = 'selector'): AIAgentSelector {
|
|
118
|
-
const input = record(value, [], ['
|
|
119
|
-
const hasId = Object.prototype.hasOwnProperty.call(input, '
|
|
120
|
-
const hasName = Object.prototype.hasOwnProperty.call(input, '
|
|
118
|
+
const input = record(value, [], ['byId', 'byName'], location);
|
|
119
|
+
const hasId = Object.prototype.hasOwnProperty.call(input, 'byId');
|
|
120
|
+
const hasName = Object.prototype.hasOwnProperty.call(input, 'byName');
|
|
121
121
|
if (hasId === hasName) fail('AI_AGENT_SELECTOR_EXACTLY_ONE', location, 'Selector must contain exactly one supported key.');
|
|
122
122
|
return Object.freeze(hasId
|
|
123
|
-
? {
|
|
124
|
-
: {
|
|
123
|
+
? { byId: identity(input.byId, `${location}.byId`) }
|
|
124
|
+
: { byName: identity(input.byName, `${location}.byName`) });
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
export function normalizeAIAgentEffectConfig(value: unknown, location = 'config'): AIAgentEffectConfig {
|
package/src/checkpoint-store.ts
CHANGED
|
@@ -399,7 +399,9 @@ function validateAIDataProfileTransition(
|
|
|
399
399
|
throw new FlowRunCheckpointTransitionError(`AI Data patch operation ${operation.op} is unsupported.`);
|
|
400
400
|
}
|
|
401
401
|
for (const [field, expected] of Object.entries(operation.changes)) {
|
|
402
|
-
|
|
402
|
+
const actual = (candidate as Readonly<Record<string, unknown>>)[field];
|
|
403
|
+
if (field === 'subflow' && expected === null && actual === undefined) continue;
|
|
404
|
+
if (!sameClosedValue(actual, expected)) {
|
|
403
405
|
throw new FlowRunCheckpointTransitionError(`AI Data update-node ${nodeId} does not project declared ${field}.`);
|
|
404
406
|
}
|
|
405
407
|
}
|
package/src/filesystem.ts
CHANGED
|
@@ -4,12 +4,32 @@ import {
|
|
|
4
4
|
} from 'halfcode-compiler.xnl/resource-core';
|
|
5
5
|
import type { LoadedAIWorkflowResourcePackage } from 'ai-workflow-contract';
|
|
6
6
|
import { projectAIWorkflowAppBundles } from './index.js';
|
|
7
|
+
import {
|
|
8
|
+
resolveAIWorkflowResourceTree,
|
|
9
|
+
type AIWorkflowResourceResolutionContext,
|
|
10
|
+
} from './resource-resolution.js';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
AI_WORKFLOW_RESOURCE_SCHEMA_VALIDATOR,
|
|
14
|
+
DEPA_AI_RESOURCE_READER_PROFILE_ID,
|
|
15
|
+
createAIWorkflowResourceResolutionContext,
|
|
16
|
+
createResolvedAIWorkflowContentIdentityIndex,
|
|
17
|
+
resolveAIWorkflowResourceTree,
|
|
18
|
+
} from './resource-resolution.js';
|
|
19
|
+
export type { AIWorkflowResourceResolutionContext } from './resource-resolution.js';
|
|
20
|
+
|
|
21
|
+
export interface LoadAIWorkflowResourcePackageOptions extends LoadResourceTreeOptions {
|
|
22
|
+
readonly resolutionContext?: AIWorkflowResourceResolutionContext;
|
|
23
|
+
}
|
|
7
24
|
|
|
8
25
|
export async function loadAIWorkflowResourcePackage(
|
|
9
|
-
options:
|
|
26
|
+
options: LoadAIWorkflowResourcePackageOptions,
|
|
10
27
|
): Promise<LoadedAIWorkflowResourcePackage> {
|
|
11
|
-
const
|
|
28
|
+
const { resolutionContext, ...loadOptions } = options;
|
|
29
|
+
const authoredTree = await loadResourceTree(loadOptions);
|
|
30
|
+
const tree = resolveAIWorkflowResourceTree(authoredTree, resolutionContext);
|
|
12
31
|
return Object.freeze({
|
|
32
|
+
authoredTree,
|
|
13
33
|
tree,
|
|
14
34
|
appBundles: projectAIWorkflowAppBundles(tree.registry, {
|
|
15
35
|
packageResourceId: tree.manifest.resourceId,
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,21 @@ import { AIWorkflowFoundationError, AIWorkflowResourceProjectionError } from './
|
|
|
36
36
|
|
|
37
37
|
export { AIWorkflowFoundationError, AIWorkflowResourceProjectionError } from './errors.js';
|
|
38
38
|
export { projectAIWorkflowAgentResources } from './agent-resources.js';
|
|
39
|
+
export {
|
|
40
|
+
AIAgentDefinitionSelectionError,
|
|
41
|
+
admitAIAgentDefinitionSelection,
|
|
42
|
+
freezeSelectedAIAgentTaskBinding,
|
|
43
|
+
normalizeAIAgentTaskRequirement,
|
|
44
|
+
projectAIAgentDefinitionCandidates,
|
|
45
|
+
reconcileAuthoredAIAgentDefinition,
|
|
46
|
+
} from './agent-definition-selection.js';
|
|
47
|
+
export type {
|
|
48
|
+
AIAgentDefinitionSelectionErrorCode,
|
|
49
|
+
AdmitAIAgentDefinitionSelectionInput,
|
|
50
|
+
FreezeSelectedAIAgentTaskBindingInput,
|
|
51
|
+
ProjectAIAgentDefinitionCandidatesInput,
|
|
52
|
+
ReconcileAuthoredAIAgentDefinitionInput,
|
|
53
|
+
} from './agent-definition-selection.js';
|
|
39
54
|
export {
|
|
40
55
|
FlowRunContractValidationError,
|
|
41
56
|
normalizeAIWorkflowProfileDurableState,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import Ajv, { type ValidateFunction } from 'ajv';
|
|
2
|
+
import {
|
|
3
|
+
DEPA_AI_RESOURCE_KIND_OWNERS,
|
|
4
|
+
DEPA_AI_RESOURCE_KIND_READER_REGISTRATIONS,
|
|
5
|
+
DEPA_AI_RESOURCE_KIND_SPEC_REVISIONS,
|
|
6
|
+
} from 'ai-workflow-contract';
|
|
7
|
+
import type {
|
|
8
|
+
AuthoredResourceTree,
|
|
9
|
+
JsonSchemaValidator,
|
|
10
|
+
KindSpecRevision,
|
|
11
|
+
PortableSpec,
|
|
12
|
+
ResolvedResourceTree,
|
|
13
|
+
ResourceContentIdentity,
|
|
14
|
+
} from 'halfcode-compiler.xnl/resource-core';
|
|
15
|
+
import { createResourceContentIdentity } from 'halfcode-compiler.xnl/resource-core';
|
|
16
|
+
import {
|
|
17
|
+
CORE_KIND_READER_REGISTRATIONS,
|
|
18
|
+
CORE_KIND_SPEC_REVISIONS,
|
|
19
|
+
CORE_KIND_SUBJECT_OWNERS,
|
|
20
|
+
KindContractRegistry,
|
|
21
|
+
KindReaderRegistry,
|
|
22
|
+
SpecResolutionRegistry,
|
|
23
|
+
createReaderProfile,
|
|
24
|
+
resolveResourceTree,
|
|
25
|
+
type ReaderProfile,
|
|
26
|
+
} from 'halfcode-compiler.xnl/kind-definition';
|
|
27
|
+
|
|
28
|
+
export const DEPA_AI_RESOURCE_READER_PROFILE_ID = 'depa.flows.ai-resources.v1' as const;
|
|
29
|
+
|
|
30
|
+
export interface AIWorkflowResourceResolutionContext {
|
|
31
|
+
readonly contracts: KindContractRegistry;
|
|
32
|
+
readonly readers: KindReaderRegistry;
|
|
33
|
+
readonly readerProfile: ReaderProfile;
|
|
34
|
+
readonly resolutions: SpecResolutionRegistry;
|
|
35
|
+
readonly validator: JsonSchemaValidator;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ajv = new Ajv({ allErrors: true, strict: false, validateFormats: false });
|
|
39
|
+
const compiledSchemas = new Map<string, ValidateFunction>();
|
|
40
|
+
|
|
41
|
+
export const AI_WORKFLOW_RESOURCE_SCHEMA_VALIDATOR: JsonSchemaValidator = Object.freeze({
|
|
42
|
+
validate(revision: KindSpecRevision, value: PortableSpec): readonly string[] {
|
|
43
|
+
let validate = compiledSchemas.get(revision.contractFingerprint);
|
|
44
|
+
if (!validate) {
|
|
45
|
+
validate = ajv.compile(revision.specSchema);
|
|
46
|
+
compiledSchemas.set(revision.contractFingerprint, validate);
|
|
47
|
+
}
|
|
48
|
+
return validate(value)
|
|
49
|
+
? Object.freeze([])
|
|
50
|
+
: Object.freeze((validate.errors ?? []).map((error) =>
|
|
51
|
+
`${error.instancePath || '/'} ${error.message ?? 'invalid'}`));
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export function createAIWorkflowResourceResolutionContext(
|
|
56
|
+
validator: JsonSchemaValidator = AI_WORKFLOW_RESOURCE_SCHEMA_VALIDATOR,
|
|
57
|
+
): AIWorkflowResourceResolutionContext {
|
|
58
|
+
const contracts = new KindContractRegistry();
|
|
59
|
+
for (const owner of [...CORE_KIND_SUBJECT_OWNERS, ...DEPA_AI_RESOURCE_KIND_OWNERS]) {
|
|
60
|
+
contracts.registerOwner(owner);
|
|
61
|
+
}
|
|
62
|
+
for (const revision of [...CORE_KIND_SPEC_REVISIONS, ...DEPA_AI_RESOURCE_KIND_SPEC_REVISIONS]) {
|
|
63
|
+
contracts.registerRevision(revision);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const readerRegistrations = [
|
|
67
|
+
...CORE_KIND_READER_REGISTRATIONS,
|
|
68
|
+
...DEPA_AI_RESOURCE_KIND_READER_REGISTRATIONS,
|
|
69
|
+
];
|
|
70
|
+
const readers = new KindReaderRegistry();
|
|
71
|
+
for (const reader of readerRegistrations) readers.register(reader);
|
|
72
|
+
const readerProfile = createReaderProfile(
|
|
73
|
+
DEPA_AI_RESOURCE_READER_PROFILE_ID,
|
|
74
|
+
readerRegistrations.map((reader) => ({
|
|
75
|
+
readerId: reader.readerId,
|
|
76
|
+
subjectFqn: reader.subjectFqn,
|
|
77
|
+
readerSpecVersion: reader.readerSpecVersion,
|
|
78
|
+
contractFingerprint: reader.contractFingerprint,
|
|
79
|
+
readerImplementationFingerprint: reader.readerImplementationFingerprint,
|
|
80
|
+
})),
|
|
81
|
+
);
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
contracts,
|
|
84
|
+
readers,
|
|
85
|
+
readerProfile,
|
|
86
|
+
resolutions: new SpecResolutionRegistry(contracts),
|
|
87
|
+
validator,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resolveAIWorkflowResourceTree(
|
|
92
|
+
tree: AuthoredResourceTree,
|
|
93
|
+
context: AIWorkflowResourceResolutionContext = createAIWorkflowResourceResolutionContext(),
|
|
94
|
+
): ResolvedResourceTree {
|
|
95
|
+
return resolveResourceTree({ tree, ...context });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Projects Halfcode's exact resolved content identity into the public dependency
|
|
100
|
+
* snapshot shape. The digest is still constructed by Halfcode; depa-flows only
|
|
101
|
+
* chooses the resolved receipt identity as the authority input.
|
|
102
|
+
*/
|
|
103
|
+
export function createResolvedAIWorkflowContentIdentityIndex(
|
|
104
|
+
tree: ResolvedResourceTree,
|
|
105
|
+
): ReadonlyMap<string, ResourceContentIdentity> {
|
|
106
|
+
const entries: Array<readonly [string, ResourceContentIdentity]> = [];
|
|
107
|
+
for (const records of tree.registry.byKind.values()) {
|
|
108
|
+
for (const record of records) {
|
|
109
|
+
entries.push([record.resourceId, createResourceContentIdentity({
|
|
110
|
+
resourceId: record.resourceId,
|
|
111
|
+
authorityDigest: record.effectiveContentDigest,
|
|
112
|
+
})]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
entries.sort(([left], [right]) => compareCodeUnits(left, right));
|
|
116
|
+
return new Map(entries);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function compareCodeUnits(left: string, right: string): number {
|
|
120
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
121
|
+
}
|