@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.
Files changed (193) hide show
  1. package/dist/engine/context-loader.d.ts +31 -0
  2. package/dist/engine/context-loader.d.ts.map +1 -0
  3. package/dist/engine/context-loader.js +90 -0
  4. package/dist/engine/context-loader.js.map +1 -0
  5. package/dist/engine/index.d.ts +6 -0
  6. package/dist/engine/index.d.ts.map +1 -0
  7. package/dist/engine/index.js +6 -0
  8. package/dist/engine/index.js.map +1 -0
  9. package/dist/engine/runner.d.ts +92 -0
  10. package/dist/engine/runner.d.ts.map +1 -0
  11. package/dist/engine/runner.js +852 -0
  12. package/dist/engine/runner.js.map +1 -0
  13. package/dist/engine/schema-validator.d.ts +32 -0
  14. package/dist/engine/schema-validator.d.ts.map +1 -0
  15. package/dist/engine/schema-validator.js +69 -0
  16. package/dist/engine/schema-validator.js.map +1 -0
  17. package/dist/engine/system-config-loader.d.ts +39 -0
  18. package/dist/engine/system-config-loader.d.ts.map +1 -0
  19. package/dist/engine/system-config-loader.js +80 -0
  20. package/dist/engine/system-config-loader.js.map +1 -0
  21. package/dist/engine/types.d.ts +324 -0
  22. package/dist/engine/types.d.ts.map +1 -0
  23. package/dist/engine/types.js +10 -0
  24. package/dist/engine/types.js.map +1 -0
  25. package/dist/evals/index.d.ts +3 -0
  26. package/dist/evals/index.d.ts.map +1 -0
  27. package/dist/evals/index.js +3 -0
  28. package/dist/evals/index.js.map +1 -0
  29. package/dist/evals/scorer.d.ts +23 -0
  30. package/dist/evals/scorer.d.ts.map +1 -0
  31. package/dist/evals/scorer.js +133 -0
  32. package/dist/evals/scorer.js.map +1 -0
  33. package/dist/evals/types.d.ts +128 -0
  34. package/dist/evals/types.d.ts.map +1 -0
  35. package/dist/evals/types.js +11 -0
  36. package/dist/evals/types.js.map +1 -0
  37. package/dist/extensions/builtin.d.ts +44 -0
  38. package/dist/extensions/builtin.d.ts.map +1 -0
  39. package/dist/extensions/builtin.js +66 -0
  40. package/dist/extensions/builtin.js.map +1 -0
  41. package/dist/extensions/index.d.ts +4 -0
  42. package/dist/extensions/index.d.ts.map +1 -0
  43. package/dist/extensions/index.js +4 -0
  44. package/dist/extensions/index.js.map +1 -0
  45. package/dist/extensions/progress-loggers/file-logger.d.ts +27 -0
  46. package/dist/extensions/progress-loggers/file-logger.d.ts.map +1 -0
  47. package/dist/extensions/progress-loggers/file-logger.js +54 -0
  48. package/dist/extensions/progress-loggers/file-logger.js.map +1 -0
  49. package/dist/extensions/registry.d.ts +74 -0
  50. package/dist/extensions/registry.d.ts.map +1 -0
  51. package/dist/extensions/registry.js +126 -0
  52. package/dist/extensions/registry.js.map +1 -0
  53. package/dist/extensions/types.d.ts +78 -0
  54. package/dist/extensions/types.d.ts.map +1 -0
  55. package/dist/extensions/types.js +7 -0
  56. package/dist/extensions/types.js.map +1 -0
  57. package/dist/index.d.ts +10 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +10 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/mounts/hydrator.d.ts +46 -0
  62. package/dist/mounts/hydrator.d.ts.map +1 -0
  63. package/dist/mounts/hydrator.js +142 -0
  64. package/dist/mounts/hydrator.js.map +1 -0
  65. package/dist/mounts/index.d.ts +4 -0
  66. package/dist/mounts/index.d.ts.map +1 -0
  67. package/dist/mounts/index.js +4 -0
  68. package/dist/mounts/index.js.map +1 -0
  69. package/dist/mounts/interpolation.d.ts +33 -0
  70. package/dist/mounts/interpolation.d.ts.map +1 -0
  71. package/dist/mounts/interpolation.js +59 -0
  72. package/dist/mounts/interpolation.js.map +1 -0
  73. package/dist/mounts/types.d.ts +80 -0
  74. package/dist/mounts/types.d.ts.map +1 -0
  75. package/dist/mounts/types.js +10 -0
  76. package/dist/mounts/types.js.map +1 -0
  77. package/dist/policies/enforcer.d.ts +23 -0
  78. package/dist/policies/enforcer.d.ts.map +1 -0
  79. package/dist/policies/enforcer.js +111 -0
  80. package/dist/policies/enforcer.js.map +1 -0
  81. package/dist/policies/index.d.ts +3 -0
  82. package/dist/policies/index.d.ts.map +1 -0
  83. package/dist/policies/index.js +3 -0
  84. package/dist/policies/index.js.map +1 -0
  85. package/dist/policies/types.d.ts +87 -0
  86. package/dist/policies/types.d.ts.map +1 -0
  87. package/dist/policies/types.js +11 -0
  88. package/dist/policies/types.js.map +1 -0
  89. package/dist/protocols/a2a/a2a-transport.d.ts +40 -0
  90. package/dist/protocols/a2a/a2a-transport.d.ts.map +1 -0
  91. package/dist/protocols/a2a/a2a-transport.js +212 -0
  92. package/dist/protocols/a2a/a2a-transport.js.map +1 -0
  93. package/dist/protocols/a2a/index.d.ts +3 -0
  94. package/dist/protocols/a2a/index.d.ts.map +1 -0
  95. package/dist/protocols/a2a/index.js +2 -0
  96. package/dist/protocols/a2a/index.js.map +1 -0
  97. package/dist/protocols/agent-transport.d.ts +101 -0
  98. package/dist/protocols/agent-transport.d.ts.map +1 -0
  99. package/dist/protocols/agent-transport.js +11 -0
  100. package/dist/protocols/agent-transport.js.map +1 -0
  101. package/dist/protocols/index.d.ts +5 -0
  102. package/dist/protocols/index.d.ts.map +1 -0
  103. package/dist/protocols/index.js +5 -0
  104. package/dist/protocols/index.js.map +1 -0
  105. package/dist/protocols/mcp/index.d.ts +2 -0
  106. package/dist/protocols/mcp/index.d.ts.map +1 -0
  107. package/dist/protocols/mcp/index.js +2 -0
  108. package/dist/protocols/mcp/index.js.map +1 -0
  109. package/dist/protocols/mcp/mcp-tool-invoker.d.ts +30 -0
  110. package/dist/protocols/mcp/mcp-tool-invoker.d.ts.map +1 -0
  111. package/dist/protocols/mcp/mcp-tool-invoker.js +121 -0
  112. package/dist/protocols/mcp/mcp-tool-invoker.js.map +1 -0
  113. package/dist/protocols/tool-invoker.d.ts +91 -0
  114. package/dist/protocols/tool-invoker.d.ts.map +1 -0
  115. package/dist/protocols/tool-invoker.js +11 -0
  116. package/dist/protocols/tool-invoker.js.map +1 -0
  117. package/dist/providers/index.d.ts +4 -0
  118. package/dist/providers/index.d.ts.map +1 -0
  119. package/dist/providers/index.js +4 -0
  120. package/dist/providers/index.js.map +1 -0
  121. package/dist/providers/model-provider.d.ts +132 -0
  122. package/dist/providers/model-provider.d.ts.map +1 -0
  123. package/dist/providers/model-provider.js +10 -0
  124. package/dist/providers/model-provider.js.map +1 -0
  125. package/dist/providers/ollama/index.d.ts +3 -0
  126. package/dist/providers/ollama/index.d.ts.map +1 -0
  127. package/dist/providers/ollama/index.js +2 -0
  128. package/dist/providers/ollama/index.js.map +1 -0
  129. package/dist/providers/ollama/ollama-provider.d.ts +41 -0
  130. package/dist/providers/ollama/ollama-provider.d.ts.map +1 -0
  131. package/dist/providers/ollama/ollama-provider.js +113 -0
  132. package/dist/providers/ollama/ollama-provider.js.map +1 -0
  133. package/dist/providers/openai/index.d.ts +3 -0
  134. package/dist/providers/openai/index.d.ts.map +1 -0
  135. package/dist/providers/openai/index.js +2 -0
  136. package/dist/providers/openai/index.js.map +1 -0
  137. package/dist/providers/openai/openai-provider.d.ts +41 -0
  138. package/dist/providers/openai/openai-provider.d.ts.map +1 -0
  139. package/dist/providers/openai/openai-provider.js +150 -0
  140. package/dist/providers/openai/openai-provider.js.map +1 -0
  141. package/dist/testing/cassette.d.ts +88 -0
  142. package/dist/testing/cassette.d.ts.map +1 -0
  143. package/dist/testing/cassette.js +123 -0
  144. package/dist/testing/cassette.js.map +1 -0
  145. package/dist/testing/index.d.ts +5 -0
  146. package/dist/testing/index.d.ts.map +1 -0
  147. package/dist/testing/index.js +5 -0
  148. package/dist/testing/index.js.map +1 -0
  149. package/dist/testing/mock-agent-transport.d.ts +49 -0
  150. package/dist/testing/mock-agent-transport.d.ts.map +1 -0
  151. package/dist/testing/mock-agent-transport.js +71 -0
  152. package/dist/testing/mock-agent-transport.js.map +1 -0
  153. package/dist/testing/mock-model-provider.d.ts +69 -0
  154. package/dist/testing/mock-model-provider.d.ts.map +1 -0
  155. package/dist/testing/mock-model-provider.js +92 -0
  156. package/dist/testing/mock-model-provider.js.map +1 -0
  157. package/dist/testing/mock-tool-invoker.d.ts +65 -0
  158. package/dist/testing/mock-tool-invoker.d.ts.map +1 -0
  159. package/dist/testing/mock-tool-invoker.js +85 -0
  160. package/dist/testing/mock-tool-invoker.js.map +1 -0
  161. package/dist/tracing/collector.d.ts +75 -0
  162. package/dist/tracing/collector.d.ts.map +1 -0
  163. package/dist/tracing/collector.js +106 -0
  164. package/dist/tracing/collector.js.map +1 -0
  165. package/dist/tracing/exporters/console.d.ts +17 -0
  166. package/dist/tracing/exporters/console.d.ts.map +1 -0
  167. package/dist/tracing/exporters/console.js +76 -0
  168. package/dist/tracing/exporters/console.js.map +1 -0
  169. package/dist/tracing/exporters/index.d.ts +4 -0
  170. package/dist/tracing/exporters/index.d.ts.map +1 -0
  171. package/dist/tracing/exporters/index.js +3 -0
  172. package/dist/tracing/exporters/index.js.map +1 -0
  173. package/dist/tracing/exporters/json-file.d.ts +30 -0
  174. package/dist/tracing/exporters/json-file.d.ts.map +1 -0
  175. package/dist/tracing/exporters/json-file.js +28 -0
  176. package/dist/tracing/exporters/json-file.js.map +1 -0
  177. package/dist/tracing/formatter.d.ts +16 -0
  178. package/dist/tracing/formatter.d.ts.map +1 -0
  179. package/dist/tracing/formatter.js +81 -0
  180. package/dist/tracing/formatter.js.map +1 -0
  181. package/dist/tracing/graph.d.ts +17 -0
  182. package/dist/tracing/graph.d.ts.map +1 -0
  183. package/dist/tracing/graph.js +116 -0
  184. package/dist/tracing/graph.js.map +1 -0
  185. package/dist/tracing/index.d.ts +6 -0
  186. package/dist/tracing/index.d.ts.map +1 -0
  187. package/dist/tracing/index.js +6 -0
  188. package/dist/tracing/index.js.map +1 -0
  189. package/dist/tracing/types.d.ts +124 -0
  190. package/dist/tracing/types.d.ts.map +1 -0
  191. package/dist/tracing/types.js +11 -0
  192. package/dist/tracing/types.js.map +1 -0
  193. 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