@positronic/core 0.0.1 → 0.0.3

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 (79) hide show
  1. package/CLAUDE.md +141 -0
  2. package/dist/src/adapters/types.js +1 -16
  3. package/dist/src/clients/types.js +4 -1
  4. package/dist/src/dsl/brain-runner.js +487 -0
  5. package/dist/src/dsl/brain-runner.test.js +733 -0
  6. package/dist/src/dsl/brain.js +1128 -0
  7. package/dist/src/dsl/brain.test.js +4225 -0
  8. package/dist/src/dsl/constants.js +6 -6
  9. package/dist/src/dsl/json-patch.js +37 -9
  10. package/dist/src/index.js +11 -10
  11. package/dist/src/resources/resources.js +371 -0
  12. package/dist/src/test-utils.js +474 -0
  13. package/dist/src/testing.js +3 -0
  14. package/dist/types/adapters/types.d.ts +3 -8
  15. package/dist/types/adapters/types.d.ts.map +1 -1
  16. package/dist/types/clients/types.d.ts +46 -6
  17. package/dist/types/clients/types.d.ts.map +1 -1
  18. package/dist/types/dsl/brain-runner.d.ts +24 -0
  19. package/dist/types/dsl/brain-runner.d.ts.map +1 -0
  20. package/dist/types/dsl/brain.d.ts +136 -0
  21. package/dist/types/dsl/brain.d.ts.map +1 -0
  22. package/dist/types/dsl/constants.d.ts +5 -5
  23. package/dist/types/dsl/constants.d.ts.map +1 -1
  24. package/dist/types/dsl/json-patch.d.ts +2 -1
  25. package/dist/types/dsl/json-patch.d.ts.map +1 -1
  26. package/dist/types/index.d.ts +13 -11
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/dist/types/resources/resource-loader.d.ts +6 -0
  29. package/dist/types/resources/resource-loader.d.ts.map +1 -0
  30. package/dist/types/resources/resources.d.ts +23 -0
  31. package/dist/types/resources/resources.d.ts.map +1 -0
  32. package/dist/types/test-utils.d.ts +94 -0
  33. package/dist/types/test-utils.d.ts.map +1 -0
  34. package/dist/types/testing.d.ts +2 -0
  35. package/dist/types/testing.d.ts.map +1 -0
  36. package/docs/core-testing-guide.md +289 -0
  37. package/package.json +26 -7
  38. package/src/adapters/types.ts +3 -22
  39. package/src/clients/types.ts +50 -10
  40. package/src/dsl/brain-runner.test.ts +384 -0
  41. package/src/dsl/brain-runner.ts +111 -0
  42. package/src/dsl/brain.test.ts +1981 -0
  43. package/src/dsl/brain.ts +740 -0
  44. package/src/dsl/constants.ts +6 -6
  45. package/src/dsl/json-patch.ts +24 -9
  46. package/src/dsl/types.ts +1 -1
  47. package/src/index.ts +30 -16
  48. package/src/resources/resource-loader.ts +8 -0
  49. package/src/resources/resources.ts +267 -0
  50. package/src/test-utils.ts +254 -0
  51. package/test/resources.test.ts +248 -0
  52. package/tsconfig.json +2 -2
  53. package/.swcrc +0 -31
  54. package/dist/src/dsl/extensions.js +0 -19
  55. package/dist/src/dsl/workflow-runner.js +0 -93
  56. package/dist/src/dsl/workflow.js +0 -308
  57. package/dist/src/file-stores/local-file-store.js +0 -12
  58. package/dist/src/utils/temp-files.js +0 -27
  59. package/dist/types/dsl/extensions.d.ts +0 -18
  60. package/dist/types/dsl/extensions.d.ts.map +0 -1
  61. package/dist/types/dsl/workflow-runner.d.ts +0 -28
  62. package/dist/types/dsl/workflow-runner.d.ts.map +0 -1
  63. package/dist/types/dsl/workflow.d.ts +0 -118
  64. package/dist/types/dsl/workflow.d.ts.map +0 -1
  65. package/dist/types/file-stores/local-file-store.d.ts +0 -7
  66. package/dist/types/file-stores/local-file-store.d.ts.map +0 -1
  67. package/dist/types/file-stores/types.d.ts +0 -4
  68. package/dist/types/file-stores/types.d.ts.map +0 -1
  69. package/dist/types/utils/temp-files.d.ts +0 -12
  70. package/dist/types/utils/temp-files.d.ts.map +0 -1
  71. package/src/dsl/extensions.ts +0 -58
  72. package/src/dsl/workflow-runner.test.ts +0 -203
  73. package/src/dsl/workflow-runner.ts +0 -146
  74. package/src/dsl/workflow.test.ts +0 -1435
  75. package/src/dsl/workflow.ts +0 -554
  76. package/src/file-stores/local-file-store.ts +0 -11
  77. package/src/file-stores/types.ts +0 -3
  78. package/src/utils/temp-files.ts +0 -46
  79. /package/dist/src/{file-stores/types.js → resources/resource-loader.js} +0 -0
