@executioncontrolprotocol/runtime 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/context-loader.d.ts +31 -0
- package/dist/engine/context-loader.d.ts.map +1 -0
- package/dist/engine/context-loader.js +90 -0
- package/dist/engine/context-loader.js.map +1 -0
- package/dist/engine/index.d.ts +6 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +6 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/runner.d.ts +92 -0
- package/dist/engine/runner.d.ts.map +1 -0
- package/dist/engine/runner.js +852 -0
- package/dist/engine/runner.js.map +1 -0
- package/dist/engine/schema-validator.d.ts +32 -0
- package/dist/engine/schema-validator.d.ts.map +1 -0
- package/dist/engine/schema-validator.js +69 -0
- package/dist/engine/schema-validator.js.map +1 -0
- package/dist/engine/system-config-loader.d.ts +39 -0
- package/dist/engine/system-config-loader.d.ts.map +1 -0
- package/dist/engine/system-config-loader.js +80 -0
- package/dist/engine/system-config-loader.js.map +1 -0
- package/dist/engine/types.d.ts +324 -0
- package/dist/engine/types.d.ts.map +1 -0
- package/dist/engine/types.js +10 -0
- package/dist/engine/types.js.map +1 -0
- package/dist/evals/index.d.ts +3 -0
- package/dist/evals/index.d.ts.map +1 -0
- package/dist/evals/index.js +3 -0
- package/dist/evals/index.js.map +1 -0
- package/dist/evals/scorer.d.ts +23 -0
- package/dist/evals/scorer.d.ts.map +1 -0
- package/dist/evals/scorer.js +133 -0
- package/dist/evals/scorer.js.map +1 -0
- package/dist/evals/types.d.ts +128 -0
- package/dist/evals/types.d.ts.map +1 -0
- package/dist/evals/types.js +11 -0
- package/dist/evals/types.js.map +1 -0
- package/dist/extensions/builtin.d.ts +44 -0
- package/dist/extensions/builtin.d.ts.map +1 -0
- package/dist/extensions/builtin.js +66 -0
- package/dist/extensions/builtin.js.map +1 -0
- package/dist/extensions/index.d.ts +4 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/index.js +4 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/extensions/progress-loggers/file-logger.d.ts +27 -0
- package/dist/extensions/progress-loggers/file-logger.d.ts.map +1 -0
- package/dist/extensions/progress-loggers/file-logger.js +54 -0
- package/dist/extensions/progress-loggers/file-logger.js.map +1 -0
- package/dist/extensions/registry.d.ts +74 -0
- package/dist/extensions/registry.d.ts.map +1 -0
- package/dist/extensions/registry.js +126 -0
- package/dist/extensions/registry.js.map +1 -0
- package/dist/extensions/types.d.ts +78 -0
- package/dist/extensions/types.d.ts.map +1 -0
- package/dist/extensions/types.js +7 -0
- package/dist/extensions/types.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/mounts/hydrator.d.ts +46 -0
- package/dist/mounts/hydrator.d.ts.map +1 -0
- package/dist/mounts/hydrator.js +142 -0
- package/dist/mounts/hydrator.js.map +1 -0
- package/dist/mounts/index.d.ts +4 -0
- package/dist/mounts/index.d.ts.map +1 -0
- package/dist/mounts/index.js +4 -0
- package/dist/mounts/index.js.map +1 -0
- package/dist/mounts/interpolation.d.ts +33 -0
- package/dist/mounts/interpolation.d.ts.map +1 -0
- package/dist/mounts/interpolation.js +59 -0
- package/dist/mounts/interpolation.js.map +1 -0
- package/dist/mounts/types.d.ts +80 -0
- package/dist/mounts/types.d.ts.map +1 -0
- package/dist/mounts/types.js +10 -0
- package/dist/mounts/types.js.map +1 -0
- package/dist/policies/enforcer.d.ts +23 -0
- package/dist/policies/enforcer.d.ts.map +1 -0
- package/dist/policies/enforcer.js +111 -0
- package/dist/policies/enforcer.js.map +1 -0
- package/dist/policies/index.d.ts +3 -0
- package/dist/policies/index.d.ts.map +1 -0
- package/dist/policies/index.js +3 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/policies/types.d.ts +87 -0
- package/dist/policies/types.d.ts.map +1 -0
- package/dist/policies/types.js +11 -0
- package/dist/policies/types.js.map +1 -0
- package/dist/protocols/a2a/a2a-transport.d.ts +40 -0
- package/dist/protocols/a2a/a2a-transport.d.ts.map +1 -0
- package/dist/protocols/a2a/a2a-transport.js +212 -0
- package/dist/protocols/a2a/a2a-transport.js.map +1 -0
- package/dist/protocols/a2a/index.d.ts +3 -0
- package/dist/protocols/a2a/index.d.ts.map +1 -0
- package/dist/protocols/a2a/index.js +2 -0
- package/dist/protocols/a2a/index.js.map +1 -0
- package/dist/protocols/agent-transport.d.ts +101 -0
- package/dist/protocols/agent-transport.d.ts.map +1 -0
- package/dist/protocols/agent-transport.js +11 -0
- package/dist/protocols/agent-transport.js.map +1 -0
- package/dist/protocols/index.d.ts +5 -0
- package/dist/protocols/index.d.ts.map +1 -0
- package/dist/protocols/index.js +5 -0
- package/dist/protocols/index.js.map +1 -0
- package/dist/protocols/mcp/index.d.ts +2 -0
- package/dist/protocols/mcp/index.d.ts.map +1 -0
- package/dist/protocols/mcp/index.js +2 -0
- package/dist/protocols/mcp/index.js.map +1 -0
- package/dist/protocols/mcp/mcp-tool-invoker.d.ts +30 -0
- package/dist/protocols/mcp/mcp-tool-invoker.d.ts.map +1 -0
- package/dist/protocols/mcp/mcp-tool-invoker.js +121 -0
- package/dist/protocols/mcp/mcp-tool-invoker.js.map +1 -0
- package/dist/protocols/tool-invoker.d.ts +91 -0
- package/dist/protocols/tool-invoker.d.ts.map +1 -0
- package/dist/protocols/tool-invoker.js +11 -0
- package/dist/protocols/tool-invoker.js.map +1 -0
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/model-provider.d.ts +132 -0
- package/dist/providers/model-provider.d.ts.map +1 -0
- package/dist/providers/model-provider.js +10 -0
- package/dist/providers/model-provider.js.map +1 -0
- package/dist/providers/ollama/index.d.ts +3 -0
- package/dist/providers/ollama/index.d.ts.map +1 -0
- package/dist/providers/ollama/index.js +2 -0
- package/dist/providers/ollama/index.js.map +1 -0
- package/dist/providers/ollama/ollama-provider.d.ts +41 -0
- package/dist/providers/ollama/ollama-provider.d.ts.map +1 -0
- package/dist/providers/ollama/ollama-provider.js +113 -0
- package/dist/providers/ollama/ollama-provider.js.map +1 -0
- package/dist/providers/openai/index.d.ts +3 -0
- package/dist/providers/openai/index.d.ts.map +1 -0
- package/dist/providers/openai/index.js +2 -0
- package/dist/providers/openai/index.js.map +1 -0
- package/dist/providers/openai/openai-provider.d.ts +41 -0
- package/dist/providers/openai/openai-provider.d.ts.map +1 -0
- package/dist/providers/openai/openai-provider.js +150 -0
- package/dist/providers/openai/openai-provider.js.map +1 -0
- package/dist/testing/cassette.d.ts +88 -0
- package/dist/testing/cassette.d.ts.map +1 -0
- package/dist/testing/cassette.js +123 -0
- package/dist/testing/cassette.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-agent-transport.d.ts +49 -0
- package/dist/testing/mock-agent-transport.d.ts.map +1 -0
- package/dist/testing/mock-agent-transport.js +71 -0
- package/dist/testing/mock-agent-transport.js.map +1 -0
- package/dist/testing/mock-model-provider.d.ts +69 -0
- package/dist/testing/mock-model-provider.d.ts.map +1 -0
- package/dist/testing/mock-model-provider.js +92 -0
- package/dist/testing/mock-model-provider.js.map +1 -0
- package/dist/testing/mock-tool-invoker.d.ts +65 -0
- package/dist/testing/mock-tool-invoker.d.ts.map +1 -0
- package/dist/testing/mock-tool-invoker.js +85 -0
- package/dist/testing/mock-tool-invoker.js.map +1 -0
- package/dist/tracing/collector.d.ts +75 -0
- package/dist/tracing/collector.d.ts.map +1 -0
- package/dist/tracing/collector.js +106 -0
- package/dist/tracing/collector.js.map +1 -0
- package/dist/tracing/exporters/console.d.ts +17 -0
- package/dist/tracing/exporters/console.d.ts.map +1 -0
- package/dist/tracing/exporters/console.js +76 -0
- package/dist/tracing/exporters/console.js.map +1 -0
- package/dist/tracing/exporters/index.d.ts +4 -0
- package/dist/tracing/exporters/index.d.ts.map +1 -0
- package/dist/tracing/exporters/index.js +3 -0
- package/dist/tracing/exporters/index.js.map +1 -0
- package/dist/tracing/exporters/json-file.d.ts +30 -0
- package/dist/tracing/exporters/json-file.d.ts.map +1 -0
- package/dist/tracing/exporters/json-file.js +28 -0
- package/dist/tracing/exporters/json-file.js.map +1 -0
- package/dist/tracing/formatter.d.ts +16 -0
- package/dist/tracing/formatter.d.ts.map +1 -0
- package/dist/tracing/formatter.js +81 -0
- package/dist/tracing/formatter.js.map +1 -0
- package/dist/tracing/graph.d.ts +17 -0
- package/dist/tracing/graph.d.ts.map +1 -0
- package/dist/tracing/graph.js +116 -0
- package/dist/tracing/graph.js.map +1 -0
- package/dist/tracing/index.d.ts +6 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +6 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/tracing/types.d.ts +124 -0
- package/dist/tracing/types.d.ts.map +1 -0
- package/dist/tracing/types.js +11 -0
- package/dist/tracing/types.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECP Execution Engine — the core runner that orchestrates a Context
|
|
3
|
+
* execution from start to finish.
|
|
4
|
+
*
|
|
5
|
+
* Slice 1: Single executor, seed mounts, model generation, schema validation.
|
|
6
|
+
* Slice 2 (added in Milestone 7): Controller-specialist delegation.
|
|
7
|
+
*
|
|
8
|
+
* @category Engine
|
|
9
|
+
*/
|
|
10
|
+
import { loadContext, resolveInputs } from "./context-loader.js";
|
|
11
|
+
import { validateOutput } from "./schema-validator.js";
|
|
12
|
+
import { DefaultMountHydrator } from "../mounts/hydrator.js";
|
|
13
|
+
import { createPolicyEnforcer } from "../policies/enforcer.js";
|
|
14
|
+
/**
|
|
15
|
+
* The ECP execution engine.
|
|
16
|
+
*
|
|
17
|
+
* Wires together the model provider, tool invoker, and agent transport
|
|
18
|
+
* to execute a Context manifest end-to-end.
|
|
19
|
+
*
|
|
20
|
+
* @category Engine
|
|
21
|
+
*/
|
|
22
|
+
export class ECPEngine {
|
|
23
|
+
modelProvider;
|
|
24
|
+
extensionRegistry;
|
|
25
|
+
modelProviderCache = new Map();
|
|
26
|
+
toolInvoker;
|
|
27
|
+
agentTransport;
|
|
28
|
+
config;
|
|
29
|
+
hydrator;
|
|
30
|
+
traceCollector;
|
|
31
|
+
constructor(modelProvider, toolInvoker, agentTransport, config = {}) {
|
|
32
|
+
this.modelProvider = modelProvider;
|
|
33
|
+
this.extensionRegistry = config.extensions?.registry;
|
|
34
|
+
this.toolInvoker = toolInvoker;
|
|
35
|
+
this.agentTransport = agentTransport;
|
|
36
|
+
this.config = config;
|
|
37
|
+
this.hydrator = new DefaultMountHydrator(toolInvoker);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Attach a trace collector to the engine.
|
|
41
|
+
* When attached, the engine emits structured trace spans during execution.
|
|
42
|
+
*/
|
|
43
|
+
setTraceCollector(collector) {
|
|
44
|
+
this.traceCollector = collector;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Execute a Context manifest end-to-end.
|
|
48
|
+
*
|
|
49
|
+
* @param options - Run configuration.
|
|
50
|
+
* @returns The execution result.
|
|
51
|
+
*/
|
|
52
|
+
async run(options) {
|
|
53
|
+
const startTime = Date.now();
|
|
54
|
+
const state = await this.initRunState(options);
|
|
55
|
+
try {
|
|
56
|
+
await this.connectToolServers(state);
|
|
57
|
+
const strategy = this.getStrategy(state.context);
|
|
58
|
+
if (strategy === "single") {
|
|
59
|
+
await this.runSingleExecutor(state, options.signal);
|
|
60
|
+
}
|
|
61
|
+
else if (strategy === "sequential" || strategy === "delegate" || strategy === "swarm") {
|
|
62
|
+
await this.runControllerSpecialist(state, options.signal);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
throw new Error(`Unsupported orchestration strategy: "${strategy}"`);
|
|
66
|
+
}
|
|
67
|
+
state.status = "completed";
|
|
68
|
+
state.endedAt = new Date().toISOString();
|
|
69
|
+
await this.emitProgress(state, { type: "phase", status: "completed" });
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
state.status = "failed";
|
|
73
|
+
state.endedAt = new Date().toISOString();
|
|
74
|
+
await this.emitProgress(state, { type: "phase", status: "failed" });
|
|
75
|
+
this.log(state, "error", `Execution failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await this.toolInvoker.disconnectAll();
|
|
79
|
+
}
|
|
80
|
+
const result = this.buildResult(state, startTime);
|
|
81
|
+
if (this.traceCollector) {
|
|
82
|
+
const trace = this.traceCollector.buildTrace({
|
|
83
|
+
executionId: state.runId,
|
|
84
|
+
contextName: state.context.metadata.name,
|
|
85
|
+
contextVersion: state.context.metadata.version,
|
|
86
|
+
strategy: this.getStrategy(state.context),
|
|
87
|
+
startedAt: state.startedAt,
|
|
88
|
+
endedAt: state.endedAt ?? new Date().toISOString(),
|
|
89
|
+
durationMs: result.durationMs,
|
|
90
|
+
success: result.success,
|
|
91
|
+
error: result.error,
|
|
92
|
+
});
|
|
93
|
+
result.trace = trace;
|
|
94
|
+
await this.traceCollector.exportAll(trace);
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Slice 1: Single executor
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
async runSingleExecutor(state, signal) {
|
|
102
|
+
const entrypointName = this.getEntrypointName(state.context);
|
|
103
|
+
const executorState = state.executors.get(entrypointName);
|
|
104
|
+
if (!executorState) {
|
|
105
|
+
throw new Error(`Entrypoint executor "${entrypointName}" not found`);
|
|
106
|
+
}
|
|
107
|
+
state.status = "hydrating-seed";
|
|
108
|
+
await this.emitProgress(state, { type: "phase", status: "hydrating-seed" });
|
|
109
|
+
this.log(state, "info", `Hydrating seed mounts for "${entrypointName}"`);
|
|
110
|
+
await this.hydrateMounts(state, executorState, state.inputs, "seed");
|
|
111
|
+
state.status = "running-orchestrator";
|
|
112
|
+
await this.emitProgress(state, { type: "phase", status: "running-orchestrator" });
|
|
113
|
+
this.log(state, "info", `Running executor "${entrypointName}"`);
|
|
114
|
+
await this.runExecutor(executorState, state, signal);
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Slice 2: Controller-specialist
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
async runControllerSpecialist(state, signal) {
|
|
120
|
+
const entrypointName = this.getEntrypointName(state.context);
|
|
121
|
+
const orchestratorState = state.executors.get(entrypointName);
|
|
122
|
+
if (!orchestratorState) {
|
|
123
|
+
throw new Error(`Orchestrator executor "${entrypointName}" not found`);
|
|
124
|
+
}
|
|
125
|
+
// Phase 1: Hydrate orchestrator seed mounts
|
|
126
|
+
state.status = "hydrating-seed";
|
|
127
|
+
await this.emitProgress(state, { type: "phase", status: "hydrating-seed" });
|
|
128
|
+
this.log(state, "info", `Hydrating seed mounts for orchestrator "${entrypointName}"`);
|
|
129
|
+
await this.hydrateMounts(state, orchestratorState, state.inputs, "seed");
|
|
130
|
+
// Phase 2: Run orchestrator to produce plan
|
|
131
|
+
state.status = "running-orchestrator";
|
|
132
|
+
await this.emitProgress(state, { type: "phase", status: "running-orchestrator" });
|
|
133
|
+
this.log(state, "info", `Running orchestrator "${entrypointName}"`);
|
|
134
|
+
await this.runExecutor(orchestratorState, state, signal);
|
|
135
|
+
if (!orchestratorState.output) {
|
|
136
|
+
throw new Error("Orchestrator did not produce an output");
|
|
137
|
+
}
|
|
138
|
+
const plan = orchestratorState.output;
|
|
139
|
+
const delegations = (plan.delegate ?? []);
|
|
140
|
+
if (delegations.length === 0) {
|
|
141
|
+
this.log(state, "info", "No delegations in plan, execution complete");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Phase 3: Hydrate focus mounts for specialists and delegate tasks
|
|
145
|
+
state.status = "delegating";
|
|
146
|
+
await this.emitProgress(state, { type: "phase", status: "delegating" });
|
|
147
|
+
this.log(state, "info", `Delegating ${delegations.length} task(s) to specialists`);
|
|
148
|
+
for (const delegation of delegations) {
|
|
149
|
+
const specialistState = state.executors.get(delegation.executor);
|
|
150
|
+
if (!specialistState) {
|
|
151
|
+
this.log(state, "warn", `Specialist "${delegation.executor}" not found, skipping`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// Hydrate focus mounts using plan output as selector source
|
|
155
|
+
state.status = "hydrating-focus";
|
|
156
|
+
await this.emitProgress(state, { type: "phase", status: "hydrating-focus" });
|
|
157
|
+
await this.hydrateMounts(state, specialistState, state.inputs, "focus", plan);
|
|
158
|
+
state.status = "hydrating-deep";
|
|
159
|
+
await this.emitProgress(state, { type: "phase", status: "hydrating-deep" });
|
|
160
|
+
await this.hydrateMounts(state, specialistState, state.inputs, "deep", plan);
|
|
161
|
+
// Run specialist (locally or via A2A)
|
|
162
|
+
state.status = "running-specialist";
|
|
163
|
+
await this.emitProgress(state, { type: "phase", status: "running-specialist" });
|
|
164
|
+
const endpoint = this.config.agentEndpoints?.[delegation.executor];
|
|
165
|
+
if (endpoint) {
|
|
166
|
+
await this.delegateViaA2A(specialistState, delegation, endpoint, state);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
this.log(state, "info", `Running specialist "${delegation.executor}" locally`);
|
|
170
|
+
await this.runExecutor(specialistState, state, signal, delegation.task);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Phase 4: Merge outputs if there's a publisher/merger executor
|
|
174
|
+
const producesSchema = state.context.orchestration?.produces;
|
|
175
|
+
if (producesSchema) {
|
|
176
|
+
state.status = "merging";
|
|
177
|
+
await this.emitProgress(state, { type: "phase", status: "merging" });
|
|
178
|
+
await this.mergeOutputs(state, producesSchema, signal);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async delegateViaA2A(specialistState, delegation, endpoint, state) {
|
|
182
|
+
this.log(state, "info", `Delegating to "${delegation.executor}" via A2A at ${endpoint}`);
|
|
183
|
+
const stepNum = this.nextStep(state);
|
|
184
|
+
await this.emitProgress(state, {
|
|
185
|
+
type: "step_start",
|
|
186
|
+
step: stepNum,
|
|
187
|
+
kind: "delegation",
|
|
188
|
+
executorName: delegation.executor,
|
|
189
|
+
description: `Delegate to ${delegation.executor}`,
|
|
190
|
+
});
|
|
191
|
+
const startDel = Date.now();
|
|
192
|
+
const agentRef = {
|
|
193
|
+
name: delegation.executor,
|
|
194
|
+
endpoint,
|
|
195
|
+
};
|
|
196
|
+
const task = {
|
|
197
|
+
id: `${state.runId}-${delegation.executor}`,
|
|
198
|
+
executorName: delegation.executor,
|
|
199
|
+
task: delegation.task,
|
|
200
|
+
context: {
|
|
201
|
+
mountData: specialistState.mountOutputs.map((m) => ({
|
|
202
|
+
name: m.mountName,
|
|
203
|
+
data: m.data,
|
|
204
|
+
})),
|
|
205
|
+
},
|
|
206
|
+
hints: delegation.hints,
|
|
207
|
+
};
|
|
208
|
+
const result = await this.agentTransport.delegate(agentRef, task);
|
|
209
|
+
await this.emitProgress(state, {
|
|
210
|
+
type: "step_complete",
|
|
211
|
+
step: this.nextCompleteStep(state),
|
|
212
|
+
kind: "delegation",
|
|
213
|
+
executorName: delegation.executor,
|
|
214
|
+
description: `Delegate to ${delegation.executor}`,
|
|
215
|
+
durationMs: Date.now() - startDel,
|
|
216
|
+
});
|
|
217
|
+
specialistState.status = result.success ? "completed" : "failed";
|
|
218
|
+
specialistState.output = result.output;
|
|
219
|
+
specialistState.error = result.error;
|
|
220
|
+
}
|
|
221
|
+
async mergeOutputs(state, producesSchemaName, signal) {
|
|
222
|
+
const executorNames = [...state.executors.keys()];
|
|
223
|
+
const mergerName = executorNames.find((name) => {
|
|
224
|
+
const ex = state.executors.get(name);
|
|
225
|
+
const ref = ex.executor.outputSchemaRef?.replace("#/schemas/", "");
|
|
226
|
+
return ref === producesSchemaName && name !== this.getEntrypointName(state.context);
|
|
227
|
+
});
|
|
228
|
+
if (mergerName) {
|
|
229
|
+
const mergerState = state.executors.get(mergerName);
|
|
230
|
+
this.log(state, "info", `Running merger "${mergerName}"`);
|
|
231
|
+
const priorOutputs = {};
|
|
232
|
+
for (const [name, es] of state.executors) {
|
|
233
|
+
if (es.output && name !== mergerName) {
|
|
234
|
+
priorOutputs[name] = es.output;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
mergerState.mountOutputs.push({
|
|
238
|
+
mountName: "__prior_outputs",
|
|
239
|
+
stage: "seed",
|
|
240
|
+
data: priorOutputs,
|
|
241
|
+
itemCount: Object.keys(priorOutputs).length,
|
|
242
|
+
});
|
|
243
|
+
await this.runExecutor(mergerState, state, signal);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
this.log(state, "info", "No dedicated merger executor; orchestrator output is final");
|
|
247
|
+
}
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Executor runner (shared by all strategies)
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
async runExecutor(executorState, state, signal, taskOverride) {
|
|
252
|
+
const executor = executorState.executor;
|
|
253
|
+
executorState.status = "running";
|
|
254
|
+
const executorStepNum = this.nextStep(state);
|
|
255
|
+
await this.emitProgress(state, {
|
|
256
|
+
type: "step_start",
|
|
257
|
+
step: executorStepNum,
|
|
258
|
+
kind: "executor",
|
|
259
|
+
executorName: executor.name,
|
|
260
|
+
description: `Executor ${executor.name}`,
|
|
261
|
+
});
|
|
262
|
+
const modelProvider = this.resolveModelProvider(executor, state.context);
|
|
263
|
+
const enforcer = createPolicyEnforcer(executor.policies ?? {});
|
|
264
|
+
const startTime = Date.now();
|
|
265
|
+
const executorSpanId = this.traceCollector?.startSpan({
|
|
266
|
+
type: "executor",
|
|
267
|
+
executorName: executor.name,
|
|
268
|
+
});
|
|
269
|
+
const messages = this.buildMessages(executor, executorState, state.inputs, taskOverride);
|
|
270
|
+
const tools = await this.getAvailableTools(executor, modelProvider, enforcer, state);
|
|
271
|
+
const model = this.config.modelOverride ?? executor.model?.name ?? this.config.defaultModel ?? "gpt-4o";
|
|
272
|
+
const outputSchema = this.getOutputSchema(executor, state.context);
|
|
273
|
+
const currentMessages = [...messages];
|
|
274
|
+
const maxRounds = 10;
|
|
275
|
+
let lastTokenUsage = {
|
|
276
|
+
prompt: 0,
|
|
277
|
+
completion: 0,
|
|
278
|
+
total: 0,
|
|
279
|
+
};
|
|
280
|
+
let lastModel = model;
|
|
281
|
+
for (let round = 0; round < maxRounds; round++) {
|
|
282
|
+
const budgetCheck = enforcer.checkBudget(executorState.budgetUsage);
|
|
283
|
+
if (!budgetCheck.withinBudget) {
|
|
284
|
+
this.log(state, "warn", `Budget exceeded for "${executor.name}": ${budgetCheck.message}`);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
const modelStepNum = this.nextStep(state);
|
|
288
|
+
await this.emitProgress(state, {
|
|
289
|
+
type: "step_start",
|
|
290
|
+
step: modelStepNum,
|
|
291
|
+
kind: "model",
|
|
292
|
+
executorName: executor.name,
|
|
293
|
+
description: `Executor ${executor.name} — ${model}`,
|
|
294
|
+
});
|
|
295
|
+
const genSpanId = this.traceCollector?.startSpan({
|
|
296
|
+
type: "model-generation",
|
|
297
|
+
executorName: executor.name,
|
|
298
|
+
parentId: executorSpanId,
|
|
299
|
+
model,
|
|
300
|
+
});
|
|
301
|
+
const genStart = Date.now();
|
|
302
|
+
const result = await modelProvider.generate({
|
|
303
|
+
messages: currentMessages,
|
|
304
|
+
model,
|
|
305
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
306
|
+
temperature: this.config.defaultTemperature,
|
|
307
|
+
responseFormat: outputSchema ? {
|
|
308
|
+
type: "json-schema",
|
|
309
|
+
schema: this.schemaToJsonSchema(outputSchema, executor),
|
|
310
|
+
} : undefined,
|
|
311
|
+
signal,
|
|
312
|
+
});
|
|
313
|
+
const reasoning = result.finishReason === "tool-calls" ? result.content || undefined : undefined;
|
|
314
|
+
if (reasoning) {
|
|
315
|
+
await this.emitProgress(state, {
|
|
316
|
+
type: "executor_reasoning",
|
|
317
|
+
executorName: executor.name,
|
|
318
|
+
reasoning,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (genSpanId) {
|
|
322
|
+
this.traceCollector?.endSpan(genSpanId, {
|
|
323
|
+
tokens: {
|
|
324
|
+
prompt: result.usage.promptTokens,
|
|
325
|
+
completion: result.usage.completionTokens,
|
|
326
|
+
total: result.usage.totalTokens,
|
|
327
|
+
},
|
|
328
|
+
reasoning: reasoning ?? undefined,
|
|
329
|
+
output: result.finishReason !== "tool-calls" ? this.tryParseJson(result.content) : undefined,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
lastTokenUsage = {
|
|
333
|
+
prompt: lastTokenUsage.prompt + result.usage.promptTokens,
|
|
334
|
+
completion: lastTokenUsage.completion + result.usage.completionTokens,
|
|
335
|
+
total: lastTokenUsage.total + result.usage.totalTokens,
|
|
336
|
+
};
|
|
337
|
+
lastModel = model;
|
|
338
|
+
await this.emitProgress(state, {
|
|
339
|
+
type: "step_complete",
|
|
340
|
+
step: this.nextCompleteStep(state),
|
|
341
|
+
kind: "model",
|
|
342
|
+
executorName: executor.name,
|
|
343
|
+
description: `Executor ${executor.name} — ${model}`,
|
|
344
|
+
durationMs: Date.now() - genStart,
|
|
345
|
+
reasoning: reasoning ?? undefined,
|
|
346
|
+
});
|
|
347
|
+
if (result.finishReason === "tool-calls" && result.toolCalls.length > 0) {
|
|
348
|
+
currentMessages.push({
|
|
349
|
+
role: "assistant",
|
|
350
|
+
content: result.content,
|
|
351
|
+
});
|
|
352
|
+
const toolResults = await this.executeToolCalls(result.toolCalls, executor, enforcer, executorState, state, executorSpanId);
|
|
353
|
+
for (const tr of toolResults) {
|
|
354
|
+
currentMessages.push(tr);
|
|
355
|
+
}
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
executorState.budgetUsage.runtimeSeconds = (Date.now() - startTime) / 1000;
|
|
359
|
+
if (result.content) {
|
|
360
|
+
try {
|
|
361
|
+
const parsed = JSON.parse(result.content);
|
|
362
|
+
executorState.output = typeof parsed === "object" && parsed !== null
|
|
363
|
+
? parsed
|
|
364
|
+
: { result: parsed };
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
executorState.output = { result: result.content };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
if (executorState.output && outputSchema) {
|
|
373
|
+
const validation = validateOutput(executorState.output, outputSchema);
|
|
374
|
+
if (!validation.valid) {
|
|
375
|
+
this.log(state, "warn", `Output validation failed for "${executor.name}": ${validation.errors.join(", ")}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
executorState.status = executorState.output ? "completed" : "failed";
|
|
379
|
+
executorState.error = executorState.output ? undefined : "No output produced";
|
|
380
|
+
await this.emitProgress(state, {
|
|
381
|
+
type: "step_complete",
|
|
382
|
+
step: this.nextExecutorStep(state),
|
|
383
|
+
kind: "executor",
|
|
384
|
+
executorName: executor.name,
|
|
385
|
+
description: `Executor ${executor.name}`,
|
|
386
|
+
durationMs: Date.now() - startTime,
|
|
387
|
+
output: executorState.output,
|
|
388
|
+
tokens: lastTokenUsage.total > 0 ? lastTokenUsage : undefined,
|
|
389
|
+
model: lastModel,
|
|
390
|
+
});
|
|
391
|
+
if (executorSpanId) {
|
|
392
|
+
this.traceCollector?.endSpan(executorSpanId, {
|
|
393
|
+
output: executorState.output,
|
|
394
|
+
error: executorState.error,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
this.log(state, "info", `Executor "${executor.name}" ${executorState.status} (${executorState.budgetUsage.runtimeSeconds.toFixed(1)}s)`);
|
|
398
|
+
}
|
|
399
|
+
async executeToolCalls(toolCalls, _executor, enforcer, executorState, state, parentSpanId) {
|
|
400
|
+
const results = [];
|
|
401
|
+
for (const tc of toolCalls) {
|
|
402
|
+
executorState.budgetUsage.toolCalls++;
|
|
403
|
+
const [serverName, toolName] = this.parseToolName(tc.name);
|
|
404
|
+
const accessCheck = enforcer.checkToolAccess(tc.name);
|
|
405
|
+
if (!accessCheck.allowed) {
|
|
406
|
+
this.log(state, "warn", `Tool call denied: ${tc.name} — ${accessCheck.reason}`);
|
|
407
|
+
results.push({
|
|
408
|
+
role: "tool",
|
|
409
|
+
toolCallId: tc.id,
|
|
410
|
+
content: JSON.stringify({ error: accessCheck.reason }),
|
|
411
|
+
});
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const toolStepNum = this.nextStep(state);
|
|
415
|
+
await this.emitProgress(state, {
|
|
416
|
+
type: "step_start",
|
|
417
|
+
step: toolStepNum,
|
|
418
|
+
kind: "tool",
|
|
419
|
+
executorName: executorState.executor.name,
|
|
420
|
+
description: `Tool ${tc.name}`,
|
|
421
|
+
});
|
|
422
|
+
const toolStart = Date.now();
|
|
423
|
+
const toolSpanId = this.traceCollector?.startSpan({
|
|
424
|
+
type: "tool-call",
|
|
425
|
+
executorName: executorState.executor.name,
|
|
426
|
+
parentId: parentSpanId,
|
|
427
|
+
toolName: tc.name,
|
|
428
|
+
toolArgs: tc.arguments,
|
|
429
|
+
});
|
|
430
|
+
try {
|
|
431
|
+
const result = await this.toolInvoker.callTool(serverName, toolName, tc.arguments);
|
|
432
|
+
this.log(state, "debug", `Tool ${tc.name} returned (isError: ${result.isError})`);
|
|
433
|
+
if (toolSpanId) {
|
|
434
|
+
this.traceCollector?.endSpan(toolSpanId, {
|
|
435
|
+
toolResult: result.content,
|
|
436
|
+
toolIsError: result.isError,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
await this.emitProgress(state, {
|
|
440
|
+
type: "step_complete",
|
|
441
|
+
step: this.nextCompleteStep(state),
|
|
442
|
+
kind: "tool",
|
|
443
|
+
executorName: executorState.executor.name,
|
|
444
|
+
description: `Tool ${tc.name}`,
|
|
445
|
+
durationMs: Date.now() - toolStart,
|
|
446
|
+
});
|
|
447
|
+
results.push({
|
|
448
|
+
role: "tool",
|
|
449
|
+
toolCallId: tc.id,
|
|
450
|
+
content: typeof result.content === "string"
|
|
451
|
+
? result.content
|
|
452
|
+
: JSON.stringify(result.content),
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
if (toolSpanId) {
|
|
457
|
+
this.traceCollector?.endSpan(toolSpanId, {
|
|
458
|
+
error: err instanceof Error ? err.message : String(err),
|
|
459
|
+
toolIsError: true,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
await this.emitProgress(state, {
|
|
463
|
+
type: "step_complete",
|
|
464
|
+
step: this.nextCompleteStep(state),
|
|
465
|
+
kind: "tool",
|
|
466
|
+
executorName: executorState.executor.name,
|
|
467
|
+
description: `Tool ${tc.name}`,
|
|
468
|
+
durationMs: Date.now() - toolStart,
|
|
469
|
+
});
|
|
470
|
+
results.push({
|
|
471
|
+
role: "tool",
|
|
472
|
+
toolCallId: tc.id,
|
|
473
|
+
content: JSON.stringify({
|
|
474
|
+
error: err instanceof Error ? err.message : String(err),
|
|
475
|
+
}),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return results;
|
|
480
|
+
}
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// Helpers
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
async initRunState(options) {
|
|
485
|
+
const context = options.context ?? loadContext(options.contextPath);
|
|
486
|
+
const inputs = resolveInputs(context, options.inputs ?? {});
|
|
487
|
+
const state = {
|
|
488
|
+
runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
489
|
+
context,
|
|
490
|
+
inputs,
|
|
491
|
+
status: "loading",
|
|
492
|
+
executors: new Map(),
|
|
493
|
+
log: [],
|
|
494
|
+
startedAt: new Date().toISOString(),
|
|
495
|
+
};
|
|
496
|
+
for (const executor of this.getExecutionObjects(context)) {
|
|
497
|
+
state.executors.set(executor.name, {
|
|
498
|
+
executor,
|
|
499
|
+
status: "pending",
|
|
500
|
+
mountOutputs: [],
|
|
501
|
+
budgetUsage: { toolCalls: 0, runtimeSeconds: 0 },
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
this.log(state, "info", `Loaded Context "${context.metadata.name}" v${context.metadata.version} ` +
|
|
505
|
+
`(${state.executors.size} execution objects, strategy: ${this.getStrategy(context)})`);
|
|
506
|
+
state.progressStepCounter = 0;
|
|
507
|
+
state.progressCompleteCounter = 0;
|
|
508
|
+
state.progressExecutorStepCounter = 0;
|
|
509
|
+
await this.emitProgress(state, { type: "phase", status: "loading" });
|
|
510
|
+
return state;
|
|
511
|
+
}
|
|
512
|
+
async emitProgress(_state, event) {
|
|
513
|
+
const callbacks = this.config.onProgress
|
|
514
|
+
? Array.isArray(this.config.onProgress)
|
|
515
|
+
? this.config.onProgress
|
|
516
|
+
: [this.config.onProgress]
|
|
517
|
+
: [];
|
|
518
|
+
for (const cb of callbacks) {
|
|
519
|
+
try {
|
|
520
|
+
await cb(event);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// Ignore progress callback errors so they do not break the run.
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
nextStep(state) {
|
|
528
|
+
state.progressStepCounter = (state.progressStepCounter ?? 0) + 1;
|
|
529
|
+
return state.progressStepCounter;
|
|
530
|
+
}
|
|
531
|
+
/** Return the next completion step number (1, 2, 3...) for step_complete events. */
|
|
532
|
+
nextCompleteStep(state) {
|
|
533
|
+
state.progressCompleteCounter = (state.progressCompleteCounter ?? 0) + 1;
|
|
534
|
+
return state.progressCompleteCounter;
|
|
535
|
+
}
|
|
536
|
+
/** Return the next executor step number (1, 2, 3...) for executor step_complete only. */
|
|
537
|
+
nextExecutorStep(state) {
|
|
538
|
+
state.progressExecutorStepCounter = (state.progressExecutorStepCounter ?? 0) + 1;
|
|
539
|
+
return state.progressExecutorStepCounter;
|
|
540
|
+
}
|
|
541
|
+
async connectToolServers(state) {
|
|
542
|
+
const servers = this.config.toolServers ?? {};
|
|
543
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
544
|
+
try {
|
|
545
|
+
await this.toolInvoker.connect({ name, transport: config.transport });
|
|
546
|
+
this.log(state, "info", `Connected to tool server "${name}"`);
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
this.log(state, "warn", `Failed to connect to tool server "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async hydrateMounts(state, executorState, inputs, stage, planOutput) {
|
|
554
|
+
const mounts = executorState.executor.mounts ?? [];
|
|
555
|
+
const executorName = executorState.executor.name;
|
|
556
|
+
for (const mount of mounts) {
|
|
557
|
+
if (mount.stage !== stage)
|
|
558
|
+
continue;
|
|
559
|
+
const stepNum = this.nextStep(state);
|
|
560
|
+
await this.emitProgress(state, {
|
|
561
|
+
type: "step_start",
|
|
562
|
+
step: stepNum,
|
|
563
|
+
kind: "mount",
|
|
564
|
+
executorName,
|
|
565
|
+
description: `Mount ${mount.name} (${stage})`,
|
|
566
|
+
});
|
|
567
|
+
const mountSpanId = this.traceCollector?.startSpan({
|
|
568
|
+
type: "mount-hydration",
|
|
569
|
+
executorName: executorState.executor.name,
|
|
570
|
+
mountName: mount.name,
|
|
571
|
+
mountStage: stage,
|
|
572
|
+
});
|
|
573
|
+
const startMount = Date.now();
|
|
574
|
+
const outputs = await this.hydrator.hydrateStage([mount], stage, inputs, planOutput);
|
|
575
|
+
executorState.mountOutputs.push(...outputs);
|
|
576
|
+
const durationMs = Date.now() - startMount;
|
|
577
|
+
if (mountSpanId) {
|
|
578
|
+
const totalItems = outputs.reduce((sum, o) => sum + o.itemCount, 0);
|
|
579
|
+
this.traceCollector?.endSpan(mountSpanId, { mountItemCount: totalItems });
|
|
580
|
+
}
|
|
581
|
+
await this.emitProgress(state, {
|
|
582
|
+
type: "step_complete",
|
|
583
|
+
step: this.nextCompleteStep(state),
|
|
584
|
+
kind: "mount",
|
|
585
|
+
executorName,
|
|
586
|
+
description: `Mount ${mount.name} (${stage})`,
|
|
587
|
+
durationMs,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
buildMessages(executor, executorState, inputs, taskOverride) {
|
|
592
|
+
const messages = [];
|
|
593
|
+
if (executor.instructions) {
|
|
594
|
+
messages.push({
|
|
595
|
+
role: "system",
|
|
596
|
+
content: executor.instructions,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
const mountContext = executorState.mountOutputs
|
|
600
|
+
.filter((m) => m.itemCount > 0)
|
|
601
|
+
.map((m) => `## ${m.mountName} (${m.stage}, ${m.itemCount} items)\n${JSON.stringify(m.data, null, 2)}`)
|
|
602
|
+
.join("\n\n");
|
|
603
|
+
const inputsBlock = Object.keys(inputs).length > 0
|
|
604
|
+
? `Context inputs:\n${JSON.stringify(inputs, null, 2)}\n\n`
|
|
605
|
+
: "";
|
|
606
|
+
const userContent = taskOverride
|
|
607
|
+
? `Task: ${taskOverride}\n\n${inputsBlock}Available data:\n${mountContext}`
|
|
608
|
+
: `${inputsBlock}Execute your role using the following data:\n\n${mountContext}`.trim();
|
|
609
|
+
messages.push({
|
|
610
|
+
role: "user",
|
|
611
|
+
content: userContent,
|
|
612
|
+
});
|
|
613
|
+
return messages;
|
|
614
|
+
}
|
|
615
|
+
async getAvailableTools(executor, modelProvider, _enforcer, _state) {
|
|
616
|
+
if (!modelProvider.supportsToolCalling())
|
|
617
|
+
return [];
|
|
618
|
+
const allowed = executor.policies?.toolAccess?.allow ?? [];
|
|
619
|
+
if (allowed.length === 0)
|
|
620
|
+
return [];
|
|
621
|
+
const tools = [];
|
|
622
|
+
for (const toolRef of allowed) {
|
|
623
|
+
const [serverName, toolName] = this.parseToolName(toolRef);
|
|
624
|
+
try {
|
|
625
|
+
const serverTools = await this.toolInvoker.listTools(serverName);
|
|
626
|
+
const match = serverTools.find((t) => t.name === toolName);
|
|
627
|
+
if (match) {
|
|
628
|
+
tools.push({
|
|
629
|
+
name: toolRef,
|
|
630
|
+
description: match.description,
|
|
631
|
+
parameters: match.inputSchema,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
// ignore: server not connected or tool not found
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return tools;
|
|
640
|
+
}
|
|
641
|
+
getOutputSchema(executor, context) {
|
|
642
|
+
if (executor.outputSchema) {
|
|
643
|
+
return executor.outputSchema;
|
|
644
|
+
}
|
|
645
|
+
if (!executor.outputSchemaRef)
|
|
646
|
+
return undefined;
|
|
647
|
+
const schemaName = executor.outputSchemaRef.replace("#/schemas/", "");
|
|
648
|
+
return context.schemas?.[schemaName];
|
|
649
|
+
}
|
|
650
|
+
schemaToJsonSchema(schema, _executor) {
|
|
651
|
+
return {
|
|
652
|
+
type: schema.type,
|
|
653
|
+
properties: schema.properties ?? {},
|
|
654
|
+
required: schema.required ?? [],
|
|
655
|
+
additionalProperties: true,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
tryParseJson(content) {
|
|
659
|
+
try {
|
|
660
|
+
return JSON.parse(content);
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
return content || undefined;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
parseToolName(qualifiedName) {
|
|
667
|
+
const colonIdx = qualifiedName.indexOf(":");
|
|
668
|
+
if (colonIdx === -1) {
|
|
669
|
+
return ["default", qualifiedName];
|
|
670
|
+
}
|
|
671
|
+
return [qualifiedName.slice(0, colonIdx), qualifiedName.slice(colonIdx + 1)];
|
|
672
|
+
}
|
|
673
|
+
log(state, level, message) {
|
|
674
|
+
const entry = {
|
|
675
|
+
timestamp: new Date().toISOString(),
|
|
676
|
+
level,
|
|
677
|
+
message,
|
|
678
|
+
};
|
|
679
|
+
state.log.push(entry);
|
|
680
|
+
if (this.config.debug || level === "error") {
|
|
681
|
+
const prefix = level.toUpperCase().padEnd(5);
|
|
682
|
+
console.error(`[ECP ${prefix}] ${message}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
buildResult(state, startTime) {
|
|
686
|
+
const executorOutputs = {};
|
|
687
|
+
const totalBudget = { toolCalls: 0, runtimeSeconds: 0 };
|
|
688
|
+
for (const [name, es] of state.executors) {
|
|
689
|
+
if (es.output) {
|
|
690
|
+
executorOutputs[name] = es.output;
|
|
691
|
+
}
|
|
692
|
+
totalBudget.toolCalls += es.budgetUsage.toolCalls;
|
|
693
|
+
totalBudget.runtimeSeconds += es.budgetUsage.runtimeSeconds;
|
|
694
|
+
}
|
|
695
|
+
const producesSchema = state.context.orchestration?.produces;
|
|
696
|
+
let finalOutput;
|
|
697
|
+
if (producesSchema) {
|
|
698
|
+
for (const [, es] of state.executors) {
|
|
699
|
+
const ref = es.executor.outputSchemaRef?.replace("#/schemas/", "");
|
|
700
|
+
if (ref === producesSchema && es.output) {
|
|
701
|
+
finalOutput = es.output;
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (!finalOutput) {
|
|
707
|
+
const entrypoint = this.getEntrypointName(state.context);
|
|
708
|
+
finalOutput = state.executors.get(entrypoint)?.output;
|
|
709
|
+
}
|
|
710
|
+
const failed = state.status === "failed";
|
|
711
|
+
const errorEntries = state.log.filter((e) => e.level === "error");
|
|
712
|
+
return {
|
|
713
|
+
success: !failed,
|
|
714
|
+
runId: state.runId,
|
|
715
|
+
contextName: state.context.metadata.name,
|
|
716
|
+
contextVersion: state.context.metadata.version,
|
|
717
|
+
output: finalOutput,
|
|
718
|
+
executorOutputs,
|
|
719
|
+
totalBudgetUsage: totalBudget,
|
|
720
|
+
log: state.log,
|
|
721
|
+
durationMs: Date.now() - startTime,
|
|
722
|
+
error: failed ? errorEntries.map((e) => e.message).join("; ") : undefined,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
getEntrypointName(context) {
|
|
726
|
+
const entrypoint = context.orchestrator?.name ?? context.orchestration?.entrypoint;
|
|
727
|
+
if (!entrypoint) {
|
|
728
|
+
throw new Error("Context entrypoint is not defined. Set orchestrator.name or orchestration.entrypoint.");
|
|
729
|
+
}
|
|
730
|
+
return entrypoint;
|
|
731
|
+
}
|
|
732
|
+
getStrategy(context) {
|
|
733
|
+
const strategy = context.orchestration?.strategy ?? context.orchestrator?.strategy;
|
|
734
|
+
if (!strategy) {
|
|
735
|
+
throw new Error("Context strategy is not defined. Set orchestration.strategy or orchestrator.strategy.");
|
|
736
|
+
}
|
|
737
|
+
return strategy;
|
|
738
|
+
}
|
|
739
|
+
getExecutionObjects(context) {
|
|
740
|
+
const executionObjects = new Map();
|
|
741
|
+
const addExecutionObject = (executor) => {
|
|
742
|
+
if (!executionObjects.has(executor.name)) {
|
|
743
|
+
executionObjects.set(executor.name, executor);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
const visitOrchestrator = (orchestrator) => {
|
|
747
|
+
addExecutionObject(orchestrator);
|
|
748
|
+
for (const executor of orchestrator.executors ?? []) {
|
|
749
|
+
addExecutionObject(executor);
|
|
750
|
+
}
|
|
751
|
+
for (const child of orchestrator.orchestrators ?? []) {
|
|
752
|
+
visitOrchestrator(child);
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
if (context.orchestrator) {
|
|
756
|
+
visitOrchestrator(context.orchestrator);
|
|
757
|
+
}
|
|
758
|
+
for (const executor of context.executors ?? []) {
|
|
759
|
+
addExecutionObject(executor);
|
|
760
|
+
}
|
|
761
|
+
return [...executionObjects.values()];
|
|
762
|
+
}
|
|
763
|
+
resolveModelProvider(executor, context) {
|
|
764
|
+
const providerSelector = executor.model?.provider;
|
|
765
|
+
if (!providerSelector || !this.extensionRegistry) {
|
|
766
|
+
return this.modelProvider;
|
|
767
|
+
}
|
|
768
|
+
const providerRef = this.normalizeProviderSelector(providerSelector);
|
|
769
|
+
this.assertModelProviderAllowed(providerRef, context);
|
|
770
|
+
const registration = this.extensionRegistry.getModelProviderRegistration(providerRef.name);
|
|
771
|
+
if (!registration) {
|
|
772
|
+
throw new Error(`Model provider extension "${providerRef.name}" is not registered.`);
|
|
773
|
+
}
|
|
774
|
+
if (registration.sourceType !== providerRef.type) {
|
|
775
|
+
throw new Error(`Model provider "${providerRef.name}" expected source type "${providerRef.type}", got "${registration.sourceType}".`);
|
|
776
|
+
}
|
|
777
|
+
if (registration.version !== providerRef.version) {
|
|
778
|
+
throw new Error(`Model provider "${providerRef.name}" version mismatch: expected "${providerRef.version}", got "${registration.version}".`);
|
|
779
|
+
}
|
|
780
|
+
const cacheKey = `${providerRef.type}:${providerRef.name}:${providerRef.version}`;
|
|
781
|
+
const cached = this.modelProviderCache.get(cacheKey);
|
|
782
|
+
if (cached) {
|
|
783
|
+
return cached;
|
|
784
|
+
}
|
|
785
|
+
const providerConfig = context.extensions?.config?.[providerRef.name];
|
|
786
|
+
const created = this.extensionRegistry.createModelProvider(providerRef.name, providerConfig);
|
|
787
|
+
this.modelProviderCache.set(cacheKey, created);
|
|
788
|
+
return created;
|
|
789
|
+
}
|
|
790
|
+
normalizeProviderSelector(selector) {
|
|
791
|
+
if (typeof selector === "string") {
|
|
792
|
+
return {
|
|
793
|
+
name: selector,
|
|
794
|
+
type: "builtin",
|
|
795
|
+
version: "0.3.0",
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
return selector;
|
|
799
|
+
}
|
|
800
|
+
assertModelProviderAllowed(provider, context) {
|
|
801
|
+
const security = this.getEffectiveExtensionSecurity(context);
|
|
802
|
+
if (security.allowKinds?.length && !security.allowKinds.includes("model-provider")) {
|
|
803
|
+
throw new Error(`Model provider "${provider.name}" denied: kind "model-provider" is not allowed.`);
|
|
804
|
+
}
|
|
805
|
+
if (security.allowSourceTypes?.length &&
|
|
806
|
+
!security.allowSourceTypes.includes(provider.type)) {
|
|
807
|
+
throw new Error(`Model provider "${provider.name}" denied: source type "${provider.type}" is not allowed.`);
|
|
808
|
+
}
|
|
809
|
+
if (security.allowIds?.length && !security.allowIds.includes(provider.name)) {
|
|
810
|
+
throw new Error(`Model provider "${provider.name}" denied: provider is not in extensions allowIds.`);
|
|
811
|
+
}
|
|
812
|
+
if (security.denyIds?.includes(provider.name)) {
|
|
813
|
+
throw new Error(`Model provider "${provider.name}" denied: provider is listed in extensions denyIds.`);
|
|
814
|
+
}
|
|
815
|
+
const runtimeEnable = this.config.extensions?.enable;
|
|
816
|
+
const allowEnable = this.config.extensions?.allowEnable;
|
|
817
|
+
if (allowEnable !== undefined && allowEnable.length > 0) {
|
|
818
|
+
if (!allowEnable.includes(provider.name)) {
|
|
819
|
+
throw new Error(`Model provider "${provider.name}" denied: not in system config allow-list (allowEnable).`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (runtimeEnable !== undefined && runtimeEnable.length > 0) {
|
|
823
|
+
if (!runtimeEnable.includes(provider.name)) {
|
|
824
|
+
throw new Error(`Model provider "${provider.name}" denied: provider is not enabled for this run (extensions.enable).`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
const contextProviderIds = new Set(context.extensions?.providers?.map((p) => p.name) ?? []);
|
|
829
|
+
if (contextProviderIds.size > 0 && !contextProviderIds.has(provider.name)) {
|
|
830
|
+
throw new Error(`Model provider "${provider.name}" denied: provider is not declared in context.extensions.providers.`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
getEffectiveExtensionSecurity(context) {
|
|
835
|
+
const contextPolicy = context.extensions?.security;
|
|
836
|
+
const systemPolicy = this.config.extensions?.security;
|
|
837
|
+
const allowKinds = systemPolicy?.allowKinds ?? contextPolicy?.allowKinds;
|
|
838
|
+
const allowSourceTypes = systemPolicy?.allowSourceTypes ?? contextPolicy?.allowSourceTypes;
|
|
839
|
+
return {
|
|
840
|
+
...contextPolicy,
|
|
841
|
+
...systemPolicy,
|
|
842
|
+
allowKinds,
|
|
843
|
+
// Default: allow all builtin extensions when not specified
|
|
844
|
+
allowSourceTypes: allowSourceTypes !== undefined && allowSourceTypes.length > 0
|
|
845
|
+
? allowSourceTypes
|
|
846
|
+
: ["builtin"],
|
|
847
|
+
allowIds: systemPolicy?.allowIds ?? contextPolicy?.allowIds,
|
|
848
|
+
denyIds: systemPolicy?.denyIds ?? contextPolicy?.denyIds,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
//# sourceMappingURL=runner.js.map
|