Compare commits

...

10 Commits

Author SHA1 Message Date
Alex Yang 502e48bbc5 chore: bump 2025-04-24 14:24:49 -07:00
Alex Yang 8994904b06 fix: exports 2025-04-24 14:16:45 -07:00
Alex Yang 8d49887131 Merge remote-tracking branch 'origin/migrate' into migrate 2025-04-24 14:07:43 -07:00
Alex Yang b3309a10dd chore: bump version 2025-04-24 14:07:26 -07:00
Alex Yang 48888569dc Update clean-pots-burn.md 2025-04-24 13:32:04 -07:00
Alex Yang c816c16a97 Merge remote-tracking branch 'origin/migrate' into migrate 2025-04-24 13:13:21 -07:00
Alex Yang 94dbe381b0 fix: test 2025-04-24 13:13:12 -07:00
Alex Yang bf33bf04c3 Create clean-pots-burn.md 2025-04-24 10:57:46 -07:00
Alex Yang 8707a3e688 fix: test 2025-04-24 10:54:03 -07:00
Alex Yang be66ccc6c8 fix: migrate to llamaflow 2025-04-24 10:50:26 -07:00
19 changed files with 222 additions and 2075 deletions
+9
View File
@@ -0,0 +1,9 @@
---
"@llamaindex/workflow": minor
---
refactor!: migrate to llamaflow
- remove `outputs` in workflow. You shuld use TypeScript and define returns type to validate the workflow correctly.
- remove `timeout` and `verbose` in workflow. Workflow now is a very lightly engine, so you should do this by youself. For example, `abortSignal.timeout`, `console.log`...
- `workflow.run` now retunrs `ReadableStream | Promise<WorkflowEvent<Result>>`, you shouldn't use steram and promise in both time.
@@ -10,8 +10,8 @@
},
"devDependencies": {
"typescript": "^5.7.3",
"vite": "^5.4.16",
"vite-plugin-wasm": "^3.3.0"
"vite": "^6.3.3",
"vite-plugin-wasm": "^3.4.1"
},
"dependencies": {
"@llamaindex/cloud": "workspace:*"
@@ -16,7 +16,7 @@
"@size-limit/preset-big-lib": "^11.1.6",
"size-limit": "^11.1.6",
"typescript": "^5.7.3",
"vite": "^5.4.16"
"vite": "^6.3.3"
},
"dependencies": {
"llamaindex": "workspace:*"
-1
View File
@@ -11,7 +11,6 @@ const workflow = new Workflow<ContextData, string, string>();
workflow.addStep(
{
inputs: [StartEvent<string>],
outputs: [StopEvent<string>],
},
async (context, startEvent) => {
const input = startEvent.data;
+5 -20
View File
@@ -11,24 +11,6 @@
],
"exports": {
".": {
"node": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"default": "./dist/index.cjs"
},
"workerd": {
"types": "./dist/index.workerd.d.ts",
"default": "./dist/index.workerd.js"
},
"edge-light": {
"types": "./dist/index.edge-light.d.ts",
"default": "./dist/index.edge-light.js"
},
"browser": {
"types": "./dist/index.browser.d.ts",
"default": "./dist/index.browser.js"
},
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
@@ -55,14 +37,17 @@
"test": "vitest run"
},
"devDependencies": {
"@llamaindex/env": "workspace:*",
"@llamaindex/core": "workspace:*",
"@llamaindex/env": "workspace:*",
"@types/node": "^22.9.0",
"vitest": "^2.1.5"
},
"peerDependencies": {
"@llamaindex/env": "workspace:*",
"@llamaindex/core": "workspace:*",
"@llamaindex/env": "workspace:*",
"zod": "^3.23.8"
},
"dependencies": {
"@llama-flow/llamaindex": "^0.0.12"
}
}
+26 -31
View File
@@ -1,12 +1,16 @@
import {
StartEvent,
type StepContext,
StopEvent,
Workflow,
WorkflowEvent,
} from "@llama-flow/llamaindex";
import type { ChatMessage } from "@llamaindex/core/llms";
import { ChatMemoryBuffer } from "@llamaindex/core/memory";
import { PromptTemplate } from "@llamaindex/core/prompts";
import { FunctionTool } from "@llamaindex/core/tools";
import { stringifyJSONToMessageContent } from "@llamaindex/core/utils";
import { z } from "zod";
import { Workflow } from "../workflow";
import type { HandlerContext, WorkflowContext } from "../workflow-context";
import { StartEvent, StopEvent, WorkflowEvent } from "../workflow-event";
import type { AgentWorkflowContext, BaseWorkflowAgent } from "./base";
import {
AgentInput,
@@ -115,10 +119,7 @@ export class AgentWorkflow {
private rootAgentName: string;
constructor({ agents, rootAgent, verbose, timeout }: AgentWorkflowParams) {
this.workflow = new Workflow({
verbose: verbose ?? false,
timeout: timeout ?? 60,
});
this.workflow = new Workflow();
this.verbose = verbose ?? false;
// Handle AgentWorkflow cases for agents
@@ -254,7 +255,7 @@ export class AgentWorkflow {
}
private handleInputStep = async (
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
event: StartEvent<AgentInputData>,
): Promise<AgentInput> => {
const { userInput, chatHistory } = event.data;
@@ -290,7 +291,7 @@ export class AgentWorkflow {
};
private setupAgent = async (
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
event: AgentInput,
): Promise<AgentSetup> => {
const currentAgentName = event.data.currentAgentName;
@@ -314,9 +315,9 @@ export class AgentWorkflow {
};
private runAgentStep = async (
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
event: AgentSetup,
): Promise<AgentStepEvent> => {
) => {
const agent = this.agents.get(event.data.currentAgentName);
if (!agent) {
throw new Error("No valid agent found");
@@ -330,17 +331,19 @@ export class AgentWorkflow {
const output = await agent.takeStep(ctx, event.data.input, agent.tools);
ctx.sendEvent(output);
ctx.sendEvent(
new AgentStepEvent({
agentName: agent.name,
response: output.data.response,
toolCalls: output.data.toolCalls,
}),
);
return new AgentStepEvent({
agentName: agent.name,
response: output.data.response,
toolCalls: output.data.toolCalls,
});
ctx.sendEvent(output);
};
private parseAgentOutput = async (
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
event: AgentStepEvent,
): Promise<ToolCallsEvent | StopEvent<{ result: string }>> => {
const { agentName, response, toolCalls } = event.data;
@@ -374,7 +377,7 @@ export class AgentWorkflow {
};
private executeToolCalls = async (
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
event: ToolCallsEvent,
): Promise<ToolResultsEvent | StopEvent<{ result: string }>> => {
const { agentName, toolCalls } = event.data;
@@ -423,7 +426,7 @@ export class AgentWorkflow {
};
private processToolResults = async (
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
event: ToolResultsEvent,
): Promise<AgentInput | StopEvent<{ result: string }>> => {
const { agentName, results } = event.data;
@@ -491,7 +494,6 @@ export class AgentWorkflow {
this.workflow.addStep(
{
inputs: [StartEvent<AgentInputData>],
outputs: [AgentInput],
},
this.handleInputStep,
);
@@ -499,7 +501,6 @@ export class AgentWorkflow {
this.workflow.addStep(
{
inputs: [AgentInput],
outputs: [AgentSetup],
},
this.setupAgent,
);
@@ -507,7 +508,6 @@ export class AgentWorkflow {
this.workflow.addStep(
{
inputs: [AgentSetup],
outputs: [AgentStepEvent],
},
this.runAgentStep,
);
@@ -515,7 +515,6 @@ export class AgentWorkflow {
this.workflow.addStep(
{
inputs: [AgentStepEvent],
outputs: [ToolCallsEvent, StopEvent],
},
this.parseAgentOutput,
);
@@ -523,7 +522,6 @@ export class AgentWorkflow {
this.workflow.addStep(
{
inputs: [ToolCallsEvent],
outputs: [ToolResultsEvent, StopEvent],
},
this.executeToolCalls,
);
@@ -531,7 +529,6 @@ export class AgentWorkflow {
this.workflow.addStep(
{
inputs: [ToolResultsEvent],
outputs: [AgentInput, StopEvent],
},
this.processToolResults,
);
@@ -541,7 +538,7 @@ export class AgentWorkflow {
private callTool(
toolCall: AgentToolCall,
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
) {
const tool = this.agents
.get(toolCall.data.agentName)
@@ -563,7 +560,7 @@ export class AgentWorkflow {
chatHistory?: ChatMessage[];
context?: AgentWorkflowContext;
},
): WorkflowContext<AgentInputData, string, AgentWorkflowContext> {
) {
if (this.agents.size === 0) {
throw new Error("No agents added to workflow");
}
@@ -577,15 +574,13 @@ export class AgentWorkflow {
nextAgentName: null,
};
const result = this.workflow.run(
return this.workflow.run(
{
userInput: userInput,
chatHistory: params?.chatHistory,
},
contextData,
);
return result;
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
import type { StepContext } from "@llama-flow/llamaindex";
import type { BaseToolWithCall, ChatMessage, LLM } from "@llamaindex/core/llms";
import { BaseMemory } from "@llamaindex/core/memory";
import type { HandlerContext } from "../workflow-context";
import type { AgentOutput, AgentToolCallResult } from "./events";
export type AgentWorkflowContext = {
@@ -28,7 +28,7 @@ export interface BaseWorkflowAgent {
* Using memory directly to get messages instead of requiring them to be passed in
*/
takeStep(
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
llmInput: ChatMessage[],
tools: BaseToolWithCall[],
): Promise<AgentOutput>;
@@ -37,7 +37,7 @@ export interface BaseWorkflowAgent {
* Handle results from tool calls
*/
handleToolCallResults(
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
results: AgentToolCallResult[],
): Promise<void>;
@@ -45,7 +45,7 @@ export interface BaseWorkflowAgent {
* Finalize the agent's output
*/
finalize(
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
output: AgentOutput,
memory: BaseMemory,
): Promise<AgentOutput>;
+1 -1
View File
@@ -1,6 +1,6 @@
import { WorkflowEvent } from "@llama-flow/llamaindex";
import type { JSONValue } from "@llamaindex/core/global";
import type { ChatMessage, ToolResult } from "@llamaindex/core/llms";
import { WorkflowEvent } from "../workflow-event";
export class AgentToolCall extends WorkflowEvent<{
agentName: string;
@@ -1,3 +1,4 @@
import type { StepContext } from "@llama-flow/llamaindex";
import type { JSONObject } from "@llamaindex/core/global";
import { Settings } from "@llamaindex/core/global";
import {
@@ -7,7 +8,6 @@ import {
type ChatResponseChunk,
} from "@llamaindex/core/llms";
import { BaseMemory } from "@llamaindex/core/memory";
import type { HandlerContext } from "../workflow-context";
import { AgentWorkflow } from "./agent-workflow";
import { type AgentWorkflowContext, type BaseWorkflowAgent } from "./base";
import {
@@ -110,7 +110,7 @@ export class FunctionAgent implements BaseWorkflowAgent {
}
async takeStep(
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
llmInput: ChatMessage[],
tools: BaseToolWithCall[],
): Promise<AgentOutput> {
@@ -170,7 +170,7 @@ export class FunctionAgent implements BaseWorkflowAgent {
}
async handleToolCallResults(
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
results: AgentToolCallResult[],
): Promise<void> {
const scratchpad: ChatMessage[] = ctx.data.scratchpad;
@@ -196,7 +196,7 @@ export class FunctionAgent implements BaseWorkflowAgent {
}
async finalize(
ctx: HandlerContext<AgentWorkflowContext>,
ctx: StepContext<AgentWorkflowContext>,
output: AgentOutput,
memory: BaseMemory,
): Promise<AgentOutput> {
+1 -7
View File
@@ -1,8 +1,2 @@
export * from "@llama-flow/llamaindex";
export * from "./agent/index.js";
export {
WorkflowContext,
type HandlerContext,
type StepHandler,
} from "./workflow-context.js";
export { StartEvent, StopEvent, WorkflowEvent } from "./workflow-event.js";
export { Workflow, type StepParameters } from "./workflow.js";
-628
View File
@@ -1,628 +0,0 @@
import { CustomEvent, randomUUID } from "@llamaindex/env";
import {
type AnyWorkflowEventConstructor,
StartEvent,
type StartEventConstructor,
StopEvent,
type StopEventConstructor,
WorkflowEvent,
} from "./workflow-event";
export type StepHandler<
Data = unknown,
Inputs extends [
AnyWorkflowEventConstructor | StartEventConstructor,
...(AnyWorkflowEventConstructor | StopEventConstructor)[],
] = [AnyWorkflowEventConstructor | StartEventConstructor],
Out extends (AnyWorkflowEventConstructor | StopEventConstructor)[] = [],
> = (
context: HandlerContext<Data>,
...events: {
[K in keyof Inputs]: InstanceType<Inputs[K]>;
}
) => Promise<
Out extends []
? void
: {
[K in keyof Out]: InstanceType<Out[K]>;
}[number]
>;
export type ReadonlyStepMap<Data> = ReadonlyMap<
StepHandler<Data, never, never>,
{
inputs: AnyWorkflowEventConstructor[];
outputs: AnyWorkflowEventConstructor[];
}
>;
type GlobalEvent = typeof globalThis.Event;
export type Wait = () => Promise<void>;
export type ContextParams<Start, Stop, Data> = {
startEvent: StartEvent<Start>;
contextData: Data;
steps: ReadonlyStepMap<Data>;
timeout: number | null;
verbose: boolean;
wait: Wait;
queue: QueueProtocol[] | undefined;
pendingInputQueue: WorkflowEvent<unknown>[] | undefined;
resolved: StopEvent<Stop> | null | undefined;
rejected: Error | null | undefined;
};
function flattenEvents(
acceptEventTypes: AnyWorkflowEventConstructor[],
inputEvents: WorkflowEvent<unknown>[],
): WorkflowEvent<unknown>[] {
const eventMap = new Map<
AnyWorkflowEventConstructor,
WorkflowEvent<unknown>
>();
for (const event of inputEvents) {
for (const acceptType of acceptEventTypes) {
if (event instanceof acceptType && !eventMap.has(acceptType)) {
eventMap.set(acceptType, event);
break; // Once matched, no need to check other accept types
}
}
}
return Array.from(eventMap.values());
}
export type HandlerContext<Data = unknown> = {
get data(): Data;
sendEvent(event: WorkflowEvent<unknown>): void;
requireEvent<T extends AnyWorkflowEventConstructor>(
event: T,
): Promise<InstanceType<T>>;
};
export type QueueProtocol =
| {
type: "event";
event: WorkflowEvent<unknown>;
}
| {
type: "requestEvent";
id: string;
requestEvent: AnyWorkflowEventConstructor;
};
export class WorkflowContext<Start = string, Stop = string, Data = unknown>
implements
AsyncIterable<WorkflowEvent<unknown>, unknown, void>,
Promise<StopEvent<Stop>>
{
readonly #steps: ReadonlyStepMap<Data>;
readonly #startEvent: StartEvent<Start>;
readonly #queue: QueueProtocol[] = [];
readonly #queueEventTarget = new EventTarget();
readonly #wait: Wait;
#timeout: number | null = null;
#verbose: boolean = false;
#data: Data;
#stepCache: WeakMap<
WorkflowEvent<unknown>,
[
step: Set<StepHandler<Data, never, never>>,
stepInputs: WeakMap<
StepHandler<Data, never, never>,
AnyWorkflowEventConstructor[]
>,
stepOutputs: WeakMap<
StepHandler<Data, never, never>,
AnyWorkflowEventConstructor[]
>,
]
> = new Map();
#getStepFunction(
event: WorkflowEvent<unknown>,
): [
step: Set<StepHandler<Data, never, never>>,
stepInputs: WeakMap<
StepHandler<Data, never, never>,
AnyWorkflowEventConstructor[]
>,
stepOutputs: WeakMap<
StepHandler<Data, never, never>,
AnyWorkflowEventConstructor[]
>,
] {
if (this.#stepCache.has(event)) {
return this.#stepCache.get(event)!;
}
const set = new Set<StepHandler<Data, never, never>>();
const stepInputs = new WeakMap<
StepHandler<Data, never, never>,
AnyWorkflowEventConstructor[]
>();
const stepOutputs = new WeakMap<
StepHandler<Data, never, never>,
AnyWorkflowEventConstructor[]
>();
const res: [
step: Set<StepHandler<Data, never, never>>,
stepInputs: WeakMap<
StepHandler<Data, never, never>,
AnyWorkflowEventConstructor[]
>,
stepOutputs: WeakMap<
StepHandler<Data, never, never>,
AnyWorkflowEventConstructor[]
>,
] = [set, stepInputs, stepOutputs];
this.#stepCache.set(event, res);
for (const [step, { inputs, outputs }] of this.#steps) {
if (inputs.some((input) => event instanceof input)) {
set.add(step);
stepInputs.set(step, inputs);
stepOutputs.set(step, outputs);
}
}
return res;
}
constructor(params: ContextParams<Start, Stop, Data>) {
this.#steps = params.steps;
this.#startEvent = params.startEvent;
if (typeof params.timeout === "number") {
this.#timeout = params.timeout;
}
this.#data = params.contextData;
this.#verbose = params.verbose ?? false;
this.#wait = params.wait;
// push start event to the queue
const [step] = this.#getStepFunction(this.#startEvent);
if (step.size === 0) {
throw new TypeError("No step found for start event");
}
// restore from snapshot
if (params.queue) {
params.queue.forEach((protocol) => {
this.#queue.push(protocol);
});
} else {
this.#sendEvent(this.#startEvent);
}
if (params.pendingInputQueue) {
this.#pendingInputQueue = params.pendingInputQueue;
}
if (params.resolved) {
this.#resolved = params.resolved;
}
if (params.rejected) {
this.#rejected = params.rejected;
}
}
// make sure it will only be called once
#iterator: AsyncIterableIterator<WorkflowEvent<unknown>> | null = null;
#signal: AbortSignal | null = null;
get #iteratorSingleton(): AsyncIterableIterator<WorkflowEvent<unknown>> {
if (this.#iterator === null) {
this.#iterator = this.#createStreamEvents();
}
return this.#iterator;
}
[Symbol.asyncIterator](): AsyncIterableIterator<WorkflowEvent<unknown>> {
return this.#iteratorSingleton;
}
#sendEvent = (event: WorkflowEvent<unknown>): void => {
this.#queue.push({
type: "event",
event,
});
};
#requireEvent = async <T extends AnyWorkflowEventConstructor>(
event: T,
): Promise<InstanceType<T>> => {
const requestId = randomUUID();
this.#queue.push({
type: "requestEvent",
id: requestId,
requestEvent: event,
});
return new Promise((resolve) => {
const handler = (event: InstanceType<GlobalEvent>) => {
if (event instanceof CustomEvent) {
const { id } = event.detail;
if (requestId === id) {
this.#queueEventTarget.removeEventListener("update", handler);
resolve(event.detail.event);
}
}
};
this.#queueEventTarget.addEventListener("update", handler);
});
};
#pendingInputQueue: WorkflowEvent<unknown>[] = [];
// if strict mode is enabled, it will throw an error if there's input or output events are not expected
#strict = false;
strict() {
this.#strict = true;
return this;
}
get data(): Data {
return this.#data;
}
/**
* Stream events from the start event
*
* Note that this function will stop once there's no more future events,
* if you want stop immediately once reach a StopEvent, you should handle it in the other side.
* @private
*/
#createStreamEvents(): AsyncIterableIterator<WorkflowEvent<unknown>> {
const isPendingEvents = new WeakSet<WorkflowEvent<unknown>>();
const pendingTasks = new Set<Promise<WorkflowEvent<unknown> | void>>();
const enqueuedEvents = new Set<WorkflowEvent<unknown>>();
const stream = new ReadableStream<WorkflowEvent<unknown>>({
start: async (controller) => {
while (true) {
const eventProtocol = this.#queue.shift();
if (eventProtocol) {
switch (eventProtocol.type) {
case "requestEvent": {
const { id, requestEvent } = eventProtocol;
const acceptableInput = this.#pendingInputQueue.find(
(event) => event instanceof requestEvent,
);
if (acceptableInput) {
// remove the event from the queue, in case of infinite loop
const protocolIdx = this.#queue.findIndex(
(protocol) =>
protocol.type === "event" &&
protocol.event === acceptableInput,
);
if (protocolIdx !== -1) {
this.#queue.splice(protocolIdx, 1);
}
this.#pendingInputQueue.splice(
this.#pendingInputQueue.indexOf(acceptableInput),
1,
);
this.#queueEventTarget.dispatchEvent(
new CustomEvent("update", {
detail: { id, event: acceptableInput },
}),
);
} else {
// push back to the queue as there are not enough events
this.#queue.push(eventProtocol);
}
break;
}
case "event": {
const { event } = eventProtocol;
if (isPendingEvents.has(event)) {
// this event is still processing
this.#sendEvent(event);
} else {
if (!enqueuedEvents.has(event)) {
controller.enqueue(event);
enqueuedEvents.add(event);
}
const [steps, inputsMap, outputsMap] =
this.#getStepFunction(event);
const nextEventPromises: Promise<WorkflowEvent<unknown> | void>[] =
[...steps]
.map((step) => {
const inputs = [...(inputsMap.get(step) ?? [])];
const acceptableInputs: WorkflowEvent<unknown>[] =
this.#pendingInputQueue.filter((event) =>
inputs.some((input) => event instanceof input),
);
const events: WorkflowEvent<unknown>[] = flattenEvents(
inputs,
[event, ...acceptableInputs],
);
// remove the event from the queue, in case of infinite loop
events.forEach((event) => {
const protocolIdx = this.#queue.findIndex(
(protocol) =>
protocol.type === "event" &&
protocol.event === event,
);
if (protocolIdx !== -1) {
this.#queue.splice(protocolIdx, 1);
}
});
if (events.length !== inputs.length) {
if (this.#verbose) {
console.log(
`Not enough inputs for step ${step.name}, waiting for more events`,
);
}
// not enough to run the step, push back to the queue
this.#sendEvent(event);
isPendingEvents.add(event);
return null;
}
if (isPendingEvents.has(event)) {
isPendingEvents.delete(event);
}
if (this.#verbose) {
console.log(
`Running step ${step.name} with inputs ${events}`,
);
}
const data = this.data;
return (step as StepHandler<Data>)
.call(
null,
{
get data() {
return data;
},
sendEvent: this.#sendEvent,
requireEvent: this.#requireEvent,
},
// @ts-expect-error IDK why
...events.sort((a, b) => {
const aIndex = inputs.indexOf(
a.constructor as AnyWorkflowEventConstructor,
);
const bIndex = inputs.indexOf(
b.constructor as AnyWorkflowEventConstructor,
);
return aIndex - bIndex;
}),
)
.then((nextEvent: void | WorkflowEvent<unknown>) => {
if (nextEvent === undefined) {
return;
}
if (this.#verbose) {
console.log(
`Step ${step.name} completed, next event is ${nextEvent}`,
);
}
const outputs = outputsMap.get(step) ?? [];
if (
!outputs.some(
(output) => nextEvent.constructor === output,
)
) {
if (this.#strict) {
const error = Error(
`Step ${step.name} returned an unexpected output event ${nextEvent}`,
);
controller.error(error);
} else {
console.warn(
`Step ${step.name} returned an unexpected output event ${nextEvent}`,
);
}
}
if (!(nextEvent instanceof StopEvent)) {
this.#pendingInputQueue.unshift(nextEvent);
this.#sendEvent(nextEvent);
}
return nextEvent;
});
})
.filter((promise) => promise !== null);
nextEventPromises.forEach((promise) => {
pendingTasks.add(promise);
promise
.catch((err) => {
console.error("Error in step", err);
})
.finally(() => {
pendingTasks.delete(promise);
});
});
Promise.race(nextEventPromises)
.then((fastestNextEvent) => {
if (fastestNextEvent === undefined) {
return;
}
if (!enqueuedEvents.has(fastestNextEvent)) {
controller.enqueue(fastestNextEvent);
enqueuedEvents.add(fastestNextEvent);
}
return fastestNextEvent;
})
.then(async (fastestNextEvent) =>
Promise.all(nextEventPromises).then((nextEvents) => {
const events = nextEvents.filter(
(event) => event !== undefined,
);
for (const nextEvent of events) {
// do not enqueue the same event twice
if (fastestNextEvent !== nextEvent) {
if (!enqueuedEvents.has(nextEvent)) {
controller.enqueue(nextEvent);
enqueuedEvents.add(nextEvent);
}
}
}
}),
)
.catch((err) => {
// when the step raise an error, should go back to the previous step
this.#sendEvent(event);
isPendingEvents.add(event);
controller.error(err);
});
}
break;
}
}
}
if (this.#queue.length === 0 && pendingTasks.size === 0) {
if (this.#verbose) {
console.log("No more events in the queue");
}
break;
}
await this.#wait();
}
controller.close();
},
});
return stream[Symbol.asyncIterator]();
}
with<Initial extends Data>(
data: Initial,
): WorkflowContext<Start, Stop, Initial> {
return new WorkflowContext({
startEvent: this.#startEvent,
wait: this.#wait,
contextData: data,
steps: this.#steps,
timeout: this.#timeout,
verbose: this.#verbose,
queue: this.#queue,
pendingInputQueue: this.#pendingInputQueue,
resolved: this.#resolved,
rejected: this.#rejected,
});
}
// PromiseLike implementation, this is following the Promise/A+ spec
// It will consume the iterator and resolve the promise once it reaches the StopEvent
// If you want to customize the behavior, you can use the async iterator directly
#resolved: StopEvent<Stop> | null = null;
#rejected: Error | null = null;
async then<TResult1, TResult2 = never>(
onfulfilled?:
| ((value: StopEvent<Stop>) => TResult1 | PromiseLike<TResult1>)
| null
| undefined,
onrejected?:
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
| null
| undefined,
) {
onfulfilled ??= (value) => value as TResult1;
onrejected ??= (reason) => {
throw reason;
};
if (this.#resolved !== null) {
return Promise.resolve(this.#resolved).then(onfulfilled, onrejected);
} else if (this.#rejected !== null) {
return Promise.reject(this.#rejected).then(onfulfilled, onrejected);
}
if (this.#timeout !== null) {
const timeout = this.#timeout;
this.#signal = AbortSignal.timeout(timeout * 1000);
}
this.#signal?.addEventListener("abort", () => {
this.#rejected = new Error(
`Operation timed out after ${this.#timeout} seconds`,
);
onrejected?.(this.#rejected);
});
try {
for await (const event of this.#iteratorSingleton) {
if (this.#rejected !== null) {
return onrejected?.(this.#rejected);
}
if (event instanceof StartEvent) {
if (this.#verbose) {
console.log(`Starting workflow with event ${event}`);
}
}
if (event instanceof StopEvent) {
if (this.#verbose && this.#pendingInputQueue.length > 0) {
// fixme: #pendingInputQueue might should be cleanup correctly?
}
this.#resolved = event;
return onfulfilled?.(event);
}
}
} catch (err) {
if (err instanceof Error) {
this.#rejected = err;
}
return onrejected?.(err);
}
const nextValue = await this.#iteratorSingleton.next();
if (nextValue.done === false) {
this.#rejected = new Error("Workflow did not complete");
return onrejected?.(this.#rejected);
}
return onrejected?.(new Error("UNREACHABLE"));
}
catch<TResult = never>(
onrejected?:
| ((reason: unknown) => TResult | PromiseLike<TResult>)
| null
| undefined,
) {
return this.then((v) => v, onrejected);
}
finally(onfinally?: (() => void) | undefined | null) {
return this.then(
() => {
onfinally?.();
},
() => {
onfinally?.();
},
) as Promise<never>;
}
[Symbol.toStringTag]: string = "Context";
// for worker thread
snapshot(): ArrayBuffer {
const state = {
startEvent: this.#startEvent,
queue: this.#queue,
pendingInputQueue: this.#pendingInputQueue,
data: this.#data,
timeout: this.#timeout,
verbose: this.#verbose,
resolved: this.#resolved,
rejected: this.#rejected,
};
const jsonString = JSON.stringify(state, (_, value) => {
// If value is an instance of a class, serialize only its properties
if (value instanceof WorkflowEvent) {
return { data: value.data, constructor: value.constructor.name };
}
// value is Subtype of WorkflowEvent
if (
typeof value === "object" &&
value !== null &&
value?.prototype instanceof WorkflowEvent
) {
return { constructor: value.prototype.constructor.name };
}
return value;
});
return new TextEncoder().encode(jsonString).buffer;
}
}
-63
View File
@@ -1,63 +0,0 @@
export class WorkflowEvent<Data> {
displayName: string;
data: Data;
constructor(data: Data) {
this.data = data;
this.displayName = this.constructor.name;
}
toString() {
return this.displayName;
}
static or<
A extends AnyWorkflowEventConstructor,
B extends AnyWorkflowEventConstructor,
>(AEvent: A, BEvent: B): A | B {
function OrEvent() {
throw new Error("Cannot instantiate OrEvent");
}
OrEvent.prototype = Object.create(AEvent.prototype);
Object.getOwnPropertyNames(BEvent.prototype).forEach((property) => {
if (!(property in OrEvent.prototype)) {
Object.defineProperty(
OrEvent.prototype,
property,
Object.getOwnPropertyDescriptor(BEvent.prototype, property)!,
);
}
});
OrEvent.prototype.constructor = OrEvent;
Object.defineProperty(OrEvent, Symbol.hasInstance, {
value: function (instance: unknown) {
return instance instanceof AEvent || instance instanceof BEvent;
},
});
return OrEvent as unknown as A | B;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyWorkflowEventConstructor = new (data: any) => WorkflowEvent<any>;
export type StartEventConstructor<T = string> = new (data: T) => StartEvent<T>;
export type StopEventConstructor<T = string> = new (data: T) => StopEvent<T>;
// These are special events that are used to control the workflow
export class StartEvent<T = string> extends WorkflowEvent<T> {
constructor(data: T) {
super(data);
}
}
export class StopEvent<T = string> extends WorkflowEvent<T> {
constructor(data: T) {
super(data);
}
}
-194
View File
@@ -1,194 +0,0 @@
import {
WorkflowContext,
type HandlerContext,
type QueueProtocol,
type StepHandler,
type Wait,
} from "./workflow-context.js";
import {
StartEvent,
StopEvent,
type AnyWorkflowEventConstructor,
type StartEventConstructor,
type StopEventConstructor,
} from "./workflow-event.js";
export type StepParameters<
In extends AnyWorkflowEventConstructor[],
Out extends AnyWorkflowEventConstructor[],
> = {
inputs: In;
outputs: Out;
};
export class Workflow<ContextData, Start, Stop> {
#steps: Map<
StepHandler<ContextData, never, never>,
{
inputs: AnyWorkflowEventConstructor[];
outputs: AnyWorkflowEventConstructor[];
}
> = new Map();
#verbose: boolean = false;
#timeout: number | null = null;
// fixme: allow microtask
#nextTick: Wait = () => new Promise((resolve) => setTimeout(resolve, 0));
constructor(
params: {
verbose?: boolean;
timeout?: number | null;
wait?: Wait;
} = {},
) {
if (params.verbose) {
this.#verbose = params.verbose;
}
if (params.timeout) {
this.#timeout = params.timeout;
}
if (params.wait) {
this.#nextTick = params.wait;
}
}
addStep<
const In extends [
AnyWorkflowEventConstructor | StartEventConstructor,
...(AnyWorkflowEventConstructor | StopEventConstructor)[],
],
const Out extends (AnyWorkflowEventConstructor | StopEventConstructor)[],
>(
parameters: StepParameters<In, Out>,
stepFn: (
context: HandlerContext<ContextData>,
...events: {
[K in keyof In]: InstanceType<In[K]>;
}
) => Promise<
Out extends []
? void
: {
[K in keyof Out]: InstanceType<Out[K]>;
}[number]
>,
): this {
const { inputs, outputs } = parameters;
this.#steps.set(stepFn as never, { inputs, outputs });
return this;
}
hasStep(stepFn: StepHandler): boolean {
return this.#steps.has(stepFn);
}
removeStep(stepFn: StepHandler): this {
this.#steps.delete(stepFn);
return this;
}
run(
event: StartEvent<Start> | Start,
): unknown extends ContextData
? WorkflowContext<Start, Stop, ContextData>
: WorkflowContext<Start, Stop, ContextData | undefined>;
run<Data extends ContextData>(
event: StartEvent<Start> | Start,
data: Data,
): WorkflowContext<Start, Stop, Data>;
run<Data extends ContextData>(
event: StartEvent<Start> | Start,
data?: Data,
): WorkflowContext<Start, Stop, Data> {
const startEvent: StartEvent<Start> =
event instanceof StartEvent ? event : new StartEvent(event);
return new WorkflowContext<Start, Stop, Data>({
startEvent,
wait: this.#nextTick,
contextData: data!,
steps: new Map(this.#steps),
timeout: this.#timeout,
verbose: this.#verbose,
queue: undefined,
pendingInputQueue: undefined,
resolved: null,
rejected: null,
});
}
recover(data: ArrayBuffer): WorkflowContext<Start, Stop, ContextData> {
const jsonString = new TextDecoder().decode(data);
const state = JSON.parse(jsonString);
const reconstructedStartEvent = new StartEvent<Start>(state.startEvent);
const AllEvents = [...this.#steps]
.map(([, { inputs, outputs }]) => [...inputs, ...(outputs ?? [])])
.flat();
const reconstructedQueue: QueueProtocol[] = state.queue.map(
(protocol: QueueProtocol): QueueProtocol => {
switch (protocol.type) {
case "requestEvent": {
const { requestEvent, id } = protocol;
const EventType = AllEvents.find(
(type) =>
type.prototype.constructor.name ===
(requestEvent.constructor as unknown as string),
);
if (!EventType) {
throw new TypeError(
`Event type not found: ${requestEvent.constructor}`,
);
}
return {
type: "requestEvent",
id,
requestEvent: EventType,
};
}
case "event": {
const { event } = protocol;
const EventType = AllEvents.find(
(type) =>
type.prototype.constructor.name ===
(event.constructor as unknown as string),
);
if (!EventType) {
throw new TypeError(`Event type not found: ${event.constructor}`);
}
return {
type: "event",
event: new EventType(event.data),
};
}
}
},
);
const reconstructedPendingInputQueue = state.pendingInputQueue.map(
(event: Record<string, unknown>) => {
const EventType = AllEvents.find(
(type) => type.prototype.constructor.name === event.constructor,
);
if (!EventType) {
throw new TypeError(`Event type not found: ${event.constructor}`);
}
return new EventType(event.data);
},
);
return new WorkflowContext<Start, Stop, ContextData>({
startEvent: reconstructedStartEvent,
contextData: state.data,
wait: this.#nextTick,
steps: this.#steps, // Assuming steps do not change and are part of the class prototype or similar
timeout: state.timeout,
verbose: state.verbose,
queue: reconstructedQueue,
pendingInputQueue: reconstructedPendingInputQueue,
resolved: state.resolved ? new StopEvent<Stop>(state.resolved) : null,
rejected: state.rejected ? new Error(state.rejected) : null,
});
}
}
@@ -128,17 +128,17 @@ describe("AgentWorkflow", () => {
"StartEvent",
"AgentInput",
"AgentSetup",
"AgentStepEvent",
"AgentStream",
"AgentStepEvent",
"AgentOutput",
"ToolCallsEvent",
"ToolResultsEvent",
"AgentToolCall",
"AgentToolCallResult",
"ToolResultsEvent",
"AgentInput",
"AgentSetup",
"AgentStepEvent",
"AgentStream",
"AgentStepEvent",
"AgentOutput",
"StopEvent",
];
@@ -39,7 +39,7 @@ describe("FunctionAgent", () => {
returnDirect: false,
},
displayName: "test",
} as AgentToolCallResult;
} as unknown as AgentToolCallResult;
const dummyContext = {
data: {
+1
View File
@@ -26,6 +26,7 @@ export function setupToolCallingMockLLM(
topP: 1,
contextWindow: 4096,
tokenizer: undefined,
structuredOutput: false,
},
});
mockLLM.supportToolCall = true;
-972
View File
@@ -1,972 +0,0 @@
import {
beforeEach,
describe,
expect,
expectTypeOf,
test,
vi,
type Mocked,
} from "vitest";
import type { HandlerContext, StepHandler, StepParameters } from "../src";
import { StartEvent, StopEvent, Workflow, WorkflowEvent } from "../src";
class JokeEvent extends WorkflowEvent<{ joke: string }> {}
class AnalysisEvent extends WorkflowEvent<{ analysis: string }> {}
describe("type system", () => {
test("handler", () => {
type Parameters = StepParameters<
[typeof StartEvent<string>],
[typeof StopEvent<string>]
>;
type Handler = (
context: HandlerContext,
ev: StartEvent<string>,
) => Promise<StopEvent<string>>;
type Handler2 = (
context: HandlerContext,
ev: StartEvent<string>,
) => Promise<StopEvent<number>>;
type Handler3 = (
context: HandlerContext,
ev: StartEvent<string>,
) => Promise<AnalysisEvent>;
expectTypeOf<Parameters>().toEqualTypeOf<{
inputs: [typeof StartEvent<string>];
outputs: [typeof StopEvent<string>];
}>();
expectTypeOf<
StepHandler<
unknown,
[typeof StartEvent<string>],
[typeof StopEvent<string>]
>
>().toEqualTypeOf<Handler>();
expectTypeOf<
StepHandler<
unknown,
[typeof StartEvent<string>],
[typeof StopEvent<string>]
>
>().not.toEqualTypeOf<Handler2>();
expectTypeOf<
StepHandler<
unknown,
[typeof StartEvent<string>],
[typeof StopEvent<string>]
>
>().not.toEqualTypeOf<Handler3>();
});
});
describe("workflow basic", () => {
let generateJoke: Mocked<
(context: HandlerContext, ev: StartEvent) => Promise<JokeEvent>
>;
let critiqueJoke: Mocked<
(context: HandlerContext, ev: JokeEvent) => Promise<StopEvent<string>>
>;
let analyzeJoke: Mocked<
(context: HandlerContext, ev: JokeEvent) => Promise<AnalysisEvent>
>;
beforeEach(() => {
generateJoke = vi.fn(async (_context, _: StartEvent) => {
return new JokeEvent({ joke: "a joke" });
});
critiqueJoke = vi.fn(async (_context, _: JokeEvent) => {
return new StopEvent("stop");
});
analyzeJoke = vi.fn(async (_context: HandlerContext, _: JokeEvent) => {
return new AnalysisEvent({ analysis: "an analysis" });
});
});
test("workflow basic", async () => {
const workflow = new Workflow<
{
foo: string;
bar: number;
},
string,
string
>();
workflow.addStep(
{
inputs: [StartEvent],
outputs: [StopEvent],
},
async ({ data }, start) => {
expect(start).toBeInstanceOf(StartEvent);
expect(start.data).toBe("start");
expect(data.bar).toBe(42);
expect(data.foo).toBe("foo");
return new StopEvent("stopped");
},
);
const result = workflow.run("start", {
foo: "foo",
bar: 42,
});
await result;
});
test("run workflow", async () => {
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
jokeFlow.addStep(
{ inputs: [StartEvent<string>], outputs: [JokeEvent] },
generateJoke,
);
jokeFlow.addStep(
{ inputs: [JokeEvent], outputs: [StopEvent] },
critiqueJoke,
);
const result = await jokeFlow.run("pirates");
expect(generateJoke).toHaveBeenCalledTimes(1);
expect(critiqueJoke).toHaveBeenCalledTimes(1);
expect(result.data).toBe("stop");
});
test("stream events", async () => {
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
jokeFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [JokeEvent],
},
generateJoke,
);
jokeFlow.addStep(
{
inputs: [JokeEvent],
outputs: [StopEvent],
},
critiqueJoke,
);
const run = jokeFlow.run("pirates");
const event = await run[Symbol.asyncIterator]().next(); // get one event to avoid testing timeout
const result = await run;
expect(generateJoke).toHaveBeenCalledTimes(1);
expect(critiqueJoke).toHaveBeenCalledTimes(1);
expect(result.data).toBe("stop");
expect(event).not.toBeNull();
});
test("workflow timeout", async () => {
const TIMEOUT = 1;
const jokeFlow = new Workflow<unknown, string, string>({
verbose: true,
timeout: TIMEOUT,
});
const longRunning = async (_context: HandlerContext, ev: StartEvent) => {
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for 2 seconds
return new StopEvent("We waited 2 seconds");
};
jokeFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [StopEvent],
},
longRunning,
);
const run = jokeFlow.run("Let's start");
await expect(run).rejects.toThrow(
`Operation timed out after ${TIMEOUT} seconds`,
);
});
test("workflow validation", async () => {
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
jokeFlow.addStep(
{ inputs: [StartEvent<string>], outputs: [StopEvent] },
generateJoke,
);
jokeFlow.addStep(
{ inputs: [JokeEvent], outputs: [StopEvent] },
critiqueJoke,
);
expect(async () => {
await jokeFlow.run("pirates").strict();
}).rejects.toThrow(
"Step spy returned an unexpected output event JokeEvent",
);
});
test("requireEvents - 1", async () => {
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
jokeFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [StopEvent],
},
async (ctx, start) => {
ctx.sendEvent(new AnalysisEvent({ analysis: "an analysis" }));
await ctx.requireEvent(JokeEvent);
return new StopEvent("Report generated");
},
);
const fn = vi.fn(async () => {
return new JokeEvent({ joke: "a joke" });
});
jokeFlow.addStep(
{
inputs: [AnalysisEvent],
outputs: [JokeEvent],
},
fn,
);
const result = await jokeFlow.run("pirates");
expect(fn).toHaveBeenCalledTimes(1);
expect(result.data).toBe("Report generated");
});
test("run workflow with multiple in-degree", async () => {
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
jokeFlow.addStep(
{
inputs: [StartEvent],
outputs: [JokeEvent],
},
async (context, _) => {
context.sendEvent(
new AnalysisEvent({
analysis: "an analysis",
}),
);
return new JokeEvent({
joke: "a joke",
});
},
);
jokeFlow.addStep(
{
inputs: [JokeEvent, AnalysisEvent],
outputs: [StopEvent<string>],
},
async () => {
return new StopEvent("The analysis is insightful and helpful.");
},
);
const result = await jokeFlow.run("pirates");
expect(result.data).toBe("The analysis is insightful and helpful.");
});
test("run invalid workflow", async () => {
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
jokeFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [JokeEvent],
},
generateJoke,
);
jokeFlow.addStep(
{
inputs: [JokeEvent],
outputs: [StopEvent<string>],
},
// @ts-expect-error it actually returns AnalysisEvent
analyzeJoke,
);
jokeFlow.addStep(
{
inputs: [JokeEvent],
outputs: [StopEvent<string>],
},
async () => {
return new StopEvent("The analysis is insightful and helpful.");
},
);
const consoleSpy = vi.spyOn(console, "warn");
expect(consoleSpy).toHaveBeenCalledTimes(0);
const result = await jokeFlow.run("pirates");
expect(consoleSpy).toHaveBeenCalledTimes(1);
consoleSpy.mockRestore();
expect(result.data).toBe("The analysis is insightful and helpful.");
});
test("run workflow with object-based StartEvent and StopEvent", async () => {
const objectFlow = new Workflow<
unknown,
Person,
{
result: {
greeting: string;
};
}
>({ verbose: true });
type Person = { name: string; age: number };
const processObject = vi.fn(async (_context, ev: StartEvent<Person>) => {
const { name, age } = ev.data;
return new StopEvent({
result: { greeting: `Hello ${name}, you are ${age} years old!` },
});
});
objectFlow.addStep(
{
inputs: [StartEvent<Person>],
outputs: [
StopEvent<{
result: {
greeting: string;
};
}>,
],
},
processObject,
);
const result = await objectFlow.run(
new StartEvent<Person>({ name: "Alice", age: 30 }),
);
expect(processObject).toHaveBeenCalledTimes(1);
expect(result.data.result).toEqual({
greeting: "Hello Alice, you are 30 years old!",
});
});
test("workflow with two concurrent steps", async () => {
const concurrentFlow = new Workflow<unknown, string, string>({
verbose: true,
});
const step1 = vi.fn(async (_context, _ev: StartEvent) => {
await new Promise((resolve) => setTimeout(resolve, 200));
return new StopEvent("Step 1 completed");
});
const step2 = vi.fn(async (_context, _ev: StartEvent) => {
await new Promise((resolve) => setTimeout(resolve, 100));
return new StopEvent("Step 2 completed");
});
concurrentFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [StopEvent<string>],
},
step1,
);
concurrentFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [StopEvent<string>],
},
step2,
);
const startTime = new Date();
const result = await concurrentFlow.run("start");
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
expect(step1).toHaveBeenCalledTimes(1);
expect(step2).toHaveBeenCalledTimes(1);
expect(duration).toBeLessThan(200);
expect(result.data).toBe("Step 2 completed");
});
test("workflow with two concurrent cyclic steps", async () => {
const concurrentCyclicFlow = new Workflow<unknown, string, string>({
verbose: true,
});
class Step1Event extends WorkflowEvent<{
result: string;
}> {}
class Step2Event extends WorkflowEvent<{
result: string;
}> {}
let step2Count = 0;
const step1 = vi.fn(async (_context, ev: StartEvent | Step1Event) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return new Step1Event({ result: "Step 1 completed" });
});
const step2 = vi.fn(async (_context, ev: StartEvent | Step2Event) => {
await new Promise((resolve) => setTimeout(resolve, 100));
step2Count++;
if (step2Count >= 5) {
return new StopEvent("Step 2 completed 5 times");
}
return new Step2Event({ result: "Step 2 completed" });
});
concurrentCyclicFlow.addStep(
{
inputs: [WorkflowEvent.or(StartEvent<string>, Step1Event)],
outputs: [Step1Event],
},
step1,
);
concurrentCyclicFlow.addStep(
{
inputs: [WorkflowEvent.or(StartEvent<string>, Step2Event)],
outputs: [Step2Event, StopEvent],
},
step2,
);
const startTime = new Date();
const result = await concurrentCyclicFlow.run("start");
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
expect(step1).toHaveBeenCalledTimes(1);
expect(step2).toHaveBeenCalledTimes(5);
expect(duration).toBeGreaterThanOrEqual(500); // At least 5 * 100ms for step2
expect(duration).toBeLessThan(1000); // Less than 1 second
expect(result.data).toBe("Step 2 completed 5 times");
});
test("sendEvent", async () => {
const myWorkflow = new Workflow<unknown, string, string>({ verbose: true });
class QueryEvent extends WorkflowEvent<{ query: string }> {}
class QueryResultEvent extends WorkflowEvent<{ result: string }> {}
class PendingEvent extends WorkflowEvent<void> {}
myWorkflow.addStep(
{
inputs: [StartEvent],
outputs: [PendingEvent],
},
async (context: HandlerContext, events) => {
context.sendEvent(new QueryEvent({ query: "something" }));
return new PendingEvent();
},
);
myWorkflow.addStep(
{
inputs: [QueryEvent],
outputs: [QueryResultEvent],
},
async (context, event) => {
return new QueryResultEvent({ result: "query result" });
},
);
myWorkflow.addStep(
{
inputs: [PendingEvent, QueryResultEvent],
outputs: [StopEvent<string>],
},
async (context, ev0, ev1) => {
return new StopEvent(ev1.data.result);
},
);
const result = await myWorkflow.run("start");
expect(result.data).toBe("query result");
});
test("requireEvents - 2", async () => {
const myWorkflow = new Workflow<unknown, string, string>({ verbose: true });
class QueryEvent extends WorkflowEvent<{ query: string }> {}
class QueryResultEvent extends WorkflowEvent<{ result: string }> {}
myWorkflow.addStep(
{
inputs: [StartEvent],
outputs: [StopEvent<string>],
},
async (context: HandlerContext) => {
context.sendEvent(new QueryEvent({ query: "something" }));
const queryResultEvent = await context.requireEvent(QueryResultEvent);
return new StopEvent(queryResultEvent.data.result);
},
);
myWorkflow.addStep(
{
inputs: [QueryEvent],
outputs: [QueryResultEvent],
},
async (context, event) => {
await new Promise((resolve) => setTimeout(resolve, 100));
return new QueryResultEvent({ result: "query result" });
},
);
const result = await myWorkflow.run("start");
expect(result.data).toBe("query result");
});
test("allow output with send event", async () => {
const myFlow = new Workflow<unknown, string, string>({ verbose: true });
myFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [],
},
async (context, ev) => {
context.sendEvent(new StopEvent(`Hello ${ev.data}!`));
},
);
const result = myFlow.run("world");
expect((await result).data).toBe("Hello world!");
});
});
describe("workflow event loop", () => {
test("basic", async () => {
const jokeFlow = new Workflow<unknown, string, string>({ verbose: true });
jokeFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [StopEvent<string>],
},
async (_context, ev: StartEvent) => {
return new StopEvent(`Hello ${ev.data}!`);
},
);
const result = await jokeFlow.run("world");
expect(result.data).toBe("Hello world!");
});
test("branch", async () => {
const myFlow = new Workflow<unknown, string, string>({ verbose: true });
class BranchA1Event extends WorkflowEvent<{ payload: string }> {}
class BranchA2Event extends WorkflowEvent<{ payload: string }> {}
class BranchB1Event extends WorkflowEvent<{ payload: string }> {}
class BranchB2Event extends WorkflowEvent<{ payload: string }> {}
let control = false;
myFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [BranchA1Event, BranchB1Event],
},
async (_context, ev) => {
if (control) {
return new BranchA1Event({ payload: ev.data });
} else {
return new BranchB1Event({ payload: ev.data });
}
},
);
myFlow.addStep(
{
inputs: [BranchA1Event],
outputs: [BranchA2Event],
},
async (_context, ev) => {
return new BranchA2Event({ payload: ev.data.payload });
},
);
myFlow.addStep(
{
inputs: [BranchB1Event],
outputs: [BranchB2Event],
},
async (_context, ev) => {
return new BranchB2Event({ payload: ev.data.payload });
},
);
myFlow.addStep(
{
inputs: [BranchA2Event],
outputs: [StopEvent<string>],
},
async (_context, ev) => {
return new StopEvent(`Branch A2: ${ev.data.payload}`);
},
);
myFlow.addStep(
{
inputs: [BranchB2Event],
outputs: [StopEvent],
},
async (_context, ev) => {
return new StopEvent(`Branch B2: ${ev.data.payload}`);
},
);
{
const result = await myFlow.run("world");
expect(result.data).toMatch(/Branch B2: world/);
}
control = true;
{
const result = await myFlow.run("world");
expect(result.data).toMatch(/Branch A2: world/);
}
{
const context = myFlow.run("world");
for await (const event of context) {
if (event instanceof BranchA2Event) {
expect(event.data.payload).toBe("world");
}
if (event instanceof StopEvent) {
expect(event.data).toMatch(/Branch A2: world/);
}
}
}
});
test("one event have multiple outputs", async () => {
const myFlow = new Workflow<unknown, string, string>({ verbose: true });
class AEvent extends WorkflowEvent<{ payload: string }> {}
class BEvent extends WorkflowEvent<{ payload: string }> {}
class CEvent extends WorkflowEvent<{ payload: string }> {}
class DEvent extends WorkflowEvent<{ payload: string }> {}
myFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [StopEvent<string>],
},
async (_context, ev) => {
return new StopEvent("STOP");
},
);
const fn = vi.fn(async (_context, ev: StartEvent) => {
return new AEvent({ payload: ev.data });
});
myFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [AEvent],
},
fn,
);
myFlow.addStep(
{
inputs: [AEvent],
outputs: [BEvent, CEvent],
},
async (_context, ev: AEvent) => {
return new BEvent({ payload: ev.data.payload });
},
);
myFlow.addStep(
{
inputs: [AEvent],
outputs: [CEvent],
},
async (_context, ev: AEvent) => {
return new CEvent({ payload: ev.data.payload });
},
);
myFlow.addStep(
{
inputs: [BEvent],
outputs: [DEvent],
},
async (_context, ev: BEvent) => {
return new DEvent({ payload: ev.data.payload });
},
);
myFlow.addStep(
{
inputs: [CEvent],
outputs: [DEvent],
},
async (_context, ev: CEvent) => {
return new DEvent({ payload: ev.data.payload });
},
);
myFlow.addStep(
{
inputs: [DEvent],
outputs: [StopEvent<string>],
},
async (_context, ev: DEvent) => {
return new StopEvent(`Hello ${ev.data.payload}!`);
},
);
const result = await myFlow.run("world");
expect(result.data).toBe("STOP");
expect(fn).toHaveBeenCalledTimes(1);
// streaming events will allow to consume event even stop event is reached
const stream = myFlow.run("world");
for await (const _ of stream) {
/* empty */
}
expect(fn).toHaveBeenCalledTimes(2);
});
test("run with custom context", async () => {
type MyContext = { name: string };
const myFlow = new Workflow<MyContext, string, string>({ verbose: true });
myFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [StopEvent<string>],
},
async ({ data }, _: StartEvent) => {
return new StopEvent(`Hello ${data.name}!`);
},
);
const result = await myFlow.run("world", { name: "Alice" });
expect(result.data).toBe("Hello Alice!");
});
test("run with custom context with two streaming", async () => {
type MyContext = { name: string };
const myFlow = new Workflow<MyContext, string, string>({ verbose: true });
myFlow.addStep(
{
inputs: [StartEvent],
outputs: [StopEvent],
},
async ({ data }, _) => {
if (data == null) {
return new StopEvent({ result: "EMPTY" });
}
return new StopEvent({ result: `Hello ${data.name}!` });
},
);
const context1 = myFlow.run("world");
const context2 = context1.with({ name: "Alice" });
const context3 = context1.with({ name: "Bob" });
expect(await context1).toMatchInlineSnapshot(`
StopEvent {
"data": {
"result": "EMPTY",
},
"displayName": "StopEvent",
}
`);
expect(await context2).toMatchInlineSnapshot(`
StopEvent {
"data": {
"result": "Hello Alice!",
},
"displayName": "StopEvent",
}
`);
expect(await context3).toMatchInlineSnapshot(`
StopEvent {
"data": {
"result": "Hello Bob!",
},
"displayName": "StopEvent",
}
`);
});
test("workflow multiple output", async () => {
const myFlow = new Workflow<unknown, string, string>({ verbose: true });
myFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [StopEvent<string>, StopEvent<string>],
},
async (_context, ev) => {
return new StopEvent(`Hello ${ev.data}!`);
},
);
const result = await myFlow.run("world").strict();
expect(result.data).toBe("Hello world!");
});
});
describe("snapshot", async () => {
test("snapshot and recover", async () => {
const myFlow = new Workflow({ verbose: true });
myFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [StopEvent<string>],
},
async (_, ev: StartEvent) => {
return new StopEvent(`Hello ${ev.data}!`);
},
);
const context = myFlow.run("world");
const arrayBuffer = context.snapshot();
expect(arrayBuffer).toBeInstanceOf(ArrayBuffer);
const context2 = await myFlow.recover(arrayBuffer);
expect(context2.data).toBe("Hello world!");
});
test("snapshot in middle of workflow run ", async () => {
const myFlow = new Workflow<
{
value: number;
},
string,
string
>({ verbose: true });
class AEvent extends WorkflowEvent<{ payload: string }> {}
const fn = vi.fn(async (_, ev: StartEvent) => {
return new AEvent({ payload: ev.data });
});
myFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [AEvent],
},
fn,
);
myFlow.addStep(
{
inputs: [AEvent],
outputs: [StopEvent],
},
async ({ data }, _: AEvent) => {
return new StopEvent(`Hello ${data.value}!`);
},
);
const context = myFlow.run("world", {
value: 1,
});
for await (const event of context) {
if (event instanceof AEvent) {
expect(fn).toHaveBeenCalledTimes(1);
const arrayBuffer = context.snapshot();
expect(arrayBuffer).toBeInstanceOf(ArrayBuffer);
const context2 = await myFlow.recover(arrayBuffer).with({
value: 2,
});
expect(context2.data).toBe("Hello 2!");
break;
}
if (event instanceof StopEvent) {
expect(event.data).toBe("Hello 1!");
}
}
expect(fn).toHaveBeenCalledTimes(1);
});
});
describe("error", () => {
test("error in handler", async () => {
const myFlow = new Workflow<boolean, string, string>({ verbose: true });
myFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [StopEvent<string>],
},
async ({ data }) => {
if (!data) {
throw new Error("Something went wrong");
} else {
return new StopEvent(`Hello ${data}!`);
}
},
);
await expect(myFlow.run("world")).rejects.toThrow("Something went wrong");
{
const context = myFlow.run("world");
try {
for await (const _ of context) {
// do nothing
}
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe("Something went wrong");
const snapshot = context.snapshot();
const newContext = myFlow.recover(snapshot).with(true);
expect((await newContext).data).toBe("Hello true!");
}
}
});
test("recover in the middle of workflow", async () => {
const myFlow = new Workflow<string | undefined, string, string>({
verbose: true,
});
class AEvent extends WorkflowEvent<string> {}
myFlow.addStep(
{
inputs: [StartEvent<string>],
outputs: [AEvent],
},
async ({ data }) => {
if (data !== undefined) {
throw new Error("Something went wrong");
}
return new AEvent("world");
},
);
myFlow.addStep(
{
inputs: [AEvent],
outputs: [StopEvent],
},
async ({ data }, ev) => {
if (data === undefined) {
throw new Error("Something went wrong");
}
return new StopEvent(`Hello, ${data}!`);
},
);
// no context, so will throw error
const context = myFlow.run("world");
try {
for await (const _ of context) {
// do nothing
}
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe("Something went wrong");
const snapshot = context.snapshot();
const newContext = myFlow.recover(snapshot).with("Recovered Data");
expect((await newContext).data).toBe("Hello, Recovered Data!");
}
});
});
+164 -45
View File
@@ -67,7 +67,7 @@ importers:
version: 8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3)
vitest:
specifier: ^3.1.1
version: 3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(terser@5.39.0)
version: 3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(jiti@2.4.2)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
apps/next:
dependencies:
@@ -424,11 +424,11 @@ importers:
specifier: ^5.7.3
version: 5.7.3
vite:
specifier: ^5.4.16
version: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
specifier: ^6.3.3
version: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
vite-plugin-wasm:
specifier: ^3.3.0
version: 3.4.1(vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0))
specifier: ^3.4.1
version: 3.4.1(vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))
e2e/examples/nextjs-agent:
dependencies:
@@ -551,8 +551,8 @@ importers:
specifier: ^5.7.3
version: 5.7.3
vite:
specifier: ^5.4.16
version: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
specifier: ^6.3.3
version: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
e2e/examples/waku-query-engine:
dependencies:
@@ -1452,7 +1452,7 @@ importers:
version: link:../../openai
vitest:
specifier: ^3.0.9
version: 3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.39.0)
version: 3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(jiti@2.4.2)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
packages/providers/storage/firestore:
dependencies:
@@ -1979,6 +1979,9 @@ importers:
packages/workflow:
dependencies:
'@llama-flow/llamaindex':
specifier: ^0.0.12
version: 0.0.12(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)
zod:
specifier: ^3.23.8
version: 3.24.2
@@ -3861,9 +3864,35 @@ packages:
zod:
optional: true
'@llama-flow/core@0.3.9':
resolution: {integrity: sha512-/s0L16qo/hbF8Ahe5KEjJp6YfomIizp5lyhwgwQ0bomOijP3wX6L2xHnXucPRg5RGxgzpIIlDFW1sTQkjIamqg==}
peerDependencies:
'@modelcontextprotocol/sdk': ^1.7.0
hono: ^4.7.4
next: ^15.2.2
p-retry: ^6.2.1
rxjs: ^7.8.2
zod: ^3.24.2
peerDependenciesMeta:
'@modelcontextprotocol/sdk':
optional: true
hono:
optional: true
next:
optional: true
p-retry:
optional: true
rxjs:
optional: true
zod:
optional: true
'@llama-flow/docs@0.0.5':
resolution: {integrity: sha512-iAEqqWgPnJNxm4syNSxudDE1aHYNi1eTrxHg+FjcPeZhxyksLYRzpmzUikcZv0uhxgLwRinF7UPTnH9ioKlaEw==}
'@llama-flow/llamaindex@0.0.12':
resolution: {integrity: sha512-NoUCyVaZTHkdwzllcEY0mzqOkGFTemgpOBHg96fQx5ewZczsy+QpW09pJkMNsLaGQ6JJeBZ4mrBjMkgNUI57Mw==}
'@llamaindex/chat-ui@0.2.0':
resolution: {integrity: sha512-9U5+9l2UVBaOG8fSuMjnere5R2QSNxCEcixMwBgt4L4b0evo8jU4ZzlSxLPunWfpn1PWFVMUwKLlSSwa1qTTyA==}
peerDependencies:
@@ -8180,6 +8209,14 @@ packages:
picomatch:
optional: true
fdir@6.4.4:
resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fecha@4.2.3:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
@@ -12466,6 +12503,10 @@ packages:
resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==}
engines: {node: '>=12.0.0'}
tinyglobby@0.2.13:
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
engines: {node: '>=12.0.0'}
tinypool@1.0.2:
resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -13065,6 +13106,46 @@ packages:
yaml:
optional: true
vite@6.3.3:
resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
jiti: '>=1.21.0'
less: '*'
lightningcss: ^1.21.0
sass: '*'
sass-embedded: '*'
stylus: '*'
sugarss: '*'
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
vitest@2.1.0:
resolution: {integrity: sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -15961,8 +16042,27 @@ snapshots:
p-retry: 6.2.1
zod: 3.24.2
'@llama-flow/core@0.3.9(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)':
optionalDependencies:
'@modelcontextprotocol/sdk': 1.9.0
hono: 4.7.7
next: 15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
p-retry: 6.2.1
zod: 3.24.2
'@llama-flow/docs@0.0.5': {}
'@llama-flow/llamaindex@0.0.12(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)':
dependencies:
'@llama-flow/core': 0.3.9(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- hono
- next
- p-retry
- rxjs
- zod
'@llamaindex/chat-ui@0.2.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@llamaindex/pdf-viewer': 1.3.0(@types/react@19.0.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -18828,23 +18928,23 @@ snapshots:
msw: 2.7.4(@types/node@22.9.0)(typescript@5.8.3)
vite: 5.4.16(@types/node@22.9.0)(lightningcss@1.29.3)(terser@5.39.0)
'@vitest/mocker@3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0))':
'@vitest/mocker@3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))':
dependencies:
'@vitest/spy': 3.1.1
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
msw: 2.7.4(@types/node@22.14.1)(typescript@5.7.3)
vite: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
vite: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
'@vitest/mocker@3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0))':
'@vitest/mocker@3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))':
dependencies:
'@vitest/spy': 3.1.1
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
msw: 2.7.4(@types/node@22.14.1)(typescript@5.8.3)
vite: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
vite: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
'@vitest/pretty-format@2.1.0':
dependencies:
@@ -20810,7 +20910,7 @@ snapshots:
'@typescript-eslint/parser': 8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.22.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2))
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.7.0)(eslint@9.22.0(jiti@2.4.2))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.22.0(jiti@2.4.2))
eslint-plugin-react: 7.37.2(eslint@9.22.0(jiti@2.4.2))
@@ -20840,22 +20940,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.0
enhanced-resolve: 5.18.1
eslint: 9.22.0(jiti@2.4.2)
fast-glob: 3.3.3
get-tsconfig: 4.10.0
is-bun-module: 1.3.0
is-glob: 4.0.3
stable-hash: 0.0.4
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.7.0)(eslint@9.22.0(jiti@2.4.2))
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@9.16.0(jiti@2.4.2)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
@@ -20884,7 +20968,7 @@ snapshots:
is-glob: 4.0.3
stable-hash: 0.0.4
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@9.22.0(jiti@2.4.2))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.7.0)(eslint@9.22.0(jiti@2.4.2))
transitivePeerDependencies:
- supports-color
@@ -20910,14 +20994,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)):
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.22.0(jiti@2.4.2)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.22.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2))
eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@9.22.0(jiti@2.4.2))
transitivePeerDependencies:
- supports-color
@@ -20990,7 +21074,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.22.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2)))(eslint@9.22.0(jiti@2.4.2))
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.22.0(jiti@2.4.2))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -21452,6 +21536,10 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
fdir@6.4.4(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
fecha@4.2.3: {}
fetch-h2@3.0.2:
@@ -26994,6 +27082,11 @@ snapshots:
fdir: 6.4.3(picomatch@4.0.2)
picomatch: 4.0.2
tinyglobby@0.2.13:
dependencies:
fdir: 6.4.4(picomatch@4.0.2)
picomatch: 4.0.2
tinypool@1.0.2: {}
tinyrainbow@1.2.0: {}
@@ -27623,15 +27716,16 @@ snapshots:
- supports-color
- terser
vite-node@3.1.1(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0):
vite-node@3.1.1(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1):
dependencies:
cac: 6.7.14
debug: 4.4.0
es-module-lexer: 1.6.0
pathe: 2.0.3
vite: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
vite: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
@@ -27640,10 +27734,12 @@ snapshots:
- sugarss
- supports-color
- terser
- tsx
- yaml
vite-plugin-wasm@3.4.1(vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)):
vite-plugin-wasm@3.4.1(vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)):
dependencies:
vite: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
vite: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0):
dependencies:
@@ -27681,6 +27777,23 @@ snapshots:
tsx: 4.19.3
yaml: 2.7.1
vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1):
dependencies:
esbuild: 0.25.2
fdir: 6.4.4(picomatch@4.0.2)
picomatch: 4.0.2
postcss: 8.5.3
rollup: 4.38.0
tinyglobby: 0.2.13
optionalDependencies:
'@types/node': 22.14.1
fsevents: 2.3.3
jiti: 2.4.2
lightningcss: 1.29.3
terser: 5.39.0
tsx: 4.19.3
yaml: 2.7.1
vitest@2.1.0(@edge-runtime/vm@5.0.0)(@types/node@22.14.1)(happy-dom@17.4.4)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.39.0):
dependencies:
'@vitest/expect': 2.1.0
@@ -27865,10 +27978,10 @@ snapshots:
- supports-color
- terser
vitest@3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(terser@5.39.0):
vitest@3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(jiti@2.4.2)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1):
dependencies:
'@vitest/expect': 3.1.1
'@vitest/mocker': 3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0))
'@vitest/mocker': 3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.7.3))(vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))
'@vitest/pretty-format': 3.1.1
'@vitest/runner': 3.1.1
'@vitest/snapshot': 3.1.1
@@ -27884,8 +27997,8 @@ snapshots:
tinyexec: 0.3.2
tinypool: 1.0.2
tinyrainbow: 2.0.0
vite: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
vite-node: 3.1.1(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
vite: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
vite-node: 3.1.1(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@edge-runtime/vm': 5.0.0
@@ -27893,6 +28006,7 @@ snapshots:
'@types/node': 22.14.1
happy-dom: 17.4.4
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
@@ -27902,11 +28016,13 @@ snapshots:
- sugarss
- supports-color
- terser
- tsx
- yaml
vitest@3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.39.0):
vitest@3.1.1(@edge-runtime/vm@5.0.0)(@types/debug@4.1.12)(@types/node@22.14.1)(happy-dom@17.4.4)(jiti@2.4.2)(lightningcss@1.29.3)(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1):
dependencies:
'@vitest/expect': 3.1.1
'@vitest/mocker': 3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(vite@5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0))
'@vitest/mocker': 3.1.1(msw@2.7.4(@types/node@22.14.1)(typescript@5.8.3))(vite@6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))
'@vitest/pretty-format': 3.1.1
'@vitest/runner': 3.1.1
'@vitest/snapshot': 3.1.1
@@ -27922,8 +28038,8 @@ snapshots:
tinyexec: 0.3.2
tinypool: 1.0.2
tinyrainbow: 2.0.0
vite: 5.4.16(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
vite-node: 3.1.1(@types/node@22.14.1)(lightningcss@1.29.3)(terser@5.39.0)
vite: 6.3.3(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
vite-node: 3.1.1(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@edge-runtime/vm': 5.0.0
@@ -27931,6 +28047,7 @@ snapshots:
'@types/node': 22.14.1
happy-dom: 17.4.4
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
@@ -27940,6 +28057,8 @@ snapshots:
- sugarss
- supports-color
- terser
- tsx
- yaml
voyageai@0.0.3-1:
dependencies:
-98
View File
@@ -1,98 +0,0 @@
import { StartEvent, StopEvent, Workflow, WorkflowEvent } from "llamaindex";
import type { ReactNode } from "react";
import { describe, expect, test } from "vitest";
describe("workflow integration", () => {
type Context = {
pending: string[];
};
type Start = string;
type Stop = ReactNode;
test("nodejs", async () => {
const workflow = new Workflow<never, Start, Stop>({
wait: async () => await new Promise((resolve) => setTimeout(resolve, 0)),
});
workflow.addStep(
{
inputs: [StartEvent],
outputs: [StopEvent],
},
async (_, __) => {
await new Promise((resolve) => setTimeout(resolve, 100));
return new StopEvent("hello");
},
);
console.log("start");
const run = workflow.run("start");
await run.then((stop) => {
expect(stop.data).toBe("hello");
});
});
test("with jsx", async () => {
const workflow = new Workflow<never, Start, Stop>();
workflow.addStep(
{
inputs: [StartEvent],
outputs: [StopEvent],
},
async (_, __) => {
return new StopEvent(<div>Hey there!</div>);
},
);
const run = workflow.run("start");
const stop = await run;
expect(stop.data).toEqual(<div>Hey there!</div>);
});
test("with message channel", async () => {
const workflow = new Workflow<Context, Start, Stop>();
class AnalysisStartEvent extends WorkflowEvent<string> {}
class AnalysisStopEvent extends WorkflowEvent<string> {}
workflow.addStep(
{
inputs: [StartEvent],
outputs: [StopEvent],
},
async ({ data, sendEvent, requireEvent }) => {
data.pending.push("analyzing");
sendEvent(new AnalysisStartEvent("analysis my document"));
const event = await requireEvent(AnalysisStopEvent);
await new Promise((resolve) => setTimeout(resolve, 100));
data.pending.push("analysis complete");
return new StopEvent(event.data);
},
);
workflow.addStep(
{
inputs: [AnalysisStartEvent],
outputs: [AnalysisStopEvent],
},
async ({ data }) => {
data.pending.push("loading document");
await new Promise((resolve) => setTimeout(resolve, 100));
data.pending.push("document loaded");
return new AnalysisStopEvent("analysis complete");
},
);
const run = workflow.run("start").with({
pending: [],
});
await run;
expect(run.data.pending).toEqual([
"analyzing",
"loading document",
"document loaded",
"analysis complete",
]);
});
});