@positronic/core 0.0.1
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/.swcrc +31 -0
- package/dist/src/adapters/types.js +16 -0
- package/dist/src/clients/types.js +1 -0
- package/dist/src/dsl/constants.js +15 -0
- package/dist/src/dsl/extensions.js +19 -0
- package/dist/src/dsl/json-patch.js +30 -0
- package/dist/src/dsl/types.js +1 -0
- package/dist/src/dsl/workflow-runner.js +93 -0
- package/dist/src/dsl/workflow.js +308 -0
- package/dist/src/file-stores/local-file-store.js +12 -0
- package/dist/src/file-stores/types.js +1 -0
- package/dist/src/index.js +10 -0
- package/dist/src/utils/temp-files.js +27 -0
- package/dist/types/adapters/types.d.ts +10 -0
- package/dist/types/adapters/types.d.ts.map +1 -0
- package/dist/types/clients/types.d.ts +10 -0
- package/dist/types/clients/types.d.ts.map +1 -0
- package/dist/types/dsl/constants.d.ts +16 -0
- package/dist/types/dsl/constants.d.ts.map +1 -0
- package/dist/types/dsl/extensions.d.ts +18 -0
- package/dist/types/dsl/extensions.d.ts.map +1 -0
- package/dist/types/dsl/json-patch.d.ts +11 -0
- package/dist/types/dsl/json-patch.d.ts.map +1 -0
- package/dist/types/dsl/types.d.ts +14 -0
- package/dist/types/dsl/types.d.ts.map +1 -0
- package/dist/types/dsl/workflow-runner.d.ts +28 -0
- package/dist/types/dsl/workflow-runner.d.ts.map +1 -0
- package/dist/types/dsl/workflow.d.ts +118 -0
- package/dist/types/dsl/workflow.d.ts.map +1 -0
- package/dist/types/file-stores/local-file-store.d.ts +7 -0
- package/dist/types/file-stores/local-file-store.d.ts.map +1 -0
- package/dist/types/file-stores/types.d.ts +4 -0
- package/dist/types/file-stores/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/utils/temp-files.d.ts +12 -0
- package/dist/types/utils/temp-files.d.ts.map +1 -0
- package/package.json +21 -0
- package/src/adapters/types.ts +24 -0
- package/src/clients/types.ts +14 -0
- package/src/dsl/constants.ts +16 -0
- package/src/dsl/extensions.ts +58 -0
- package/src/dsl/json-patch.ts +27 -0
- package/src/dsl/types.ts +13 -0
- package/src/dsl/workflow-runner.test.ts +203 -0
- package/src/dsl/workflow-runner.ts +146 -0
- package/src/dsl/workflow.test.ts +1435 -0
- package/src/dsl/workflow.ts +554 -0
- package/src/file-stores/local-file-store.ts +11 -0
- package/src/file-stores/types.ts +3 -0
- package/src/index.ts +22 -0
- package/src/utils/temp-files.ts +46 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import type { PromptClient } from "../clients/types";
|
|
4
|
+
import type { State, JsonPatch } from "./types";
|
|
5
|
+
import { STATUS, WORKFLOW_EVENTS } from './constants';
|
|
6
|
+
import { createPatch, applyPatches } from './json-patch';
|
|
7
|
+
import type { FileStore } from "../file-stores/types";
|
|
8
|
+
|
|
9
|
+
export type SerializedError = {
|
|
10
|
+
name: string;
|
|
11
|
+
message: string;
|
|
12
|
+
stack?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// New Event Type System
|
|
16
|
+
// Base event interface with only type and options
|
|
17
|
+
interface BaseEvent<TOptions extends object = {}> {
|
|
18
|
+
type: typeof WORKFLOW_EVENTS[keyof typeof WORKFLOW_EVENTS];
|
|
19
|
+
options: TOptions;
|
|
20
|
+
workflowRunId: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 1. Workflow Events (all include workflow title/description)
|
|
24
|
+
interface WorkflowBaseEvent<TOptions extends object = {}> extends BaseEvent<TOptions> {
|
|
25
|
+
workflowTitle: string;
|
|
26
|
+
workflowDescription?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WorkflowStartEvent<TOptions extends object = {}> extends WorkflowBaseEvent<TOptions> {
|
|
30
|
+
type: typeof WORKFLOW_EVENTS.START | typeof WORKFLOW_EVENTS.RESTART;
|
|
31
|
+
initialState: State;
|
|
32
|
+
status: typeof STATUS.RUNNING;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WorkflowCompleteEvent<TOptions extends object = {}> extends WorkflowBaseEvent<TOptions> {
|
|
36
|
+
type: typeof WORKFLOW_EVENTS.COMPLETE;
|
|
37
|
+
status: typeof STATUS.COMPLETE;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WorkflowErrorEvent<TOptions extends object = {}> extends WorkflowBaseEvent<TOptions> {
|
|
41
|
+
type: typeof WORKFLOW_EVENTS.ERROR;
|
|
42
|
+
status: typeof STATUS.ERROR;
|
|
43
|
+
error: SerializedError;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Step Status Event (just steps array and base event properties)
|
|
47
|
+
export interface StepStatusEvent<TOptions extends object = {}> extends BaseEvent<TOptions> {
|
|
48
|
+
type: typeof WORKFLOW_EVENTS.STEP_STATUS;
|
|
49
|
+
steps: SerializedStep[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 3. Step Events (include step-specific properties)
|
|
53
|
+
export interface StepStartedEvent<TOptions extends object = {}> extends BaseEvent<TOptions> {
|
|
54
|
+
type: typeof WORKFLOW_EVENTS.STEP_START;
|
|
55
|
+
status: typeof STATUS.RUNNING;
|
|
56
|
+
stepTitle: string;
|
|
57
|
+
stepId: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface StepCompletedEvent<TOptions extends object = {}> extends BaseEvent<TOptions> {
|
|
61
|
+
type: typeof WORKFLOW_EVENTS.STEP_COMPLETE;
|
|
62
|
+
status: typeof STATUS.RUNNING;
|
|
63
|
+
stepTitle: string;
|
|
64
|
+
stepId: string;
|
|
65
|
+
patch: JsonPatch;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Union type of all possible events
|
|
69
|
+
export type WorkflowEvent<TOptions extends object = {}> =
|
|
70
|
+
| WorkflowStartEvent<TOptions>
|
|
71
|
+
| WorkflowCompleteEvent<TOptions>
|
|
72
|
+
| WorkflowErrorEvent<TOptions>
|
|
73
|
+
| StepStatusEvent<TOptions>
|
|
74
|
+
| StepStartedEvent<TOptions>
|
|
75
|
+
| StepCompletedEvent<TOptions>;
|
|
76
|
+
|
|
77
|
+
export interface SerializedStep {
|
|
78
|
+
title: string;
|
|
79
|
+
status: typeof STATUS[keyof typeof STATUS];
|
|
80
|
+
id: string;
|
|
81
|
+
patch?: JsonPatch;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type StepBlock<TStateIn, TStateOut, TOptions extends object = {}> = {
|
|
85
|
+
type: 'step';
|
|
86
|
+
title: string;
|
|
87
|
+
action: (params: {
|
|
88
|
+
state: TStateIn;
|
|
89
|
+
options: TOptions;
|
|
90
|
+
client: PromptClient;
|
|
91
|
+
fileStore: FileStore;
|
|
92
|
+
}) => TStateOut | Promise<TStateOut>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type WorkflowBlock<
|
|
96
|
+
TOuterState,
|
|
97
|
+
TInnerState extends State,
|
|
98
|
+
TNewState,
|
|
99
|
+
TOptions extends object = {}
|
|
100
|
+
> = {
|
|
101
|
+
type: 'workflow';
|
|
102
|
+
title: string;
|
|
103
|
+
innerWorkflow: Workflow<TOptions, TInnerState>;
|
|
104
|
+
initialState: State | ((outerState: TOuterState) => State);
|
|
105
|
+
action: (outerState: TOuterState, innerState: TInnerState) => TNewState;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
type Block<TStateIn, TStateOut, TOptions extends object = {}> =
|
|
109
|
+
| StepBlock<TStateIn, TStateOut, TOptions>
|
|
110
|
+
| WorkflowBlock<TStateIn, any, TStateOut, TOptions>;
|
|
111
|
+
|
|
112
|
+
interface BaseRunParams<TOptions extends object = {}> {
|
|
113
|
+
fileStore: FileStore;
|
|
114
|
+
client: PromptClient;
|
|
115
|
+
options?: TOptions;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface InitialRunParams<TOptions extends object = {}> extends BaseRunParams<TOptions> {
|
|
119
|
+
initialState?: State;
|
|
120
|
+
initialCompletedSteps?: never;
|
|
121
|
+
workflowRunId?: never;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface RerunParams<TOptions extends object = {}> extends BaseRunParams<TOptions> {
|
|
125
|
+
initialState: State;
|
|
126
|
+
initialCompletedSteps: SerializedStep[];
|
|
127
|
+
workflowRunId: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export class Workflow<
|
|
131
|
+
TOptions extends object = {},
|
|
132
|
+
TState extends State = {}
|
|
133
|
+
> {
|
|
134
|
+
private blocks: Block<any, any, TOptions>[] = [];
|
|
135
|
+
public type: 'workflow' = 'workflow';
|
|
136
|
+
|
|
137
|
+
constructor(
|
|
138
|
+
public readonly title: string,
|
|
139
|
+
private description?: string
|
|
140
|
+
) {}
|
|
141
|
+
|
|
142
|
+
step<TNewState extends State>(
|
|
143
|
+
title: string,
|
|
144
|
+
action: (params: {
|
|
145
|
+
state: TState;
|
|
146
|
+
options: TOptions;
|
|
147
|
+
client: PromptClient;
|
|
148
|
+
fileStore: FileStore;
|
|
149
|
+
}) => TNewState | Promise<TNewState>
|
|
150
|
+
) {
|
|
151
|
+
const stepBlock: StepBlock<TState, TNewState, TOptions> = {
|
|
152
|
+
type: 'step',
|
|
153
|
+
title,
|
|
154
|
+
action
|
|
155
|
+
};
|
|
156
|
+
this.blocks.push(stepBlock);
|
|
157
|
+
return this.nextWorkflow<TNewState>();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
workflow<
|
|
161
|
+
TInnerState extends State,
|
|
162
|
+
TNewState extends State
|
|
163
|
+
>(
|
|
164
|
+
title: string,
|
|
165
|
+
innerWorkflow: Workflow<TOptions, TInnerState>,
|
|
166
|
+
action: (params: { state: TState; workflowState: TInnerState }) => TNewState,
|
|
167
|
+
initialState?: State | ((state: TState) => State)
|
|
168
|
+
) {
|
|
169
|
+
const nestedBlock: WorkflowBlock<
|
|
170
|
+
TState,
|
|
171
|
+
TInnerState,
|
|
172
|
+
TNewState,
|
|
173
|
+
TOptions
|
|
174
|
+
> = {
|
|
175
|
+
type: 'workflow',
|
|
176
|
+
title,
|
|
177
|
+
innerWorkflow,
|
|
178
|
+
initialState: initialState || (() => ({} as State)),
|
|
179
|
+
action: (outerState, innerState) => action({ state: outerState, workflowState: innerState})
|
|
180
|
+
};
|
|
181
|
+
this.blocks.push(nestedBlock);
|
|
182
|
+
return this.nextWorkflow<TNewState>();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// TResponseKey:
|
|
186
|
+
// The response key must be a string literal, so if defining a response model
|
|
187
|
+
// a consumer of this workflow must use "as const" to ensure the key is a string literal
|
|
188
|
+
// this type makes sure that the will get a ts error if they don't.
|
|
189
|
+
prompt<
|
|
190
|
+
TResponseKey extends string & { readonly brand?: unique symbol },
|
|
191
|
+
TSchema extends z.ZodObject<any>,
|
|
192
|
+
TNewState extends State = TState & { [K in TResponseKey]: z.infer<TSchema> }
|
|
193
|
+
>(
|
|
194
|
+
title: string,
|
|
195
|
+
config: {
|
|
196
|
+
template: (state: TState) => string;
|
|
197
|
+
responseModel: {
|
|
198
|
+
schema: TSchema;
|
|
199
|
+
name: TResponseKey & (string extends TResponseKey ? never : unknown);
|
|
200
|
+
};
|
|
201
|
+
client?: PromptClient;
|
|
202
|
+
},
|
|
203
|
+
reduce?: (params: {
|
|
204
|
+
state: TState,
|
|
205
|
+
response: z.infer<TSchema>,
|
|
206
|
+
options: TOptions,
|
|
207
|
+
prompt: string
|
|
208
|
+
}) => TNewState | Promise<TNewState>,
|
|
209
|
+
) {
|
|
210
|
+
const promptBlock: StepBlock<
|
|
211
|
+
TState,
|
|
212
|
+
TNewState,
|
|
213
|
+
TOptions
|
|
214
|
+
> = {
|
|
215
|
+
type: 'step',
|
|
216
|
+
title,
|
|
217
|
+
action: async ({ state, client: runClient, options }) => {
|
|
218
|
+
const { template, responseModel, client: stepClient } = config;
|
|
219
|
+
const client = stepClient ?? runClient;
|
|
220
|
+
const promptString = template(state);
|
|
221
|
+
const response = await client.execute(promptString, responseModel);
|
|
222
|
+
const stateWithResponse = {
|
|
223
|
+
...state,
|
|
224
|
+
[config.responseModel.name]: response
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return reduce
|
|
228
|
+
? reduce({ state, response, options, prompt: promptString })
|
|
229
|
+
: stateWithResponse as unknown as TNewState;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
this.blocks.push(promptBlock);
|
|
233
|
+
return this.nextWorkflow<TNewState>();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Overload signatures
|
|
237
|
+
run(params: InitialRunParams<TOptions>): AsyncGenerator<WorkflowEvent<TOptions>>;
|
|
238
|
+
run(params: RerunParams<TOptions>): AsyncGenerator<WorkflowEvent<TOptions>>;
|
|
239
|
+
|
|
240
|
+
// Implementation signature
|
|
241
|
+
async *run(params: InitialRunParams<TOptions> | RerunParams<TOptions>): AsyncGenerator<WorkflowEvent<TOptions>> {
|
|
242
|
+
const { title, description, blocks } = this;
|
|
243
|
+
const stream = new WorkflowEventStream({
|
|
244
|
+
title,
|
|
245
|
+
description,
|
|
246
|
+
blocks,
|
|
247
|
+
...params
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
yield* stream.next();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private withBlocks(blocks: Block<any, any, TOptions>[]): this {
|
|
254
|
+
this.blocks = blocks;
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private nextWorkflow<TNewState extends State>(): Workflow<TOptions, TNewState> {
|
|
259
|
+
return new Workflow<TOptions, TNewState>(
|
|
260
|
+
this.title,
|
|
261
|
+
this.description
|
|
262
|
+
).withBlocks(this.blocks);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
class Step {
|
|
267
|
+
public id: string;
|
|
268
|
+
private patch?: JsonPatch | string;
|
|
269
|
+
private status: typeof STATUS[keyof typeof STATUS] = STATUS.PENDING;
|
|
270
|
+
|
|
271
|
+
constructor(
|
|
272
|
+
public block: Block<any, any, any>,
|
|
273
|
+
id?: string
|
|
274
|
+
) {
|
|
275
|
+
this.id = id || uuidv4();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
withPatch(patch: JsonPatch | undefined) {
|
|
279
|
+
this.patch = patch;
|
|
280
|
+
return this;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
withStatus(status: typeof STATUS[keyof typeof STATUS]) {
|
|
284
|
+
this.status = status;
|
|
285
|
+
return this;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
get serialized(): SerializedStep {
|
|
289
|
+
return {
|
|
290
|
+
id: this.id,
|
|
291
|
+
title: this.block.title,
|
|
292
|
+
status: this.status,
|
|
293
|
+
patch: typeof this.patch === 'string' ? JSON.parse(this.patch) : this.patch
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
class WorkflowEventStream<TOptions extends object = {}, TState extends State = {}> {
|
|
299
|
+
private currentState: TState;
|
|
300
|
+
private steps: Step[];
|
|
301
|
+
private currentStepIndex: number = 0;
|
|
302
|
+
private initialState: TState;
|
|
303
|
+
private workflowRunId: string;
|
|
304
|
+
private title: string;
|
|
305
|
+
private description?: string;
|
|
306
|
+
private fileStore: FileStore;
|
|
307
|
+
private client: PromptClient;
|
|
308
|
+
private options: TOptions;
|
|
309
|
+
|
|
310
|
+
constructor(params: (InitialRunParams<TOptions> | RerunParams<TOptions>) & {
|
|
311
|
+
title: string;
|
|
312
|
+
description?: string;
|
|
313
|
+
blocks: Block<any, any, TOptions>[];
|
|
314
|
+
}) {
|
|
315
|
+
const {
|
|
316
|
+
initialState = {} as TState,
|
|
317
|
+
initialCompletedSteps,
|
|
318
|
+
blocks,
|
|
319
|
+
title,
|
|
320
|
+
description,
|
|
321
|
+
workflowRunId: providedWorkflowRunId,
|
|
322
|
+
options = {} as TOptions,
|
|
323
|
+
fileStore,
|
|
324
|
+
client
|
|
325
|
+
} = params;
|
|
326
|
+
|
|
327
|
+
this.initialState = initialState as TState;
|
|
328
|
+
this.title = title;
|
|
329
|
+
this.description = description;
|
|
330
|
+
this.fileStore = fileStore;
|
|
331
|
+
this.client = client;
|
|
332
|
+
this.options = options;
|
|
333
|
+
|
|
334
|
+
// Initialize steps array with UUIDs and pending status
|
|
335
|
+
this.steps = blocks.map((block, index) => {
|
|
336
|
+
const completedStep = initialCompletedSteps?.[index];
|
|
337
|
+
if (completedStep) {
|
|
338
|
+
return new Step(block, completedStep.id)
|
|
339
|
+
.withStatus(completedStep.status)
|
|
340
|
+
.withPatch(completedStep.patch);
|
|
341
|
+
}
|
|
342
|
+
return new Step(block);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
this.currentState = clone(this.initialState);
|
|
346
|
+
|
|
347
|
+
for (const step of this.steps) {
|
|
348
|
+
if (step.serialized.status === STATUS.COMPLETE && step.serialized.patch) {
|
|
349
|
+
this.currentState = applyPatches(this.currentState, [step.serialized.patch]) as TState;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
this.workflowRunId = providedWorkflowRunId ?? uuidv4();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async *next(): AsyncGenerator<WorkflowEvent<TOptions>> {
|
|
357
|
+
const {
|
|
358
|
+
steps,
|
|
359
|
+
title: workflowTitle,
|
|
360
|
+
description: workflowDescription,
|
|
361
|
+
currentState,
|
|
362
|
+
options,
|
|
363
|
+
workflowRunId
|
|
364
|
+
} = this;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const hasCompletedSteps = steps.some(step => step.serialized.status !== STATUS.PENDING);
|
|
368
|
+
yield {
|
|
369
|
+
type: hasCompletedSteps ? WORKFLOW_EVENTS.RESTART : WORKFLOW_EVENTS.START,
|
|
370
|
+
status: STATUS.RUNNING,
|
|
371
|
+
workflowTitle,
|
|
372
|
+
workflowDescription,
|
|
373
|
+
initialState: currentState,
|
|
374
|
+
options,
|
|
375
|
+
workflowRunId
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Emit initial step status after workflow starts
|
|
379
|
+
yield {
|
|
380
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
381
|
+
steps: steps.map(step => step.serialized),
|
|
382
|
+
options,
|
|
383
|
+
workflowRunId
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// Process each step
|
|
387
|
+
while (this.currentStepIndex < steps.length) {
|
|
388
|
+
const step = steps[this.currentStepIndex];
|
|
389
|
+
|
|
390
|
+
// Skip completed steps
|
|
391
|
+
if (step.serialized.status === STATUS.COMPLETE) {
|
|
392
|
+
this.currentStepIndex++;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
// Step start event
|
|
396
|
+
yield {
|
|
397
|
+
type: WORKFLOW_EVENTS.STEP_START,
|
|
398
|
+
status: STATUS.RUNNING,
|
|
399
|
+
stepTitle: step.block.title,
|
|
400
|
+
stepId: step.id,
|
|
401
|
+
options,
|
|
402
|
+
workflowRunId
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// Execute step and yield the STEP_COMPLETE event and
|
|
406
|
+
// all events from inner workflows if any
|
|
407
|
+
yield* this.executeStep(step);
|
|
408
|
+
|
|
409
|
+
// Step Status Event
|
|
410
|
+
yield {
|
|
411
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
412
|
+
steps: steps.map(step => step.serialized),
|
|
413
|
+
options,
|
|
414
|
+
workflowRunId
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
this.currentStepIndex++;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
yield {
|
|
421
|
+
type: WORKFLOW_EVENTS.COMPLETE,
|
|
422
|
+
status: STATUS.COMPLETE,
|
|
423
|
+
workflowTitle,
|
|
424
|
+
workflowDescription,
|
|
425
|
+
workflowRunId,
|
|
426
|
+
options
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
} catch (err: any) {
|
|
430
|
+
const error = err as Error;
|
|
431
|
+
const currentStep = steps[this.currentStepIndex];
|
|
432
|
+
currentStep?.withStatus(STATUS.ERROR);
|
|
433
|
+
|
|
434
|
+
yield {
|
|
435
|
+
type: WORKFLOW_EVENTS.ERROR,
|
|
436
|
+
status: STATUS.ERROR,
|
|
437
|
+
workflowTitle,
|
|
438
|
+
workflowDescription,
|
|
439
|
+
workflowRunId,
|
|
440
|
+
error: {
|
|
441
|
+
name: error.name,
|
|
442
|
+
message: error.message,
|
|
443
|
+
stack: error.stack
|
|
444
|
+
},
|
|
445
|
+
options,
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Step Status Event
|
|
449
|
+
yield {
|
|
450
|
+
type: WORKFLOW_EVENTS.STEP_STATUS,
|
|
451
|
+
steps: steps.map(step => step.serialized),
|
|
452
|
+
options,
|
|
453
|
+
workflowRunId
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private async *executeStep(step: Step): AsyncGenerator<WorkflowEvent<TOptions>> {
|
|
461
|
+
const block = step.block;
|
|
462
|
+
|
|
463
|
+
if (block.type === 'workflow') {
|
|
464
|
+
const initialState = typeof block.initialState === 'function'
|
|
465
|
+
? block.initialState(this.currentState)
|
|
466
|
+
: block.initialState;
|
|
467
|
+
|
|
468
|
+
// Run inner workflow and yield all its events
|
|
469
|
+
let patches: JsonPatch[] = [];
|
|
470
|
+
const innerRun = block.innerWorkflow.run({
|
|
471
|
+
fileStore: this.fileStore,
|
|
472
|
+
client: this.client,
|
|
473
|
+
initialState,
|
|
474
|
+
options: this.options ?? {} as TOptions,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
for await (const event of innerRun) {
|
|
478
|
+
yield event; // Forward all inner workflow events
|
|
479
|
+
if (event.type === WORKFLOW_EVENTS.STEP_COMPLETE) {
|
|
480
|
+
patches.push(event.patch);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Apply collected patches to get final inner state
|
|
485
|
+
const innerState = applyPatches(initialState, patches);
|
|
486
|
+
|
|
487
|
+
// Get previous state before action
|
|
488
|
+
const prevState = this.currentState;
|
|
489
|
+
|
|
490
|
+
// Update state with inner workflow results
|
|
491
|
+
this.currentState = await block.action(this.currentState, innerState);
|
|
492
|
+
yield* this.completeStep(step, prevState);
|
|
493
|
+
} else {
|
|
494
|
+
// Get previous state before action
|
|
495
|
+
const prevState = this.currentState;
|
|
496
|
+
|
|
497
|
+
// Execute regular step
|
|
498
|
+
this.currentState = await block.action({
|
|
499
|
+
state: this.currentState,
|
|
500
|
+
options: this.options ?? {} as TOptions,
|
|
501
|
+
client: this.client,
|
|
502
|
+
fileStore: this.fileStore,
|
|
503
|
+
});
|
|
504
|
+
yield* this.completeStep(step, prevState);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private *completeStep(step: Step, prevState: TState): Generator<WorkflowEvent<TOptions>> {
|
|
509
|
+
step.withStatus(STATUS.COMPLETE);
|
|
510
|
+
|
|
511
|
+
// Create patch for the state change
|
|
512
|
+
const patch = createPatch(prevState, this.currentState);
|
|
513
|
+
step.withPatch(patch);
|
|
514
|
+
|
|
515
|
+
yield {
|
|
516
|
+
type: WORKFLOW_EVENTS.STEP_COMPLETE,
|
|
517
|
+
status: STATUS.RUNNING,
|
|
518
|
+
stepTitle: step.block.title,
|
|
519
|
+
stepId: step.id,
|
|
520
|
+
patch,
|
|
521
|
+
options: this.options ?? {} as TOptions,
|
|
522
|
+
workflowRunId: this.workflowRunId
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let workflowNamesAreUnique = true;
|
|
528
|
+
export function disableWorkflowNameUniqueness() {
|
|
529
|
+
workflowNamesAreUnique = false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function enableWorkflowNameUniqueness() {
|
|
533
|
+
workflowNamesAreUnique = true;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const workflowNames = new Set<string>();
|
|
537
|
+
export function workflow<
|
|
538
|
+
TOptions extends object = {},
|
|
539
|
+
TState extends State = {}
|
|
540
|
+
>(
|
|
541
|
+
workflowConfig: string | { title: string; description?: string }
|
|
542
|
+
) {
|
|
543
|
+
const title = typeof workflowConfig === 'string' ? workflowConfig : workflowConfig.title;
|
|
544
|
+
const description = typeof workflowConfig === 'string' ? undefined : workflowConfig.description;
|
|
545
|
+
if (workflowNamesAreUnique && workflowNames.has(title)) {
|
|
546
|
+
throw new Error(`Workflow with name "${title}" already exists. Workflow names must be unique.`);
|
|
547
|
+
}
|
|
548
|
+
if (workflowNamesAreUnique) {
|
|
549
|
+
workflowNames.add(title);
|
|
550
|
+
}
|
|
551
|
+
return new Workflow<TOptions, TState>(title, description);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const clone = <T>(value: T): T => structuredClone(value);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import type { FileStore } from './types';
|
|
4
|
+
|
|
5
|
+
export class LocalFileStore implements FileStore {
|
|
6
|
+
constructor(private baseDir: string) {}
|
|
7
|
+
async readFile(path: string): Promise<string> {
|
|
8
|
+
const filePath = join(this.baseDir, path);
|
|
9
|
+
return fs.readFile(filePath, 'utf-8');
|
|
10
|
+
}
|
|
11
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { Workflow, workflow } from "./dsl/workflow";
|
|
2
|
+
export { WorkflowRunner } from "./dsl/workflow-runner";
|
|
3
|
+
export { createExtension } from "./dsl/extensions";
|
|
4
|
+
export { STATUS, WORKFLOW_EVENTS } from "./dsl/constants";
|
|
5
|
+
export { Adapter } from "./adapters/types";
|
|
6
|
+
export type {
|
|
7
|
+
WorkflowEvent,
|
|
8
|
+
SerializedStep,
|
|
9
|
+
InitialRunParams,
|
|
10
|
+
RerunParams,
|
|
11
|
+
WorkflowStartEvent,
|
|
12
|
+
WorkflowCompleteEvent,
|
|
13
|
+
WorkflowErrorEvent,
|
|
14
|
+
StepStatusEvent,
|
|
15
|
+
StepStartedEvent,
|
|
16
|
+
StepCompletedEvent
|
|
17
|
+
} from "./dsl/workflow";
|
|
18
|
+
export type { PromptClient, ResponseModel } from "./clients/types";
|
|
19
|
+
export type { State } from "./dsl/types";
|
|
20
|
+
export type { FileStore } from "./file-stores/types";
|
|
21
|
+
export { createPatch, applyPatches } from "./dsl/json-patch";
|
|
22
|
+
export { LocalFileStore } from "./file-stores/local-file-store";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import { FileStore } from '../file-stores/types';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Writes content to a temporary file and opens it in cursor
|
|
12
|
+
*/
|
|
13
|
+
export async function writeAndOpenTemp(
|
|
14
|
+
content: string,
|
|
15
|
+
prefix: string,
|
|
16
|
+
extension: string = 'txt'
|
|
17
|
+
): Promise<string> {
|
|
18
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'positronic-'));
|
|
19
|
+
const filename = `${prefix}-${Date.now()}.${extension}`;
|
|
20
|
+
const filepath = path.join(tempDir, filename);
|
|
21
|
+
|
|
22
|
+
await fs.writeFile(filepath, content, 'utf8');
|
|
23
|
+
|
|
24
|
+
// Open in cursor using the cursor command
|
|
25
|
+
await execAsync(`cursor ${filepath}`);
|
|
26
|
+
|
|
27
|
+
return filepath;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Writes both prompt and response to temp files and opens them in cursor
|
|
32
|
+
*/
|
|
33
|
+
export async function writePromptAndResponse(
|
|
34
|
+
prompt: string,
|
|
35
|
+
response: unknown,
|
|
36
|
+
prefix: string = 'debug'
|
|
37
|
+
): Promise<{ promptPath: string, responsePath: string }> {
|
|
38
|
+
const promptPath = await writeAndOpenTemp(prompt, `${prefix}-prompt`, 'md');
|
|
39
|
+
const responsePath = await writeAndOpenTemp(
|
|
40
|
+
typeof response === 'string' ? response : JSON.stringify(response, null, 2),
|
|
41
|
+
`${prefix}-response`,
|
|
42
|
+
'tsx'
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return { promptPath, responsePath };
|
|
46
|
+
}
|
package/tsconfig.json
ADDED