package/package.json CHANGED
@@ -1,21 +1,40 @@
1
1
  {
2
2
  "name": "@positronic/core",
3
- "version": "0.0.1",
4
- "description": "A DSL for AI Workflows",
3
+ "version": "0.0.3",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Core services and tools for positronic AI brains",
5
8
  "type": "module",
6
9
  "main": "dist/src/index.js",
7
10
  "types": "dist/types/index.d.ts",
11
+ "license": "MIT",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/types/index.d.ts",
15
+ "import": "./dist/src/index.js"
16
+ },
17
+ "./testing": {
18
+ "types": "./dist/types/testing.d.ts",
19
+ "import": "./dist/src/testing.js"
20
+ }
21
+ },
8
22
  "scripts": {
9
23
  "tsc": "tsc --project tsconfig.json",
10
- "build": "npm run tsc && swc src -d dist",
11
- "clean": "rm -rf tsconfig.tsbuildinfo dist"
24
+ "swc": "swc src -d dist",
25
+ "build": "npm run tsc && npm run swc",
26
+ "clean": "rm -rf tsconfig.tsbuildinfo dist node_modules",
27
+ "test": "jest --silent"
28
+ },
29
+ "peerDependencies": {
30
+ "zod": "^3.24.1"
12
31
  },
13
32
  "dependencies": {
14
33
  "fast-json-patch": "^3.1.1",
15
- "uuid": "^11.0.5",
16
- "zod": "^3.24.1"
34
+ "uuid": "^11.0.5"
17
35
  },
18
36
  "devDependencies": {
19
- "@types/uuid": "^10.0.0"
37
+ "@types/uuid": "^10.0.0",
38
+ "zod": "^3.24.1"
20
39
  }
21
40
  }
@@ -1,24 +1,5 @@
1
- import { WORKFLOW_EVENTS } from '../dsl/constants';
2
- import type { WorkflowEvent } from '../dsl/workflow';
1
+ import type { BrainEvent } from '../dsl/brain.js';
3
2
 
4
- export abstract class Adapter<Options extends object = any> {
5
- async started?(event: WorkflowEvent<Options>): Promise<void>;
6
- async updated?(event: WorkflowEvent<Options>): Promise<void>;
7
- async completed?(event: WorkflowEvent<Options>): Promise<void>;
8
- async error?(event: WorkflowEvent<Options>): Promise<void>;
9
- async restarted?(event: WorkflowEvent<Options>): Promise<void>;
10
-
11
- async dispatch(event: WorkflowEvent<Options>) {
12
- if (event.type === WORKFLOW_EVENTS.START && this.started) {
13
- await this.started(event);
14
- } else if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE && this.updated) {
15
- await this.updated(event);
16
- } else if (event.type === WORKFLOW_EVENTS.COMPLETE && this.completed) {
17
- await this.completed(event);
18
- } else if (event.type === WORKFLOW_EVENTS.ERROR && this.error) {
19
- await this.error(event);
20
- } else if (event.type === WORKFLOW_EVENTS.RESTART && this.restarted) {
21
- await this.restarted(event);
22
- }
23
- }
3
+ export interface Adapter<Options extends object = any> {
4
+ dispatch(event: BrainEvent<Options>): void | Promise<void>;
24
5
  }
