agent-relay 2.3.11 → 2.3.13

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 (81) hide show
  1. package/install.sh +32 -0
  2. package/package.json +21 -21
  3. package/packages/acp-bridge/package.json +2 -2
  4. package/packages/bridge/package.json +7 -7
  5. package/packages/broker-sdk/README.md +32 -0
  6. package/packages/broker-sdk/dist/__tests__/unit.test.js +70 -2
  7. package/packages/broker-sdk/dist/__tests__/unit.test.js.map +1 -1
  8. package/packages/broker-sdk/dist/client.d.ts +2 -0
  9. package/packages/broker-sdk/dist/client.d.ts.map +1 -1
  10. package/packages/broker-sdk/dist/client.js +10 -0
  11. package/packages/broker-sdk/dist/client.js.map +1 -1
  12. package/packages/broker-sdk/dist/protocol.d.ts +4 -0
  13. package/packages/broker-sdk/dist/protocol.d.ts.map +1 -1
  14. package/packages/broker-sdk/dist/relay.d.ts +10 -0
  15. package/packages/broker-sdk/dist/relay.d.ts.map +1 -1
  16. package/packages/broker-sdk/dist/relay.js +53 -0
  17. package/packages/broker-sdk/dist/relay.js.map +1 -1
  18. package/packages/broker-sdk/dist/relaycast.d.ts +10 -0
  19. package/packages/broker-sdk/dist/relaycast.d.ts.map +1 -1
  20. package/packages/broker-sdk/dist/relaycast.js +40 -0
  21. package/packages/broker-sdk/dist/relaycast.js.map +1 -1
  22. package/packages/broker-sdk/dist/workflows/coordinator.d.ts +1 -0
  23. package/packages/broker-sdk/dist/workflows/coordinator.d.ts.map +1 -1
  24. package/packages/broker-sdk/dist/workflows/coordinator.js +239 -7
  25. package/packages/broker-sdk/dist/workflows/coordinator.js.map +1 -1
  26. package/packages/broker-sdk/dist/workflows/index.d.ts +1 -0
  27. package/packages/broker-sdk/dist/workflows/index.d.ts.map +1 -1
  28. package/packages/broker-sdk/dist/workflows/index.js +1 -0
  29. package/packages/broker-sdk/dist/workflows/index.js.map +1 -1
  30. package/packages/broker-sdk/dist/workflows/run.d.ts +3 -1
  31. package/packages/broker-sdk/dist/workflows/run.d.ts.map +1 -1
  32. package/packages/broker-sdk/dist/workflows/run.js +4 -0
  33. package/packages/broker-sdk/dist/workflows/run.js.map +1 -1
  34. package/packages/broker-sdk/dist/workflows/runner.d.ts +9 -0
  35. package/packages/broker-sdk/dist/workflows/runner.d.ts.map +1 -1
  36. package/packages/broker-sdk/dist/workflows/runner.js +203 -14
  37. package/packages/broker-sdk/dist/workflows/runner.js.map +1 -1
  38. package/packages/broker-sdk/dist/workflows/trajectory.d.ts +80 -0
  39. package/packages/broker-sdk/dist/workflows/trajectory.d.ts.map +1 -0
  40. package/packages/broker-sdk/dist/workflows/trajectory.js +362 -0
  41. package/packages/broker-sdk/dist/workflows/trajectory.js.map +1 -0
  42. package/packages/broker-sdk/dist/workflows/types.d.ts +15 -1
  43. package/packages/broker-sdk/dist/workflows/types.d.ts.map +1 -1
  44. package/packages/broker-sdk/package.json +2 -2
  45. package/packages/broker-sdk/src/__tests__/swarm-coordinator.test.ts +356 -0
  46. package/packages/broker-sdk/src/__tests__/unit.test.ts +92 -1
  47. package/packages/broker-sdk/src/__tests__/workflow-trajectory.test.ts +408 -0
  48. package/packages/broker-sdk/src/client.ts +15 -0
  49. package/packages/broker-sdk/src/protocol.ts +5 -0
  50. package/packages/broker-sdk/src/relay.ts +59 -0
  51. package/packages/broker-sdk/src/relaycast.ts +42 -0
  52. package/packages/broker-sdk/src/workflows/README.md +64 -0
  53. package/packages/broker-sdk/src/workflows/coordinator.ts +246 -8
  54. package/packages/broker-sdk/src/workflows/index.ts +1 -0
  55. package/packages/broker-sdk/src/workflows/run.ts +9 -1
  56. package/packages/broker-sdk/src/workflows/runner.ts +249 -14
  57. package/packages/broker-sdk/src/workflows/schema.json +13 -1
  58. package/packages/broker-sdk/src/workflows/trajectory.ts +507 -0
  59. package/packages/broker-sdk/src/workflows/types.ts +31 -1
  60. package/packages/broker-sdk/tsconfig.json +1 -0
  61. package/packages/broker-sdk/vitest.config.ts +9 -0
  62. package/packages/config/package.json +2 -2
  63. package/packages/continuity/package.json +2 -2
  64. package/packages/daemon/package.json +12 -12
  65. package/packages/hooks/package.json +4 -4
  66. package/packages/mcp/package.json +5 -5
  67. package/packages/memory/package.json +2 -2
  68. package/packages/policy/package.json +2 -2
  69. package/packages/protocol/package.json +1 -1
  70. package/packages/resiliency/package.json +1 -1
  71. package/packages/sdk/package.json +3 -3
  72. package/packages/sdk-py/src/agent_relay/builder.py +4 -0
  73. package/packages/sdk-py/src/agent_relay/types.py +15 -0
  74. package/packages/spawner/package.json +1 -1
  75. package/packages/state/package.json +1 -1
  76. package/packages/storage/package.json +2 -2
  77. package/packages/telemetry/package.json +1 -1
  78. package/packages/trajectory/package.json +2 -2
  79. package/packages/user-directory/package.json +2 -2
  80. package/packages/utils/package.json +3 -3
  81. package/packages/wrapper/package.json +5 -5
