agent-relay 2.3.12 → 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 (53) hide show
  1. package/package.json +19 -19
  2. package/packages/acp-bridge/package.json +2 -2
  3. package/packages/bridge/package.json +7 -7
  4. package/packages/broker-sdk/dist/relaycast.d.ts +10 -0
  5. package/packages/broker-sdk/dist/relaycast.d.ts.map +1 -1
  6. package/packages/broker-sdk/dist/relaycast.js +40 -0
  7. package/packages/broker-sdk/dist/relaycast.js.map +1 -1
  8. package/packages/broker-sdk/dist/workflows/index.d.ts +1 -0
  9. package/packages/broker-sdk/dist/workflows/index.d.ts.map +1 -1
  10. package/packages/broker-sdk/dist/workflows/index.js +1 -0
  11. package/packages/broker-sdk/dist/workflows/index.js.map +1 -1
  12. package/packages/broker-sdk/dist/workflows/run.d.ts +3 -1
  13. package/packages/broker-sdk/dist/workflows/run.d.ts.map +1 -1
  14. package/packages/broker-sdk/dist/workflows/run.js +4 -0
  15. package/packages/broker-sdk/dist/workflows/run.js.map +1 -1
  16. package/packages/broker-sdk/dist/workflows/runner.d.ts +9 -0
  17. package/packages/broker-sdk/dist/workflows/runner.d.ts.map +1 -1
  18. package/packages/broker-sdk/dist/workflows/runner.js +202 -14
  19. package/packages/broker-sdk/dist/workflows/runner.js.map +1 -1
  20. package/packages/broker-sdk/dist/workflows/trajectory.d.ts +80 -0
  21. package/packages/broker-sdk/dist/workflows/trajectory.d.ts.map +1 -0
  22. package/packages/broker-sdk/dist/workflows/trajectory.js +362 -0
  23. package/packages/broker-sdk/dist/workflows/trajectory.js.map +1 -0
  24. package/packages/broker-sdk/dist/workflows/types.d.ts +12 -0
  25. package/packages/broker-sdk/dist/workflows/types.d.ts.map +1 -1
  26. package/packages/broker-sdk/package.json +2 -2
  27. package/packages/broker-sdk/src/__tests__/workflow-trajectory.test.ts +408 -0
  28. package/packages/broker-sdk/src/relaycast.ts +42 -0
  29. package/packages/broker-sdk/src/workflows/index.ts +1 -0
  30. package/packages/broker-sdk/src/workflows/run.ts +9 -1
  31. package/packages/broker-sdk/src/workflows/runner.ts +248 -14
  32. package/packages/broker-sdk/src/workflows/trajectory.ts +507 -0
  33. package/packages/broker-sdk/src/workflows/types.ts +15 -0
  34. package/packages/broker-sdk/tsconfig.json +1 -0
  35. package/packages/broker-sdk/vitest.config.ts +9 -0
  36. package/packages/config/package.json +2 -2
  37. package/packages/continuity/package.json +2 -2
  38. package/packages/daemon/package.json +12 -12
  39. package/packages/hooks/package.json +4 -4
  40. package/packages/mcp/package.json +5 -5
  41. package/packages/memory/package.json +2 -2
  42. package/packages/policy/package.json +2 -2
  43. package/packages/protocol/package.json +1 -1
  44. package/packages/resiliency/package.json +1 -1
  45. package/packages/sdk/package.json +3 -3
  46. package/packages/spawner/package.json +1 -1
  47. package/packages/state/package.json +1 -1
  48. package/packages/storage/package.json +2 -2
  49. package/packages/telemetry/package.json +1 -1
  50. package/packages/trajectory/package.json +2 -2
  51. package/packages/user-directory/package.json +2 -2
  52. package/packages/utils/package.json +3 -3
  53. package/packages/wrapper/package.json +5 -5
