# llama-flow llama-flow 🌊 is a simple, lightweight workflow engine, in TypeScript. [![Bundle Size](https://img.shields.io/bundlephobia/min/@llama-flow/core)](https://bundlephobia.com/result?p=@llama-flow/core) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@llama-flow/core)](https://bundlephobia.com/result?p=@llama-flow/core) - Minimal core API (<=2kb) - 100% Type safe - Event-driven, stream oriented programming - Support multiple JS runtime/framework ## Usage ```shell npm i @llama-flow/core yarn add @llama-flow/core pnpm add @llama-flow/core deno add jsr:@llama-flow/core ``` ### First, define events ```ts import { workflowEvent } from "@llama-flow/core"; const startEvent = workflowEvent(); const stopEvent = workflowEvent<1 | -1>(); ``` ### Connect events with workflow ```ts import { createWorkflow } from "@llama-flow/core"; const convertEvent = workflowEvent(); const workflow = createWorkflow(); workflow.handle([startEvent], (start) => { return convertEvent.with(Number.parseInt(start.data, 10)); }); workflow.handle([convertEvent], (convert) => { return stopEvent.with(convert.data > 0 ? 1 : -1); }); ``` ### Trigger workflow ```ts import { pipeline } from "node:stream/promises"; const { stream, sendEvent } = workflow.createContext(); sendEvent(startEvent.with()); const result = await pipeline(stream, async function (source) { for await (const event of source) { if (stopEvent.include(event)) { return "stop received!"; } } }); console.log(result); // stop received! // or import { until } from "@llama-flow/core/stream/until"; import { collect } from "@llama-flow/core/stream/consumer"; const allEvents = await collect(until(stream, stopEvent)); ``` ### Fan-out (Parallelism) By default, we provide a simple fan-out utility to run multiple workflows in parallel - `getContext().sendEvent` will emit a new event to current workflow - `getContext().stream` will return a stream of events emitted by the sub-workflow ```ts import { until } from "@llama-flow/core/stream/until"; import { collect } from "@llama-flow/core/stream/consumer"; let condition = false; workflow.handle([startEvent], (start) => { const { sendEvent, stream } = getContext(); for (let i = 0; i < 10; i++) { sendEvent(convertEvent.with(i)); } // You define the condition to stop the workflow const results = collect( until(stream, () => condition).filter((ev) => convertStopEvent.includes(ev), ), ); console.log(results.length); // 10 return stopEvent.with(); }); workflow.handle([convertEvent], (convert) => { if (convert.data === 9) { condition = true; } return convertStopEvent.with(/* ... */); }); ``` ### With RxJS, or any stream API Workflow is event-driven, you can use any stream API to handle the workflow like `rxjs` ```ts import { from, pipe } from "rxjs"; const { stream, sendEvent } = workflow.createContext(); from(stream) .pipe(filter((ev) => eventSource(ev) === messageEvent)) .subscribe((ev) => { console.log(ev.data); }); sendEvent(fileParseWorkflow.startEvent(directory)); ``` ### Connect with Server endpoint Workflow can be used as middleware in any server framework, like `express`, `hono`, `fastify`, etc. ```ts import { Hono } from "hono"; import { serve } from "@hono/node-server"; import { createHonoHandler } from "@llama-flow/core/interrupter/hono"; import { agentWorkflow, startEvent, stopEvent, } from "../workflows/tool-call-agent.js"; const app = new Hono(); app.post( "/workflow", createHonoHandler( agentWorkflow, async (ctx) => startEvent(await ctx.req.text()), stopEvent, ), ); serve(app, ({ port }) => { console.log(`Server started at http://localhost:${port}`); }); ``` ### Error Handling You can use `signal` in `getContext` to handle error ```ts workflow.handle([convertEvent], () => { const { signal } = getContext(); signal.onabort = () => { console.error("error in convert event:", abort.reason); }; }); ``` ### Pitfall in **browser** You must call `getContext()` in the top level of the workflow, otherwise we will lose the async context of the workflow. ```ts workflow.handle([startEvent], async () => { const { stream } = getContext(); // ✅ this is ok await fetchData(); }); workflow.handle([startEvent], async () => { await fetchData(); const { stream } = getContext(); // ❌ this is not ok // we have no way to know this code was originally part of the workflow // w/o AsyncContext }); ``` Due to missing API of `async_hooks` in browser, we are looking for [Async Context](https://github.com/tc39/proposal-async-context) to solve this problem in the future. ## Middleware ### `withStore` Adding a `getStore()` method to the workflow context, which returns a store object, each store is linked to the workflow context. ```ts import { withStore } from "@llama-flow/core/middleware/store"; const workflow = withStore( () => ({ pendingTasks: new Set>(), }), createWorkflow(), ); workflow.handle([startEvent], () => { workflow.getStore().pendingTasks.add( new Promise((resolve) => { setTimeout(() => { resolve(); }, 100); }), ); }); const { getStore } = workflow.createContext(); ``` ### `withValidation` Make first parameter of `handler` to be `sendEvent` and its type safe and runtime safe when you create a workflow using `withValidation`. ```ts // before: workflow.handle([startEvent], (start) => {}); // after: workflow.handle([startEvent], (sendEvent, start) => {}); ``` ```ts import { withValidation } from "@llama-flow/core/middleware/validation"; const startEvent = workflowEvent(); const disallowedEvent = workflowEvent({ debugLabel: "disallowed", }); const parseEvent = workflowEvent(); const stopEvent = workflowEvent(); const workflow = withValidation(createWorkflow(), [ [[startEvent], [stopEvent]], [[startEvent], [parseEvent]], ]); workflow.strictHandle([startEvent], (sendEvent, start) => { sendEvent( disallowedEvent.with(), // <-- ❌ Type Check Failed, Runtime Error ); sendEvent(parseEvent.with("")); // <-- ✅ sendEvent(stopEvent.with(1)); // <-- ✅ }); ``` ### `withTraceEvents` Adds tracing capabilities to your workflow, allowing you to monitor/decorate handler and debug event flows easily. When enabled, it collects events based on the directed graph of the runtime and provide lifecycle hooks for each handler. ```ts import { withTraceEvents, runOnce, } from "@llama-flow/core/middleware/trace-events"; const workflow = withTraceEvents(createWorkflow()); workflow.handle( [messageEvent], runOnce(() => { console.log("This message handler will only run once"); }), ); workflow.handle([startEvent], () => { getContext().sendEvent(messageEvent.with()); getContext().sendEvent(messageEvent.with()); }); { const { sendEvent } = workflow.createContext(); sendEvent(startEvent.with()); sendEvent(messageEvent.with()); // This message handler will only run once! } { const { sendEvent } = workflow.createContext(); // For each new context, the decorator is isolated. sendEvent(startEvent.with()); sendEvent(messageEvent.with()); // This message handler will only run once! } ``` #### `workflow.substream(target, stream)` You can use `substream` to create a substream from the workflow context, which will only emit events that are emitted by the target event. ```ts const ev = startEvent.with(); const { sendEvent, stream } = workflow.createContext(); sendEvent(ev); sendEvent(messageEvent.with()); // <- this will not be included in the substream const substream = workflow.substream(ev, stream); ``` This is helpful when you have async requests, and you want to track the events that are emitted by the target event. For example: - Parallel requests
without substream ```ts workflow.handle([startEvent], async ({ data: uuid }) => { const { sendEvent, stream } = getContext(); const ev = networkRequestEvent.with(uuid); sendEvent(networkRequestEvent); // you need bypass uuid to all events to get the correct response const responses = await collect( filter(workflow.substream(ev, stream), (ev) => ev.data === uuid), ); }); sendEvent(startEvent.with(crypto.randomUUID())); sendEvent(startEvent.with(crypto.randomUUID())); ```
```ts workflow.handle([startEvent], async () => { const { sendEvent, stream } = getContext(); const ev = networkRequestEvent.with(); sendEvent(networkRequestEvent); const responses = await collect(workflow.substream(ev, stream)); }); sendEvent(startEvent.with()); sendEvent(startEvent.with()); ``` #### `createHandlerDecorator` You can create your own handler decorator to modify the behavior of the handler. ```ts import { createHandlerDecorator } from "@llama-flow/core/middleware/trace-events"; const noop: (...args: any[]) => void = function noop() {}; export const runOnce = createHandlerDecorator({ debugLabel: "onceHook", getInitialValue: () => false, onBeforeHandler: (handler, handlerContext, tracked) => tracked ? noop : handler, onAfterHandler: () => true, }); ``` #### `HandlerContext` The `HandlerContext` includes the runtime information of the handler in the directed graph of the workflow. ```ts type BaseHandlerContext = { // ... some other properties are hidden handler: Handler[], any>; inputEvents: WorkflowEvent[]; // events data that are accepted by the handler inputs: WorkflowEventData[]; // events data that are emitted by the handler outputs: WorkflowEventData[]; //#region linked list data structure prev: HandlerContext; next: Set; root: HandlerContext; //#endregion }; type SyncHandlerContext = BaseHandlerContext & { async: false; pending: null; }; type AsyncHandlerContext = BaseHandlerContext & { async: true; pending: Promise | void> | null; }; type HandlerContext = AsyncHandlerContext | SyncHandlerContext; ``` For example, when you send two `startEvent` events, and send `messageEvent` twice (once in the handler and once in the global), the `HandlerContext` from root to leaf is: ```ts let once = false; workflow.handle([startEvent], () => { const { sendEvent } = getContext(); if (once) { return; } once = true; sendEvent(messageEvent.with()); }); const { sendEvent } = workflow.createContext(); sendEvent(startEvent.with()); sendEvent(startEvent.with()); sendEvent(messageEvent.with()); ``` ``` rootHandlerContext(0) ├── startEventContext(0) │ └── messageEventContext(0) ├── startEventContext(1) └── messageEventContext(1) ``` You can use any directed graph library to visualize the directed graph of the workflow. # LICENSE MIT