@@ -1,14 +1,54 @@
1
1
  import { z } from 'zod';
2
2
 
3
- export type ResponseModel<T extends z.AnyZodObject> = {
3
+ /**
4
+ * Represents a message in a conversation, used as input for the Generator.
5
+ */
6
+ export type Message = {
7
+ role: 'user' | 'assistant' | 'system';
8
+ content: string;
9
+ };
10
+
11
+ /**
12
+ * Interface for AI model interactions, focused on generating structured objects
13
+ * and potentially other types of content in the future.
14
+ */
15
+ export interface ObjectGenerator {
16
+ /**
17
+ * Generates a structured JSON object that conforms to the provided Zod schema.
18
+ *
19
+ * This method supports both simple single-string prompts and more complex
20
+ * multi-turn conversations via the `messages` array.
21
+ */
22
+ generateObject<T extends z.AnyZodObject>(params: {
23
+ /**
24
+ * The definition of the expected output object, including its Zod schema
25
+ * and a name for state management within the brain.
26
+ */
4
27
  schema: T;
5
- name: string;
6
- description?: string;
7
- }
28
+ schemaName: string;
29
+ schemaDescription?: string;
30
+
31
+ /**
32
+ * A simple prompt string for single-turn requests.
33
+ * If provided, this will typically be treated as the latest user input.
34
+ * If `messages` are also provided, this `prompt` is usually appended
35
+ * as a new user message to the existing `messages` array.
36
+ */
37
+ prompt?: string;
8
38
 
9
- export interface PromptClient {
10
- execute<T extends z.AnyZodObject>(
11
- prompt: string,
12
- responseModel: ResponseModel<T>,
13
- ): Promise<z.infer<T>>;
14
- }
39
+ /**
40
+ * An array of messages forming the conversation history.
41
+ * Use this for multi-turn conversations or when you need to provide
42
+ * a sequence of interactions (e.g., user, assistant, tool calls).
43
+ * If `prompt` is also provided, it's typically added to this history.
44
+ */
45
+ messages?: Message[];
46
+
47
+ /**
48
+ * An optional system-level instruction or context to guide the model's
49
+ * behavior for the entire interaction. Implementations will typically
50
+ * prepend this as a `system` role message to the full message list.
51
+ */
52
+ system?: string;
53
+ }): Promise<z.infer<T>>;
54
+ }
@@ -0,0 +1,384 @@
1
+ import { BrainRunner } from './brain-runner.js';
2
+ import { brain, type SerializedStep } from './brain.js';
3
+ import { BRAIN_EVENTS, STATUS } from './constants.js';
4
+ import { jest, describe, it, expect, beforeEach } from '@jest/globals';
5
+ import { ObjectGenerator } from '../clients/types.js';
6
+ import { Adapter } from '../adapters/types.js';
7
+ import { createResources, type Resources } from '../resources/resources.js';
8
+ import type { ResourceLoader } from '../resources/resource-loader.js';
9
+ import { z } from 'zod';
10
+
11
+ describe('BrainRunner', () => {
12
+ const mockGenerateObject = jest.fn<ObjectGenerator['generateObject']>();
13
+ const mockClient: jest.Mocked<ObjectGenerator> = {
14
+ generateObject: mockGenerateObject,
15
+ };
16
+
17
+ const mockDispatch = jest.fn<Adapter['dispatch']>();
18
+ const mockAdapter: jest.Mocked<Adapter> = {
19
+ dispatch: mockDispatch,
20
+ };
21
+
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ });
25
+
26
+ it('should run a brain and dispatch events to adapters', async () => {
27
+ const runner = new BrainRunner({
28
+ adapters: [mockAdapter],
29
+ client: mockClient,
30
+ });
31
+
32
+ const testBrain = brain('Test Brain')
33
+ .step('First Step', () => ({ value: 42 }))
34
+ .step('Async Step', async ({ state }) => {
35
+ await new Promise((resolve) => setTimeout(resolve, 10));
36
+ return { ...state, asyncValue: 'completed' };
37
+ })
38
+ .step('Final Step', ({ state }) => ({
39
+ ...state,
40
+ finalValue: state.value * 2,
41
+ }));
42
+
43
+ await runner.run(testBrain);
44
+
45
+ // Verify adapter received all events in correct order
46
+ expect(mockAdapter.dispatch).toHaveBeenCalledWith(
47
+ expect.objectContaining({
48
+ type: BRAIN_EVENTS.START,
49
+ brainTitle: 'Test Brain',
50
+ })
51
+ );
52
+
53
+ expect(mockAdapter.dispatch).toHaveBeenCalledWith(
54
+ expect.objectContaining({
55
+ type: BRAIN_EVENTS.STEP_COMPLETE,
56
+ stepTitle: 'First Step',
57
+ })
58
+ );
59
+
60
+ expect(mockAdapter.dispatch).toHaveBeenCalledWith(
61
+ expect.objectContaining({
62
+ type: BRAIN_EVENTS.STEP_COMPLETE,
63
+ stepTitle: 'Async Step',
64
+ })
65
+ );
66
+
67
+ expect(mockAdapter.dispatch).toHaveBeenCalledWith(
68
+ expect.objectContaining({
69
+ type: BRAIN_EVENTS.STEP_COMPLETE,
70
+ stepTitle: 'Final Step',
71
+ })
72
+ );
73
+
74
+ expect(mockAdapter.dispatch).toHaveBeenCalledWith(
75
+ expect.objectContaining({
76
+ type: BRAIN_EVENTS.COMPLETE,
77
+ status: STATUS.COMPLETE,
78
+ })
79
+ );
80
+
81
+ // Verify the order of events
82
+ const stepCompletions = mockAdapter.dispatch.mock.calls
83
+ .filter((call) => (call[0] as any).type === BRAIN_EVENTS.STEP_COMPLETE)
84
+ .map((call) => (call[0] as any).stepTitle);
85
+
86
+ expect(stepCompletions).toEqual(['First Step', 'Async Step', 'Final Step']);
87
+ });
88
+
89
+ it('should handle brain errors', async () => {
90
+ const runner = new BrainRunner({
91
+ adapters: [mockAdapter],
92
+ client: mockClient,
93
+ });
94
+
95
+ const errorBrain = brain('Error Brain').step('Error Step', () => {
96
+ throw new Error('Test error');
97
+ });
98
+
99
+ try {
100
+ await runner.run(errorBrain);
101
+ } catch (error) {
102
+ // Expected error
103
+ }
104
+
105
+ // Verify error event was dispatched
106
+ expect(mockAdapter.dispatch).toHaveBeenCalledWith(
107
+ expect.objectContaining({
108
+ type: BRAIN_EVENTS.ERROR,
109
+ error: expect.objectContaining({
110
+ message: 'Test error',
111
+ }),
112
+ })
113
+ );
114
+ });
115
+
116
+ it('should maintain state between steps', async () => {
117
+ const runner = new BrainRunner({
118
+ adapters: [],
119
+ client: mockClient,
120
+ });
121
+
122
+ const testBrain = brain('Test Brain')
123
+ .step('First Step', () => ({ count: 1 }))
124
+ .step('Second Step', ({ state }) => ({
125
+ count: state.count + 1,
126
+ }));
127
+
128
+ const result = await runner.run(testBrain);
129
+
130
+ expect(result.count).toEqual(2);
131
+ });
132
+
133
+ it('should pass resources to step actions', async () => {
134
+ const mockLoad = jest.fn(
135
+ async (
136
+ resourceName: string,
137
+ type?: 'text' | 'binary'
138
+ ): Promise<string | Buffer> => {
139
+ if (type === 'binary') {
140
+ return Buffer.from(`content of ${resourceName}`);
141
+ }
142
+ return `content of ${resourceName}`;
143
+ }
144
+ ) as jest.MockedFunction<ResourceLoader['load']>;
145
+
146
+ const mockResourceLoader: ResourceLoader = {
147
+ load: mockLoad,
148
+ };
149
+
150
+ const testManifest = {
151
+ myTextFile: {
152
+ type: 'text' as const,
153
+ key: 'myTextFile',
154
+ path: '/test/myTextFile.txt',
155
+ },
156
+ myBinaryFile: {
157
+ type: 'binary' as const,
158
+ key: 'myBinaryFile',
159
+ path: '/test/myBinaryFile.bin',
160
+ },
161
+ } as const;
162
+
163
+ const testResources = createResources(mockResourceLoader, testManifest);
164
+
165
+ const runner = new BrainRunner({
166
+ adapters: [],
167
+ client: mockClient,
168
+ }).withResources(testResources);
169
+
170
+ let textContent: string | undefined;
171
+ let binaryContent: Buffer | undefined;
172
+
173
+ const resourceConsumingBrain = brain('Resource Brain').step(
174
+ 'Load Resources',
175
+ async ({ resources }) => {
176
+ textContent = await (resources.myTextFile as any).loadText();
177
+ binaryContent = await (resources.myBinaryFile as any).loadBinary();
178
+ return {};
179
+ }
180
+ );
181
+
182
+ await runner.run(resourceConsumingBrain);
183
+
184
+ expect(mockLoad).toHaveBeenCalledWith('myTextFile', 'text');
185
+ expect(mockLoad).toHaveBeenCalledWith('myBinaryFile', 'binary');
186
+ expect(textContent).toBe('content of myTextFile');
187
+ expect(binaryContent?.toString()).toBe('content of myBinaryFile');
188
+ });
189
+
190
+ it('should chain adapters with withAdapters method', async () => {
191
+ const mockAdapter2: jest.Mocked<Adapter> = {
192
+ dispatch: jest.fn(),
193
+ };
194
+ const mockAdapter3: jest.Mocked<Adapter> = {
195
+ dispatch: jest.fn(),
196
+ };
197
+
198
+ const runner = new BrainRunner({
199
+ adapters: [mockAdapter],
200
+ client: mockClient,
201
+ });
202
+
203
+ // Chain additional adapters
204
+ const updatedRunner = runner.withAdapters([mockAdapter2, mockAdapter3]);
205
+
206
+ const testBrain = brain('Test Brain').step('Step 1', () => ({ value: 1 }));
207
+
208
+ await updatedRunner.run(testBrain);
209
+
210
+ // Verify all adapters received events
211
+ expect(mockAdapter.dispatch).toHaveBeenCalledWith(
212
+ expect.objectContaining({
213
+ type: BRAIN_EVENTS.START,
214
+ })
215
+ );
216
+ expect(mockAdapter2.dispatch).toHaveBeenCalledWith(
217
+ expect.objectContaining({
218
+ type: BRAIN_EVENTS.START,
219
+ })
220
+ );
221
+ expect(mockAdapter3.dispatch).toHaveBeenCalledWith(
222
+ expect.objectContaining({
223
+ type: BRAIN_EVENTS.START,
224
+ })
225
+ );
226
+
227
+ // Verify all adapters received the same number of events
228
+ expect(mockAdapter.dispatch).toHaveBeenCalledTimes(
229
+ mockAdapter2.dispatch.mock.calls.length
230
+ );
231
+ expect(mockAdapter2.dispatch).toHaveBeenCalledTimes(
232
+ mockAdapter3.dispatch.mock.calls.length
233
+ );
234
+ });
235
+
236
+ it('should replace client with withClient method', async () => {
237
+ const originalClient: jest.Mocked<ObjectGenerator> = {
238
+ generateObject: jest.fn(),
239
+ };
240
+ const newClient: jest.Mocked<ObjectGenerator> = {
241
+ generateObject: jest.fn(),
242
+ };
243
+
244
+ // Configure the new client's response
245
+ newClient.generateObject.mockResolvedValue({ result: 'from new client' });
246
+
247
+ const runner = new BrainRunner({
248
+ adapters: [],
249
+ client: originalClient,
250
+ });
251
+
252
+ // Replace the client
253
+ const updatedRunner = runner.withClient(newClient);
254
+
255
+ // Define schema once to ensure same reference
256
+ const testSchema = z.object({ result: z.string() });
257
+
258
+ const testBrain = brain('Test Brain').step(
259
+ 'Generate',
260
+ async ({ client }) => {
261
+ const response = await client.generateObject({
262
+ prompt: 'test prompt',
263
+ schema: testSchema,
264
+ schemaName: 'TestSchema',
265
+ });
266
+ return { generated: response.result };
267
+ }
268
+ );
269
+
270
+ const result = await updatedRunner.run(testBrain);
271
+
272
+ // Verify new client was used, not the original
273
+ expect(originalClient.generateObject).not.toHaveBeenCalled();
274
+ expect(newClient.generateObject).toHaveBeenCalledWith({
275
+ prompt: 'test prompt',
276
+ schema: testSchema,
277
+ schemaName: 'TestSchema',
278
+ });
279
+ expect(result.generated).toBe('from new client');
280
+ });
281
+
282
+ it('should apply patches from initialCompletedSteps and continue from correct state', async () => {
283
+ const runner = new BrainRunner({
284
+ adapters: [mockAdapter],
285
+ client: mockClient,
286
+ });
287
+
288
+ // Simulate completed steps with patches
289
+ const completedSteps: SerializedStep[] = [
290
+ {
291
+ id: 'step-1',
292
+ title: 'First Step',
293
+ status: STATUS.COMPLETE,
294
+ patch: [
295
+ {
296
+ op: 'add',
297
+ path: '/count',
298
+ value: 10,
299
+ },
300
+ ],
301
+ },
302
+ {
303
+ id: 'step-2',
304
+ title: 'Second Step',
305
+ status: STATUS.COMPLETE,
306
+ patch: [
307
+ {
308
+ op: 'add',
309
+ path: '/name',
310
+ value: 'test',
311
+ },
312
+ ],
313
+ },
314
+ ];
315
+
316
+ const testBrain = brain('Test Brain')
317
+ .step('First Step', () => ({ count: 10 }))
318
+ .step('Second Step', ({ state }) => ({ ...state, name: 'test' }))
319
+ .step('Third Step', ({ state }) => ({
320
+ ...state,
321
+ count: state.count + 5,
322
+ message: `${state.name} completed`,
323
+ }));
324
+
325
+ const result = await runner.run(testBrain, {
326
+ initialCompletedSteps: completedSteps,
327
+ brainRunId: 'test-run-123',
328
+ });
329
+
330
+ // Verify the final state includes patches from completed steps
331
+ expect(result).toEqual({
332
+ count: 15,
333
+ name: 'test',
334
+ message: 'test completed',
335
+ });
336
+
337
+ // Verify that the brain runner applied the patches correctly
338
+ // The runner should have seen all steps execute, but the first two were already completed
339
+ const stepCompleteEvents = mockAdapter.dispatch.mock.calls.filter(
340
+ (call) => call[0].type === BRAIN_EVENTS.STEP_COMPLETE
341
+ );
342
+
343
+ // All steps will emit complete events in the current implementation
344
+ expect(stepCompleteEvents.length).toBeGreaterThanOrEqual(1);
345
+ });
346
+
347
+ it('should stop execution after specified number of steps with endAfter parameter', async () => {
348
+ const runner = new BrainRunner({
349
+ adapters: [mockAdapter],
350
+ client: mockClient,
351
+ });
352
+
353
+ const testBrain = brain('Test Brain')
354
+ .step('Step 1', () => ({ step1: 'done' }))
355
+ .step('Step 2', ({ state }) => ({ ...state, step2: 'done' }))
356
+ .step('Step 3', ({ state }) => ({ ...state, step3: 'done' }))
357
+ .step('Step 4', ({ state }) => ({ ...state, step4: 'done' }));
358
+
359
+ // Run brain but stop after 2 steps
360
+ const result = await runner.run(testBrain, {
361
+ endAfter: 2,
362
+ });
363
+
364
+ // Verify state only has results from first 2 steps
365
+ expect(result).toEqual({
366
+ step1: 'done',
367
+ step2: 'done',
368
+ });
369
+
370
+ // Verify only 2 step complete events were dispatched
371
+ const stepCompleteEvents = mockAdapter.dispatch.mock.calls
372
+ .filter((call) => call[0].type === BRAIN_EVENTS.STEP_COMPLETE)
373
+ .map((call) => (call[0] as any).stepTitle);
374
+
375
+ expect(stepCompleteEvents).toEqual(['Step 1', 'Step 2']);
376
+
377
+ // Verify that COMPLETE event was NOT dispatched (brain didn't finish)
378
+ const completeEvents = mockAdapter.dispatch.mock.calls.filter(
379
+ (call) => call[0].type === BRAIN_EVENTS.COMPLETE
380
+ );
381
+
382
+ expect(completeEvents.length).toBe(0);
383
+ });
384
+ });
@@ -0,0 +1,111 @@
1
+ import { BRAIN_EVENTS } from './constants.js';
2
+ import { applyPatches } from './json-patch.js';
3
+ import type { Adapter } from '../adapters/types.js';
4
+ import type { SerializedStep, Brain } from './brain.js';
5
+ import type { State } from './types.js';
6
+ import type { ObjectGenerator } from '../clients/types.js';
7
+ import type { Resources } from '../resources/resources.js';
8
+
9
+ export class BrainRunner {
10
+ constructor(
11
+ private options: {
12
+ adapters: Adapter[];
13
+ client: ObjectGenerator;
14
+ resources?: Resources;
15
+ }
16
+ ) {}
17
+
18
+ withAdapters(adapters: Adapter[]): BrainRunner {
19
+ const { adapters: existingAdapters } = this.options;
20
+ return new BrainRunner({
21
+ ...this.options,
22
+ adapters: [...existingAdapters, ...adapters],
23
+ });
24
+ }
25
+
26
+ withClient(client: ObjectGenerator): BrainRunner {
27
+ return new BrainRunner({
28
+ ...this.options,
29
+ client,
30
+ });
31
+ }
32
+
33
+ withResources(resources: Resources): BrainRunner {
34
+ return new BrainRunner({
35
+ ...this.options,
36
+ resources,
37
+ });
38
+ }
39
+
40
+ async run<TOptions extends object = {}, TState extends State = {}>(
41
+ brain: Brain<TOptions, TState, any>,
42
+ {
43
+ initialState = {} as TState,
44
+ options,
45
+ initialCompletedSteps,
46
+ brainRunId,
47
+ endAfter,
48
+ }: {
49
+ initialState?: TState;
50
+ options?: TOptions;
51
+ initialCompletedSteps?: SerializedStep[] | never;
52
+ brainRunId?: string | never;
53
+ endAfter?: number;
54
+ } = {}
55
+ ): Promise<TState> {
56
+ const { adapters, client, resources } = this.options;
57
+
58
+ let currentState = initialState ?? ({} as TState);
59
+ let stepNumber = 1;
60
+
61
+ // Apply any patches from completed steps
62
+ // to the initial state so that the brain
63
+ // starts with a state that reflects all of the completed steps.
64
+ // Need to do this when a brain is restarted with completed steps.
65
+ initialCompletedSteps?.forEach((step) => {
66
+ if (step.patch) {
67
+ currentState = applyPatches(currentState, [step.patch]) as TState;
68
+ stepNumber++;
69
+ }
70
+ });
71
+
72
+ const brainRun =
73
+ brainRunId && initialCompletedSteps
74
+ ? brain.run({
75
+ initialState,
76
+ initialCompletedSteps,
77
+ brainRunId,
78
+ options,
79
+ client,
80
+ resources: resources ?? {},
81
+ })
82
+ : brain.run({
83
+ initialState,
84
+ options,
85
+ client,
86
+ brainRunId,
87
+ resources: resources ?? {},
88
+ });
89
+
90
+ for await (const event of brainRun) {
91
+ // Dispatch event to all adapters
92
+ await Promise.all(adapters.map((adapter) => adapter.dispatch(event)));
93
+
94
+ // Update current state when steps complete
95
+ if (event.type === BRAIN_EVENTS.STEP_COMPLETE) {
96
+ if (event.patch) {
97
+ currentState = applyPatches(currentState, [event.patch]) as TState;
98
+ }
99
+
100
+ // Check if we should stop after this step
101
+ if (endAfter && stepNumber >= endAfter) {
102
+ return currentState;
103
+ }
104
+
105
+ stepNumber++;
106
+ }
107
+ }
108
+
109
+ return currentState;
110
+ }
111
+ }