@@ -173,6 +173,8 @@ verification:
173
173
 
174
174
  The `swarm.pattern` field controls how agents are coordinated:
175
175
 
176
+ ### Core Patterns
177
+
176
178
  | Pattern | Description |
177
179
  |---------|-------------|
178
180
  | `dag` | Directed acyclic graph — steps run based on dependency edges (default) |
@@ -186,6 +188,68 @@ The `swarm.pattern` field controls how agents are coordinated:
186
188
  | `debate` | Agents propose and counter-argue |
187
189
  | `hierarchical` | Multi-level reporting structure |
188
190
 
191
+ ### Data Processing Patterns
192
+
193
+ | Pattern | Description |
194
+ |---------|-------------|
195
+ | `map-reduce` | Split work into chunks (mappers), process in parallel, aggregate results (reducers) |
196
+ | `scatter-gather` | Fan out requests to workers, collect and synthesize responses |
197
+
198
+ ### Supervision & Quality Patterns
199
+
200
+ | Pattern | Description |
201
+ |---------|-------------|
202
+ | `supervisor` | Monitor agent monitors workers, restarts on failure, manages health |
203
+ | `reflection` | Agent produces output, critic reviews and provides feedback for iteration |
204
+ | `verifier` | Producer agents submit work to verifier agents for validation |
205
+
206
+ ### Adversarial & Validation Patterns
207
+
208
+ | Pattern | Description |
209
+ |---------|-------------|
210
+ | `red-team` | Attacker agents probe for weaknesses, defender agents respond |
211
+ | `auction` | Auctioneer broadcasts tasks, agents bid based on capability/cost |
212
+
213
+ ### Resilience Patterns
214
+
215
+ | Pattern | Description |
216
+ |---------|-------------|
217
+ | `escalation` | Start with fast/cheap agents, escalate to more capable on failure |
218
+ | `saga` | Distributed transactions with compensating actions on failure |
219
+ | `circuit-breaker` | Primary agent with fallback chain, fail fast and recover |
220
+
221
+ ### Collaborative Patterns
222
+
223
+ | Pattern | Description |
224
+ |---------|-------------|
225
+ | `blackboard` | Shared workspace where agents contribute incrementally to a solution |
226
+ | `swarm` | Emergent behavior from simple agent rules (neighbor communication) |
227
+
228
+ ### Auto-Selection by Role
229
+
230
+ When `swarm.pattern` is omitted, the coordinator auto-selects based on agent roles.
231
+ Patterns are checked in priority order below (first match wins):
232
+
233
+ | Priority | Pattern | Required Roles/Config |
234
+ |----------|---------|----------------------|
235
+ | 1 | `dag` | Steps with `dependsOn` |
236
+ | 2 | `consensus` | Uses `coordination.consensusStrategy` config |
237
+ | 3 | `map-reduce` | `mapper` + `reducer` |
238
+ | 4 | `red-team` | (`attacker` OR `red-team`) + (`defender` OR `blue-team`) |
239
+ | 5 | `reflection` | `critic` |
240
+ | 6 | `escalation` | `tier-1`, `tier-2`, etc. |
241
+ | 7 | `auction` | `auctioneer` |
242
+ | 8 | `saga` | `saga-orchestrator` OR `compensate-handler` |
243
+ | 9 | `circuit-breaker` | `fallback`, `backup`, OR `primary` |
244
+ | 10 | `blackboard` | `blackboard` OR `shared-workspace` |
245
+ | 11 | `swarm` | `hive-mind` OR `swarm-agent` |
246
+ | 12 | `verifier` | `verifier` |
247
+ | 13 | `supervisor` | `supervisor` |
248
+ | 14 | `hierarchical` | `lead` (with 4+ agents) |
249
+ | 15 | `hub-spoke` | `hub` OR `coordinator` |
250
+ | 16 | `pipeline` | Unique agents per step, 3+ steps |
251
+ | 17 | `fan-out` | Default fallback |
252
+
189
253
  ## Error Handling
