@dotdrelle/wiki-manager 0.11.10 → 0.12.1
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/README.md +32 -7
- package/agents.docker-compose.yml +3 -0
- package/docker-compose.yml +1 -0
- package/package.json +3 -2
- package/src/activity/activityAggregator.js +109 -0
- package/src/activity/activityAggregator.test.js +60 -0
- package/src/activity/activityDeduplicator.js +50 -0
- package/src/activity/progressCalculator.js +61 -0
- package/src/activity/runSynthesis.js +15 -0
- package/src/agent/graph.js +101 -38
- package/src/agent/graph.test.js +75 -5
- package/src/cli/wiki-manager.js +22 -0
- package/src/contracts/schemas.js +249 -13
- package/src/contracts/schemas.test.js +147 -2
- package/src/core/activity.js +8 -5
- package/src/core/agentEvents.js +260 -23
- package/src/core/agentEvents.test.js +53 -0
- package/src/core/compose.js +6 -1
- package/src/core/dockerCompose.test.js +12 -0
- package/src/core/documentIntake.js +33 -38
- package/src/core/documentIntake.test.js +25 -0
- package/src/core/env.js +11 -1
- package/src/core/jobQueue.js +26 -2
- package/src/core/mcp.js +30 -3
- package/src/core/mcp.test.js +69 -1
- package/src/core/plan.js +46 -4
- package/src/core/plan.test.js +16 -1
- package/src/core/planPatch.js +64 -3
- package/src/core/planPatch.test.js +110 -1
- package/src/core/queueStore.test.js +21 -0
- package/src/core/runtimeLog.js +119 -0
- package/src/core/runtimeLog.test.js +84 -0
- package/src/core/wikirc.js +22 -0
- package/src/core/wikirc.test.js +49 -1
- package/src/core/workflow.js +14 -3
- package/src/graph/graphAggregator.js +5 -0
- package/src/graph/graphPatch.js +8 -0
- package/src/graph/graphSnapshot.js +14 -0
- package/src/graph/graphVisibilityPolicy.js +40 -0
- package/src/graph/runGraphProjector.js +87 -0
- package/src/graph/runGraphProjector.test.js +111 -0
- package/src/orchestrator/agentRegistry.js +172 -0
- package/src/orchestrator/agentRegistry.test.js +114 -0
- package/src/orchestrator/approvalPolicy.js +126 -0
- package/src/orchestrator/approvalPolicy.test.js +78 -0
- package/src/orchestrator/assignmentManager.js +80 -0
- package/src/orchestrator/attemptManager.js +168 -0
- package/src/orchestrator/attemptManager.test.js +160 -0
- package/src/orchestrator/budgetManager.js +127 -0
- package/src/orchestrator/capabilityRegistry.js +64 -0
- package/src/orchestrator/capabilityRegistry.test.js +59 -0
- package/src/orchestrator/capabilityResolver.js +151 -0
- package/src/orchestrator/capabilityResolver.test.js +127 -0
- package/src/orchestrator/dependencyResolver.js +112 -0
- package/src/orchestrator/dispatcher.js +302 -0
- package/src/orchestrator/lockManager.js +51 -0
- package/src/orchestrator/planIntegrator.js +268 -0
- package/src/orchestrator/planIntegrator.test.js +213 -0
- package/src/orchestrator/planValidator.js +534 -0
- package/src/orchestrator/planValidator.test.js +262 -0
- package/src/orchestrator/resultAggregator.js +248 -0
- package/src/orchestrator/resultAggregator.test.js +211 -0
- package/src/orchestrator/scheduler.js +103 -0
- package/src/orchestrator/scheduler.test.js +138 -0
- package/src/runtime/approvals.js +130 -1
- package/src/runtime/controlMessages.js +43 -0
- package/src/runtime/controlMessages.test.js +21 -0
- package/src/runtime/donna-contract.test.js +296 -33
- package/src/runtime/recoveryManager.js +176 -0
- package/src/runtime/recoveryManager.test.js +162 -0
- package/src/runtime/runner.e2e.test.js +98 -12
- package/src/runtime/runner.js +261 -227
- package/src/runtime/runner.test.js +249 -452
- package/src/runtime/server.js +142 -24
- package/src/runtime/server.test.js +192 -62
- package/src/runtime/store.js +837 -2
- package/src/runtime/store.test.js +269 -28
- package/src/runtime/supervisor.js +33 -1
- package/src/runtime/supervisor.test.js +47 -1
- package/src/shell/RightPane.tsx +8 -5
- package/src/shell/repl.js +7 -11
- package/src/shell/repl.test.js +26 -5
- package/src/shell/tui.tsx +1 -0
- package/src/shell/useAgent.ts +5 -1
- package/src/shell/useSession.ts +75 -19
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { validateContract } from '../contracts/schemas.js';
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_CONTRACT_VERSIONS = new Set(['1']);
|
|
4
|
+
const MUTATING_OPERATIONS = new Set([
|
|
5
|
+
'copy',
|
|
6
|
+
'ingest',
|
|
7
|
+
'ingest_plan',
|
|
8
|
+
'ingest_apply',
|
|
9
|
+
'build',
|
|
10
|
+
'export',
|
|
11
|
+
'polish',
|
|
12
|
+
'pipeline',
|
|
13
|
+
'publish',
|
|
14
|
+
'update',
|
|
15
|
+
'delete',
|
|
16
|
+
'write',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const VALIDATED_FRAGMENT = Symbol('validatedTaskGraphFragment');
|
|
20
|
+
|
|
21
|
+
export class PlanValidationError extends Error {
|
|
22
|
+
constructor(errors) {
|
|
23
|
+
super(`Plan validation failed: ${errors.map((error) => error.code).join(', ')}`);
|
|
24
|
+
this.name = 'PlanValidationError';
|
|
25
|
+
this.errors = errors;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validateFragment(fragment, { registry, run = {}, budgets = {} } = {}) {
|
|
30
|
+
const normalizedFragment = normalizeFragment(fragment);
|
|
31
|
+
const controls = [
|
|
32
|
+
() => validateShape(normalizedFragment),
|
|
33
|
+
() => validateContractVersion(normalizedFragment),
|
|
34
|
+
() => validateAgentScope(normalizedFragment, run),
|
|
35
|
+
() => validateSummary(normalizedFragment),
|
|
36
|
+
() => validateTaskIds(normalizedFragment),
|
|
37
|
+
() => validateGroups(normalizedFragment),
|
|
38
|
+
() => validateDependencyReferences(normalizedFragment),
|
|
39
|
+
() => validateDependencyAcyclic(normalizedFragment),
|
|
40
|
+
() => validateConcreteInputRefs(normalizedFragment),
|
|
41
|
+
() => validateCapabilities(normalizedFragment, registry),
|
|
42
|
+
() => validateIdempotency(normalizedFragment, registry),
|
|
43
|
+
() => validateLocks(normalizedFragment, registry),
|
|
44
|
+
() => validateBudgets(normalizedFragment, budgets),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const control of controls) {
|
|
48
|
+
const error = control();
|
|
49
|
+
if (error) return { ok: false, errors: [error], normalizedFragment: null };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
markValidated(normalizedFragment);
|
|
53
|
+
return { ok: true, errors: [], normalizedFragment };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function assertValidatedFragment(fragment) {
|
|
57
|
+
if (!isValidatedFragment(fragment)) {
|
|
58
|
+
throw new PlanValidationError([issue('fragment_not_validated', 'TaskGraphFragment must pass validateFragment before scheduler entry.')]);
|
|
59
|
+
}
|
|
60
|
+
return fragment;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isValidatedFragment(fragment) {
|
|
64
|
+
return Boolean(fragment?.[VALIDATED_FRAGMENT]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeFragment(fragment) {
|
|
68
|
+
const item = cloneJson(fragment) ?? {};
|
|
69
|
+
const tasks = Array.isArray(item.tasks) ? item.tasks.map(normalizeTask) : [];
|
|
70
|
+
const groups = Array.isArray(item.groups) ? item.groups.map(normalizeGroup) : [];
|
|
71
|
+
const idMap = new Map();
|
|
72
|
+
|
|
73
|
+
for (const task of tasks) {
|
|
74
|
+
const original = String(task.id ?? '');
|
|
75
|
+
const slug = slugify(original);
|
|
76
|
+
task.id = slug;
|
|
77
|
+
if (original) idMap.set(original, slug);
|
|
78
|
+
}
|
|
79
|
+
for (const group of groups) {
|
|
80
|
+
const original = String(group.id ?? '');
|
|
81
|
+
const slug = slugify(original);
|
|
82
|
+
group.id = slug;
|
|
83
|
+
if (original) idMap.set(original, slug);
|
|
84
|
+
}
|
|
85
|
+
for (const task of tasks) {
|
|
86
|
+
task.dependsOn = task.dependsOn.map((dep) => idMap.get(String(dep)) ?? slugify(dep));
|
|
87
|
+
if (task.groupId != null) task.groupId = idMap.get(String(task.groupId)) ?? slugify(task.groupId);
|
|
88
|
+
if (task.dependsOnGroup != null) task.dependsOnGroup = idMap.get(String(task.dependsOnGroup)) ?? slugify(task.dependsOnGroup);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const summary = item.summary && typeof item.summary === 'object' && !Array.isArray(item.summary)
|
|
92
|
+
? { ...item.summary }
|
|
93
|
+
: {};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
...item,
|
|
97
|
+
contractVersion: String(item.contractVersion ?? '1'),
|
|
98
|
+
agentInstanceId: String(item.agentInstanceId ?? ''),
|
|
99
|
+
capability: String(item.capability ?? ''),
|
|
100
|
+
summary: {
|
|
101
|
+
label: String(summary.label ?? item.capability ?? 'Task graph fragment'),
|
|
102
|
+
initialSynthesis: Array.isArray(summary.initialSynthesis) ? summary.initialSynthesis.map(String) : [],
|
|
103
|
+
estimatedTasks: Number.isInteger(summary.estimatedTasks) && summary.estimatedTasks >= 0 ? summary.estimatedTasks : tasks.length,
|
|
104
|
+
},
|
|
105
|
+
groups,
|
|
106
|
+
tasks,
|
|
107
|
+
expectedOutputs: normalizeRefs(item.expectedOutputs),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeGroup(raw) {
|
|
112
|
+
const item = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
113
|
+
const group = {
|
|
114
|
+
...item,
|
|
115
|
+
id: String(item.id ?? ''),
|
|
116
|
+
label: String(item.label ?? item.id ?? 'Task group'),
|
|
117
|
+
};
|
|
118
|
+
const recommendedConcurrency = normalizePositiveInteger(item.recommendedConcurrency);
|
|
119
|
+
const progressWeight = normalizeNonNegativeNumber(item.progressWeight);
|
|
120
|
+
if (recommendedConcurrency != null) group.recommendedConcurrency = recommendedConcurrency;
|
|
121
|
+
else delete group.recommendedConcurrency;
|
|
122
|
+
if (progressWeight != null) group.progressWeight = progressWeight;
|
|
123
|
+
else delete group.progressWeight;
|
|
124
|
+
return group;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeTask(raw) {
|
|
128
|
+
const item = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
129
|
+
const task = {
|
|
130
|
+
...item,
|
|
131
|
+
id: String(item.id ?? ''),
|
|
132
|
+
label: String(item.label ?? item.id ?? 'Task'),
|
|
133
|
+
requiredCapability: String(item.requiredCapability ?? ''),
|
|
134
|
+
operation: String(item.operation ?? ''),
|
|
135
|
+
arguments: item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments) ? { ...item.arguments } : {},
|
|
136
|
+
dependsOn: Array.isArray(item.dependsOn) ? item.dependsOn.map(String) : [],
|
|
137
|
+
parallelizable: item.parallelizable === true,
|
|
138
|
+
inputRefs: normalizeRefs(item.inputRefs),
|
|
139
|
+
expectedOutputRefs: normalizeRefs(item.expectedOutputRefs),
|
|
140
|
+
locks: Array.isArray(item.locks) ? item.locks.map(String).filter(Boolean) : [],
|
|
141
|
+
requiresApproval: item.requiresApproval === true,
|
|
142
|
+
idempotencyKey: item.idempotencyKey == null ? null : String(item.idempotencyKey),
|
|
143
|
+
progressWeight: normalizeNonNegativeNumber(item.progressWeight) ?? 1,
|
|
144
|
+
};
|
|
145
|
+
const optionalInteger = normalizePositiveInteger(item.recommendedConcurrency);
|
|
146
|
+
const priority = normalizeFiniteNumber(item.priority);
|
|
147
|
+
if (item.groupId != null) task.groupId = String(item.groupId);
|
|
148
|
+
else delete task.groupId;
|
|
149
|
+
if (item.dependsOnGroup != null) task.dependsOnGroup = String(item.dependsOnGroup);
|
|
150
|
+
else delete task.dependsOnGroup;
|
|
151
|
+
if (item.barrier === true) task.barrier = true;
|
|
152
|
+
else delete task.barrier;
|
|
153
|
+
if (optionalInteger != null) task.recommendedConcurrency = optionalInteger;
|
|
154
|
+
else delete task.recommendedConcurrency;
|
|
155
|
+
if (item.approvalClass != null) task.approvalClass = String(item.approvalClass);
|
|
156
|
+
else delete task.approvalClass;
|
|
157
|
+
if (item.approvalSummary != null) task.approvalSummary = String(item.approvalSummary);
|
|
158
|
+
else delete task.approvalSummary;
|
|
159
|
+
if (priority != null) task.priority = priority;
|
|
160
|
+
else delete task.priority;
|
|
161
|
+
if (item.retryPolicy != null) task.retryPolicy = normalizeRetryPolicy(item.retryPolicy);
|
|
162
|
+
else delete task.retryPolicy;
|
|
163
|
+
return task;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validateShape(fragment) {
|
|
167
|
+
const result = validateContract('taskGraphFragment', fragment);
|
|
168
|
+
return result.ok ? null : issue('invalid_contract_shape', 'TaskGraphFragment does not match the contract schema.', { errors: result.errors });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function validateContractVersion(fragment) {
|
|
172
|
+
return SUPPORTED_CONTRACT_VERSIONS.has(fragment.contractVersion)
|
|
173
|
+
? null
|
|
174
|
+
: issue('unsupported_contract_version', `Unsupported TaskGraphFragment contractVersion: ${fragment.contractVersion}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function validateAgentScope(fragment, run) {
|
|
178
|
+
const expected = run?.agentInstanceId ?? run?.plannerAgentInstanceId ?? null;
|
|
179
|
+
if (!expected || fragment.agentInstanceId === String(expected)) return null;
|
|
180
|
+
return issue('agent_instance_mismatch', 'Fragment agentInstanceId does not match the run planner agent.', {
|
|
181
|
+
expected: String(expected),
|
|
182
|
+
actual: fragment.agentInstanceId,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validateSummary(fragment) {
|
|
187
|
+
return fragment.summary.estimatedTasks === fragment.tasks.length
|
|
188
|
+
? null
|
|
189
|
+
: issue('estimated_tasks_mismatch', 'summary.estimatedTasks must match tasks.length.', {
|
|
190
|
+
estimatedTasks: fragment.summary.estimatedTasks,
|
|
191
|
+
actualTasks: fragment.tasks.length,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function validateTaskIds(fragment) {
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
for (const task of fragment.tasks) {
|
|
198
|
+
if (!task.id) return issue('empty_task_id', 'Task id is empty after normalization.');
|
|
199
|
+
if (seen.has(task.id)) return issue('duplicate_task_id', `Duplicate task id after normalization: ${task.id}`, { taskId: task.id });
|
|
200
|
+
seen.add(task.id);
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function validateGroups(fragment) {
|
|
206
|
+
const groupIds = new Set();
|
|
207
|
+
for (const group of fragment.groups) {
|
|
208
|
+
if (!group.id) return issue('empty_group_id', 'Task group id is empty after normalization.');
|
|
209
|
+
if (groupIds.has(group.id)) return issue('duplicate_group_id', `Duplicate group id after normalization: ${group.id}`, { groupId: group.id });
|
|
210
|
+
groupIds.add(group.id);
|
|
211
|
+
}
|
|
212
|
+
for (const task of fragment.tasks) {
|
|
213
|
+
if (task.groupId && !groupIds.has(task.groupId)) {
|
|
214
|
+
return issue('unknown_group_id', `Task references unknown groupId: ${task.groupId}`, { taskId: task.id, groupId: task.groupId });
|
|
215
|
+
}
|
|
216
|
+
if (task.dependsOnGroup && !groupIds.has(task.dependsOnGroup)) {
|
|
217
|
+
return issue('unknown_depends_on_group', `Task references unknown dependsOnGroup: ${task.dependsOnGroup}`, {
|
|
218
|
+
taskId: task.id,
|
|
219
|
+
groupId: task.dependsOnGroup,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function validateDependencyReferences(fragment) {
|
|
227
|
+
const taskIds = new Set(fragment.tasks.map((task) => task.id));
|
|
228
|
+
for (const task of fragment.tasks) {
|
|
229
|
+
for (const dep of task.dependsOn) {
|
|
230
|
+
if (dep === task.id) return issue('self_dependency', `Task depends on itself: ${task.id}`, { taskId: task.id });
|
|
231
|
+
if (!taskIds.has(dep)) return issue('unknown_dependency', `Task references unknown dependency: ${dep}`, { taskId: task.id, dependencyId: dep });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function validateDependencyAcyclic(fragment) {
|
|
238
|
+
const result = topologicalSort(fragment.tasks);
|
|
239
|
+
return result.cycle
|
|
240
|
+
? issue('dependency_cycle', 'Task dependency cycle detected.', { remainingTaskIds: result.remainingTaskIds })
|
|
241
|
+
: null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function validateConcreteInputRefs(fragment) {
|
|
245
|
+
for (const task of fragment.tasks) {
|
|
246
|
+
for (const ref of task.inputRefs) {
|
|
247
|
+
const value = typeof ref === 'string' ? ref : ref?.ref;
|
|
248
|
+
if (!value || /[*?[\]]/.test(String(value))) {
|
|
249
|
+
return issue('non_concrete_input_ref', `Task inputRefs must be concrete: ${task.id}`, {
|
|
250
|
+
taskId: task.id,
|
|
251
|
+
ref: value ?? null,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function validateCapabilities(fragment, registry) {
|
|
260
|
+
if (!registry || typeof registry.providersFor !== 'function') {
|
|
261
|
+
return issue('missing_capability_registry', 'Plan validation requires a capability registry.');
|
|
262
|
+
}
|
|
263
|
+
for (const task of fragment.tasks) {
|
|
264
|
+
const providers = healthyProviders(registry.providersFor(task.requiredCapability) ?? [], registry);
|
|
265
|
+
if (providers.length === 0) {
|
|
266
|
+
return issue('capability_unavailable', `No healthy provider for capability: ${task.requiredCapability}`, {
|
|
267
|
+
taskId: task.id,
|
|
268
|
+
capability: task.requiredCapability,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
const operationProviders = providers.filter((provider) => supportedOperations(provider).includes(task.operation));
|
|
272
|
+
if (operationProviders.length === 0) {
|
|
273
|
+
return issue('operation_unsupported', `No provider supports operation ${task.operation} for capability ${task.requiredCapability}.`, {
|
|
274
|
+
taskId: task.id,
|
|
275
|
+
capability: task.requiredCapability,
|
|
276
|
+
operation: task.operation,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const argumentErrors = operationProviders
|
|
280
|
+
.map((provider) => validateJsonSchema(provider.capability?.inputSchema ?? {}, task.arguments, `tasks.${task.id}.arguments`))
|
|
281
|
+
.filter((errors) => errors.length === 0);
|
|
282
|
+
if (argumentErrors.length === 0) {
|
|
283
|
+
return issue('invalid_arguments', `Task arguments do not satisfy provider inputSchema: ${task.id}`, {
|
|
284
|
+
taskId: task.id,
|
|
285
|
+
capability: task.requiredCapability,
|
|
286
|
+
operation: task.operation,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function validateIdempotency(fragment, registry) {
|
|
294
|
+
for (const task of fragment.tasks) {
|
|
295
|
+
if (isMutatingTask(task, registry) && !task.idempotencyKey) {
|
|
296
|
+
return issue('missing_idempotency_key', `Mutating task requires idempotencyKey: ${task.id}`, { taskId: task.id });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function validateLocks(fragment, registry) {
|
|
303
|
+
for (const task of fragment.tasks) {
|
|
304
|
+
if (isMutatingTask(task, registry) && task.locks.length === 0) {
|
|
305
|
+
return issue('missing_locks', `Mutating task requires at least one lock: ${task.id}`, { taskId: task.id });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function validateBudgets(fragment, budgets) {
|
|
312
|
+
const maxTasks = normalizePositiveInteger(budgets?.maxTasks);
|
|
313
|
+
if (maxTasks != null && fragment.tasks.length > maxTasks) {
|
|
314
|
+
return issue('budget_max_tasks_exceeded', 'Task count exceeds budget.maxTasks.', { maxTasks, actualTasks: fragment.tasks.length });
|
|
315
|
+
}
|
|
316
|
+
const maxConcurrency = normalizePositiveInteger(budgets?.maxConcurrency);
|
|
317
|
+
if (maxConcurrency != null) {
|
|
318
|
+
const tooHigh = [
|
|
319
|
+
...fragment.groups.filter((group) => Number(group.recommendedConcurrency ?? 1) > maxConcurrency),
|
|
320
|
+
...fragment.tasks.filter((task) => Number(task.recommendedConcurrency ?? 1) > maxConcurrency),
|
|
321
|
+
][0];
|
|
322
|
+
if (tooHigh) {
|
|
323
|
+
return issue('budget_max_concurrency_exceeded', 'Recommended concurrency exceeds budget.maxConcurrency.', {
|
|
324
|
+
maxConcurrency,
|
|
325
|
+
id: tooHigh.id,
|
|
326
|
+
recommendedConcurrency: tooHigh.recommendedConcurrency,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const maxDepth = normalizePositiveInteger(budgets?.maxDepth);
|
|
331
|
+
const depth = graphDepth(fragment.tasks);
|
|
332
|
+
if (maxDepth != null && depth > maxDepth) {
|
|
333
|
+
return issue('budget_max_depth_exceeded', 'DAG depth exceeds budget.maxDepth.', { maxDepth, depth });
|
|
334
|
+
}
|
|
335
|
+
const maxProgressWeight = normalizeNonNegativeNumber(budgets?.maxProgressWeight);
|
|
336
|
+
const progressWeight = fragment.tasks.reduce((sum, task) => sum + Number(task.progressWeight ?? 0), 0);
|
|
337
|
+
if (maxProgressWeight != null && progressWeight > maxProgressWeight) {
|
|
338
|
+
return issue('budget_progress_weight_exceeded', 'Progress weight exceeds budget.maxProgressWeight.', {
|
|
339
|
+
maxProgressWeight,
|
|
340
|
+
progressWeight,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function topologicalSort(tasks) {
|
|
347
|
+
const ids = new Set(tasks.map((task) => task.id));
|
|
348
|
+
const indegree = new Map(tasks.map((task) => [task.id, 0]));
|
|
349
|
+
const outgoing = new Map(tasks.map((task) => [task.id, []]));
|
|
350
|
+
for (const task of tasks) {
|
|
351
|
+
for (const dep of task.dependsOn) {
|
|
352
|
+
if (!ids.has(dep)) continue;
|
|
353
|
+
indegree.set(task.id, (indegree.get(task.id) ?? 0) + 1);
|
|
354
|
+
outgoing.get(dep)?.push(task.id);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const queue = [...indegree.entries()].filter(([, count]) => count === 0).map(([id]) => id);
|
|
358
|
+
const ordered = [];
|
|
359
|
+
while (queue.length > 0) {
|
|
360
|
+
const id = queue.shift();
|
|
361
|
+
ordered.push(id);
|
|
362
|
+
for (const next of outgoing.get(id) ?? []) {
|
|
363
|
+
const count = (indegree.get(next) ?? 0) - 1;
|
|
364
|
+
indegree.set(next, count);
|
|
365
|
+
if (count === 0) queue.push(next);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (ordered.length === tasks.length) return { ordered, cycle: false, remainingTaskIds: [] };
|
|
369
|
+
return {
|
|
370
|
+
ordered,
|
|
371
|
+
cycle: true,
|
|
372
|
+
remainingTaskIds: [...indegree.entries()].filter(([, count]) => count > 0).map(([id]) => id),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function graphDepth(tasks) {
|
|
377
|
+
const order = topologicalSort(tasks);
|
|
378
|
+
if (order.cycle) return Infinity;
|
|
379
|
+
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
380
|
+
const depth = new Map(tasks.map((task) => [task.id, 1]));
|
|
381
|
+
for (const id of order.ordered) {
|
|
382
|
+
const task = byId.get(id);
|
|
383
|
+
for (const dep of task?.dependsOn ?? []) {
|
|
384
|
+
depth.set(id, Math.max(depth.get(id) ?? 1, (depth.get(dep) ?? 1) + 1));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return Math.max(0, ...depth.values());
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function healthyProviders(providers, registry) {
|
|
391
|
+
return providers.filter((provider) => {
|
|
392
|
+
const contractVersion = provider.description?.contractVersion ?? provider.contractVersion;
|
|
393
|
+
const compatible = typeof registry.isCompatible === 'function'
|
|
394
|
+
? registry.isCompatible(contractVersion)
|
|
395
|
+
: String(contractVersion ?? '1') === '1';
|
|
396
|
+
const health = String(provider.health ?? provider.description?.health?.status ?? '');
|
|
397
|
+
return compatible && ['available', 'degraded'].includes(health);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function supportedOperations(provider) {
|
|
402
|
+
return Array.isArray(provider.capability?.supportedOperations)
|
|
403
|
+
? provider.capability.supportedOperations.map(String)
|
|
404
|
+
: [];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function isMutatingTask(task, registry) {
|
|
408
|
+
if (task.requiresApproval === true || MUTATING_OPERATIONS.has(task.operation)) return true;
|
|
409
|
+
const providers = registry?.providersFor?.(task.requiredCapability) ?? [];
|
|
410
|
+
return providers.some((provider) => {
|
|
411
|
+
const capability = provider.capability ?? {};
|
|
412
|
+
return capability.defaultRequiresApproval === true || typeof capability.mutationClass === 'string';
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function validateJsonSchema(schema, value, path) {
|
|
417
|
+
const errors = [];
|
|
418
|
+
validateSchema(schema || {}, value, path, errors);
|
|
419
|
+
return errors;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function validateSchema(schema, value, path, errors) {
|
|
423
|
+
if (schema.oneOf) {
|
|
424
|
+
const matches = schema.oneOf.filter((candidate) => validateJsonSchema(candidate, value, path).length === 0);
|
|
425
|
+
if (matches.length !== 1) errors.push(`${path} must match exactly one schema`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (schema.anyOf) {
|
|
429
|
+
const matches = schema.anyOf.filter((candidate) => validateJsonSchema(candidate, value, path).length === 0);
|
|
430
|
+
if (matches.length < 1) errors.push(`${path} must match at least one schema`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (schema.const !== undefined && value !== schema.const) errors.push(`${path} must equal ${JSON.stringify(schema.const)}`);
|
|
434
|
+
if (schema.enum && !schema.enum.includes(value)) errors.push(`${path} must be one of ${schema.enum.join(', ')}`);
|
|
435
|
+
if (schema.type && !schemaTypeMatches(schema.type, value)) errors.push(`${path} must be ${formatSchemaType(schema.type)}`);
|
|
436
|
+
if (typeof value === 'string' && schema.minLength != null && value.length < schema.minLength) {
|
|
437
|
+
errors.push(`${path} must have length >= ${schema.minLength}`);
|
|
438
|
+
}
|
|
439
|
+
if (typeof value === 'number') {
|
|
440
|
+
if (schema.minimum != null && value < schema.minimum) errors.push(`${path} must be >= ${schema.minimum}`);
|
|
441
|
+
if (schema.maximum != null && value > schema.maximum) errors.push(`${path} must be <= ${schema.maximum}`);
|
|
442
|
+
}
|
|
443
|
+
if (Array.isArray(value)) {
|
|
444
|
+
value.forEach((item, index) => validateSchema(schema.items ?? {}, item, `${path}[${index}]`, errors));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
448
|
+
for (const key of schema.required ?? []) {
|
|
449
|
+
if (!Object.hasOwn(value, key)) errors.push(`${path}.${key} is required`);
|
|
450
|
+
}
|
|
451
|
+
for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
|
|
452
|
+
if (Object.hasOwn(value, key)) validateSchema(childSchema, value[key], `${path}.${key}`, errors);
|
|
453
|
+
}
|
|
454
|
+
if (schema.additionalProperties === false) {
|
|
455
|
+
const allowed = new Set(Object.keys(schema.properties ?? {}));
|
|
456
|
+
for (const key of Object.keys(value)) {
|
|
457
|
+
if (!allowed.has(key)) errors.push(`${path}.${key} is not allowed`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function schemaTypeMatches(type, value) {
|
|
464
|
+
const types = Array.isArray(type) ? type : [type];
|
|
465
|
+
return types.some((candidate) => {
|
|
466
|
+
if (candidate === 'array') return Array.isArray(value);
|
|
467
|
+
if (candidate === 'null') return value === null;
|
|
468
|
+
if (candidate === 'integer') return Number.isInteger(value);
|
|
469
|
+
if (candidate === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
470
|
+
if (candidate === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
471
|
+
return typeof value === candidate;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function formatSchemaType(type) {
|
|
476
|
+
return Array.isArray(type) ? type.join('|') : type;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function normalizeRefs(value) {
|
|
480
|
+
if (!Array.isArray(value)) return [];
|
|
481
|
+
return value.map((item) => {
|
|
482
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) return { ...item };
|
|
483
|
+
return String(item);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function normalizeRetryPolicy(value) {
|
|
488
|
+
const item = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
489
|
+
return {
|
|
490
|
+
maxAttempts: normalizePositiveInteger(item.maxAttempts) ?? 1,
|
|
491
|
+
retryableErrors: Array.isArray(item.retryableErrors) ? item.retryableErrors.map(String) : [],
|
|
492
|
+
allowAgentFallback: item.allowAgentFallback === true,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function normalizePositiveInteger(value) {
|
|
497
|
+
const number = Number(value);
|
|
498
|
+
return Number.isInteger(number) && number >= 1 ? number : null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function normalizeNonNegativeNumber(value) {
|
|
502
|
+
const number = Number(value);
|
|
503
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function normalizeFiniteNumber(value) {
|
|
507
|
+
const number = Number(value);
|
|
508
|
+
return Number.isFinite(number) ? number : null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function slugify(value) {
|
|
512
|
+
const slug = String(value ?? '')
|
|
513
|
+
.trim()
|
|
514
|
+
.toLowerCase()
|
|
515
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
516
|
+
.replace(/^-+|-+$/g, '');
|
|
517
|
+
return slug;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function markValidated(fragment) {
|
|
521
|
+
Object.defineProperty(fragment, VALIDATED_FRAGMENT, {
|
|
522
|
+
value: true,
|
|
523
|
+
enumerable: false,
|
|
524
|
+
configurable: false,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function issue(code, message, details = {}) {
|
|
529
|
+
return { code, message, details };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function cloneJson(value) {
|
|
533
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
534
|
+
}
|