@aiwg/cli 2026.10.4 → 2026.10.6
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/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
- package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
- package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
- package/agentic/code/providers/model-capabilities.v1.json +74 -50
- package/bin/aiwg.mjs +3 -1
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/citation-parser.js +8 -1
- package/dist/src/artifacts/cli.js +72 -9
- package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
- package/dist/src/artifacts/fortemi-core-sync.js +26 -8
- package/dist/src/artifacts/graph-query.js +18 -8
- package/dist/src/artifacts/index-adapters.js +339 -0
- package/dist/src/artifacts/index-builder.js +146 -16
- package/dist/src/artifacts/index-files.js +15 -4
- package/dist/src/artifacts/index-preparation.js +91 -26
- package/dist/src/artifacts/index-reader.js +44 -0
- package/dist/src/artifacts/index-refresh.js +130 -56
- package/dist/src/artifacts/index-status.js +20 -5
- package/dist/src/artifacts/index-worker.mjs +9 -7
- package/dist/src/artifacts/memory-maintenance.js +503 -0
- package/dist/src/artifacts/prebuilt-relations.js +3 -1
- package/dist/src/artifacts/query-engine.js +263 -108
- package/dist/src/artifacts/relations-builder.js +2 -2
- package/dist/src/artifacts/source-graph.js +2 -2
- package/dist/src/artifacts/types.js +9 -1
- package/dist/src/cli/handlers/decision.js +19 -1
- package/dist/src/cli/handlers/guided-setup.js +470 -0
- package/dist/src/cli/handlers/help.js +2 -2
- package/dist/src/cli/handlers/housekeeper.js +1110 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/init.js +41 -6
- package/dist/src/cli/handlers/local-executor.js +5 -3
- package/dist/src/cli/handlers/mc.js +509 -53
- package/dist/src/cli/handlers/models.js +32 -2
- package/dist/src/cli/handlers/ralph-launcher.js +76 -4
- package/dist/src/cli/handlers/rules.js +1 -1
- package/dist/src/cli/handlers/script-runner.js +73 -2
- package/dist/src/cli/handlers/serve.js +5 -3
- package/dist/src/cli/handlers/sessions.js +40 -6
- package/dist/src/cli/handlers/setup.js +428 -1
- package/dist/src/cli/handlers/skill-lint.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +118 -8
- package/dist/src/cli/handlers/use.js +279 -93
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/prompt-utils.js +8 -0
- package/dist/src/cli/router.js +16 -1
- package/dist/src/config/aiwg-config.js +42 -0
- package/dist/src/config/cli.js +7 -3
- package/dist/src/config/decision-config.js +14 -1
- package/dist/src/config/native-sandbox-warnings.js +25 -0
- package/dist/src/contributors/validation.js +6 -5
- package/dist/src/decision/admission.js +7 -3
- package/dist/src/decision/benchmark.js +469 -0
- package/dist/src/decision/ledger.js +22 -3
- package/dist/src/decision/pricing.js +147 -0
- package/dist/src/decision/qualification/gate-evidence.js +18 -15
- package/dist/src/decision/qualification/load-staged.js +539 -0
- package/dist/src/decision/qualification/load.js +109 -33
- package/dist/src/decision/qualification/runner.js +5 -2
- package/dist/src/decision/review/service.js +6 -4
- package/dist/src/decision/review/validate.js +23 -2
- package/dist/src/decision/savings.js +8 -3
- package/dist/src/discovery/index.js +492 -0
- package/dist/src/discovery/interview.js +233 -0
- package/dist/src/discovery/plan-apply.js +571 -0
- package/dist/src/discovery/plan-store.js +171 -0
- package/dist/src/discovery/plan-types.js +2 -0
- package/dist/src/discovery/plan-validation.js +139 -0
- package/dist/src/discovery/types.js +2 -0
- package/dist/src/extensions/commands/definitions.js +49 -9
- package/dist/src/extensions/manifest.js +71 -44
- package/dist/src/extensions/project-local-discovery.js +1 -1
- package/dist/src/extensions/project-quickref.js +18 -17
- package/dist/src/extensions/validation.js +32 -29
- package/dist/src/housekeeper/daemon-config.js +148 -0
- package/dist/src/housekeeper/ipc.js +84 -0
- package/dist/src/housekeeper/lifecycle.js +148 -0
- package/dist/src/housekeeper/platform.js +35 -0
- package/dist/src/housekeeper/status.js +70 -0
- package/dist/src/housekeeper/units.js +114 -0
- package/dist/src/jobs/flow.js +22 -21
- package/dist/src/lint/rule-activation.js +20 -5
- package/dist/src/lint/runner.js +1 -1
- package/dist/src/mcp/cli.mjs +5 -6
- package/dist/src/mcp/config-file.mjs +6 -2
- package/dist/src/mcp/config-layers.mjs +23 -7
- package/dist/src/mcp/profiles.js +18 -31
- package/dist/src/mcp/profiles.mjs +18 -32
- package/dist/src/mcp/registry.js +7 -27
- package/dist/src/mcp/registry.mjs +7 -28
- package/dist/src/mcp/server.mjs +34 -1
- package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
- package/dist/src/mcp/tools/command-run.mjs +3 -2
- package/dist/src/mcp/tools/decision.mjs +49 -48
- package/dist/src/mcp/tools/discovery.mjs +46 -21
- package/dist/src/mcp/tools/orchestration.mjs +22 -22
- package/dist/src/mcp/tools/subsystems.mjs +9 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
- package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
- package/dist/src/memory/audit.js +719 -0
- package/dist/src/memory/canonical-context.js +392 -13
- package/dist/src/memory/canonical-maintenance.js +95 -0
- package/dist/src/memory/cli-result.js +22 -0
- package/dist/src/memory/cli.js +176 -0
- package/dist/src/memory/context-pack.js +57 -6
- package/dist/src/memory/cost.js +512 -0
- package/dist/src/memory/firewall-classification.d.mts +7 -0
- package/dist/src/memory/firewall-classification.mjs +32 -0
- package/dist/src/memory/index.js +8 -0
- package/dist/src/memory/line-maintenance.js +68 -0
- package/dist/src/memory/maintenance-cli.js +286 -0
- package/dist/src/memory/maintenance-contract.js +136 -0
- package/dist/src/memory/maintenance-index.js +18 -0
- package/dist/src/memory/maintenance-owner.js +2 -0
- package/dist/src/memory/maintenance-record.js +138 -0
- package/dist/src/memory/maintenance.js +254 -0
- package/dist/src/memory/provider-memory-adapter.js +154 -0
- package/dist/src/memory/recommend-cli.js +83 -0
- package/dist/src/memory/recommend.js +269 -0
- package/dist/src/memory/scheduled.js +81 -0
- package/dist/src/memory/taxonomy.js +44 -0
- package/dist/src/mission-protocol/codecs.js +19 -9
- package/dist/src/models/config-loader.js +11 -1
- package/dist/src/models/model-capabilities.v1.json +74 -50
- package/dist/src/models/provider-policy.js +125 -62
- package/dist/src/models/resolver.js +45 -8
- package/dist/src/models/worker-installation.d.mts +7 -0
- package/dist/src/models/worker-installation.mjs +169 -37
- package/dist/src/models/wrapper-route.js +1 -1
- package/dist/src/output-modes/registry.js +8 -7
- package/dist/src/plugin/metadata-validator.js +1 -1
- package/dist/src/providers/deploy-version.d.mts +1 -0
- package/dist/src/providers/deploy-version.mjs +17 -0
- package/dist/src/providers/provider-definitions.js +19 -16
- package/dist/src/providers/skill-ownership.d.mts +11 -2
- package/dist/src/providers/skill-ownership.mjs +35 -17
- package/dist/src/rules/activation.d.mts +11 -2
- package/dist/src/rules/activation.mjs +82 -6
- package/dist/src/rules/inventory.js +1 -1
- package/dist/src/rules/rule-frontmatter.schema.json +2 -0
- package/dist/src/schema/zod-validation.d.mts +6 -0
- package/dist/src/schema/zod-validation.mjs +50 -0
- package/dist/src/serve/executor-registry.js +297 -20
- package/dist/src/serve/executor-token.d.mts +1 -0
- package/dist/src/serve/executor-token.mjs +8 -0
- package/dist/src/serve/shared-host-scheduler.js +1102 -31
- package/dist/src/serve/worker-invocation.js +114 -15
- package/dist/src/sessions/adapters/claude.js +19 -18
- package/dist/src/sessions/adapters/codex.js +33 -19
- package/dist/src/sessions/adapters/copilot.js +14 -13
- package/dist/src/sessions/adapters/cursor.js +19 -18
- package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
- package/dist/src/sessions/adapters/factory.js +11 -10
- package/dist/src/sessions/adapters/generic.js +32 -23
- package/dist/src/sessions/adapters/grokbot.js +35 -35
- package/dist/src/sessions/adapters/hermes.js +20 -19
- package/dist/src/sessions/adapters/muse.js +50 -49
- package/dist/src/sessions/adapters/omp.js +5 -4
- package/dist/src/sessions/adapters/openclaw.js +30 -29
- package/dist/src/sessions/adapters/opencode.js +26 -26
- package/dist/src/sessions/adapters/openhuman.js +24 -23
- package/dist/src/sessions/adapters/pi.js +5 -4
- package/dist/src/sessions/adapters/windsurf.js +8 -8
- package/dist/src/sessions/analytics.js +10 -9
- package/dist/src/sessions/batch-contracts.js +1 -1
- package/dist/src/sessions/batch-import.js +202 -2
- package/dist/src/sessions/candidates.js +7 -6
- package/dist/src/sessions/contracts.js +73 -54
- package/dist/src/sessions/coverage-preflight.js +26 -1
- package/dist/src/sessions/output-registration.js +11 -10
- package/dist/src/sessions/policy.js +18 -7
- package/dist/src/sessions/refresh.js +2 -3
- package/dist/src/sessions/repository.js +4 -2
- package/dist/src/sessions/workspace-discovery.js +92 -26
- package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
- package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
- package/dist/src/update/checker.d.mts +4 -0
- package/dist/src/update/checker.mjs +14 -0
- package/dist/src/writing/exemplar-selection.js +6 -6
- package/dist/src/writing/voice-evaluation.js +24 -23
- package/dist/src/writing/writer-migration.js +14 -13
- package/dist/src/writing/writer-profile.js +19 -18
- package/dist/src/writing/writing-brief.js +18 -17
- package/dist/src/writing/writing-receipt.js +47 -46
- package/package.json +5 -2
- package/schemas/decision/DecisionReview.schema.json +78 -1
- package/schemas/mission-v1.schema.json +3 -0
- package/tools/agents/deploy-agents.mjs +67 -39
- package/tools/agents/providers/antigravity.mjs +6 -1
- package/tools/agents/providers/base.mjs +19 -13
- package/tools/agents/providers/claude.mjs +1 -1
- package/tools/agents/providers/codex.mjs +50 -56
- package/tools/agents/providers/copilot.mjs +8 -2
- package/tools/agents/providers/cursor.mjs +1 -1
- package/tools/agents/providers/deepseek-harness.mjs +1 -1
- package/tools/agents/providers/factory.mjs +7 -1
- package/tools/agents/providers/grok-build.mjs +18 -2
- package/tools/agents/providers/grokbot.mjs +1 -1
- package/tools/agents/providers/hermes.mjs +7 -13
- package/tools/agents/providers/muse-hooks.mjs +21 -14
- package/tools/agents/providers/muse.mjs +7 -2
- package/tools/agents/providers/omp.mjs +28 -4
- package/tools/agents/providers/opencode.mjs +14 -5
- package/tools/agents/providers/openhuman.mjs +1 -1
- package/tools/agents/providers/pi.mjs +1 -1
- package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
- package/tools/agents/providers/rule-delivery.mjs +263 -71
- package/tools/agents/providers/windsurf.mjs +7 -0
- package/tools/cli/workspace-status.mjs +5 -0
- package/tools/commands/deploy-prompts-codex.mjs +11 -8
- package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
- package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
- package/tools/ralph-external/process-monitor.mjs +7 -4
- package/tools/ralph-external/session-launcher.mjs +280 -32
- package/tools/skills/deploy-skills-codex.mjs +11 -7
- package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
|
@@ -0,0 +1,1401 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import Ajv from 'ajv';
|
|
6
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
7
|
+
import addFormats from 'ajv-formats';
|
|
8
|
+
import { validateFlowGraph } from './validator.mjs';
|
|
9
|
+
|
|
10
|
+
const RESOURCE_KEYS = ['activations', 'tokens', 'costUsd', 'timeMs'];
|
|
11
|
+
|
|
12
|
+
// Legacy adapters cannot consume worker values without the managed handshake.
|
|
13
|
+
const MISSION_WORKER_SCHEMA_ID = 'https://aiwg.io/schemas/mission.aiwg.io/v1';
|
|
14
|
+
const MISSION_WORKER_DEFINITIONS = new Set([
|
|
15
|
+
'workerArtifact',
|
|
16
|
+
'workerEffort',
|
|
17
|
+
'workerTuple',
|
|
18
|
+
'workerLimits',
|
|
19
|
+
'workerSelectionIntent',
|
|
20
|
+
'workerBrief',
|
|
21
|
+
'workerClaims',
|
|
22
|
+
'workerUsage',
|
|
23
|
+
'workerExecution',
|
|
24
|
+
'workerVerification',
|
|
25
|
+
'workerResult',
|
|
26
|
+
'workerDispatch',
|
|
27
|
+
'workerDirectIntent',
|
|
28
|
+
'workerDirectBrief',
|
|
29
|
+
'workerResolvedInvocation',
|
|
30
|
+
'workerDirectVerification',
|
|
31
|
+
'workerDirectResult',
|
|
32
|
+
'workerHealthSummary',
|
|
33
|
+
'workerEffectiveHealth',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// Resolve against the owning schema, never infer ownership from a $defs name.
|
|
37
|
+
function workerContractReference(reference, base = 'https://aiwg.invalid/local-schema') {
|
|
38
|
+
if (typeof reference !== 'string') return false;
|
|
39
|
+
try {
|
|
40
|
+
const resolved = new URL(reference, base);
|
|
41
|
+
const fragment = decodeURIComponent(resolved.hash).split('/');
|
|
42
|
+
resolved.hash = '';
|
|
43
|
+
return resolved.href === MISSION_WORKER_SCHEMA_ID
|
|
44
|
+
&& fragment[0] === '#' && fragment[1] === '$defs'
|
|
45
|
+
&& MISSION_WORKER_DEFINITIONS.has(fragment[2]);
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertNoWorkerBinding(value) {
|
|
52
|
+
const seenExtensions = new Set();
|
|
53
|
+
const seenRecords = new Set();
|
|
54
|
+
const seenSchemas = new Map();
|
|
55
|
+
function unsupported() {
|
|
56
|
+
throw new FlowGraphRuntimeError('OG_CONTRACT_UNSUPPORTED', 'Flow runtime does not advertise worker-flow-values.v1 validation/accounting support.');
|
|
57
|
+
}
|
|
58
|
+
function extensions(item) {
|
|
59
|
+
if (!item || typeof item !== 'object' || Array.isArray(item) || seenExtensions.has(item)) return;
|
|
60
|
+
seenExtensions.add(item);
|
|
61
|
+
if (Object.hasOwn(item, 'aiwg.worker-dispatch')) unsupported();
|
|
62
|
+
for (const [key, child] of Object.entries(item)) if (key.startsWith('aiwg.source.')) {
|
|
63
|
+
extensions(child);
|
|
64
|
+
record(child);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function schema(item, inheritedBase = 'https://aiwg.invalid/local-schema') {
|
|
68
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return;
|
|
69
|
+
let base = inheritedBase;
|
|
70
|
+
if (typeof item.$id === 'string') {
|
|
71
|
+
try { base = new URL(item.$id, inheritedBase).href; } catch { /* invalid schemas belong to validation */ }
|
|
72
|
+
}
|
|
73
|
+
const bases = seenSchemas.get(item) ?? new Set();
|
|
74
|
+
if (bases.has(base)) return;
|
|
75
|
+
bases.add(base);
|
|
76
|
+
seenSchemas.set(item, bases);
|
|
77
|
+
if (workerContractReference(item.$ref, base) || workerContractReference(item.schemaRef, base)) unsupported();
|
|
78
|
+
// Only JSON Schema subschemas inherit an owner. const/default/examples
|
|
79
|
+
// and properties' names are data, not active references or extensions.
|
|
80
|
+
for (const key of ['$defs', 'definitions', 'properties', 'patternProperties', 'dependentSchemas', 'dependencies']) {
|
|
81
|
+
for (const child of Object.values(item[key] ?? {})) schema(child, base);
|
|
82
|
+
}
|
|
83
|
+
for (const key of ['allOf', 'anyOf', 'oneOf', 'prefixItems']) {
|
|
84
|
+
if (Array.isArray(item[key])) for (const child of item[key]) schema(child, base);
|
|
85
|
+
}
|
|
86
|
+
for (const key of ['items', 'additionalItems', 'additionalProperties', 'unevaluatedItems', 'unevaluatedProperties', 'contains', 'propertyNames', 'not', 'if', 'then', 'else']) {
|
|
87
|
+
if (Array.isArray(item[key])) for (const child of item[key]) schema(child, base);
|
|
88
|
+
else schema(item[key], base);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function record(item) {
|
|
92
|
+
if (!item || typeof item !== 'object' || Array.isArray(item) || seenRecords.has(item)) return;
|
|
93
|
+
seenRecords.add(item);
|
|
94
|
+
// All mission-v1 carriers: /extensions and /status/artifacts/*/extensions.
|
|
95
|
+
extensions(item.extensions);
|
|
96
|
+
if (Array.isArray(item.status?.artifacts)) item.status.artifacts.forEach(record);
|
|
97
|
+
// Native protocols also use metadata and top-level artifacts.
|
|
98
|
+
extensions(item.metadata);
|
|
99
|
+
if (Array.isArray(item.artifacts)) item.artifacts.forEach(record);
|
|
100
|
+
if (typeof item.$ref === 'string' || typeof item.schemaRef === 'string'
|
|
101
|
+
|| typeof item.$id === 'string' || typeof item.$schema === 'string') schema(item);
|
|
102
|
+
schema(item.schema);
|
|
103
|
+
if (workerContractReference(item.output?.schemaRef)) unsupported();
|
|
104
|
+
}
|
|
105
|
+
record(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Named input/output maps and their values are user data, not contract
|
|
109
|
+
// records. Only a value that declares itself a Mission record can carry a
|
|
110
|
+
// worker binding, so an input named `extensions` or `metadata` stays data.
|
|
111
|
+
function assertNoWorkerValues(values) {
|
|
112
|
+
for (const value of Object.values(values ?? {})) {
|
|
113
|
+
if (value && typeof value === 'object' && !Array.isArray(value)
|
|
114
|
+
&& value.apiVersion === 'mission.aiwg.io/v1' && value.kind === 'Mission') assertNoWorkerBinding(value);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function assertNoWorkerResult(result) {
|
|
119
|
+
assertNoWorkerBinding(result);
|
|
120
|
+
assertNoWorkerValues(result?.outputs);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function assertNoWorkerFlow(manifest, resume) {
|
|
124
|
+
assertNoWorkerBinding(manifest);
|
|
125
|
+
// Visit declared schemas and typed values, never arbitrary manifest data.
|
|
126
|
+
for (const candidate of manifest?.spec?.candidates ?? []) assertNoWorkerBinding(candidate);
|
|
127
|
+
for (const field of manifest?.spec?.state?.fields ?? []) {
|
|
128
|
+
assertNoWorkerBinding(field);
|
|
129
|
+
assertNoWorkerBinding(field.initial);
|
|
130
|
+
}
|
|
131
|
+
for (const node of manifest?.spec?.nodes ?? []) {
|
|
132
|
+
assertNoWorkerBinding(node);
|
|
133
|
+
for (const binding of [...(node.inputs ?? []), ...(node.outputs ?? [])]) {
|
|
134
|
+
assertNoWorkerBinding(binding);
|
|
135
|
+
assertNoWorkerBinding(binding.value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
assertNoWorkerBinding(manifest?.spec?.output);
|
|
139
|
+
assertNoWorkerBinding(resume);
|
|
140
|
+
assertNoWorkerValues(resume?.state);
|
|
141
|
+
assertNoWorkerBinding(resume?.results);
|
|
142
|
+
for (const result of Object.values(resume?.results ?? {})) assertNoWorkerResult(result);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function clone(value) {
|
|
146
|
+
return value === undefined ? undefined : structuredClone(value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function canonical(value) {
|
|
150
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
151
|
+
if (value && typeof value === 'object') {
|
|
152
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
153
|
+
}
|
|
154
|
+
return JSON.stringify(value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function digest(value) {
|
|
158
|
+
return `sha256:${createHash('sha256').update(canonical(value)).digest('hex')}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getPointer(root, pointer) {
|
|
162
|
+
if (pointer === '' || pointer === '/') return root;
|
|
163
|
+
return pointer.split('/').slice(1).reduce((value, token) => (
|
|
164
|
+
value?.[token.replaceAll('~1', '/').replaceAll('~0', '~')]
|
|
165
|
+
), root);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function setPointer(root, pointer, replacement) {
|
|
169
|
+
const tokens = pointer.split('/').slice(1).map((token) => token.replaceAll('~1', '/').replaceAll('~0', '~'));
|
|
170
|
+
if (tokens.length === 0) return replacement;
|
|
171
|
+
const output = clone(root);
|
|
172
|
+
let cursor = output;
|
|
173
|
+
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
174
|
+
const token = tokens[index];
|
|
175
|
+
if (!cursor[token] || typeof cursor[token] !== 'object') return output;
|
|
176
|
+
cursor = cursor[token];
|
|
177
|
+
}
|
|
178
|
+
if (Object.prototype.hasOwnProperty.call(cursor, tokens.at(-1))) cursor[tokens.at(-1)] = replacement;
|
|
179
|
+
return output;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function redact(value, pointers) {
|
|
183
|
+
let result = clone(value);
|
|
184
|
+
for (const pointer of pointers) result = setPointer(result, pointer, '[REDACTED]');
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function reduceValue(current, next, reducer) {
|
|
189
|
+
switch (reducer) {
|
|
190
|
+
case 'replace': return clone(next);
|
|
191
|
+
case 'append': return [...(current ?? []), ...(Array.isArray(next) ? next : [next])];
|
|
192
|
+
case 'merge': return { ...(current ?? {}), ...(next ?? {}) };
|
|
193
|
+
case 'sum': return (current ?? 0) + next;
|
|
194
|
+
case 'min': return current === undefined ? next : Math.min(current, next);
|
|
195
|
+
case 'max': return current === undefined ? next : Math.max(current, next);
|
|
196
|
+
case 'set-union': return [...new Set([...(current ?? []), ...(next ?? [])])];
|
|
197
|
+
default: throw new Error(`Unsupported state reducer '${reducer}'.`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function defaultPredicate(predicate, context) {
|
|
202
|
+
if (!predicate) return true;
|
|
203
|
+
const expression = predicate.expression.trim();
|
|
204
|
+
if (expression === 'true') return true;
|
|
205
|
+
if (expression === 'false') return false;
|
|
206
|
+
const match = expression.match(/^(state|metrics)\.([a-z][a-z0-9-]*)\s*(==|!=|>=|<=|>|<)\s*(-?\d+(?:\.\d+)?|true|false)$/);
|
|
207
|
+
if (!match) throw new Error(`Predicate '${expression}' requires an adapter evaluator.`);
|
|
208
|
+
const [, scope, key, operator, raw] = match;
|
|
209
|
+
const right = raw === 'true' ? true : raw === 'false' ? false : Number(raw);
|
|
210
|
+
const left = context[scope]?.[key];
|
|
211
|
+
switch (operator) {
|
|
212
|
+
case '==': return left === right;
|
|
213
|
+
case '!=': return left !== right;
|
|
214
|
+
case '>=': return left >= right;
|
|
215
|
+
case '<=': return left <= right;
|
|
216
|
+
case '>': return left > right;
|
|
217
|
+
case '<': return left < right;
|
|
218
|
+
default: return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const FLOW_WORKER_FEATURES = [
|
|
223
|
+
'worker-dispatch.v1', 'worker-policy-origins.v1', 'worker-admission-fencing.v1',
|
|
224
|
+
'worker-claims.v1', 'worker-activation.v1', 'worker-health.v1', 'worker-flow-values.v1',
|
|
225
|
+
];
|
|
226
|
+
let bundledSchemas;
|
|
227
|
+
const managedInvokers = new WeakSet();
|
|
228
|
+
|
|
229
|
+
function loadBundledSchemas() {
|
|
230
|
+
if (bundledSchemas) return bundledSchemas;
|
|
231
|
+
let root = path.dirname(fileURLToPath(import.meta.url));
|
|
232
|
+
for (;;) {
|
|
233
|
+
try {
|
|
234
|
+
const name = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name;
|
|
235
|
+
if (name === 'aiwg' || name === '@aiwg/cli') break;
|
|
236
|
+
} catch { /* source and installed packages have different depths */ }
|
|
237
|
+
const parent = path.dirname(root);
|
|
238
|
+
if (parent === root) throw new FlowGraphRuntimeError('OG_CONTRACT_UNSUPPORTED', 'Cannot locate bundled Flow schema owners.');
|
|
239
|
+
root = parent;
|
|
240
|
+
}
|
|
241
|
+
const files = [
|
|
242
|
+
'schemas/mission-v1.schema.json',
|
|
243
|
+
'schemas/models/model-catalog.v1.schema.json',
|
|
244
|
+
'schemas/models/model-policy.v1.schema.json',
|
|
245
|
+
'schemas/models/model-config-compatibility.v1.schema.json',
|
|
246
|
+
'schemas/models/provider-model-capabilities.v1.schema.json',
|
|
247
|
+
'vscode-extension/schemas/aiwg.config.v1.json',
|
|
248
|
+
'agentic/code/addons/composition-engine/schemas/flow-graph.schema.json',
|
|
249
|
+
];
|
|
250
|
+
bundledSchemas = files.map((file) => JSON.parse(fs.readFileSync(path.join(root, file), 'utf8')));
|
|
251
|
+
return bundledSchemas;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Trusted adapter registration metadata, using the existing Mission handshake. */
|
|
255
|
+
export function flowWorkerConsumerSupport() {
|
|
256
|
+
const mission = loadBundledSchemas().find((schema) => schema.$id === MISSION_WORKER_SCHEMA_ID);
|
|
257
|
+
const definitions = Object.fromEntries([...MISSION_WORKER_DEFINITIONS].map((name) => [name, mission.$defs[name]]));
|
|
258
|
+
return { revision: 1, bundleDigest: digest(definitions), features: [...FLOW_WORKER_FEATURES], lossless: true };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Bind the graph evaluator to the existing project's managed execution owner. */
|
|
262
|
+
export function createManagedCompositionAdapter({ scheduler, prepareNode, invokeNode, allowanceFromOwner, ...adapter }) {
|
|
263
|
+
if (!scheduler || typeof prepareNode !== 'function' || typeof invokeNode !== 'function' || typeof allowanceFromOwner !== 'function') {
|
|
264
|
+
throw new FlowGraphRuntimeError('OG_CONTRACT_UNSUPPORTED', 'Managed Flow requires the shared admission and qualified execution owner.');
|
|
265
|
+
}
|
|
266
|
+
async function invokeManagedNode(request) {
|
|
267
|
+
const prepared = await prepareNode(clone(request));
|
|
268
|
+
const proposed = prepared?.admission;
|
|
269
|
+
if (!proposed?.managed || !request.runResources) throw new FlowGraphRuntimeError('OG_CONTRACT_UNSUPPORTED', 'Managed Flow requires a qualified admission request and parent ceilings.');
|
|
270
|
+
const allowance = {
|
|
271
|
+
...proposed.allowance,
|
|
272
|
+
...allowanceFromOwner(request.runId, proposed.allowance?.taskId ?? request.node.id, { kind: 'flow', ceilings: request.runResources }),
|
|
273
|
+
};
|
|
274
|
+
if (!allowance.previousRequestId) {
|
|
275
|
+
const previous = Object.values(scheduler.snapshot().records)
|
|
276
|
+
.filter((record) => record.requestId !== proposed.requestId && record.allowance?.runId === allowance.runId && record.allowance?.taskId === allowance.taskId)
|
|
277
|
+
.sort((left, right) => (right.accounting?.attemptNumber ?? 0) - (left.accounting?.attemptNumber ?? 0))[0];
|
|
278
|
+
if (previous) allowance.previousRequestId = previous.requestId;
|
|
279
|
+
}
|
|
280
|
+
const admission = scheduler.submit({ ...proposed, allowance });
|
|
281
|
+
if (admission.state !== 'admitted') throw Object.assign(new FlowGraphRuntimeError('MANAGED_ADMISSION_BLOCKED', `Flow node '${request.node.id}' is ${admission.state}: ${admission.reason}`), { usage: { tokens: 0, costUsd: 0, timeMs: 0 } });
|
|
282
|
+
const generation = admission.leaseGeneration;
|
|
283
|
+
let claimed = false;
|
|
284
|
+
let execution;
|
|
285
|
+
let observedUsage;
|
|
286
|
+
let finalized = false;
|
|
287
|
+
function settle(source) {
|
|
288
|
+
const evidence = source?.managedEvidence;
|
|
289
|
+
if (!evidence?.health || !evidence?.usage) return false;
|
|
290
|
+
scheduler.acceptFinal(admission.requestId, generation, evidence.health, evidence.usage);
|
|
291
|
+
const record = scheduler.settle(admission.requestId, generation, evidence.health, evidence.usage, evidence.failure, execution);
|
|
292
|
+
finalized = record.state === 'settled';
|
|
293
|
+
return finalized;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
if (prepared.brief && (prepared.brief.lease?.reservationId !== admission.requestId
|
|
297
|
+
|| prepared.brief.lease?.generation !== generation || prepared.brief.lease?.authorityRef !== 'project')) {
|
|
298
|
+
scheduler.disconnect(admission.requestId, generation);
|
|
299
|
+
throw new FlowGraphRuntimeError('MANAGED_ADMISSION_MISMATCH', 'Prepared Flow brief does not match its current shared admission lease.');
|
|
300
|
+
}
|
|
301
|
+
scheduler.claimLaunch(admission.requestId, generation);
|
|
302
|
+
claimed = true;
|
|
303
|
+
const raw = await invokeNode(clone(request), {
|
|
304
|
+
admission: clone(admission), prepared,
|
|
305
|
+
bindExecution(identity) {
|
|
306
|
+
scheduler.bindExecution(admission.requestId, generation, identity);
|
|
307
|
+
execution = clone(identity);
|
|
308
|
+
},
|
|
309
|
+
renew: () => scheduler.renew(admission.requestId, generation),
|
|
310
|
+
observeHealth: (health) => scheduler.observeHealth(admission.requestId, generation, health),
|
|
311
|
+
cancel: () => scheduler.cancel(admission.requestId, generation),
|
|
312
|
+
});
|
|
313
|
+
observedUsage = raw?.usage;
|
|
314
|
+
if (!settle(raw)) throw Object.assign(new FlowGraphRuntimeError('MANAGED_EXECUTION_UNCERTAIN', 'Managed Flow cannot deliver outputs without current-generation termination proof.'), { usage: raw?.usage });
|
|
315
|
+
if (raw.managedEvidence.health.state !== 'completed') throw Object.assign(new FlowGraphRuntimeError('MANAGED_EXECUTION_FAILED', 'Managed worker did not complete.'), { usage: raw?.usage });
|
|
316
|
+
const { managedEvidence, ...result } = raw;
|
|
317
|
+
return { ...result, managedAdmission: { requestId: admission.requestId, generation, state: 'settled', worker: clone(admission.managed) } };
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (observedUsage !== undefined && error && typeof error === 'object' && error.usage === undefined) error.usage = clone(observedUsage);
|
|
320
|
+
if (!finalized && claimed) {
|
|
321
|
+
try { settle(error); } catch {
|
|
322
|
+
// Invalid final evidence cannot replace the execution failure or its usage.
|
|
323
|
+
}
|
|
324
|
+
if (!finalized) {
|
|
325
|
+
try { scheduler.disconnect(admission.requestId, generation); } catch {
|
|
326
|
+
// A stale owner cannot mutate the current lease or deliver outputs.
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
managedInvokers.add(invokeManagedNode);
|
|
334
|
+
return { ...adapter, workerMode: 'managed', workerConsumer: flowWorkerConsumerSupport(), invokeNode: invokeManagedNode };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function managedValueValidator(consumer) {
|
|
338
|
+
const support = flowWorkerConsumerSupport();
|
|
339
|
+
if (consumer?.revision !== support.revision || consumer?.bundleDigest !== support.bundleDigest
|
|
340
|
+
|| consumer?.lossless !== true || !Array.isArray(consumer?.features)
|
|
341
|
+
|| support.features.some((feature) => !consumer.features.includes(feature))) {
|
|
342
|
+
throw new FlowGraphRuntimeError('OG_CONTRACT_UNSUPPORTED', 'Managed Flow requires the current lossless worker schema bundle and worker-flow-values.v1 adapter.');
|
|
343
|
+
}
|
|
344
|
+
const engines = {
|
|
345
|
+
draft7: addFormats(new Ajv({ strict: false, allErrors: true })),
|
|
346
|
+
draft2020: addFormats(new Ajv2020({ strict: false, allErrors: true })),
|
|
347
|
+
};
|
|
348
|
+
const dialect = (schema) => schema.$schema?.includes('2020-12') ? 'draft2020' : 'draft7';
|
|
349
|
+
const owners = new Map(loadBundledSchemas().map((schema) => [schema.$id, schema]));
|
|
350
|
+
for (const schema of owners.values()) engines[dialect(schema)].addSchema(schema);
|
|
351
|
+
// A reference to another dialect delegates to that owner's validator.
|
|
352
|
+
// The surrounding schema keeps its own dialect and sibling semantics.
|
|
353
|
+
const bridgeKeyword = 'aiwgBundledSchemaReference';
|
|
354
|
+
for (const engine of Object.values(engines)) engine.addKeyword({
|
|
355
|
+
keyword: bridgeKeyword,
|
|
356
|
+
schemaType: 'string',
|
|
357
|
+
errors: true,
|
|
358
|
+
compile(reference) {
|
|
359
|
+
const owner = owners.get(reference.split('#')[0]);
|
|
360
|
+
if (!owner) throw new Error('Only bundled schema references are supported.');
|
|
361
|
+
return engines[dialect(owner)].getSchema(reference);
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
function bridge(schema, selected, base = 'https://aiwg.invalid/flow-value') {
|
|
365
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return schema;
|
|
366
|
+
const result = { ...schema };
|
|
367
|
+
if (Object.hasOwn(schema, bridgeKeyword)) throw new Error('Reserved schema keyword.');
|
|
368
|
+
if (schema.$id) base = new URL(schema.$id, base).href;
|
|
369
|
+
if (schema.$ref) {
|
|
370
|
+
const reference = new URL(schema.$ref, base).href;
|
|
371
|
+
const owner = owners.get(reference.split('#')[0]);
|
|
372
|
+
if (owner && dialect(owner) !== selected) {
|
|
373
|
+
delete result.$ref;
|
|
374
|
+
result[bridgeKeyword] = reference;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
for (const key of ['$defs', 'definitions', 'properties', 'patternProperties', 'dependentSchemas', 'dependencies']) {
|
|
378
|
+
if (schema[key]) result[key] = Object.fromEntries(Object.entries(schema[key]).map(([name, child]) => [name, bridge(child, selected, base)]));
|
|
379
|
+
}
|
|
380
|
+
for (const key of ['allOf', 'anyOf', 'oneOf', 'prefixItems']) if (Array.isArray(schema[key])) result[key] = schema[key].map((child) => bridge(child, selected, base));
|
|
381
|
+
for (const key of ['items', 'additionalItems', 'additionalProperties', 'unevaluatedItems', 'unevaluatedProperties', 'contains', 'propertyNames', 'not', 'if', 'then', 'else']) {
|
|
382
|
+
if (Object.hasOwn(schema, key)) result[key] = Array.isArray(schema[key]) ? schema[key].map((child) => bridge(child, selected, base)) : bridge(schema[key], selected, base);
|
|
383
|
+
}
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
const validators = new Map();
|
|
387
|
+
function compile(schema) {
|
|
388
|
+
if (validators.has(schema)) return validators.get(schema);
|
|
389
|
+
let selected = dialect(schema);
|
|
390
|
+
if (!schema.$schema && schema.$ref) {
|
|
391
|
+
const owner = owners.get(schema.$ref.split('#')[0]);
|
|
392
|
+
if (owner) selected = dialect(owner);
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const validate = engines[selected].compile(bridge(schema, selected));
|
|
396
|
+
validators.set(schema, validate);
|
|
397
|
+
return validate;
|
|
398
|
+
} catch (error) {
|
|
399
|
+
throw new FlowGraphRuntimeError('FLOW_SCHEMA_UNRESOLVED', 'Typed Flow schemas must resolve offline from bundled owners in their declared dialect.', { reason: error.message });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
compile,
|
|
404
|
+
check(schema, value, code, binding) {
|
|
405
|
+
const validate = compile(schema);
|
|
406
|
+
if (!validate(value)) throw new FlowGraphRuntimeError(code, `Invalid typed Flow value '${binding}'.`, { errors: clone(validate.errors) });
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function usageOf(result, managed = false) {
|
|
412
|
+
const usage = result?.usage ?? {};
|
|
413
|
+
return Object.fromEntries(['tokens', 'costUsd', 'timeMs'].map((key) => [
|
|
414
|
+
key, managed && (usage[key] === undefined || usage[key] === null) ? null : Number(usage[key] ?? 0),
|
|
415
|
+
]));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function assertUsage(usage) {
|
|
419
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
420
|
+
if (value !== null && (!Number.isFinite(value) || value < 0)) throw new Error(`Adapter usage.${key} must be a non-negative number.`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function resultOutput(results, reference) {
|
|
425
|
+
const [nodeId, outputName] = String(reference).split('.');
|
|
426
|
+
return results[nodeId]?.outputs?.[outputName];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function initialState(manifest) {
|
|
430
|
+
return Object.fromEntries(manifest.spec.state.fields.map((field) => [field.name, clone(field.initial)]));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function resourcesExceeded(realized, ceilings) {
|
|
434
|
+
return RESOURCE_KEYS.find((key) => realized[key] >= ceilings[key]);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function publicTouches(value) {
|
|
438
|
+
const normalize = (items) => [...new Set((Array.isArray(items) ? items : [])
|
|
439
|
+
.filter((item) => typeof item === 'string' && item.length > 0))].sort();
|
|
440
|
+
return { files: normalize(value?.files), resources: normalize(value?.resources) };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function scopeEvidence(declaredValue, observedValue, complete = true) {
|
|
444
|
+
const declared = publicTouches(declaredValue);
|
|
445
|
+
const observed = publicTouches(observedValue);
|
|
446
|
+
const compare = (kind) => ({
|
|
447
|
+
declared: declared[kind],
|
|
448
|
+
observed: observed[kind],
|
|
449
|
+
undeclared: observed[kind].filter((item) => !declared[kind].includes(item)),
|
|
450
|
+
unobserved: declared[kind].filter((item) => !observed[kind].includes(item)),
|
|
451
|
+
});
|
|
452
|
+
const files = compare('files');
|
|
453
|
+
const resources = compare('resources');
|
|
454
|
+
const observedCount = observed.files.length + observed.resources.length;
|
|
455
|
+
const declaredCount = declared.files.length + declared.resources.length;
|
|
456
|
+
const undeclaredCount = files.undeclared.length + resources.undeclared.length;
|
|
457
|
+
return {
|
|
458
|
+
files,
|
|
459
|
+
resources,
|
|
460
|
+
coverage: observedCount === 0 ? 'empty'
|
|
461
|
+
: undeclaredCount > 0 ? 'divergent'
|
|
462
|
+
: observedCount < declaredCount ? 'reduced'
|
|
463
|
+
: 'complete',
|
|
464
|
+
observationComplete: Boolean(complete),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function mergedScopeEvidence(previous, declared, observed, complete) {
|
|
469
|
+
const aggregate = {
|
|
470
|
+
files: [...(previous?.files?.observed ?? []), ...(observed?.files ?? [])],
|
|
471
|
+
resources: [...(previous?.resources?.observed ?? []), ...(observed?.resources ?? [])],
|
|
472
|
+
};
|
|
473
|
+
return scopeEvidence(declared, aggregate, previous ? previous.observationComplete && complete : complete);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function emptyUsage() {
|
|
477
|
+
return { runs: 0, tokens: 0, costUsd: 0, timeMs: 0 };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function addUsage(target, usage) {
|
|
481
|
+
target.runs += 1;
|
|
482
|
+
for (const key of ['tokens', 'costUsd', 'timeMs']) target[key] = target[key] === null || usage[key] === null ? null : target[key] + usage[key];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export class FlowGraphRuntimeError extends Error {
|
|
486
|
+
constructor(code, message, details = {}) {
|
|
487
|
+
super(message);
|
|
488
|
+
this.name = 'FlowGraphRuntimeError';
|
|
489
|
+
this.code = code;
|
|
490
|
+
this.details = details;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Execute the deterministic FlowGraph planner. The returned event stream is a
|
|
496
|
+
* projection for MissionConductor (or another durable owner) to persist; this
|
|
497
|
+
* module deliberately does not create a competing mission ledger.
|
|
498
|
+
*/
|
|
499
|
+
export async function executeFlowGraph(manifest, options = {}) {
|
|
500
|
+
const managed = options.workerMode === 'managed';
|
|
501
|
+
const values = managed ? managedValueValidator(options.workerConsumer) : undefined;
|
|
502
|
+
if (managed && !managedInvokers.has(options.invokeNode)) throw new FlowGraphRuntimeError('OG_CONTRACT_UNSUPPORTED', 'Managed Flow requires an adapter bound to the shared execution owner.');
|
|
503
|
+
if (!managed) assertNoWorkerFlow(manifest, options.resumeFrom);
|
|
504
|
+
const validation = validateFlowGraph(manifest, options.validation);
|
|
505
|
+
if (!validation.valid) {
|
|
506
|
+
throw new FlowGraphRuntimeError('FLOW_GRAPH_INVALID', 'FlowGraph validation failed.', {
|
|
507
|
+
diagnostics: validation.diagnostics,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (typeof options.invokeNode !== 'function') {
|
|
511
|
+
throw new FlowGraphRuntimeError('ADAPTER_REQUIRED', 'FlowGraph execution requires invokeNode adapter function.');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const graphId = validation.normalized.identity.graphId;
|
|
515
|
+
const runId = options.runId ?? `${graphId}:run`;
|
|
516
|
+
const evaluatePredicate = options.evaluatePredicate ?? defaultPredicate;
|
|
517
|
+
const allowedCapabilities = new Set(options.allowedCapabilities ?? manifest.spec.capabilities.map((item) => item.id));
|
|
518
|
+
const allowedPermissions = new Set(options.allowedPermissions ?? manifest.spec.permissions);
|
|
519
|
+
const approvedGates = new Set(options.approvedGates ?? []);
|
|
520
|
+
const nodeOrder = new Map(manifest.spec.nodes.map((node, index) => [node.id, index]));
|
|
521
|
+
const nodes = new Map(manifest.spec.nodes.map((node) => [node.id, node]));
|
|
522
|
+
const stateFields = new Map(manifest.spec.state.fields.map((field) => [field.name, field]));
|
|
523
|
+
const resume = options.resumeFrom ?? {};
|
|
524
|
+
const state = { ...initialState(manifest), ...(clone(resume.state) ?? {}) };
|
|
525
|
+
const results = clone(resume.results) ?? {};
|
|
526
|
+
const receipts = clone(resume.receipts) ?? {};
|
|
527
|
+
const completed = new Set(resume.completed ?? []);
|
|
528
|
+
const failed = new Set(resume.failed ?? []);
|
|
529
|
+
const skipped = new Set(resume.skipped ?? []);
|
|
530
|
+
const activatedRoutes = new Set(resume.activatedRoutes ?? []);
|
|
531
|
+
const routeActivations = clone(resume.routeActivations) ?? {};
|
|
532
|
+
const routeProgress = clone(resume.routeProgress) ?? {};
|
|
533
|
+
const reactivatedNodes = new Set(resume.reactivatedNodes ?? []);
|
|
534
|
+
const events = clone(resume.events) ?? [];
|
|
535
|
+
const realized = {
|
|
536
|
+
activations: Number(resume.realized?.activations ?? 0),
|
|
537
|
+
nodeRuns: Number(resume.realized?.nodeRuns ?? 0),
|
|
538
|
+
tokens: managed && resume.realized?.tokens === null ? null : Number(resume.realized?.tokens ?? 0),
|
|
539
|
+
costUsd: managed && resume.realized?.costUsd === null ? null : Number(resume.realized?.costUsd ?? 0),
|
|
540
|
+
timeMs: managed && resume.realized?.timeMs === null ? null : Number(resume.realized?.timeMs ?? 0),
|
|
541
|
+
maxConcurrency: Number(resume.realized?.maxConcurrency ?? 0),
|
|
542
|
+
retries: Number(resume.realized?.retries ?? 0),
|
|
543
|
+
};
|
|
544
|
+
const joinState = clone(resume.joins) ?? {};
|
|
545
|
+
const evidence = clone(resume.evidence) ?? { nodes: {}, branches: {}, sideEffects: {}, joins: {} };
|
|
546
|
+
const accounting = managed ? clone(resume.accounting) ?? {
|
|
547
|
+
attempts: {},
|
|
548
|
+
pending: {},
|
|
549
|
+
known: Object.fromEntries(['tokens', 'costUsd', 'timeMs'].map((key) => [key, realized[key] ?? 0])),
|
|
550
|
+
reserved: Object.fromEntries(['tokens', 'costUsd', 'timeMs'].map((key) => [key, realized[key] === null ? manifest.spec.ceilings[key] : 0])),
|
|
551
|
+
} : undefined;
|
|
552
|
+
if (managed) {
|
|
553
|
+
for (const field of stateFields.values()) values.check(field.schema, state[field.name], 'INVALID_TYPED_STATE', field.name);
|
|
554
|
+
for (const node of nodes.values()) {
|
|
555
|
+
for (const binding of [...node.inputs, ...node.outputs]) values.compile(binding.schema);
|
|
556
|
+
for (const binding of node.inputs) if (Object.hasOwn(binding, 'value')) values.check(binding.schema, binding.value, 'INVALID_TYPED_INPUT', `${node.id}.${binding.name}`);
|
|
557
|
+
if (results[node.id]) for (const binding of node.outputs) values.check(binding.schema, results[node.id].outputs?.[binding.name], 'INVALID_TYPED_OUTPUT', `${node.id}.${binding.name}`);
|
|
558
|
+
}
|
|
559
|
+
values.compile(manifest.spec.output.schema);
|
|
560
|
+
}
|
|
561
|
+
if (managed) accounting.pending ??= {};
|
|
562
|
+
let sequence = events.length;
|
|
563
|
+
let terminal = managed && Object.keys(accounting.pending).length
|
|
564
|
+
? { code: 'MANAGED_RECONCILIATION_REQUIRED', reason: 'Managed Flow checkpoint has an invocation awaiting owner reconciliation.' }
|
|
565
|
+
: undefined;
|
|
566
|
+
|
|
567
|
+
async function emit(type, payload = {}) {
|
|
568
|
+
const event = { sequence: ++sequence, type, graphId, runId, ...clone(payload) };
|
|
569
|
+
events.push(event);
|
|
570
|
+
await options.onEvent?.(clone(event));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function checkpoint() {
|
|
574
|
+
return {
|
|
575
|
+
schemaVersion: 'flow.aiwg.io/v1alpha1',
|
|
576
|
+
kind: 'FlowGraphCheckpoint',
|
|
577
|
+
graphId,
|
|
578
|
+
runId,
|
|
579
|
+
state: clone(state),
|
|
580
|
+
results: clone(results),
|
|
581
|
+
receipts: clone(receipts),
|
|
582
|
+
completed: [...completed].sort((a, b) => nodeOrder.get(a) - nodeOrder.get(b)),
|
|
583
|
+
failed: [...failed].sort((a, b) => nodeOrder.get(a) - nodeOrder.get(b)),
|
|
584
|
+
skipped: [...skipped].sort((a, b) => nodeOrder.get(a) - nodeOrder.get(b)),
|
|
585
|
+
activatedRoutes: [...activatedRoutes].sort(),
|
|
586
|
+
routeActivations: clone(routeActivations),
|
|
587
|
+
routeProgress: clone(routeProgress),
|
|
588
|
+
reactivatedNodes: [...reactivatedNodes].sort(),
|
|
589
|
+
joins: clone(joinState),
|
|
590
|
+
realized: clone(realized),
|
|
591
|
+
...(managed ? { accounting: clone(accounting) } : {}),
|
|
592
|
+
evidence: clone(evidence),
|
|
593
|
+
events: clone(events),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function persist() {
|
|
598
|
+
await options.saveCheckpoint?.(checkpoint());
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function tracePayload(node, inputs, outputs) {
|
|
602
|
+
const base = {
|
|
603
|
+
inputDigest: digest(inputs),
|
|
604
|
+
outputDigest: digest(outputs),
|
|
605
|
+
};
|
|
606
|
+
if (manifest.spec.trace.level === 'bindings') {
|
|
607
|
+
return { ...base, inputNames: Object.keys(inputs).sort(), outputNames: Object.keys(outputs).sort() };
|
|
608
|
+
}
|
|
609
|
+
if (manifest.spec.trace.level === 'full-io') {
|
|
610
|
+
return {
|
|
611
|
+
...base,
|
|
612
|
+
inputs: redact(inputs, manifest.spec.trace.redact),
|
|
613
|
+
outputs: redact(outputs, manifest.spec.trace.redact),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
return base;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function inputsFor(node) {
|
|
620
|
+
return Object.fromEntries(node.inputs.map((binding) => {
|
|
621
|
+
if (binding.from) return [binding.name, clone(resultOutput(results, binding.from))];
|
|
622
|
+
if (binding.state) return [binding.name, clone(state[binding.state])];
|
|
623
|
+
return [binding.name, clone(binding.value)];
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function enforceAuthority(node) {
|
|
628
|
+
const missingCapability = node.capabilities.find((id) => !allowedCapabilities.has(id));
|
|
629
|
+
if (missingCapability) throw new FlowGraphRuntimeError('CAPABILITY_DENIED', `Node '${node.id}' is not authorized for capability '${missingCapability}'.`);
|
|
630
|
+
const missingPermission = node.permissions.find((id) => !allowedPermissions.has(id));
|
|
631
|
+
if (missingPermission) throw new FlowGraphRuntimeError('PERMISSION_DENIED', `Node '${node.id}' is not authorized for permission '${missingPermission}'.`);
|
|
632
|
+
if (node.kind === 'gate' && new Set(options.cancelledGates ?? []).has(node.id)) {
|
|
633
|
+
throw new FlowGraphRuntimeError('APPROVAL_CANCELLED', `Approval gate '${node.id}' was cancelled.`);
|
|
634
|
+
}
|
|
635
|
+
if (node.kind === 'gate' && !approvedGates.has(node.id)) {
|
|
636
|
+
throw new FlowGraphRuntimeError('APPROVAL_REQUIRED', `Gate '${node.id}' has not been approved.`);
|
|
637
|
+
}
|
|
638
|
+
if (node.sideEffectMode === 'approval-required') {
|
|
639
|
+
if (new Set(options.cancelledGates ?? []).has(node.approvalGate)) {
|
|
640
|
+
throw new FlowGraphRuntimeError('APPROVAL_CANCELLED', `Approval gate '${node.approvalGate}' was cancelled.`);
|
|
641
|
+
}
|
|
642
|
+
if (!approvedGates.has(node.approvalGate)) {
|
|
643
|
+
throw new FlowGraphRuntimeError('APPROVAL_REQUIRED', `Node '${node.id}' is waiting for approval gate '${node.approvalGate}'.`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function validateInputs(node, inputs) {
|
|
649
|
+
if (!managed) return assertNoWorkerValues(inputs);
|
|
650
|
+
for (const binding of node.inputs) values.check(binding.schema, inputs[binding.name], 'INVALID_TYPED_INPUT', `${node.id}.${binding.name}`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function requestedResources(node) {
|
|
654
|
+
const requested = clone(node.ceilings ?? manifest.spec.ceilings);
|
|
655
|
+
if (managed) for (const key of ['tokens', 'costUsd', 'timeMs']) {
|
|
656
|
+
const remaining = manifest.spec.ceilings[key] - accounting.known[key] - accounting.reserved[key];
|
|
657
|
+
if (remaining <= 0) throw new FlowGraphRuntimeError('RESOURCE_CEILING_EXHAUSTED', `Managed Flow has no remaining ${key} allowance.`);
|
|
658
|
+
requested[key] = Math.min(requested[key], remaining);
|
|
659
|
+
}
|
|
660
|
+
return requested;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function chargeAttempt(node, invocationId, source, requested) {
|
|
664
|
+
if (accounting.attempts[invocationId]) return accounting.attempts[invocationId].usage;
|
|
665
|
+
let usage = usageOf(source, true);
|
|
666
|
+
try { assertUsage(usage); } catch { usage = { tokens: null, costUsd: null, timeMs: null }; }
|
|
667
|
+
const reserved = {};
|
|
668
|
+
for (const key of ['tokens', 'costUsd', 'timeMs']) {
|
|
669
|
+
reserved[key] = usage[key] === null ? requested[key] : 0;
|
|
670
|
+
accounting.known[key] += usage[key] ?? 0;
|
|
671
|
+
accounting.reserved[key] += reserved[key];
|
|
672
|
+
realized[key] = realized[key] === null || usage[key] === null ? null : realized[key] + usage[key];
|
|
673
|
+
}
|
|
674
|
+
accounting.attempts[invocationId] = { nodeId: node.id, usage, reserved };
|
|
675
|
+
realized.nodeRuns += 1;
|
|
676
|
+
const nodeUsage = evidence.nodes[node.id]?.resources ?? emptyUsage();
|
|
677
|
+
addUsage(nodeUsage, usage);
|
|
678
|
+
evidence.nodes[node.id] = { ...(evidence.nodes[node.id] ?? {}), resources: nodeUsage };
|
|
679
|
+
const branchId = node.track ?? 'default';
|
|
680
|
+
const branchUsage = evidence.branches[branchId] ?? emptyUsage();
|
|
681
|
+
addUsage(branchUsage, usage);
|
|
682
|
+
evidence.branches[branchId] = branchUsage;
|
|
683
|
+
await persist();
|
|
684
|
+
return usage;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function checkChargedCeilings(node, usage, requested) {
|
|
688
|
+
for (const key of ['tokens', 'costUsd', 'timeMs']) if (usage[key] !== null && usage[key] > requested[key]) {
|
|
689
|
+
throw new FlowGraphRuntimeError('RESOURCE_CEILING_WOULD_EXCEED', `Node '${node.id}' exceeded its ${key} allowance.`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function applyOutputs(node, outputs) {
|
|
694
|
+
if (managed) for (const binding of node.outputs) values.check(binding.schema, outputs[binding.name], 'INVALID_TYPED_OUTPUT', `${node.id}.${binding.name}`);
|
|
695
|
+
for (const binding of node.outputs) {
|
|
696
|
+
if (!Object.prototype.hasOwnProperty.call(outputs, binding.name)) {
|
|
697
|
+
throw new FlowGraphRuntimeError('MISSING_TYPED_OUTPUT', `Node '${node.id}' did not return output '${binding.name}'.`);
|
|
698
|
+
}
|
|
699
|
+
if (binding.state) {
|
|
700
|
+
const field = stateFields.get(binding.state);
|
|
701
|
+
const reduced = reduceValue(state[binding.state], outputs[binding.name], field.reducer);
|
|
702
|
+
if (managed) values.check(field.schema, reduced, 'INVALID_TYPED_STATE', binding.state);
|
|
703
|
+
state[binding.state] = reduced;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function invoke(node, activationTick, iteration = 1) {
|
|
709
|
+
enforceAuthority(node);
|
|
710
|
+
const inputs = inputsFor(node);
|
|
711
|
+
validateInputs(node, inputs);
|
|
712
|
+
const inputSnapshot = clone(inputs);
|
|
713
|
+
const nodeRunId = `${runId}:${node.id}:${activationTick}:${iteration}`;
|
|
714
|
+
const invocationKey = node.idempotencyKey
|
|
715
|
+
? `${graphId}:${node.id}:${digest(node.idempotencyKey).slice(7, 23)}:${digest(inputSnapshot).slice(7, 23)}`
|
|
716
|
+
: nodeRunId;
|
|
717
|
+
const existing = receipts[invocationKey];
|
|
718
|
+
const sideEffect = evidence.sideEffects[node.id] ?? {
|
|
719
|
+
mode: node.sideEffectMode,
|
|
720
|
+
attempts: 0,
|
|
721
|
+
duplicateDetections: 0,
|
|
722
|
+
suppressions: 0,
|
|
723
|
+
successes: 0,
|
|
724
|
+
};
|
|
725
|
+
evidence.sideEffects[node.id] = sideEffect;
|
|
726
|
+
if (existing && ['idempotent', 'exactly-once'].includes(node.sideEffectMode)) {
|
|
727
|
+
sideEffect.attempts += 1;
|
|
728
|
+
sideEffect.duplicateDetections += 1;
|
|
729
|
+
sideEffect.suppressions += 1;
|
|
730
|
+
await emit('node-replayed', { nodeId: node.id, nodeRunId, activation: activationTick, invocationKey, duplicateDetected: true, suppressed: true });
|
|
731
|
+
results[node.id] = clone(existing.result);
|
|
732
|
+
completed.add(node.id);
|
|
733
|
+
return existing.result;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const attempts = (node.retry?.limit ?? 0) + 1;
|
|
737
|
+
let lastError;
|
|
738
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
739
|
+
sideEffect.attempts += 1;
|
|
740
|
+
await emit('node-started', {
|
|
741
|
+
nodeId: node.id,
|
|
742
|
+
nodeRunId,
|
|
743
|
+
activation: activationTick,
|
|
744
|
+
iteration,
|
|
745
|
+
attempt,
|
|
746
|
+
invocationKey,
|
|
747
|
+
inputDigest: digest(inputSnapshot),
|
|
748
|
+
});
|
|
749
|
+
let requested;
|
|
750
|
+
try {
|
|
751
|
+
requested = requestedResources(node);
|
|
752
|
+
if (managed) {
|
|
753
|
+
accounting.pending[`${nodeRunId}:${attempt}`] = { nodeId: node.id, requestedResources: clone(requested) };
|
|
754
|
+
await persist();
|
|
755
|
+
}
|
|
756
|
+
const raw = await options.invokeNode({
|
|
757
|
+
graphId,
|
|
758
|
+
runId,
|
|
759
|
+
node: clone(node),
|
|
760
|
+
nodeRunId,
|
|
761
|
+
activationId: `${runId}:activation:${activationTick}`,
|
|
762
|
+
activation: activationTick,
|
|
763
|
+
iteration,
|
|
764
|
+
attempt,
|
|
765
|
+
invocationKey,
|
|
766
|
+
inputs: inputSnapshot,
|
|
767
|
+
state: clone(state),
|
|
768
|
+
requestedResources: requested,
|
|
769
|
+
...(managed ? { runResources: clone(manifest.spec.ceilings), invocationId: `${nodeRunId}:${attempt}` } : {}),
|
|
770
|
+
});
|
|
771
|
+
if (!managed) assertNoWorkerResult(raw);
|
|
772
|
+
const outputs = clone(raw?.outputs ?? {});
|
|
773
|
+
const usage = managed ? await chargeAttempt(node, `${nodeRunId}:${attempt}`, raw, requested) : usageOf(raw);
|
|
774
|
+
assertUsage(usage);
|
|
775
|
+
if (managed) checkChargedCeilings(node, usage, requested);
|
|
776
|
+
if (!managed) for (const key of ['tokens', 'costUsd', 'timeMs']) {
|
|
777
|
+
const graphCeiling = manifest.spec.ceilings[key];
|
|
778
|
+
const nodeCeiling = node.ceilings?.[key] ?? graphCeiling;
|
|
779
|
+
if (usage[key] > nodeCeiling || realized[key] + usage[key] > graphCeiling) {
|
|
780
|
+
throw new FlowGraphRuntimeError(
|
|
781
|
+
'RESOURCE_CEILING_WOULD_EXCEED',
|
|
782
|
+
`Node '${node.id}' would exceed its ${key} ceiling.`,
|
|
783
|
+
{ graphCeiling, nodeCeiling, realized: realized[key], attempted: usage[key] },
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
applyOutputs(node, outputs);
|
|
788
|
+
if (!managed) {
|
|
789
|
+
realized.nodeRuns += 1;
|
|
790
|
+
realized.tokens += usage.tokens;
|
|
791
|
+
realized.costUsd += usage.costUsd;
|
|
792
|
+
realized.timeMs += usage.timeMs;
|
|
793
|
+
const nodeUsage = evidence.nodes[node.id]?.resources ?? emptyUsage();
|
|
794
|
+
addUsage(nodeUsage, usage);
|
|
795
|
+
const branchId = node.track ?? 'default';
|
|
796
|
+
const branchUsage = evidence.branches[branchId] ?? emptyUsage();
|
|
797
|
+
addUsage(branchUsage, usage);
|
|
798
|
+
evidence.branches[branchId] = branchUsage;
|
|
799
|
+
evidence.nodes[node.id] = { ...(evidence.nodes[node.id] ?? {}), resources: nodeUsage };
|
|
800
|
+
}
|
|
801
|
+
evidence.nodes[node.id] = {
|
|
802
|
+
...(evidence.nodes[node.id] ?? {}),
|
|
803
|
+
scope: mergedScopeEvidence(
|
|
804
|
+
evidence.nodes[node.id]?.scope,
|
|
805
|
+
node.scope,
|
|
806
|
+
publicTouches(raw?.observedTouches),
|
|
807
|
+
raw?.observationComplete === true,
|
|
808
|
+
),
|
|
809
|
+
};
|
|
810
|
+
sideEffect.successes += 1;
|
|
811
|
+
const result = {
|
|
812
|
+
outputs,
|
|
813
|
+
usage,
|
|
814
|
+
...(raw?.score === undefined ? {} : { score: raw.score }),
|
|
815
|
+
...(raw?.citations === undefined ? {} : { citations: clone(raw.citations) }),
|
|
816
|
+
adapter: options.adapterId ?? 'unspecified',
|
|
817
|
+
...(managed ? { admission: clone(raw?.managedAdmission) } : {}),
|
|
818
|
+
nodeRunId,
|
|
819
|
+
invocationKey,
|
|
820
|
+
};
|
|
821
|
+
results[node.id] = result;
|
|
822
|
+
completed.add(node.id);
|
|
823
|
+
failed.delete(node.id);
|
|
824
|
+
if (['idempotent', 'exactly-once'].includes(node.sideEffectMode)) receipts[invocationKey] = { result: clone(result) };
|
|
825
|
+
await emit('node-completed', {
|
|
826
|
+
nodeId: node.id,
|
|
827
|
+
nodeRunId,
|
|
828
|
+
activation: activationTick,
|
|
829
|
+
iteration,
|
|
830
|
+
attempt,
|
|
831
|
+
invocationKey,
|
|
832
|
+
usage,
|
|
833
|
+
score: raw?.score,
|
|
834
|
+
citations: clone(raw?.citations),
|
|
835
|
+
...tracePayload(node, inputSnapshot, outputs),
|
|
836
|
+
});
|
|
837
|
+
if (managed) delete accounting.pending[`${nodeRunId}:${attempt}`];
|
|
838
|
+
await persist();
|
|
839
|
+
return result;
|
|
840
|
+
} catch (error) {
|
|
841
|
+
if (managed && requested) {
|
|
842
|
+
await chargeAttempt(node, `${nodeRunId}:${attempt}`, error, requested);
|
|
843
|
+
delete accounting.pending[`${nodeRunId}:${attempt}`];
|
|
844
|
+
}
|
|
845
|
+
lastError = error;
|
|
846
|
+
await emit('node-attempt-failed', {
|
|
847
|
+
nodeId: node.id,
|
|
848
|
+
nodeRunId,
|
|
849
|
+
activation: activationTick,
|
|
850
|
+
iteration,
|
|
851
|
+
attempt,
|
|
852
|
+
invocationKey,
|
|
853
|
+
code: error?.code ?? 'ADAPTER_FAILURE',
|
|
854
|
+
message: error instanceof Error ? error.message : String(error),
|
|
855
|
+
});
|
|
856
|
+
const retryClass = error?.code === 'TIMEOUT' ? 'timeout'
|
|
857
|
+
: error?.code === 'RATE_LIMIT' ? 'rate-limit'
|
|
858
|
+
: 'failure';
|
|
859
|
+
if (attempt < attempts && node.retry?.on.includes(retryClass)) realized.retries += 1;
|
|
860
|
+
else break;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
failed.add(node.id);
|
|
864
|
+
completed.delete(node.id);
|
|
865
|
+
throw lastError;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function predicateContext(extra = {}) {
|
|
869
|
+
return {
|
|
870
|
+
state: clone(state),
|
|
871
|
+
results: clone(results),
|
|
872
|
+
metrics: { ...clone(realized), ...extra },
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
async function routeFrom(nodeId) {
|
|
877
|
+
for (const [index, route] of manifest.spec.routes.entries()) {
|
|
878
|
+
if (route.from !== nodeId) continue;
|
|
879
|
+
const routeId = route.id ?? `route-${index}-${route.from}-${route.to}`;
|
|
880
|
+
const predicateMatched = await evaluatePredicate(route.when, predicateContext());
|
|
881
|
+
const guardMatched = predicateMatched && await evaluatePredicate(route.guard, predicateContext({
|
|
882
|
+
routeIterations: Number(routeActivations[routeId] ?? 0),
|
|
883
|
+
}));
|
|
884
|
+
const exhausted = Boolean(
|
|
885
|
+
guardMatched
|
|
886
|
+
&& route.maxIterations
|
|
887
|
+
&& Number(routeActivations[routeId] ?? 0) >= route.maxIterations,
|
|
888
|
+
);
|
|
889
|
+
const active = guardMatched && !exhausted;
|
|
890
|
+
let progress;
|
|
891
|
+
if (active && route.progress) {
|
|
892
|
+
const current = state[route.progress.state];
|
|
893
|
+
const previous = routeProgress[routeId];
|
|
894
|
+
const progressed = previous === undefined || current < previous;
|
|
895
|
+
progress = { state: route.progress.state, direction: 'decrease', previous, current, progressed };
|
|
896
|
+
routeProgress[routeId] = current;
|
|
897
|
+
if (!progressed) {
|
|
898
|
+
terminal = {
|
|
899
|
+
code: 'CYCLE_PROGRESS_STALLED',
|
|
900
|
+
reason: `route '${routeId}' progress state '${route.progress.state}' did not strictly decrease (${previous} -> ${current})`,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (active && !terminal) {
|
|
905
|
+
activatedRoutes.add(routeId);
|
|
906
|
+
routeActivations[routeId] = Number(routeActivations[routeId] ?? 0) + 1;
|
|
907
|
+
// Guarded finite routes are explicit feedback edges. A completed target
|
|
908
|
+
// must become runnable again; ordinary DAG routes stay single-shot.
|
|
909
|
+
if (route.guard || route.maxIterations || reactivatedNodes.has(nodeId)) {
|
|
910
|
+
completed.delete(route.to);
|
|
911
|
+
failed.delete(route.to);
|
|
912
|
+
skipped.delete(route.to);
|
|
913
|
+
reactivatedNodes.add(route.to);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
await emit('route-evaluated', {
|
|
917
|
+
routeId,
|
|
918
|
+
edgeId: `${graphId}:route:${routeId}:${route.from}->${route.to}`,
|
|
919
|
+
from: route.from,
|
|
920
|
+
to: route.to,
|
|
921
|
+
active: Boolean(active),
|
|
922
|
+
predicateMatched: Boolean(predicateMatched),
|
|
923
|
+
guardMatched: Boolean(guardMatched),
|
|
924
|
+
exhausted,
|
|
925
|
+
iteration: Number(routeActivations[routeId] ?? 0),
|
|
926
|
+
maxIterations: route.maxIterations,
|
|
927
|
+
progress,
|
|
928
|
+
});
|
|
929
|
+
if (terminal?.code === 'CYCLE_PROGRESS_STALLED') return;
|
|
930
|
+
if (exhausted) {
|
|
931
|
+
terminal = {
|
|
932
|
+
code: 'CYCLE_GUARD_EXHAUSTED',
|
|
933
|
+
reason: `route '${routeId}' exhausted maxIterations ${route.maxIterations}`,
|
|
934
|
+
};
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function phaseIndex(node) {
|
|
941
|
+
if (!node.phase) return 0;
|
|
942
|
+
const phases = [...new Set(manifest.spec.nodes.map((item) => item.phase).filter(Boolean))];
|
|
943
|
+
return phases.indexOf(node.phase) + 1;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function incomingRoutes(nodeId) {
|
|
947
|
+
return manifest.spec.routes.map((route, index) => ({ route, index })).filter(({ route }) => route.to === nodeId);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function joinFor(nodeId) {
|
|
951
|
+
return manifest.spec.joins.find((join) => join.target === nodeId);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function recordJoinEvidence(join) {
|
|
955
|
+
if (!join) return;
|
|
956
|
+
const resources = join.sources.reduce((total, source) => {
|
|
957
|
+
const usage = evidence.nodes[source]?.resources ?? emptyUsage();
|
|
958
|
+
total.runs += usage.runs;
|
|
959
|
+
for (const key of ['tokens', 'costUsd', 'timeMs']) total[key] = total[key] === null || usage[key] === null ? null : total[key] + usage[key];
|
|
960
|
+
return total;
|
|
961
|
+
}, emptyUsage());
|
|
962
|
+
evidence.joins[join.id] = { sources: [...join.sources], resources };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function ordinaryJoinReady(join) {
|
|
966
|
+
const succeeded = join.sources.filter((id) => completed.has(id)).length;
|
|
967
|
+
if (join.policy.mode === 'all') return succeeded === join.sources.length;
|
|
968
|
+
if (join.policy.mode === 'quorum') return succeeded >= join.policy.quorum;
|
|
969
|
+
if (join.policy.mode === 'fixed') return succeeded >= join.policy.count;
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function nodeReady(node) {
|
|
974
|
+
if (completed.has(node.id) || failed.has(node.id) || skipped.has(node.id)) return false;
|
|
975
|
+
const join = joinFor(node.id);
|
|
976
|
+
if (join && !['lcm', 'converged', 'budget'].includes(join.policy.mode)) return ordinaryJoinReady(join);
|
|
977
|
+
if (join) return false;
|
|
978
|
+
if ((node.dependsOn ?? []).some((id) => !completed.has(id) && !skipped.has(id))) return false;
|
|
979
|
+
const incoming = incomingRoutes(node.id);
|
|
980
|
+
// Entry nodes run once before any feedback edge into them is activated.
|
|
981
|
+
if (manifest.spec.entry.includes(node.id) && !results[node.id]) return true;
|
|
982
|
+
if (incoming.length === 0) return manifest.spec.entry.includes(node.id) || (node.dependsOn?.length ?? 0) > 0;
|
|
983
|
+
return incoming.some(({ route, index }) => activatedRoutes.has(route.id ?? `route-${index}-${route.from}-${route.to}`));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function activation(nodesToRun, tick, iteration = 1) {
|
|
987
|
+
realized.activations = Math.max(realized.activations, tick);
|
|
988
|
+
const parallelEligible = nodesToRun.length > 1
|
|
989
|
+
&& typeof options.parallelDispatch === 'function'
|
|
990
|
+
&& nodesToRun.every((node) => node.sideEffectMode === 'none' && (node.retry?.limit ?? 0) === 0);
|
|
991
|
+
realized.maxConcurrency = Math.max(realized.maxConcurrency, parallelEligible ? nodesToRun.length : Math.min(1, nodesToRun.length));
|
|
992
|
+
await emit('activation-started', {
|
|
993
|
+
activation: tick,
|
|
994
|
+
nodeIds: nodesToRun.map((node) => node.id),
|
|
995
|
+
inputSnapshotDigests: Object.fromEntries(nodesToRun.map((node) => [node.id, digest(inputsFor(node))])),
|
|
996
|
+
});
|
|
997
|
+
for (const node of nodesToRun) recordJoinEvidence(joinFor(node.id));
|
|
998
|
+
let settled;
|
|
999
|
+
if (parallelEligible && managed) {
|
|
1000
|
+
const requests = nodesToRun.map((node) => {
|
|
1001
|
+
enforceAuthority(node);
|
|
1002
|
+
const inputs = inputsFor(node);
|
|
1003
|
+
validateInputs(node, inputs);
|
|
1004
|
+
const requested = requestedResources(node);
|
|
1005
|
+
for (const key of ['tokens', 'costUsd', 'timeMs']) {
|
|
1006
|
+
const share = (manifest.spec.ceilings[key] - accounting.known[key] - accounting.reserved[key]) / nodesToRun.length;
|
|
1007
|
+
requested[key] = Math.min(requested[key], key === 'costUsd' ? share : Math.floor(share));
|
|
1008
|
+
if (requested[key] <= 0) throw new FlowGraphRuntimeError('RESOURCE_CEILING_EXHAUSTED', `Managed parallel Flow has no remaining ${key} allowance.`);
|
|
1009
|
+
}
|
|
1010
|
+
const nodeRunId = `${runId}:${node.id}:${tick}:${iteration}`;
|
|
1011
|
+
return { graphId, runId, node: clone(node), nodeRunId, activationId: `${runId}:activation:${tick}`,
|
|
1012
|
+
activation: tick, iteration, attempt: 1, invocationKey: nodeRunId,
|
|
1013
|
+
inputs: clone(inputs), state: clone(state), requestedResources: requested, runResources: clone(manifest.spec.ceilings), invocationId: `${nodeRunId}:1` };
|
|
1014
|
+
});
|
|
1015
|
+
for (const request of requests) accounting.pending[`${request.nodeRunId}:1`] = { nodeId: request.node.id, requestedResources: clone(request.requestedResources) };
|
|
1016
|
+
await persist();
|
|
1017
|
+
const observations = new Map();
|
|
1018
|
+
const pending = new Map();
|
|
1019
|
+
let rawResults;
|
|
1020
|
+
let dispatchError;
|
|
1021
|
+
for (const request of requests) await emit('node-started', {
|
|
1022
|
+
nodeId: request.node.id, nodeRunId: request.nodeRunId, activation: tick, iteration,
|
|
1023
|
+
attempt: 1, invocationKey: request.invocationKey, inputDigest: digest(request.inputs), delegated: 'parallel-dispatch',
|
|
1024
|
+
});
|
|
1025
|
+
try {
|
|
1026
|
+
rawResults = await options.parallelDispatch(clone(requests), (request) => {
|
|
1027
|
+
const original = requests.find((item) => item.nodeRunId === request.nodeRunId);
|
|
1028
|
+
if (!original || pending.has(original.nodeRunId)) throw new FlowGraphRuntimeError('PARALLEL_RESULT_MISMATCH', 'Managed parallel dispatch must invoke each planned request once.');
|
|
1029
|
+
const call = Promise.resolve().then(() => options.invokeNode(clone(original))).then(
|
|
1030
|
+
(raw) => { observations.set(original.nodeRunId, { raw }); return raw; },
|
|
1031
|
+
(error) => { observations.set(original.nodeRunId, { error }); throw error; },
|
|
1032
|
+
);
|
|
1033
|
+
pending.set(original.nodeRunId, call);
|
|
1034
|
+
return call;
|
|
1035
|
+
});
|
|
1036
|
+
if (!Array.isArray(rawResults) || rawResults.length !== requests.length || pending.size !== requests.length) {
|
|
1037
|
+
throw new FlowGraphRuntimeError('PARALLEL_RESULT_MISMATCH', 'Managed parallelDispatch must invoke every planned request through the supplied execution owner.');
|
|
1038
|
+
}
|
|
1039
|
+
} catch (error) { dispatchError = error; }
|
|
1040
|
+
// Promise.all may reject before other dispatched attempts settle. Retain
|
|
1041
|
+
// every observed spend before returning control to the graph owner.
|
|
1042
|
+
await Promise.allSettled([...pending.values()]);
|
|
1043
|
+
settled = [];
|
|
1044
|
+
for (const [index, request] of requests.entries()) {
|
|
1045
|
+
const node = nodesToRun[index];
|
|
1046
|
+
const observation = observations.get(request.nodeRunId);
|
|
1047
|
+
const raw = observation?.raw;
|
|
1048
|
+
const error = observation?.error ?? (!raw ? dispatchError : undefined);
|
|
1049
|
+
const usage = await chargeAttempt(node, `${request.nodeRunId}:1`, raw ?? error, request.requestedResources);
|
|
1050
|
+
delete accounting.pending[`${request.nodeRunId}:1`];
|
|
1051
|
+
try {
|
|
1052
|
+
if (error) throw error;
|
|
1053
|
+
checkChargedCeilings(node, usage, request.requestedResources);
|
|
1054
|
+
const outputs = clone(raw?.outputs ?? {});
|
|
1055
|
+
applyOutputs(node, outputs);
|
|
1056
|
+
const result = { outputs, usage, adapter: options.adapterId ?? 'unspecified', admission: clone(raw?.managedAdmission), nodeRunId: request.nodeRunId, invocationKey: request.invocationKey,
|
|
1057
|
+
...(raw?.score === undefined ? {} : { score: raw.score }), ...(raw?.citations === undefined ? {} : { citations: clone(raw.citations) }) };
|
|
1058
|
+
evidence.nodes[node.id].scope = mergedScopeEvidence(evidence.nodes[node.id]?.scope, node.scope, publicTouches(raw?.observedTouches), raw?.observationComplete === true);
|
|
1059
|
+
results[node.id] = result;
|
|
1060
|
+
completed.add(node.id);
|
|
1061
|
+
failed.delete(node.id);
|
|
1062
|
+
await emit('node-completed', { nodeId: node.id, nodeRunId: request.nodeRunId, activation: tick, iteration,
|
|
1063
|
+
attempt: 1, invocationKey: request.invocationKey, delegated: 'parallel-dispatch', usage, ...tracePayload(node, request.inputs, outputs) });
|
|
1064
|
+
settled.push({ node, result });
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
failed.add(node.id);
|
|
1067
|
+
completed.delete(node.id);
|
|
1068
|
+
settled.push({ node, error });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
await persist();
|
|
1072
|
+
} else if (parallelEligible) {
|
|
1073
|
+
const requests = nodesToRun.map((node) => {
|
|
1074
|
+
enforceAuthority(node);
|
|
1075
|
+
const inputs = inputsFor(node);
|
|
1076
|
+
validateInputs(node, inputs);
|
|
1077
|
+
const nodeRunId = `${runId}:${node.id}:${tick}:${iteration}`;
|
|
1078
|
+
return {
|
|
1079
|
+
graphId,
|
|
1080
|
+
runId,
|
|
1081
|
+
node: clone(node),
|
|
1082
|
+
nodeRunId,
|
|
1083
|
+
activationId: `${runId}:activation:${tick}`,
|
|
1084
|
+
activation: tick,
|
|
1085
|
+
iteration,
|
|
1086
|
+
attempt: 1,
|
|
1087
|
+
invocationKey: nodeRunId,
|
|
1088
|
+
inputs: clone(inputs),
|
|
1089
|
+
state: clone(state),
|
|
1090
|
+
requestedResources: clone(node.ceilings ?? manifest.spec.ceilings),
|
|
1091
|
+
};
|
|
1092
|
+
});
|
|
1093
|
+
for (const request of requests) {
|
|
1094
|
+
await emit('node-started', {
|
|
1095
|
+
nodeId: request.node.id,
|
|
1096
|
+
nodeRunId: request.nodeRunId,
|
|
1097
|
+
activation: tick,
|
|
1098
|
+
iteration,
|
|
1099
|
+
attempt: 1,
|
|
1100
|
+
invocationKey: request.invocationKey,
|
|
1101
|
+
inputDigest: digest(request.inputs),
|
|
1102
|
+
delegated: 'parallel-dispatch',
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
try {
|
|
1106
|
+
const rawResults = await options.parallelDispatch(clone(requests), options.invokeNode);
|
|
1107
|
+
if (Array.isArray(rawResults)) rawResults.forEach(assertNoWorkerResult);
|
|
1108
|
+
if (!Array.isArray(rawResults) || rawResults.length !== requests.length) {
|
|
1109
|
+
throw new FlowGraphRuntimeError('PARALLEL_RESULT_MISMATCH', 'parallelDispatch must return one ordered result per request.');
|
|
1110
|
+
}
|
|
1111
|
+
settled = [];
|
|
1112
|
+
for (let index = 0; index < requests.length; index += 1) {
|
|
1113
|
+
const request = requests[index];
|
|
1114
|
+
const node = nodesToRun[index];
|
|
1115
|
+
const raw = rawResults[index];
|
|
1116
|
+
const outputs = clone(raw?.outputs ?? {});
|
|
1117
|
+
const usage = usageOf(raw);
|
|
1118
|
+
assertUsage(usage);
|
|
1119
|
+
for (const key of ['tokens', 'costUsd', 'timeMs']) {
|
|
1120
|
+
const graphCeiling = manifest.spec.ceilings[key];
|
|
1121
|
+
const nodeCeiling = node.ceilings?.[key] ?? graphCeiling;
|
|
1122
|
+
if (usage[key] > nodeCeiling || realized[key] + usage[key] > graphCeiling) {
|
|
1123
|
+
throw new FlowGraphRuntimeError('RESOURCE_CEILING_WOULD_EXCEED', `Node '${node.id}' would exceed its ${key} ceiling.`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
applyOutputs(node, outputs);
|
|
1127
|
+
realized.nodeRuns += 1;
|
|
1128
|
+
realized.tokens += usage.tokens;
|
|
1129
|
+
realized.costUsd += usage.costUsd;
|
|
1130
|
+
realized.timeMs += usage.timeMs;
|
|
1131
|
+
const nodeUsage = evidence.nodes[node.id]?.resources ?? emptyUsage();
|
|
1132
|
+
addUsage(nodeUsage, usage);
|
|
1133
|
+
const branchId = node.track ?? 'default';
|
|
1134
|
+
const branchUsage = evidence.branches[branchId] ?? emptyUsage();
|
|
1135
|
+
addUsage(branchUsage, usage);
|
|
1136
|
+
evidence.branches[branchId] = branchUsage;
|
|
1137
|
+
evidence.nodes[node.id] = {
|
|
1138
|
+
...(evidence.nodes[node.id] ?? {}),
|
|
1139
|
+
resources: nodeUsage,
|
|
1140
|
+
scope: mergedScopeEvidence(
|
|
1141
|
+
evidence.nodes[node.id]?.scope,
|
|
1142
|
+
node.scope,
|
|
1143
|
+
publicTouches(raw?.observedTouches),
|
|
1144
|
+
raw?.observationComplete === true,
|
|
1145
|
+
),
|
|
1146
|
+
};
|
|
1147
|
+
const result = {
|
|
1148
|
+
outputs,
|
|
1149
|
+
usage,
|
|
1150
|
+
...(raw?.score === undefined ? {} : { score: raw.score }),
|
|
1151
|
+
...(raw?.citations === undefined ? {} : { citations: clone(raw.citations) }),
|
|
1152
|
+
adapter: options.adapterId ?? 'unspecified',
|
|
1153
|
+
nodeRunId: request.nodeRunId,
|
|
1154
|
+
invocationKey: request.invocationKey,
|
|
1155
|
+
};
|
|
1156
|
+
results[node.id] = result;
|
|
1157
|
+
completed.add(node.id);
|
|
1158
|
+
await emit('node-completed', {
|
|
1159
|
+
nodeId: node.id,
|
|
1160
|
+
nodeRunId: request.nodeRunId,
|
|
1161
|
+
activation: tick,
|
|
1162
|
+
iteration,
|
|
1163
|
+
attempt: 1,
|
|
1164
|
+
invocationKey: request.invocationKey,
|
|
1165
|
+
delegated: 'parallel-dispatch',
|
|
1166
|
+
usage,
|
|
1167
|
+
score: raw?.score,
|
|
1168
|
+
citations: clone(raw?.citations),
|
|
1169
|
+
...tracePayload(node, request.inputs, outputs),
|
|
1170
|
+
});
|
|
1171
|
+
settled.push({ node, result });
|
|
1172
|
+
}
|
|
1173
|
+
await persist();
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
settled = nodesToRun.map((node) => ({ node, error }));
|
|
1176
|
+
}
|
|
1177
|
+
} else {
|
|
1178
|
+
settled = [];
|
|
1179
|
+
for (const node of nodesToRun) {
|
|
1180
|
+
try {
|
|
1181
|
+
settled.push({ node, result: await invoke(node, tick, iteration) });
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
settled.push({ node, error });
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
for (const item of settled) {
|
|
1188
|
+
if (item.result) await routeFrom(item.node.id);
|
|
1189
|
+
else await handleFailure(item.node, item.error, tick);
|
|
1190
|
+
}
|
|
1191
|
+
await emit('activation-completed', { activation: tick, nodeIds: nodesToRun.map((node) => node.id) });
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
async function handleFailure(node, error, tick) {
|
|
1195
|
+
if (error?.code === 'APPROVAL_REQUIRED') {
|
|
1196
|
+
failed.delete(node.id);
|
|
1197
|
+
await emit('run-paused', { nodeId: node.id, activation: tick, code: error.code, reason: error.message });
|
|
1198
|
+
terminal = { code: error.code, reason: error.message, status: 'paused' };
|
|
1199
|
+
await persist();
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (error?.code === 'APPROVAL_CANCELLED') {
|
|
1203
|
+
failed.delete(node.id);
|
|
1204
|
+
await emit('run-cancelled', { nodeId: node.id, activation: tick, code: error.code, reason: error.message });
|
|
1205
|
+
terminal = { code: error.code, reason: error.message, status: 'cancelled' };
|
|
1206
|
+
await persist();
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (manifest.spec.failure.onNodeFailure === 'skip-optional' && node.optional === true) {
|
|
1210
|
+
failed.delete(node.id);
|
|
1211
|
+
skipped.add(node.id);
|
|
1212
|
+
await emit('node-skipped', { nodeId: node.id, activation: tick, reason: 'optional node failed' });
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (manifest.spec.failure.onNodeFailure === 'continue') {
|
|
1216
|
+
failed.delete(node.id);
|
|
1217
|
+
skipped.add(node.id);
|
|
1218
|
+
await emit('node-skipped', { nodeId: node.id, activation: tick, reason: 'failure policy continued' });
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
const fallbackId = node.fallback?.node;
|
|
1222
|
+
if (fallbackId && manifest.spec.failure.onNodeFailure === 'fallback') {
|
|
1223
|
+
const fallback = nodes.get(fallbackId);
|
|
1224
|
+
await emit('fallback-started', { nodeId: node.id, fallbackNodeId: fallbackId, activation: tick });
|
|
1225
|
+
await invoke(fallback, tick, 1);
|
|
1226
|
+
completed.add(node.id);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
await emit('node-failed', {
|
|
1230
|
+
nodeId: node.id,
|
|
1231
|
+
activation: tick,
|
|
1232
|
+
code: error?.code ?? 'NODE_FAILED',
|
|
1233
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1234
|
+
});
|
|
1235
|
+
if (manifest.spec.failure.onNodeFailure === 'fail' || failed.size > manifest.spec.failure.maxFailures) {
|
|
1236
|
+
terminal = { code: error?.code ?? 'NODE_FAILED', reason: `node '${node.id}' failed` };
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async function temporalJoin(join) {
|
|
1241
|
+
const sourceNodes = join.sources.map((id) => nodes.get(id));
|
|
1242
|
+
const policy = join.policy;
|
|
1243
|
+
const maximum = policy.mode === 'lcm'
|
|
1244
|
+
? policy.periods.reduce((value, next) => {
|
|
1245
|
+
const gcd = (a, b) => (b ? gcd(b, a % b) : a);
|
|
1246
|
+
return (value * next) / gcd(value, next);
|
|
1247
|
+
}, 1)
|
|
1248
|
+
: policy.maxIterations ?? manifest.spec.ceilings.activations;
|
|
1249
|
+
let satisfied = false;
|
|
1250
|
+
let reason = 'ceiling';
|
|
1251
|
+
for (let iteration = 1; iteration <= maximum && !terminal; iteration += 1) {
|
|
1252
|
+
const tick = realized.activations + 1;
|
|
1253
|
+
if (tick > manifest.spec.ceilings.activations) break;
|
|
1254
|
+
let activeSources = sourceNodes;
|
|
1255
|
+
if (policy.mode === 'lcm') activeSources = sourceNodes.filter((_, index) => iteration % policy.periods[index] === 0);
|
|
1256
|
+
if (activeSources.length > 0) {
|
|
1257
|
+
for (const source of activeSources) {
|
|
1258
|
+
completed.delete(source.id);
|
|
1259
|
+
failed.delete(source.id);
|
|
1260
|
+
}
|
|
1261
|
+
await activation(activeSources, tick, iteration);
|
|
1262
|
+
} else {
|
|
1263
|
+
realized.activations = tick;
|
|
1264
|
+
await emit('activation-completed', { activation: tick, nodeIds: [] });
|
|
1265
|
+
}
|
|
1266
|
+
if (policy.mode === 'lcm') {
|
|
1267
|
+
satisfied = activeSources.length === sourceNodes.length;
|
|
1268
|
+
reason = satisfied ? `lcm:${maximum}` : 'period-pending';
|
|
1269
|
+
} else if (policy.mode === 'converged') {
|
|
1270
|
+
satisfied = Boolean(await evaluatePredicate(policy.predicate, predicateContext({ iteration })));
|
|
1271
|
+
reason = satisfied ? 'converged' : 'threshold-not-met';
|
|
1272
|
+
} else {
|
|
1273
|
+
satisfied = realized.costUsd >= policy.maxCostUsd;
|
|
1274
|
+
reason = satisfied ? 'budget-reached' : 'budget-available';
|
|
1275
|
+
}
|
|
1276
|
+
await emit('join-evaluated', {
|
|
1277
|
+
joinId: join.id,
|
|
1278
|
+
edgeIds: join.sources.map((source) => `${graphId}:join:${join.id}:${source}->${join.target}`),
|
|
1279
|
+
activation: realized.activations,
|
|
1280
|
+
iteration,
|
|
1281
|
+
policy: policy.mode,
|
|
1282
|
+
satisfied,
|
|
1283
|
+
reason,
|
|
1284
|
+
resources: join.sources.reduce((total, source) => {
|
|
1285
|
+
const usage = evidence.nodes[source]?.resources ?? emptyUsage();
|
|
1286
|
+
total.runs += usage.runs;
|
|
1287
|
+
for (const key of ['tokens', 'costUsd', 'timeMs']) total[key] = total[key] === null || usage[key] === null ? null : total[key] + usage[key];
|
|
1288
|
+
return total;
|
|
1289
|
+
}, emptyUsage()),
|
|
1290
|
+
});
|
|
1291
|
+
if (satisfied) break;
|
|
1292
|
+
const exceeded = resourcesExceeded(realized, manifest.spec.ceilings);
|
|
1293
|
+
if (exceeded) break;
|
|
1294
|
+
}
|
|
1295
|
+
const joinResources = join.sources.reduce((total, source) => {
|
|
1296
|
+
const usage = evidence.nodes[source]?.resources ?? emptyUsage();
|
|
1297
|
+
total.runs += usage.runs;
|
|
1298
|
+
for (const key of ['tokens', 'costUsd', 'timeMs']) total[key] = total[key] === null || usage[key] === null ? null : total[key] + usage[key];
|
|
1299
|
+
return total;
|
|
1300
|
+
}, emptyUsage());
|
|
1301
|
+
evidence.joins[join.id] = { sources: [...join.sources], resources: joinResources };
|
|
1302
|
+
joinState[join.id] = { satisfied, reason, activation: realized.activations, resources: joinResources };
|
|
1303
|
+
if (satisfied) {
|
|
1304
|
+
const target = nodes.get(join.target);
|
|
1305
|
+
completed.delete(target.id);
|
|
1306
|
+
try {
|
|
1307
|
+
await invoke(target, realized.activations, 1);
|
|
1308
|
+
await routeFrom(target.id);
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
await handleFailure(target, error, realized.activations);
|
|
1311
|
+
}
|
|
1312
|
+
} else if (!terminal) {
|
|
1313
|
+
terminal = { code: 'JOIN_CEILING_EXHAUSTED', reason: `join '${join.id}' stopped: ${reason}` };
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
await emit('run-started', {
|
|
1318
|
+
requestedResources: clone(manifest.spec.ceilings),
|
|
1319
|
+
adapter: options.adapterId ?? 'unspecified',
|
|
1320
|
+
resumed: Boolean(options.resumeFrom),
|
|
1321
|
+
});
|
|
1322
|
+
if (options.operatorAction === 'stop') {
|
|
1323
|
+
await emit('run-cancelled', { code: 'OPERATOR_STOPPED', reason: 'operator requested stop' });
|
|
1324
|
+
terminal = { code: 'OPERATOR_STOPPED', reason: 'operator requested stop', status: 'cancelled' };
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
const temporal = manifest.spec.joins.filter((join) => ['lcm', 'converged', 'budget'].includes(join.policy.mode));
|
|
1328
|
+
const temporalSources = new Set(temporal.flatMap((join) => join.sources));
|
|
1329
|
+
for (const join of temporal) {
|
|
1330
|
+
if (join.sources.some((id) => completed.has(id)) && joinState[join.id]?.satisfied) continue;
|
|
1331
|
+
await temporalJoin(join);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
while (!terminal) {
|
|
1335
|
+
const candidates = manifest.spec.nodes
|
|
1336
|
+
.filter((node) => !temporalSources.has(node.id) && nodeReady(node))
|
|
1337
|
+
.sort((left, right) => phaseIndex(left) - phaseIndex(right)
|
|
1338
|
+
|| String(left.track ?? '').localeCompare(String(right.track ?? ''))
|
|
1339
|
+
|| nodeOrder.get(left.id) - nodeOrder.get(right.id));
|
|
1340
|
+
if (candidates.length === 0) break;
|
|
1341
|
+
const minimumPhase = phaseIndex(candidates[0]);
|
|
1342
|
+
const wave = candidates.filter((node) => phaseIndex(node) === minimumPhase).slice(0, manifest.spec.ceilings.concurrency);
|
|
1343
|
+
const nextTick = realized.activations + 1;
|
|
1344
|
+
if (nextTick > manifest.spec.ceilings.activations) {
|
|
1345
|
+
terminal = { code: 'ACTIVATION_CEILING_EXHAUSTED', reason: 'activation ceiling exhausted' };
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
await activation(wave, nextTick);
|
|
1349
|
+
const exceeded = resourcesExceeded(realized, manifest.spec.ceilings);
|
|
1350
|
+
const workRemains = manifest.spec.nodes.some((node) => !completed.has(node.id) && !failed.has(node.id) && !skipped.has(node.id));
|
|
1351
|
+
if (exceeded && workRemains) terminal = { code: 'RESOURCE_CEILING_EXHAUSTED', reason: `${exceeded} ceiling exhausted` };
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const unresolved = manifest.spec.nodes.filter((node) => !completed.has(node.id) && !failed.has(node.id) && !skipped.has(node.id));
|
|
1355
|
+
if (!terminal && unresolved.length > 0) {
|
|
1356
|
+
terminal = { code: 'NO_RUNNABLE_NODES', reason: `no runnable nodes remain: ${unresolved.map((node) => node.id).join(', ')}` };
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const output = resultOutput(results, manifest.spec.output.from);
|
|
1360
|
+
if (managed && !terminal) {
|
|
1361
|
+
try { values.check(manifest.spec.output.schema, output, 'INVALID_TYPED_OUTPUT', manifest.spec.output.from); }
|
|
1362
|
+
catch (error) { terminal = { code: error.code, reason: error.message }; }
|
|
1363
|
+
}
|
|
1364
|
+
let status = terminal?.status ?? (terminal ? 'failed' : 'completed');
|
|
1365
|
+
if (terminal && !terminal.status && manifest.spec.failure.onNodeFailure === 'partial-synthesis') status = 'partial';
|
|
1366
|
+
const publicOutput = status === 'completed' && output !== undefined
|
|
1367
|
+
? clone(output)
|
|
1368
|
+
: manifest.spec.output.mode === 'typed-terminal-failure'
|
|
1369
|
+
? { code: terminal?.code ?? 'OUTPUT_UNAVAILABLE', stopReason: terminal?.reason ?? 'terminal output unavailable' }
|
|
1370
|
+
: status === 'partial'
|
|
1371
|
+
? { partial: true, state: clone(state), stopReason: terminal.reason }
|
|
1372
|
+
: undefined;
|
|
1373
|
+
|
|
1374
|
+
await emit('run-completed', {
|
|
1375
|
+
status,
|
|
1376
|
+
stopReason: terminal?.reason ?? 'completed',
|
|
1377
|
+
requestedResources: clone(manifest.spec.ceilings),
|
|
1378
|
+
realizedResources: clone(realized),
|
|
1379
|
+
evidence: clone(evidence),
|
|
1380
|
+
outputDigest: publicOutput === undefined ? undefined : digest(publicOutput),
|
|
1381
|
+
});
|
|
1382
|
+
await persist();
|
|
1383
|
+
|
|
1384
|
+
return {
|
|
1385
|
+
schemaVersion: 'flow.aiwg.io/v1alpha1',
|
|
1386
|
+
kind: 'FlowGraphRunReport',
|
|
1387
|
+
graphId,
|
|
1388
|
+
runId,
|
|
1389
|
+
status,
|
|
1390
|
+
stopReason: terminal?.reason ?? 'completed',
|
|
1391
|
+
output: publicOutput,
|
|
1392
|
+
state: clone(state),
|
|
1393
|
+
results: clone(results),
|
|
1394
|
+
requestedResources: clone(manifest.spec.ceilings),
|
|
1395
|
+
realizedResources: clone(realized),
|
|
1396
|
+
...(managed ? { accounting: clone(accounting) } : {}),
|
|
1397
|
+
evidence: clone(evidence),
|
|
1398
|
+
trace: events,
|
|
1399
|
+
checkpoint: checkpoint(),
|
|
1400
|
+
};
|
|
1401
|
+
}
|