190
254
 
191
255
  ### Step-Level
@@ -52,6 +52,7 @@ const PATTERN_HEURISTICS: Array<{
52
52
  test: (config: RelayYamlConfig) => boolean;
53
53
  pattern: SwarmPattern;
54
54
  }> = [
55
+ // ── Dependency-based patterns (highest priority) ──────────────────────
55
56
  {
56
57
  test: (c) =>
57
58
  Array.isArray(c.workflows) &&
@@ -62,15 +63,66 @@ const PATTERN_HEURISTICS: Array<{
62
63
  test: (c) => c.coordination?.consensusStrategy !== undefined,
63
64
  pattern: 'consensus',
64
65
  },
66
+
67
+ // ── Specific role-based patterns (check before generic hub patterns) ──
65
68
  {
66
- test: (c) =>
67
- Array.isArray(c.workflows) &&
68
- c.workflows.some((w) => {
69
- const names = w.steps.map((s) => s.agent);
70
- return new Set(names).size === names.length && names.length > 2;
71
- }),
72
- pattern: 'pipeline',
69
+ // Map-reduce: requires BOTH mapper AND reducer roles
70
+ test: (c) => c.agents.some((a) => a.role === 'mapper') && c.agents.some((a) => a.role === 'reducer'),
71
+ pattern: 'map-reduce',
72
+ },
73
+ {
74
+ // Red-team: requires BOTH attacker/red-team AND defender/blue-team
75
+ test: (c) => c.agents.some((a) => a.role === 'attacker' || a.role === 'red-team') &&
76
+ c.agents.some((a) => a.role === 'defender' || a.role === 'blue-team'),
77
+ pattern: 'red-team',
78
+ },
79
+ {
80
+ // Reflection: requires critic role (not just reviewer, which is too common)
81
+ test: (c) => c.agents.some((a) => a.role === 'critic'),
82
+ pattern: 'reflection',
83
+ },
84
+ {
85
+ // Escalation: has tier-N roles
86
+ test: (c) => c.agents.some((a) => a.role?.startsWith('tier-')),
87
+ pattern: 'escalation',
88
+ },
89
+ {
90
+ // Auction: has auctioneer role
91
+ test: (c) => c.agents.some((a) => a.role === 'auctioneer'),
92
+ pattern: 'auction',
93
+ },
94
+ {
95
+ // Saga: has saga-orchestrator or compensate-handler roles
96
+ test: (c) => c.agents.some((a) => a.role === 'saga-orchestrator' || a.role === 'compensate-handler'),
97
+ pattern: 'saga',
98
+ },
99
+ {
100
+ // Circuit-breaker: has fallback or backup roles
101
+ test: (c) => c.agents.some((a) => a.role === 'fallback' || a.role === 'backup' || a.role === 'primary'),
102
+ pattern: 'circuit-breaker',
103
+ },
104
+ {
105
+ // Blackboard: has blackboard or shared-workspace role
106
+ test: (c) => c.agents.some((a) => a.role === 'blackboard' || a.role === 'shared-workspace'),
107
+ pattern: 'blackboard',
108
+ },
109
+ {
110
+ // Swarm: has hive-mind or swarm-agent roles
111
+ test: (c) => c.agents.some((a) => a.role === 'hive-mind' || a.role === 'swarm-agent'),
112
+ pattern: 'swarm',
73
113
  },
114
+ {
115
+ // Verifier: has verifier role
116
+ test: (c) => c.agents.some((a) => a.role === 'verifier'),
117
+ pattern: 'verifier',
118
+ },
119
+ {
120
+ // Supervisor: has supervisor role
121
+ test: (c) => c.agents.some((a) => a.role === 'supervisor'),
122
+ pattern: 'supervisor',
123
+ },
124
+
125
+ // ── Generic hub-based patterns ────────────────────────────────────────
74
126
  {
75
127
  test: (c) => c.agents.length > 3 && c.agents.some((a) => a.role === 'lead'),
76
128
  pattern: 'hierarchical',
@@ -79,8 +131,20 @@ const PATTERN_HEURISTICS: Array<{
79
131
  test: (c) => c.agents.some((a) => a.role === 'hub' || a.role === 'coordinator'),
80
132
  pattern: 'hub-spoke',
81
133
  },
134
+
135
+ // ── Structural patterns ───────────────────────────────────────────────
136
+ {
137
+ test: (c) =>
138
+ Array.isArray(c.workflows) &&
139
+ c.workflows.some((w) => {
140
+ const names = w.steps.map((s) => s.agent);
141
+ return new Set(names).size === names.length && names.length > 2;
142
+ }),
143
+ pattern: 'pipeline',
144
+ },
145
+
146
+ // ── Default fallback ──────────────────────────────────────────────────
82
147
  {
83
- // Default: many independent agents → fan-out
84
148
  test: () => true,
85
149
  pattern: 'fan-out',
86
150
  },
@@ -207,6 +271,164 @@ export class SwarmCoordinator extends EventEmitter {
207
271
  return { pattern: p, agents, edges, hub };
208
272
  }
209
273
 
274
+ // ── Additional patterns ────────────────────────────────────────────
275
+
276
+ case 'map-reduce': {
277
+ // Mappers fan out from coordinator, all feed into reducer(s)
278
+ const coordinator = this.pickHub(agents);
279
+ const mappers = agents.filter((a) => a.role === 'mapper').map((a) => a.name);
280
+ const reducers = agents.filter((a) => a.role === 'reducer').map((a) => a.name);
281
+ const others = names.filter((n) => n !== coordinator && !mappers.includes(n) && !reducers.includes(n));
282
+
283
+ // Coordinator → mappers (excluding self if coordinator is also a mapper)
284
+ edges.set(coordinator, [...mappers.filter((m) => m !== coordinator), ...others]);
285
+ // Mappers → reducers (skip coordinator to avoid overwriting its edges)
286
+ for (const m of mappers) {
287
+ if (m === coordinator) continue;
288
+ edges.set(m, reducers.length > 0 ? reducers : [coordinator]);
289
+ }
290
+ // Reducers → coordinator
291
+ for (const r of reducers) edges.set(r, [coordinator]);
292
+ // Others → coordinator
293
+ for (const o of others) edges.set(o, [coordinator]);
294
+
295
+ return { pattern: p, agents, edges, hub: coordinator };
296
+ }
297
+
298
+ case 'scatter-gather': {
299
+ // Hub scatters to all workers, gathers responses back
300
+ const hub = this.pickHub(agents);
301
+ const workers = names.filter((n) => n !== hub);
302
+ edges.set(hub, workers);
303
+ for (const w of workers) edges.set(w, [hub]);
304
+ return { pattern: p, agents, edges, hub };
305
+ }
306
+
307
+ case 'supervisor': {
308
+ // Supervisor monitors all workers; workers report to supervisor
309
+ const supervisor = agents.find((a) => a.role === 'supervisor')?.name ?? this.pickHub(agents);
310
+ const workers = names.filter((n) => n !== supervisor);
311
+ edges.set(supervisor, workers);
312
+ for (const w of workers) edges.set(w, [supervisor]);
313
+ return { pattern: p, agents, edges, hub: supervisor };
314
+ }
315
+
316
+ case 'reflection': {
317
+ // Agent produces output, critic reviews and sends feedback
318
+ // Linear: producer → critic → producer (loop-capable)
319
+ const critic = agents.find((a) => a.role === 'critic' || a.role === 'reviewer')?.name;
320
+ const producers = names.filter((n) => n !== critic);
321
+ if (critic) {
322
+ for (const prod of producers) {
323
+ edges.set(prod, [critic]);
324
+ }
325
+ edges.set(critic, producers);
326
+ } else {
327
+ // Fallback: self-reflection via mesh
328
+ for (const n of names) edges.set(n, names.filter((o) => o !== n));
329
+ }
330
+ return { pattern: p, agents, edges };
331
+ }
332
+
333
+ case 'red-team': {
334
+ // Attacker ↔ Defender adversarial communication
335
+ const attackers = agents.filter((a) => a.role === 'attacker' || a.role === 'red-team').map((a) => a.name);
336
+ const defenders = agents.filter((a) => a.role === 'defender' || a.role === 'blue-team').map((a) => a.name);
337
+ const judges = names.filter((n) => !attackers.includes(n) && !defenders.includes(n));
338
+
339
+ // Attackers → defenders and judges
340
+ for (const a of attackers) edges.set(a, [...defenders, ...judges]);
341
+ // Defenders → attackers and judges
342
+ for (const d of defenders) edges.set(d, [...attackers, ...judges]);
343
+ // Judges receive from both, can communicate with all
344
+ for (const j of judges) edges.set(j, [...attackers, ...defenders]);
345
+
346
+ return { pattern: p, agents, edges };
347
+ }
348
+
349
+ case 'verifier': {
350
+ // Producer → Verifier chain; verifier can reject back to producer
351
+ const verifiers = agents.filter((a) => a.role === 'verifier').map((a) => a.name);
352
+ const producers = names.filter((n) => !verifiers.includes(n));
353
+
354
+ for (const prod of producers) edges.set(prod, verifiers.length > 0 ? verifiers : []);
355
+ for (const v of verifiers) edges.set(v, producers); // Can send rejections back
356
+
357
+ return { pattern: p, agents, edges };
358
+ }
359
+
360
+ case 'auction': {
361
+ // Auctioneer broadcasts tasks; bidders respond to auctioneer only
362
+ const auctioneer = agents.find((a) => a.role === 'auctioneer')?.name ?? this.pickHub(agents);
363
+ const bidders = names.filter((n) => n !== auctioneer);
364
+ edges.set(auctioneer, bidders);
365
+ for (const b of bidders) edges.set(b, [auctioneer]);
366
+ return { pattern: p, agents, edges, hub: auctioneer };
367
+ }
368
+
369
+ case 'escalation': {
370
+ // Tiered chain: each level can escalate to the next
371
+ // Uses agent order or tier role numbers
372
+ const order = this.resolveEscalationOrder(agents);
373
+ for (let i = 0; i < order.length; i++) {
374
+ // Each tier can escalate up and report down
375
+ const canEscalateTo = i < order.length - 1 ? [order[i + 1]] : [];
376
+ const canReportTo = i > 0 ? [order[i - 1]] : [];
377
+ edges.set(order[i], [...canEscalateTo, ...canReportTo]);
378
+ }
379
+ // Ensure non-tiered agents still have edge entries (prevents undefined)
380
+ for (const n of names) {
381
+ if (!edges.has(n)) edges.set(n, []);
382
+ }
383
+ return { pattern: p, agents, edges, pipelineOrder: order };
384
+ }
385
+
386
+ case 'saga': {
387
+ // Orchestrator coordinates saga steps; each step can trigger compensate
388
+ const orchestrator = agents.find((a) => a.role === 'saga-orchestrator')?.name ?? this.pickHub(agents);
389
+ const participants = names.filter((n) => n !== orchestrator);
390
+ // Orchestrator → all participants (for commands)
391
+ edges.set(orchestrator, participants);
392
+ // Participants → orchestrator (for completion/failure signals)
393
+ for (const part of participants) edges.set(part, [orchestrator]);
394
+ return { pattern: p, agents, edges, hub: orchestrator };
395
+ }
396
+
397
+ case 'circuit-breaker': {
398
+ // Primary agent with fallback chain
399
+ const order = names; // First agent is primary, rest are fallbacks
400
+ for (let i = 0; i < order.length; i++) {
401
+ // Each can trigger next fallback
402
+ edges.set(order[i], i < order.length - 1 ? [order[i + 1]] : []);
403
+ }
404
+ return { pattern: p, agents, edges, pipelineOrder: order };
405
+ }
406
+
407
+ case 'blackboard': {
408
+ // All agents can read/write to shared blackboard (full mesh)
409
+ // Plus optional moderator
410
+ const moderator = agents.find((a) => a.role === 'moderator')?.name;
411
+ for (const n of names) {
412
+ edges.set(n, names.filter((o) => o !== n));
413
+ }
414
+ return { pattern: p, agents, edges, hub: moderator };
415
+ }
416
+
417
+ case 'swarm': {
418
+ // Emergent swarm: agents communicate with nearest neighbors
419
+ // For simplicity, partial mesh based on agent index proximity
420
+ const hiveMind = agents.find((a) => a.role === 'hive-mind')?.name;
421
+ for (let i = 0; i < names.length; i++) {
422
+ const neighbors: string[] = [];
423
+ if (i > 0) neighbors.push(names[i - 1]);
424
+ if (i < names.length - 1) neighbors.push(names[i + 1]);
425
+ // Also connect to hive mind if present (avoid duplicates if already adjacent)
426
+ if (hiveMind && hiveMind !== names[i] && !neighbors.includes(hiveMind)) neighbors.push(hiveMind);
427
+ edges.set(names[i], neighbors);
428
+ }
429
+ return { pattern: p, agents, edges, hub: hiveMind };
430
+ }
431
+
210
432
  default: {
211
433
  // Fallback: full mesh.
212
434
  for (const n of names) {
@@ -490,6 +712,22 @@ export class SwarmCoordinator extends EventEmitter {
490
712
  return order.length > 0 ? order : fallback;
491
713
  }
492
714
 
715
+ private resolveEscalationOrder(agents: AgentDefinition[]): string[] {
716
+ // Sort by tier role (e.g., "tier-1", "tier-2") or by agent order
717
+ const tiered = agents.filter((a) => a.role?.startsWith('tier-'));
718
+ if (tiered.length > 0) {
719
+ return tiered
720
+ .sort((a, b) => {
721
+ const tierA = parseInt(a.role?.replace('tier-', '') ?? '0', 10);
722
+ const tierB = parseInt(b.role?.replace('tier-', '') ?? '0', 10);
723
+ return tierA - tierB;
724
+ })
725
+ .map((a) => a.name);
726
+ }
727
+ // Fallback: use agent order
728
+ return agents.map((a) => a.name);
729
+ }
730
+
493
731
  private resolveDAGEdges(config: RelayYamlConfig): Map<string, string[]> {
494
732
  const edges = new Map<string, string[]>();
495
733
  const workflows = config.workflows ?? [];
@@ -7,3 +7,4 @@ export * from "./coordinator.js";
7
7
  export * from "./barrier.js";
8
8
  export * from "./state.js";
9
9
  export * from "./templates.js";
10
+ export { WorkflowTrajectory, type StepOutcome } from "./trajectory.js";
@@ -1,5 +1,5 @@
1
1
  import type { AgentRelayOptions } from '../relay.js';
2
- import type { WorkflowRunRow } from './types.js';
2
+ import type { TrajectoryConfig, WorkflowRunRow } from './types.js';
3
3
  import { WorkflowRunner, type WorkflowEventListener, type VariableContext } from './runner.js';
4
4
 
5
5
  /**
@@ -16,6 +16,8 @@ export interface RunWorkflowOptions {
16
16
  relay?: AgentRelayOptions;
17
17
  /** Progress callback for workflow events. */
18
18
  onEvent?: WorkflowEventListener;
19
+ /** Override trajectory config. Set to false to disable trajectory recording. */
20
+ trajectories?: TrajectoryConfig | false;
19
21
  }
20
22
 
21
23
  /**
@@ -43,5 +45,11 @@ export async function runWorkflow(
43
45
  }
44
46
 
45
47
  const config = await runner.parseYamlFile(yamlPath);
48
+
49
+ // Allow programmatic trajectory override
50
+ if (options.trajectories !== undefined) {
51
+ config.trajectories = options.trajectories;
52
+ }
53
+
46
54
  return runner.execute(config, options.workflow, options.vars);
47
55
  }