@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.
- package/CLAUDE.md +141 -0
- package/dist/src/adapters/types.js +1 -16
- package/dist/src/clients/types.js +4 -1
- package/dist/src/dsl/brain-runner.js +487 -0
- package/dist/src/dsl/brain-runner.test.js +733 -0
- package/dist/src/dsl/brain.js +1128 -0
- package/dist/src/dsl/brain.test.js +4225 -0
- package/dist/src/dsl/constants.js +6 -6
- package/dist/src/dsl/json-patch.js +37 -9
- package/dist/src/index.js +11 -10
- package/dist/src/resources/resources.js +371 -0
- package/dist/src/test-utils.js +474 -0
- package/dist/src/testing.js +3 -0
- package/dist/types/adapters/types.d.ts +3 -8
- package/dist/types/adapters/types.d.ts.map +1 -1
- package/dist/types/clients/types.d.ts +46 -6
- package/dist/types/clients/types.d.ts.map +1 -1
- package/dist/types/dsl/brain-runner.d.ts +24 -0
- package/dist/types/dsl/brain-runner.d.ts.map +1 -0
- package/dist/types/dsl/brain.d.ts +136 -0
- package/dist/types/dsl/brain.d.ts.map +1 -0
- package/dist/types/dsl/constants.d.ts +5 -5
- package/dist/types/dsl/constants.d.ts.map +1 -1
- package/dist/types/dsl/json-patch.d.ts +2 -1
- package/dist/types/dsl/json-patch.d.ts.map +1 -1
- package/dist/types/index.d.ts +13 -11
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/resources/resource-loader.d.ts +6 -0
- package/dist/types/resources/resource-loader.d.ts.map +1 -0
- package/dist/types/resources/resources.d.ts +23 -0
- package/dist/types/resources/resources.d.ts.map +1 -0
- package/dist/types/test-utils.d.ts +94 -0
- package/dist/types/test-utils.d.ts.map +1 -0
- package/dist/types/testing.d.ts +2 -0
- package/dist/types/testing.d.ts.map +1 -0
- package/docs/core-testing-guide.md +289 -0
- package/package.json +26 -7
- package/src/adapters/types.ts +3 -22
- package/src/clients/types.ts +50 -10
- package/src/dsl/brain-runner.test.ts +384 -0
- package/src/dsl/brain-runner.ts +111 -0
- package/src/dsl/brain.test.ts +1981 -0
- package/src/dsl/brain.ts +740 -0
- package/src/dsl/constants.ts +6 -6
- package/src/dsl/json-patch.ts +24 -9
- package/src/dsl/types.ts +1 -1
- package/src/index.ts +30 -16
- package/src/resources/resource-loader.ts +8 -0
- package/src/resources/resources.ts +267 -0
- package/src/test-utils.ts +254 -0
- package/test/resources.test.ts +248 -0
- package/tsconfig.json +2 -2
- package/.swcrc +0 -31
- package/dist/src/dsl/extensions.js +0 -19
- package/dist/src/dsl/workflow-runner.js +0 -93
- package/dist/src/dsl/workflow.js +0 -308
- package/dist/src/file-stores/local-file-store.js +0 -12
- package/dist/src/utils/temp-files.js +0 -27
- package/dist/types/dsl/extensions.d.ts +0 -18
- package/dist/types/dsl/extensions.d.ts.map +0 -1
- package/dist/types/dsl/workflow-runner.d.ts +0 -28
- package/dist/types/dsl/workflow-runner.d.ts.map +0 -1
- package/dist/types/dsl/workflow.d.ts +0 -118
- package/dist/types/dsl/workflow.d.ts.map +0 -1
- package/dist/types/file-stores/local-file-store.d.ts +0 -7
- package/dist/types/file-stores/local-file-store.d.ts.map +0 -1
- package/dist/types/file-stores/types.d.ts +0 -4
- package/dist/types/file-stores/types.d.ts.map +0 -1
- package/dist/types/utils/temp-files.d.ts +0 -12
- package/dist/types/utils/temp-files.d.ts.map +0 -1
- package/src/dsl/extensions.ts +0 -58
- package/src/dsl/workflow-runner.test.ts +0 -203
- package/src/dsl/workflow-runner.ts +0 -146
- package/src/dsl/workflow.test.ts +0 -1435
- package/src/dsl/workflow.ts +0 -554
- package/src/file-stores/local-file-store.ts +0 -11
- package/src/file-stores/types.ts +0 -3
- package/src/utils/temp-files.ts +0 -46
- /package/dist/src/{file-stores/types.js → resources/resource-loader.js} +0 -0
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import { WorkflowRunner } from './workflow-runner';
|
|
2
|
-
import { workflow } from './workflow';
|
|
3
|
-
import { WORKFLOW_EVENTS, STATUS } from './constants';
|
|
4
|
-
import type { FileStore } from '../file-stores/types';
|
|
5
|
-
|
|
6
|
-
class TestFileStore implements FileStore {
|
|
7
|
-
async readFile(path: string) {
|
|
8
|
-
return Promise.resolve('');
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe('WorkflowRunner', () => {
|
|
13
|
-
const mockClient = {
|
|
14
|
-
execute: jest.fn()
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const mockLogger = {
|
|
18
|
-
log: jest.fn()
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const mockAdapter = {
|
|
22
|
-
dispatch: jest.fn()
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const fileStore = new TestFileStore();
|
|
26
|
-
|
|
27
|
-
beforeEach(() => {
|
|
28
|
-
jest.clearAllMocks();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should run a workflow and dispatch events to adapters', async () => {
|
|
32
|
-
const runner = new WorkflowRunner({
|
|
33
|
-
adapters: [mockAdapter],
|
|
34
|
-
fileStore,
|
|
35
|
-
logger: mockLogger,
|
|
36
|
-
verbose: false,
|
|
37
|
-
client: mockClient
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const testWorkflow = workflow('Test Workflow')
|
|
41
|
-
.step('First Step', () => ({ value: 42 }))
|
|
42
|
-
.step('Async Step', async ({ state}) => {
|
|
43
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
44
|
-
return { ...state, asyncValue: 'completed' };
|
|
45
|
-
})
|
|
46
|
-
.step('Final Step', ({ state }) => ({
|
|
47
|
-
...state,
|
|
48
|
-
finalValue: state.value * 2
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
await runner.run(testWorkflow);
|
|
52
|
-
|
|
53
|
-
// Verify adapter received all events in correct order
|
|
54
|
-
expect(mockAdapter.dispatch).toHaveBeenCalledWith(
|
|
55
|
-
expect.objectContaining({
|
|
56
|
-
type: WORKFLOW_EVENTS.START,
|
|
57
|
-
workflowTitle: 'Test Workflow'
|
|
58
|
-
})
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
expect(mockAdapter.dispatch).toHaveBeenCalledWith(
|
|
62
|
-
expect.objectContaining({
|
|
63
|
-
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
64
|
-
stepTitle: 'First Step'
|
|
65
|
-
})
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
expect(mockAdapter.dispatch).toHaveBeenCalledWith(
|
|
69
|
-
expect.objectContaining({
|
|
70
|
-
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
71
|
-
stepTitle: 'Async Step'
|
|
72
|
-
})
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
expect(mockAdapter.dispatch).toHaveBeenCalledWith(
|
|
76
|
-
expect.objectContaining({
|
|
77
|
-
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
78
|
-
stepTitle: 'Final Step'
|
|
79
|
-
})
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
expect(mockAdapter.dispatch).toHaveBeenCalledWith(
|
|
83
|
-
expect.objectContaining({
|
|
84
|
-
type: WORKFLOW_EVENTS.COMPLETE,
|
|
85
|
-
status: STATUS.COMPLETE
|
|
86
|
-
})
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Verify the order of events
|
|
90
|
-
const stepCompletions = mockAdapter.dispatch.mock.calls
|
|
91
|
-
.filter(call => call[0].type === WORKFLOW_EVENTS.STEP_COMPLETE)
|
|
92
|
-
.map(call => call[0].stepTitle);
|
|
93
|
-
|
|
94
|
-
expect(stepCompletions).toEqual([
|
|
95
|
-
'First Step',
|
|
96
|
-
'Async Step',
|
|
97
|
-
'Final Step'
|
|
98
|
-
]);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should log final state when verbose is true', async () => {
|
|
102
|
-
const runner = new WorkflowRunner({
|
|
103
|
-
adapters: [],
|
|
104
|
-
fileStore,
|
|
105
|
-
logger: mockLogger,
|
|
106
|
-
verbose: true,
|
|
107
|
-
client: mockClient
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const testWorkflow = workflow('Test Workflow')
|
|
111
|
-
.step('Test Step', () => ({ value: 42 }));
|
|
112
|
-
|
|
113
|
-
await runner.run(testWorkflow);
|
|
114
|
-
|
|
115
|
-
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
116
|
-
expect.stringContaining('Workflow completed:')
|
|
117
|
-
);
|
|
118
|
-
expect(mockLogger.log).toHaveBeenCalledWith(
|
|
119
|
-
expect.stringContaining('"value": 42')
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('should handle workflow errors', async () => {
|
|
124
|
-
const runner = new WorkflowRunner({
|
|
125
|
-
adapters: [mockAdapter],
|
|
126
|
-
fileStore,
|
|
127
|
-
logger: mockLogger,
|
|
128
|
-
verbose: true,
|
|
129
|
-
client: mockClient
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const errorWorkflow = workflow('Error Workflow')
|
|
133
|
-
.step('Error Step', () => {
|
|
134
|
-
throw new Error('Test error');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
await runner.run(errorWorkflow);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
// Expected error
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Verify error event was dispatched
|
|
144
|
-
expect(mockAdapter.dispatch).toHaveBeenCalledWith(
|
|
145
|
-
expect.objectContaining({
|
|
146
|
-
type: WORKFLOW_EVENTS.ERROR,
|
|
147
|
-
error: expect.objectContaining({
|
|
148
|
-
message: 'Test error'
|
|
149
|
-
})
|
|
150
|
-
})
|
|
151
|
-
);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('should truncate long values in verbose output', async () => {
|
|
155
|
-
const runner = new WorkflowRunner({
|
|
156
|
-
adapters: [],
|
|
157
|
-
fileStore,
|
|
158
|
-
logger: mockLogger,
|
|
159
|
-
verbose: true,
|
|
160
|
-
client: mockClient
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const longString = 'a'.repeat(1000); // Make string much longer
|
|
164
|
-
const testWorkflow = workflow('Test Workflow')
|
|
165
|
-
.step('Test Step', () => ({
|
|
166
|
-
longString,
|
|
167
|
-
nested: { longString }
|
|
168
|
-
}));
|
|
169
|
-
|
|
170
|
-
await runner.run(testWorkflow);
|
|
171
|
-
|
|
172
|
-
const logCall = mockLogger.log.mock.calls.find(call =>
|
|
173
|
-
call[0].includes('Workflow completed:')
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
expect(logCall[0]).toContain('...');
|
|
177
|
-
expect(logCall[0].length).toBeLessThan(longString.length);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should maintain state between steps', async () => {
|
|
181
|
-
const runner = new WorkflowRunner({
|
|
182
|
-
adapters: [],
|
|
183
|
-
fileStore,
|
|
184
|
-
logger: mockLogger,
|
|
185
|
-
verbose: true,
|
|
186
|
-
client: mockClient
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
const testWorkflow = workflow('Test Workflow')
|
|
190
|
-
.step('First Step', () => ({ count: 1 }))
|
|
191
|
-
.step('Second Step', ({ state }) => ({
|
|
192
|
-
count: state.count + 1
|
|
193
|
-
}));
|
|
194
|
-
|
|
195
|
-
await runner.run(testWorkflow);
|
|
196
|
-
|
|
197
|
-
const finalLog = mockLogger.log.mock.calls.find(call =>
|
|
198
|
-
call[0].includes('Workflow completed:')
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
expect(finalLog[0]).toContain('"count": 2');
|
|
202
|
-
});
|
|
203
|
-
});
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { WORKFLOW_EVENTS } from './constants';
|
|
2
|
-
import { applyPatches } from './json-patch';
|
|
3
|
-
import type { Adapter } from "../adapters/types";
|
|
4
|
-
import type { FileStore } from "../file-stores/types";
|
|
5
|
-
import type { SerializedStep, Workflow } from './workflow';
|
|
6
|
-
import type { State } from './types';
|
|
7
|
-
import type { PromptClient } from '../clients/types';
|
|
8
|
-
|
|
9
|
-
interface Logger {
|
|
10
|
-
log(...args: any[]): void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class WorkflowRunner {
|
|
14
|
-
constructor(
|
|
15
|
-
private options: {
|
|
16
|
-
adapters: Adapter[],
|
|
17
|
-
fileStore: FileStore,
|
|
18
|
-
logger: Logger,
|
|
19
|
-
verbose: boolean,
|
|
20
|
-
client: PromptClient
|
|
21
|
-
}
|
|
22
|
-
) {}
|
|
23
|
-
|
|
24
|
-
async run<
|
|
25
|
-
TOptions extends object = {},
|
|
26
|
-
TState extends State = {}
|
|
27
|
-
>(
|
|
28
|
-
workflow: Workflow<TOptions, TState>,
|
|
29
|
-
{
|
|
30
|
-
initialState = {} as TState,
|
|
31
|
-
options,
|
|
32
|
-
initialCompletedSteps,
|
|
33
|
-
workflowRunId,
|
|
34
|
-
endAfter
|
|
35
|
-
}: {
|
|
36
|
-
initialState?: TState,
|
|
37
|
-
options?: TOptions,
|
|
38
|
-
initialCompletedSteps?: SerializedStep[] | never,
|
|
39
|
-
workflowRunId?: string | never,
|
|
40
|
-
endAfter?: number
|
|
41
|
-
} = {}
|
|
42
|
-
) {
|
|
43
|
-
const {
|
|
44
|
-
adapters,
|
|
45
|
-
logger: { log },
|
|
46
|
-
verbose,
|
|
47
|
-
fileStore,
|
|
48
|
-
client,
|
|
49
|
-
} = this.options;
|
|
50
|
-
|
|
51
|
-
let currentState = initialState ?? ({} as TState);
|
|
52
|
-
let stepNumber = 1;
|
|
53
|
-
|
|
54
|
-
// Apply any patches from completed steps
|
|
55
|
-
// to the initial state so that the workflow
|
|
56
|
-
// starts with a state that reflects all of the completed steps.
|
|
57
|
-
// Need to do this when a workflow is restarted with completed steps.
|
|
58
|
-
initialCompletedSteps?.forEach(step => {
|
|
59
|
-
if (step.patch) {
|
|
60
|
-
currentState = applyPatches(currentState, [step.patch]) as TState;
|
|
61
|
-
stepNumber++;
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const workflowRun = workflowRunId && initialCompletedSteps
|
|
66
|
-
? workflow.run({ initialState, initialCompletedSteps, workflowRunId, options, client, fileStore })
|
|
67
|
-
: workflow.run({ initialState, options, client, fileStore });
|
|
68
|
-
|
|
69
|
-
for await (const event of workflowRun) {
|
|
70
|
-
// Dispatch event to all adapters
|
|
71
|
-
await Promise.all(
|
|
72
|
-
adapters.map(adapter => adapter.dispatch(event))
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
// Update current state when steps complete
|
|
76
|
-
if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
|
|
77
|
-
if (event.patch) {
|
|
78
|
-
currentState = applyPatches(currentState, [event.patch]) as TState;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Check if we should stop after this step
|
|
82
|
-
if (endAfter && stepNumber >= endAfter) {
|
|
83
|
-
// Log final state if verbose
|
|
84
|
-
if (verbose) {
|
|
85
|
-
log(`\nWorkflow stopped after step ${endAfter} as requested: \n\n ${JSON.stringify(
|
|
86
|
-
this.truncateDeep(structuredClone(currentState)), null, 2
|
|
87
|
-
)}`);
|
|
88
|
-
}
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
stepNumber++;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Log final state on workflow completion/error if verbose
|
|
96
|
-
if ((
|
|
97
|
-
event.type === WORKFLOW_EVENTS.COMPLETE ||
|
|
98
|
-
event.type === WORKFLOW_EVENTS.ERROR
|
|
99
|
-
) && verbose) {
|
|
100
|
-
log(`\nWorkflow completed: \n\n ${JSON.stringify(
|
|
101
|
-
this.truncateDeep(structuredClone(currentState)), null, 2
|
|
102
|
-
)}`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
private truncateDeep(obj: any, maxLength: number = 100): any {
|
|
108
|
-
if (obj === null || obj === undefined) return obj;
|
|
109
|
-
|
|
110
|
-
if (typeof obj === 'string') {
|
|
111
|
-
return obj.length > maxLength ? obj.slice(0, maxLength) + '...' : obj;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (Array.isArray(obj)) {
|
|
115
|
-
if (obj.length === 0) return obj;
|
|
116
|
-
|
|
117
|
-
let truncatedArray = [];
|
|
118
|
-
let currentLength = 2; // Account for [] brackets
|
|
119
|
-
|
|
120
|
-
for (let i = 0; i < obj.length; i++) {
|
|
121
|
-
const processedItem = this.truncateDeep(obj[i], maxLength);
|
|
122
|
-
const itemStr = JSON.stringify(processedItem);
|
|
123
|
-
|
|
124
|
-
if (currentLength + itemStr.length + (i > 0 ? 1 : 0) > maxLength) {
|
|
125
|
-
truncatedArray.push(`... (${obj.length})`);
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
truncatedArray.push(processedItem);
|
|
130
|
-
currentLength += itemStr.length + (i > 0 ? 1 : 0); // Add 1 for comma
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return truncatedArray;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (typeof obj === 'object') {
|
|
137
|
-
const truncated: Record<string, any> = {};
|
|
138
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
139
|
-
truncated[key] = this.truncateDeep(value, maxLength);
|
|
140
|
-
}
|
|
141
|
-
return truncated;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return obj;
|
|
145
|
-
}
|
|
146
|
-
}
|