@@ -0,0 +1,408 @@
1
+ /**
2
+ * WorkflowTrajectory unit tests.
3
+ *
4
+ * Tests trajectory recording, chapter management, reflections, decisions,
5
+ * confidence computation, and the disabled/enabled toggle.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
9
+ import { existsSync, readFileSync, readdirSync, rmSync, mkdirSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import os from 'node:os';
12
+ import { WorkflowTrajectory, type StepOutcome } from '../workflows/trajectory.js';
13
+
14
+ // ── Test helpers ─────────────────────────────────────────────────────────────
15
+
16
+ let tmpDir: string;
17
+
18
+ function makeTmpDir(): string {
19
+ const dir = path.join(os.tmpdir(), `wf-traj-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
20
+ mkdirSync(dir, { recursive: true });
21
+ return dir;
22
+ }
23
+
24
+ function readTrajectoryFile(dir: string): any {
25
+ const activeDir = path.join(dir, '.trajectories', 'active');
26
+ if (!existsSync(activeDir)) return null;
27
+
28
+ const files = readdirSync(activeDir);
29
+ const jsonFiles = files.filter((f: string) => f.endsWith('.json'));
30
+ if (jsonFiles.length === 0) return null;
31
+
32
+ return JSON.parse(readFileSync(path.join(activeDir, jsonFiles[0]), 'utf-8'));
33
+ }
34
+
35
+ function readCompletedTrajectoryFile(dir: string): any {
36
+ const completedDir = path.join(dir, '.trajectories', 'completed');
37
+ if (!existsSync(completedDir)) return null;
38
+
39
+ const files = readdirSync(completedDir);
40
+ const jsonFiles = files.filter((f: string) => f.endsWith('.json'));
41
+ if (jsonFiles.length === 0) return null;
42
+
43
+ return JSON.parse(readFileSync(path.join(completedDir, jsonFiles[0]), 'utf-8'));
44
+ }
45
+
46
+ // ── Tests ────────────────────────────────────────────────────────────────────
47
+
48
+ describe('WorkflowTrajectory', () => {
49
+ beforeEach(() => {
50
+ tmpDir = makeTmpDir();
51
+ });
52
+
53
+ afterEach(() => {
54
+ try {
55
+ rmSync(tmpDir, { recursive: true, force: true });
56
+ } catch {
57
+ // cleanup best-effort
58
+ }
59
+ });
60
+
61
+ // ── Disabled mode ──────────────────────────────────────────────────────
62
+
63
+ describe('disabled', () => {
64
+ it('should not create files when trajectories is false', async () => {
65
+ const traj = new WorkflowTrajectory(false, 'run-1', tmpDir);
66
+ await traj.start('test-workflow', 3);
67
+
68
+ expect(traj.isEnabled()).toBe(false);
69
+ expect(traj.getTrajectoryId()).toBeNull();
70
+ expect(existsSync(path.join(tmpDir, '.trajectories'))).toBe(false);
71
+ });
72
+
73
+ it('should not create files when enabled is false', async () => {
74
+ const traj = new WorkflowTrajectory({ enabled: false }, 'run-1', tmpDir);
75
+ await traj.start('test-workflow', 3);
76
+
77
+ expect(traj.isEnabled()).toBe(false);
78
+ });
79
+
80
+ it('should be enabled by default', () => {
81
+ const traj = new WorkflowTrajectory(undefined, 'run-1', tmpDir);
82
+ expect(traj.isEnabled()).toBe(true);
83
+ });
84
+ });
85
+
86
+ // ── Lifecycle ──────────────────────────────────────────────────────────
87
+
88
+ describe('lifecycle', () => {
89
+ it('should create a trajectory file on start', async () => {
90
+ const traj = new WorkflowTrajectory({}, 'run-abc', tmpDir);
91
+ await traj.start('my-workflow', 5);
92
+
93
+ expect(traj.getTrajectoryId()).toBeTruthy();
94
+ expect(traj.getTrajectoryId()!.startsWith('traj_')).toBe(true);
95
+
96
+ const data = readTrajectoryFile(tmpDir);
97
+ expect(data).toBeTruthy();
98
+ expect(data.status).toBe('active');
99
+ expect(data.task.title).toContain('my-workflow');
100
+ expect(data.agents).toHaveLength(1);
101
+ expect(data.agents[0].name).toBe('orchestrator');
102
+ });
103
+
104
+ it('should create Planning chapter on start', async () => {
105
+ const traj = new WorkflowTrajectory({}, 'run-abc', tmpDir);
106
+ await traj.start('my-workflow', 3, '3 parallel tracks, 2 barriers');
107
+
108
+ const data = readTrajectoryFile(tmpDir);
109
+ expect(data.chapters).toHaveLength(1);
110
+ expect(data.chapters[0].title).toBe('Planning');
111
+ expect(data.chapters[0].events.length).toBeGreaterThanOrEqual(1);
112
+ });
113
+
114
+ it('should complete trajectory and move to completed dir', async () => {
115
+ const traj = new WorkflowTrajectory({}, 'run-abc', tmpDir);
116
+ await traj.start('my-workflow', 2);
117
+ await traj.complete('All done', 0.95);
118
+
119
+ const active = readTrajectoryFile(tmpDir);
120
+ expect(active).toBeNull(); // Moved out of active
121
+
122
+ const completed = readCompletedTrajectoryFile(tmpDir);
123
+ expect(completed).toBeTruthy();
124
+ expect(completed.status).toBe('completed');
125
+ expect(completed.retrospective.summary).toBe('All done');
126
+ expect(completed.retrospective.confidence).toBe(0.95);
127
+ });
128
+
129
+ it('should abandon trajectory and move to completed dir', async () => {
130
+ const traj = new WorkflowTrajectory({}, 'run-abc', tmpDir);
131
+ await traj.start('my-workflow', 2);
132
+ await traj.abandon('Something went wrong');
133
+
134
+ const completed = readCompletedTrajectoryFile(tmpDir);
135
+ expect(completed).toBeTruthy();
136
+ expect(completed.status).toBe('abandoned');
137
+ });
138
+ });
139
+
140
+ // ── Step events ────────────────────────────────────────────────────────
141
+
142
+ describe('step events', () => {
143
+ it('should record step started', async () => {
144
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
145
+ await traj.start('wf', 2);
146
+ await traj.stepStarted(
147
+ { name: 'build', agent: 'builder', task: 'Build it' },
148
+ 'builder-agent',
149
+ );
150
+
151
+ const data = readTrajectoryFile(tmpDir);
152
+ expect(data.agents).toHaveLength(2); // orchestrator + builder-agent
153
+ const events = data.chapters.flatMap((c: any) => c.events);
154
+ expect(events.some((e: any) => e.content.includes('build'))).toBe(true);
155
+ });
156
+
157
+ it('should record step completed', async () => {
158
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
159
+ await traj.start('wf', 1);
160
+ await traj.stepCompleted(
161
+ { name: 'test', agent: 'tester', task: 'Run tests' },
162
+ 'All tests passing',
163
+ 1,
164
+ );
165
+
166
+ const data = readTrajectoryFile(tmpDir);
167
+ const events = data.chapters.flatMap((c: any) => c.events);
168
+ expect(events.some((e: any) => e.type === 'finding')).toBe(true);
169
+ });
170
+
171
+ it('should record step failed', async () => {
172
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
173
+ await traj.start('wf', 1);
174
+ await traj.stepFailed(
175
+ { name: 'deploy', agent: 'deployer', task: 'Deploy' },
176
+ 'Connection refused',
177
+ 1,
178
+ 3,
179
+ );
180
+
181
+ const data = readTrajectoryFile(tmpDir);
182
+ const events = data.chapters.flatMap((c: any) => c.events);
183
+ expect(events.some((e: any) => e.type === 'error')).toBe(true);
184
+ });
185
+
186
+ it('should record step skipped', async () => {
187
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
188
+ await traj.start('wf', 2);
189
+ await traj.stepSkipped(
190
+ { name: 'integration', agent: 'tester', task: 'Test' },
191
+ 'Upstream failed',
192
+ );
193
+
194
+ const data = readTrajectoryFile(tmpDir);
195
+ const events = data.chapters.flatMap((c: any) => c.events);
196
+ expect(events.some((e: any) => e.content.includes('Skipped'))).toBe(true);
197
+ });
198
+ });
199
+
200
+ // ── Chapters ───────────────────────────────────────────────────────────
201
+
202
+ describe('chapters', () => {
203
+ it('should create track chapters', async () => {
204
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
205
+ await traj.start('wf', 3);
206
+ await traj.beginTrack('backend');
207
+
208
+ const data = readTrajectoryFile(tmpDir);
209
+ expect(data.chapters.length).toBeGreaterThanOrEqual(2);
210
+ expect(data.chapters.some((c: any) => c.title === 'Execution: backend')).toBe(true);
211
+ });
212
+
213
+ it('should create convergence chapters', async () => {
214
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
215
+ await traj.start('wf', 3);
216
+ await traj.beginConvergence('all-tracks-done');
217
+
218
+ const data = readTrajectoryFile(tmpDir);
219
+ expect(data.chapters.some((c: any) => c.title === 'Convergence: all-tracks-done')).toBe(true);
220
+ });
221
+
222
+ it('should close previous chapter when opening new one', async () => {
223
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
224
+ await traj.start('wf', 3);
225
+ await traj.beginTrack('track-a');
226
+ await traj.beginTrack('track-b');
227
+
228
+ const data = readTrajectoryFile(tmpDir);
229
+ // Planning chapter should have endedAt
230
+ expect(data.chapters[0].endedAt).toBeTruthy();
231
+ // First track chapter should have endedAt
232
+ expect(data.chapters[1].endedAt).toBeTruthy();
233
+ });
234
+ });
235
+
236
+ // ── Reflections ────────────────────────────────────────────────────────
237
+
238
+ describe('reflections', () => {
239
+ it('should record reflect events', async () => {
240
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
241
+ await traj.start('wf', 2);
242
+ await traj.reflect('All parallel tracks complete', 0.85, ['step-a: completed', 'step-b: completed']);
243
+
244
+ const data = readTrajectoryFile(tmpDir);
245
+ const events = data.chapters.flatMap((c: any) => c.events);
246
+ const reflection = events.find((e: any) => e.type === 'reflection');
247
+ expect(reflection).toBeTruthy();
248
+ expect(reflection.significance).toBe('high');
249
+ expect(reflection.raw.confidence).toBe(0.85);
250
+ expect(reflection.raw.focalPoints).toHaveLength(2);
251
+ });
252
+
253
+ it('should synthesize and reflect at convergence', async () => {
254
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
255
+ await traj.start('wf', 3);
256
+
257
+ const outcomes: StepOutcome[] = [
258
+ { name: 'step-a', agent: 'a', status: 'completed', attempts: 1 },
259
+ { name: 'step-b', agent: 'b', status: 'completed', attempts: 2 },
260
+ ];
261
+
262
+ await traj.synthesizeAndReflect('backend-ready', outcomes, ['step-c']);
263
+
264
+ const data = readTrajectoryFile(tmpDir);
265
+ // Should have a convergence chapter
266
+ expect(data.chapters.some((c: any) => c.title.includes('Convergence'))).toBe(true);
267
+ const events = data.chapters.flatMap((c: any) => c.events);
268
+ const reflection = events.find((e: any) => e.type === 'reflection');
269
+ expect(reflection).toBeTruthy();
270
+ expect(reflection.content).toContain('backend-ready');
271
+ expect(reflection.content).toContain('step-b'); // retried
272
+ });
273
+ });
274
+
275
+ // ── Decisions ──────────────────────────────────────────────────────────
276
+
277
+ describe('decisions', () => {
278
+ it('should record decisions', async () => {
279
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
280
+ await traj.start('wf', 1);
281
+ await traj.decide('How to handle failure', 'retry', 'Transient error detected');
282
+
283
+ const data = readTrajectoryFile(tmpDir);
284
+ const events = data.chapters.flatMap((c: any) => c.events);
285
+ const decision = events.find((e: any) => e.type === 'decision');
286
+ expect(decision).toBeTruthy();
287
+ expect(decision.raw.chosen).toBe('retry');
288
+ });
289
+
290
+ it('should skip decisions when autoDecisions is false', async () => {
291
+ const traj = new WorkflowTrajectory({ autoDecisions: false }, 'run-1', tmpDir);
292
+ await traj.start('wf', 1);
293
+ await traj.decide('How to handle failure', 'retry', 'Transient error');
294
+
295
+ const data = readTrajectoryFile(tmpDir);
296
+ const events = data.chapters.flatMap((c: any) => c.events);
297
+ expect(events.filter((e: any) => e.type === 'decision')).toHaveLength(0);
298
+ });
299
+ });
300
+
301
+ // ── Confidence computation ─────────────────────────────────────────────
302
+
303
+ describe('computeConfidence', () => {
304
+ it('should return 1.0 for all first-attempt verified completions', () => {
305
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
306
+ const outcomes: StepOutcome[] = [
307
+ { name: 'a', agent: 'a', status: 'completed', attempts: 1, verificationPassed: true },
308
+ { name: 'b', agent: 'b', status: 'completed', attempts: 1, verificationPassed: true },
309
+ ];
310
+ expect(traj.computeConfidence(outcomes)).toBe(1.0);
311
+ });
312
+
313
+ it('should return lower confidence for retried steps', () => {
314
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
315
+ const outcomes: StepOutcome[] = [
316
+ { name: 'a', agent: 'a', status: 'completed', attempts: 1, verificationPassed: true },
317
+ { name: 'b', agent: 'b', status: 'completed', attempts: 3, verificationPassed: true },
318
+ ];
319
+ const confidence = traj.computeConfidence(outcomes);
320
+ expect(confidence).toBeLessThan(1.0);
321
+ expect(confidence).toBeGreaterThan(0.5);
322
+ });
323
+
324
+ it('should return lower confidence for failed steps', () => {
325
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
326
+ const outcomes: StepOutcome[] = [
327
+ { name: 'a', agent: 'a', status: 'completed', attempts: 1 },
328
+ { name: 'b', agent: 'b', status: 'failed', attempts: 3 },
329
+ ];
330
+ const confidence = traj.computeConfidence(outcomes);
331
+ expect(confidence).toBeLessThan(0.5);
332
+ });
333
+
334
+ it('should return 0.7 for empty outcomes', () => {
335
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
336
+ expect(traj.computeConfidence([])).toBe(0.7);
337
+ });
338
+ });
339
+
340
+ // ── Synthesis helpers ──────────────────────────────────────────────────
341
+
342
+ describe('buildSynthesis', () => {
343
+ it('should produce meaningful synthesis text', () => {
344
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
345
+ const outcomes: StepOutcome[] = [
346
+ { name: 'step-a', agent: 'a', status: 'completed', attempts: 1 },
347
+ { name: 'step-b', agent: 'b', status: 'completed', attempts: 2 },
348
+ { name: 'step-c', agent: 'c', status: 'failed', attempts: 3, error: 'timeout' },
349
+ ];
350
+
351
+ const synthesis = traj.buildSynthesis('barrier-1', outcomes, ['step-d']);
352
+ expect(synthesis).toContain('barrier-1');
353
+ expect(synthesis).toContain('2/3 steps completed');
354
+ expect(synthesis).toContain('step-c'); // failed
355
+ expect(synthesis).toContain('step-b'); // retried
356
+ expect(synthesis).toContain('step-d'); // unblocked
357
+ });
358
+
359
+ it('should note all-first-attempt when no retries', () => {
360
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
361
+ const outcomes: StepOutcome[] = [
362
+ { name: 'a', agent: 'a', status: 'completed', attempts: 1 },
363
+ { name: 'b', agent: 'b', status: 'completed', attempts: 1 },
364
+ ];
365
+
366
+ const synthesis = traj.buildSynthesis('done', outcomes);
367
+ expect(synthesis).toContain('All steps completed on first attempt');
368
+ });
369
+ });
370
+
371
+ describe('buildRunSummary', () => {
372
+ it('should produce run summary with stats', () => {
373
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
374
+ const outcomes: StepOutcome[] = [
375
+ { name: 'a', agent: 'a', status: 'completed', attempts: 1 },
376
+ { name: 'b', agent: 'b', status: 'completed', attempts: 2 },
377
+ { name: 'c', agent: 'c', status: 'failed', attempts: 3 },
378
+ { name: 'd', agent: 'd', status: 'skipped', attempts: 1 },
379
+ ];
380
+
381
+ const summary = traj.buildRunSummary(outcomes);
382
+ expect(summary).toContain('2/4 steps passed');
383
+ expect(summary).toContain('1 failed');
384
+ expect(summary).toContain('1 skipped');
385
+ expect(summary).toContain('retries');
386
+ });
387
+ });
388
+
389
+ // ── Non-blocking behavior ──────────────────────────────────────────────
390
+
391
+ describe('non-blocking', () => {
392
+ it('should not throw on flush errors', async () => {
393
+ // Use a path that will fail (read-only or invalid)
394
+ const traj = new WorkflowTrajectory({}, 'run-1', '/dev/null/impossible-path');
395
+ // Should not throw
396
+ await expect(traj.start('wf', 1)).resolves.not.toThrow();
397
+ });
398
+
399
+ it('should handle all methods gracefully when not started', async () => {
400
+ const traj = new WorkflowTrajectory({}, 'run-1', tmpDir);
401
+ // Don't call start — all methods should be no-ops
402
+ await expect(traj.stepStarted({ name: 'x', agent: 'a', task: 't' }, 'a')).resolves.not.toThrow();
403
+ await expect(traj.reflect('test', 0.5)).resolves.not.toThrow();
404
+ await expect(traj.decide('q', 'c', 'r')).resolves.not.toThrow();
405
+ await expect(traj.complete('done', 0.9)).resolves.not.toThrow();
406
+ });
407
+ });
408
+ });
@@ -126,6 +126,48 @@ export class RelaycastApi {
126
126
  }
127
127
  }
128
128
 
129
+ /** Create a channel. No-op if it already exists. */
130
+ async createChannel(name: string, topic?: string): Promise<void> {
131
+ const agent = await this.ensure();
132
+ try {
133
+ await agent.channels.create({ name, ...(topic ? { topic } : {}) });
134
+ } catch (err) {
135
+ // Ignore "already exists" errors
136
+ if (err instanceof RelayError && err.code === "channel_already_exists") {
137
+ return;
138
+ }
139
+ throw err;
140
+ }
141
+ }
142
+
143
+ /** Join a channel. Idempotent. */
144
+ async joinChannel(name: string): Promise<void> {
145
+ const agent = await this.ensure();
146
+ await agent.channels.join(name);
147
+ }
148
+
149
+ /** Invite another agent to a channel. */
150
+ async inviteToChannel(channel: string, agentName: string): Promise<void> {
151
+ const agent = await this.ensure();
152
+ await agent.channels.invite(channel, agentName);
153
+ }
154
+
155
+ /** Register an external agent in the workspace (e.g., a spawned workflow agent).
156
+ * Uses the workspace API key to register, not an agent token.
157
+ * No-op if the agent already exists. */
158
+ async registerExternalAgent(name: string, persona?: string): Promise<void> {
159
+ const apiKey = await this.resolveApiKey();
160
+ const relay = new RelayCast({ apiKey, baseUrl: this.baseUrl });
161
+ try {
162
+ await relay.agents.register({ name, type: "agent", ...(persona ? { persona } : {}) });
163
+ } catch (err) {
164
+ if (err instanceof RelayError && err.code === "agent_already_exists") {
165
+ return;
166
+ }
167
+ throw err;
168
+ }
169
+ }
170
+
129
171
  /** Fetch message history from a channel. */
130
172
  async getMessages(
131
173
  channel: string,
@@ -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
  }