@agentxjs/devtools 1.9.1-dev

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.
@@ -0,0 +1,396 @@
1
+ /**
2
+ * MockDriver - Mock Driver for Testing
3
+ *
4
+ * Plays back recorded fixtures when receiving messages.
5
+ * Implements the new Driver interface with receive() returning AsyncIterable.
6
+ *
7
+ * Usage:
8
+ * ```typescript
9
+ * const driver = new MockDriver({
10
+ * fixture: "simple-reply",
11
+ * });
12
+ *
13
+ * await driver.initialize();
14
+ *
15
+ * for await (const event of driver.receive({ content: "Hello" })) {
16
+ * if (event.type === "text_delta") {
17
+ * console.log(event.data.text);
18
+ * }
19
+ * }
20
+ *
21
+ * await driver.dispose();
22
+ * ```
23
+ */
24
+
25
+ import type {
26
+ Driver,
27
+ DriverConfig,
28
+ DriverState,
29
+ DriverStreamEvent,
30
+ } from "@agentxjs/core/driver";
31
+ import type { UserMessage } from "@agentxjs/core/agent";
32
+ import type { Fixture, FixtureEvent, MockDriverOptions } from "../types";
33
+ import { BUILTIN_FIXTURES } from "../../fixtures";
34
+ import { createLogger } from "commonxjs/logger";
35
+
36
+ const logger = createLogger("devtools/MockDriver");
37
+
38
+ /**
39
+ * MockDriver - Playback driver for testing
40
+ *
41
+ * Implements the new Driver interface:
42
+ * - receive() returns AsyncIterable<DriverStreamEvent>
43
+ * - Clear input/output boundaries for testing
44
+ */
45
+ export class MockDriver implements Driver {
46
+ readonly name = "MockDriver";
47
+
48
+ private _sessionId: string | null = null;
49
+ private _state: DriverState = "idle";
50
+
51
+ private readonly config: DriverConfig | null;
52
+ private readonly options: MockDriverOptions;
53
+ private readonly fixtures: Map<string, Fixture>;
54
+ private currentFixture: Fixture;
55
+
56
+ // For interrupt handling
57
+ private isInterrupted = false;
58
+
59
+ // Event cursor for multi-turn conversations
60
+ private eventCursor = 0;
61
+
62
+ /**
63
+ * Create a MockDriver
64
+ *
65
+ * @param options - MockDriverOptions or DriverConfig
66
+ * @param mockOptions - MockDriverOptions if first param is DriverConfig
67
+ */
68
+ constructor(
69
+ optionsOrConfig: MockDriverOptions | DriverConfig,
70
+ mockOptions?: MockDriverOptions
71
+ ) {
72
+ // Detect which constructor form is being used
73
+ if (mockOptions !== undefined || "apiKey" in optionsOrConfig) {
74
+ // Factory mode: (DriverConfig, MockDriverOptions)
75
+ this.config = optionsOrConfig as DriverConfig;
76
+ const opts = mockOptions || {};
77
+ this.options = {
78
+ defaultDelay: 10,
79
+ speedMultiplier: 0,
80
+ ...opts,
81
+ };
82
+ } else {
83
+ // Simple mode: (MockDriverOptions)
84
+ this.config = null;
85
+ const opts = optionsOrConfig as MockDriverOptions;
86
+ this.options = {
87
+ defaultDelay: 10,
88
+ speedMultiplier: 0,
89
+ ...opts,
90
+ };
91
+ }
92
+
93
+ // Initialize fixtures
94
+ this.fixtures = new Map(BUILTIN_FIXTURES);
95
+ if (this.options.fixtures) {
96
+ for (const [name, fixture] of this.options.fixtures) {
97
+ this.fixtures.set(name, fixture);
98
+ }
99
+ }
100
+
101
+ // Set initial fixture
102
+ this.currentFixture = this.resolveFixture(this.options.fixture || "simple-reply");
103
+
104
+ logger.debug("MockDriver created", {
105
+ fixture: this.currentFixture.name,
106
+ agentId: this.config?.agentId,
107
+ });
108
+ }
109
+
110
+ // ============================================================================
111
+ // Driver Interface Properties
112
+ // ============================================================================
113
+
114
+ get sessionId(): string | null {
115
+ return this._sessionId;
116
+ }
117
+
118
+ get state(): DriverState {
119
+ return this._state;
120
+ }
121
+
122
+ // ============================================================================
123
+ // Lifecycle Methods
124
+ // ============================================================================
125
+
126
+ /**
127
+ * Initialize the Driver
128
+ */
129
+ async initialize(): Promise<void> {
130
+ if (this._state !== "idle") {
131
+ throw new Error(`Cannot initialize: MockDriver is in "${this._state}" state`);
132
+ }
133
+
134
+ // Generate a mock session ID
135
+ this._sessionId = `mock-session-${Date.now()}`;
136
+
137
+ logger.debug("MockDriver initialized", {
138
+ sessionId: this._sessionId,
139
+ fixture: this.currentFixture.name,
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Dispose and cleanup resources
145
+ */
146
+ async dispose(): Promise<void> {
147
+ if (this._state === "disposed") {
148
+ return;
149
+ }
150
+
151
+ this._state = "disposed";
152
+ this.isInterrupted = true;
153
+
154
+ logger.debug("MockDriver disposed");
155
+ }
156
+
157
+ // ============================================================================
158
+ // Core Methods
159
+ // ============================================================================
160
+
161
+ /**
162
+ * Receive a user message and return stream of events
163
+ *
164
+ * Plays back the current fixture as DriverStreamEvent.
165
+ *
166
+ * @param message - User message (ignored for playback)
167
+ * @returns AsyncIterable of stream events
168
+ */
169
+ async *receive(_message: UserMessage): AsyncIterable<DriverStreamEvent> {
170
+ if (this._state === "disposed") {
171
+ throw new Error("Cannot receive: MockDriver is disposed");
172
+ }
173
+
174
+ if (this._state === "active") {
175
+ throw new Error("Cannot receive: MockDriver is already processing a message");
176
+ }
177
+
178
+ this._state = "active";
179
+ this.isInterrupted = false;
180
+
181
+ const { speedMultiplier = 0, defaultDelay = 10 } = this.options;
182
+ const events = this.currentFixture.events;
183
+
184
+ try {
185
+ // Start from cursor position and play until message_stop
186
+ while (this.eventCursor < events.length) {
187
+ const fixtureEvent = events[this.eventCursor];
188
+ this.eventCursor++;
189
+
190
+ // Check for interrupt
191
+ if (this.isInterrupted) {
192
+ yield {
193
+ type: "interrupted",
194
+ timestamp: Date.now(),
195
+ data: { reason: "user" },
196
+ };
197
+ break;
198
+ }
199
+
200
+ // Apply delay
201
+ const delay = fixtureEvent.delay || defaultDelay;
202
+ if (delay > 0 && speedMultiplier > 0) {
203
+ await this.sleep(delay * speedMultiplier);
204
+ }
205
+
206
+ // Convert and yield event
207
+ const event = this.convertFixtureEvent(fixtureEvent);
208
+ if (event) {
209
+ yield event;
210
+ }
211
+
212
+ // Stop at message_stop (end of one turn)
213
+ if (fixtureEvent.type === "message_stop") {
214
+ break;
215
+ }
216
+ }
217
+ } finally {
218
+ this._state = "idle";
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Interrupt current operation
224
+ */
225
+ interrupt(): void {
226
+ if (this._state !== "active") {
227
+ logger.debug("Interrupt called but no active operation");
228
+ return;
229
+ }
230
+
231
+ logger.debug("MockDriver interrupted");
232
+ this.isInterrupted = true;
233
+ }
234
+
235
+ // ============================================================================
236
+ // Fixture Management
237
+ // ============================================================================
238
+
239
+ /**
240
+ * Set the fixture to use for next playback
241
+ */
242
+ setFixture(fixture: string | Fixture): void {
243
+ this.currentFixture = this.resolveFixture(fixture);
244
+ this.eventCursor = 0; // Reset cursor when fixture changes
245
+ logger.debug("Fixture changed", { fixture: this.currentFixture.name });
246
+ }
247
+
248
+ /**
249
+ * Add a custom fixture
250
+ */
251
+ addFixture(fixture: Fixture): void {
252
+ this.fixtures.set(fixture.name, fixture);
253
+ }
254
+
255
+ /**
256
+ * Get the current fixture
257
+ */
258
+ getFixture(): Fixture {
259
+ return this.currentFixture;
260
+ }
261
+
262
+ /**
263
+ * Get available fixture names
264
+ */
265
+ getFixtureNames(): string[] {
266
+ return Array.from(this.fixtures.keys());
267
+ }
268
+
269
+ // ============================================================================
270
+ // Private Methods
271
+ // ============================================================================
272
+
273
+ /**
274
+ * Resolve fixture from name or Fixture object
275
+ */
276
+ private resolveFixture(fixture: string | Fixture): Fixture {
277
+ if (typeof fixture === "string") {
278
+ const found = this.fixtures.get(fixture);
279
+ if (!found) {
280
+ logger.warn(`Fixture "${fixture}" not found, using "simple-reply"`);
281
+ return this.fixtures.get("simple-reply")!;
282
+ }
283
+ return found;
284
+ }
285
+ return fixture;
286
+ }
287
+
288
+ /**
289
+ * Convert FixtureEvent to DriverStreamEvent
290
+ */
291
+ private convertFixtureEvent(fixtureEvent: FixtureEvent): DriverStreamEvent | null {
292
+ const timestamp = Date.now();
293
+ const data = fixtureEvent.data as Record<string, unknown>;
294
+
295
+ switch (fixtureEvent.type) {
296
+ case "message_start":
297
+ return {
298
+ type: "message_start",
299
+ timestamp,
300
+ data: {
301
+ messageId: (data.messageId as string) || `msg_${timestamp}`,
302
+ model: (data.model as string) || "mock-model",
303
+ },
304
+ };
305
+
306
+ case "text_delta":
307
+ return {
308
+ type: "text_delta",
309
+ timestamp,
310
+ data: { text: (data.text as string) || "" },
311
+ };
312
+
313
+ case "tool_use_start":
314
+ return {
315
+ type: "tool_use_start",
316
+ timestamp,
317
+ data: {
318
+ toolCallId: (data.toolCallId as string) || `tool_${timestamp}`,
319
+ toolName: (data.toolName as string) || "",
320
+ },
321
+ };
322
+
323
+ case "input_json_delta":
324
+ return {
325
+ type: "input_json_delta",
326
+ timestamp,
327
+ data: { partialJson: (data.partialJson as string) || "" },
328
+ };
329
+
330
+ case "tool_use_stop":
331
+ return {
332
+ type: "tool_use_stop",
333
+ timestamp,
334
+ data: {
335
+ toolCallId: (data.toolCallId as string) || "",
336
+ toolName: (data.toolName as string) || "",
337
+ input: (data.input as Record<string, unknown>) || {},
338
+ },
339
+ };
340
+
341
+ case "tool_result":
342
+ return {
343
+ type: "tool_result",
344
+ timestamp,
345
+ data: {
346
+ toolCallId: (data.toolCallId as string) || "",
347
+ result: data.result,
348
+ isError: data.isError as boolean | undefined,
349
+ },
350
+ };
351
+
352
+ case "message_stop":
353
+ return {
354
+ type: "message_stop",
355
+ timestamp,
356
+ data: {
357
+ stopReason: (data.stopReason as "end_turn" | "tool_use" | "max_tokens" | "stop_sequence" | "other") || "end_turn",
358
+ },
359
+ };
360
+
361
+ case "error":
362
+ return {
363
+ type: "error",
364
+ timestamp,
365
+ data: {
366
+ message: (data.message as string) || "Unknown error",
367
+ errorCode: (data.errorCode as string) || "mock_error",
368
+ },
369
+ };
370
+
371
+ default:
372
+ // Pass through unknown events with generic structure
373
+ logger.debug(`Unknown fixture event type: ${fixtureEvent.type}`);
374
+ return null;
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Sleep for specified milliseconds
380
+ */
381
+ private sleep(ms: number): Promise<void> {
382
+ return new Promise((resolve) => setTimeout(resolve, ms));
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Create a MockDriver factory function
388
+ *
389
+ * Returns a CreateDriver-compatible function.
390
+ *
391
+ * @param mockOptions - Options for all created drivers
392
+ * @returns CreateDriver function
393
+ */
394
+ export function createMockDriver(mockOptions: MockDriverOptions = {}): (config: DriverConfig) => Driver {
395
+ return (config: DriverConfig) => new MockDriver(config, mockOptions);
396
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Mock Driver Module
3
+ *
4
+ * Provides MockDriver for playback testing with fixtures.
5
+ *
6
+ * Usage:
7
+ * ```typescript
8
+ * import { MockDriver, createMockDriver } from "@agentxjs/devtools/mock";
9
+ *
10
+ * // Simple usage
11
+ * const driver = new MockDriver({ fixture: "simple-reply" });
12
+ * await driver.initialize();
13
+ * for await (const event of driver.receive({ content: "Hello" })) {
14
+ * console.log(event);
15
+ * }
16
+ *
17
+ * // Factory usage (for Provider)
18
+ * const createDriver = createMockDriver({ fixture: "simple-reply" });
19
+ * const driver = createDriver(config);
20
+ * ```
21
+ */
22
+
23
+ export { MockDriver, createMockDriver } from "./MockDriver";
@@ -0,0 +1,258 @@
1
+ /**
2
+ * RecordingDriver - Wraps a real driver to record events
3
+ *
4
+ * Used to capture real LLM API responses and save them as fixtures.
5
+ * These fixtures can then be played back by MockDriver for testing.
6
+ *
7
+ * Usage:
8
+ * ```typescript
9
+ * import { createClaudeDriver } from "@agentxjs/claude-driver";
10
+ * import { RecordingDriver } from "@agentxjs/devtools/recorder";
11
+ *
12
+ * // Create real driver
13
+ * const realDriver = createClaudeDriver(config);
14
+ *
15
+ * // Wrap with recorder
16
+ * const recorder = new RecordingDriver({
17
+ * driver: realDriver,
18
+ * name: "my-scenario",
19
+ * description: "User asks about weather",
20
+ * });
21
+ *
22
+ * await recorder.initialize();
23
+ *
24
+ * // Use like a normal driver - events are recorded
25
+ * for await (const event of recorder.receive({ content: "Hello" })) {
26
+ * console.log(event);
27
+ * }
28
+ *
29
+ * // Save the fixture
30
+ * await recorder.saveFixture("./fixtures/my-scenario.json");
31
+ * ```
32
+ */
33
+
34
+ import type {
35
+ Driver,
36
+ DriverState,
37
+ DriverStreamEvent,
38
+ } from "@agentxjs/core/driver";
39
+ import type { UserMessage } from "@agentxjs/core/agent";
40
+ import type { Fixture, FixtureEvent } from "../types";
41
+ import { createLogger } from "commonxjs/logger";
42
+
43
+ const logger = createLogger("devtools/RecordingDriver");
44
+
45
+ /**
46
+ * Options for RecordingDriver
47
+ */
48
+ export interface RecordingDriverOptions {
49
+ /**
50
+ * The real driver to wrap
51
+ */
52
+ driver: Driver;
53
+
54
+ /**
55
+ * Fixture name for the recording
56
+ */
57
+ name: string;
58
+
59
+ /**
60
+ * Description for the recording
61
+ */
62
+ description?: string;
63
+ }
64
+
65
+ /**
66
+ * Recorded event with timing
67
+ */
68
+ interface RecordedEvent {
69
+ event: DriverStreamEvent;
70
+ timestamp: number;
71
+ }
72
+
73
+ /**
74
+ * RecordingDriver - Records events from a real driver
75
+ *
76
+ * Implements the new Driver interface by wrapping a real driver
77
+ * and intercepting events from receive().
78
+ */
79
+ export class RecordingDriver implements Driver {
80
+ readonly name = "RecordingDriver";
81
+
82
+ private readonly realDriver: Driver;
83
+ private readonly fixtureName: string;
84
+ private readonly fixtureDescription?: string;
85
+
86
+ private recordedEvents: RecordedEvent[] = [];
87
+ private recordingStartTime: number = 0;
88
+
89
+ constructor(options: RecordingDriverOptions) {
90
+ this.realDriver = options.driver;
91
+ this.fixtureName = options.name;
92
+ this.fixtureDescription = options.description;
93
+
94
+ logger.info("RecordingDriver created", { name: this.fixtureName });
95
+ }
96
+
97
+ // ============================================================================
98
+ // Driver Interface Properties (delegate to real driver)
99
+ // ============================================================================
100
+
101
+ get sessionId(): string | null {
102
+ return this.realDriver.sessionId;
103
+ }
104
+
105
+ get state(): DriverState {
106
+ return this.realDriver.state;
107
+ }
108
+
109
+ // ============================================================================
110
+ // Lifecycle Methods (delegate to real driver)
111
+ // ============================================================================
112
+
113
+ async initialize(): Promise<void> {
114
+ await this.realDriver.initialize();
115
+ this.recordingStartTime = Date.now();
116
+ this.recordedEvents = [];
117
+ logger.info("RecordingDriver initialized, recording started", {
118
+ name: this.fixtureName,
119
+ });
120
+ }
121
+
122
+ async dispose(): Promise<void> {
123
+ await this.realDriver.dispose();
124
+ logger.info("RecordingDriver disposed", {
125
+ name: this.fixtureName,
126
+ eventsRecorded: this.recordedEvents.length,
127
+ });
128
+ }
129
+
130
+ // ============================================================================
131
+ // Core Methods
132
+ // ============================================================================
133
+
134
+ /**
135
+ * Receive a user message and return stream of events
136
+ *
137
+ * Wraps the real driver's receive() and records all events.
138
+ */
139
+ async *receive(message: UserMessage): AsyncIterable<DriverStreamEvent> {
140
+ logger.debug("RecordingDriver receiving message", {
141
+ name: this.fixtureName,
142
+ });
143
+
144
+ // Call the real driver and intercept events
145
+ for await (const event of this.realDriver.receive(message)) {
146
+ // Record the event
147
+ this.recordEvent(event);
148
+
149
+ // Pass through to caller
150
+ yield event;
151
+ }
152
+
153
+ logger.debug("RecordingDriver receive completed", {
154
+ name: this.fixtureName,
155
+ eventsRecorded: this.recordedEvents.length,
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Interrupt current operation (delegate to real driver)
161
+ */
162
+ interrupt(): void {
163
+ this.realDriver.interrupt();
164
+ }
165
+
166
+ // ============================================================================
167
+ // Recording Methods
168
+ // ============================================================================
169
+
170
+ /**
171
+ * Record an event
172
+ */
173
+ private recordEvent(event: DriverStreamEvent): void {
174
+ this.recordedEvents.push({
175
+ event,
176
+ timestamp: Date.now(),
177
+ });
178
+
179
+ logger.debug("Event recorded", {
180
+ type: event.type,
181
+ totalEvents: this.recordedEvents.length,
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Get the recorded fixture
187
+ */
188
+ getFixture(): Fixture {
189
+ const events: FixtureEvent[] = [];
190
+ let lastTimestamp = this.recordingStartTime;
191
+
192
+ for (const recorded of this.recordedEvents) {
193
+ const delay = recorded.timestamp - lastTimestamp;
194
+ lastTimestamp = recorded.timestamp;
195
+
196
+ events.push({
197
+ type: recorded.event.type,
198
+ delay: Math.max(0, delay),
199
+ data: recorded.event.data,
200
+ });
201
+ }
202
+
203
+ return {
204
+ name: this.fixtureName,
205
+ description: this.fixtureDescription,
206
+ recordedAt: this.recordingStartTime,
207
+ events,
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Save the recorded fixture to a JSON file
213
+ */
214
+ async saveFixture(filePath: string): Promise<void> {
215
+ const fixture = this.getFixture();
216
+ const json = JSON.stringify(fixture, null, 2);
217
+
218
+ // Use dynamic import for Node.js fs
219
+ const { writeFile } = await import("node:fs/promises");
220
+ await writeFile(filePath, json, "utf-8");
221
+
222
+ logger.info("Fixture saved", {
223
+ path: filePath,
224
+ name: fixture.name,
225
+ eventCount: fixture.events.length,
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Get the number of recorded events
231
+ */
232
+ get eventCount(): number {
233
+ return this.recordedEvents.length;
234
+ }
235
+
236
+ /**
237
+ * Clear recorded events (start fresh recording)
238
+ */
239
+ clearRecording(): void {
240
+ this.recordedEvents = [];
241
+ this.recordingStartTime = Date.now();
242
+ logger.debug("Recording cleared");
243
+ }
244
+
245
+ /**
246
+ * Get raw recorded events (for debugging)
247
+ */
248
+ getRawEvents(): RecordedEvent[] {
249
+ return [...this.recordedEvents];
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Create a RecordingDriver that wraps a real driver
255
+ */
256
+ export function createRecordingDriver(options: RecordingDriverOptions): RecordingDriver {
257
+ return new RecordingDriver(options);
258
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Recording Driver Module
3
+ *
4
+ * Provides RecordingDriver for capturing LLM events from any driver.
5
+ * Use these recordings as fixtures for MockDriver to test Runtime.
6
+ *
7
+ * Usage:
8
+ * ```typescript
9
+ * import { createRecordingDriver } from "@agentxjs/devtools/recorder";
10
+ *
11
+ * const recorder = createRecordingDriver({
12
+ * driver: realDriver,
13
+ * name: "my-scenario",
14
+ * });
15
+ *
16
+ * // After use, save the fixture
17
+ * await recorder.saveFixture("./fixtures/my-scenario.json");
18
+ * ```
19
+ */
20
+
21
+ export {
22
+ RecordingDriver,
23
+ createRecordingDriver,
24
+ type RecordingDriverOptions,
25
+ } from "./RecordingDriver";