mirror of
https://github.com/run-llama/workflows-ts.git
synced 2026-07-21 06:05:23 -04:00
feat: add withSnapshot API (#97)
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
---
|
||||
"demo": patch
|
||||
"@llama-flow/core": patch
|
||||
---
|
||||
|
||||
feat: add `withSnapshot` middleware API
|
||||
|
||||
Add snapshot API, for human in the loop feature. The API is designed for cross JavaScript platform, including node.js, browser, and serverless platform such as cloudflare worker and edge runtime
|
||||
|
||||
- `workflow.createContext(): Context`
|
||||
- `context.snapshot(): Promise<[requestEvent, snapshot]>`
|
||||
- `workflow.resume(data, snapshot)`
|
||||
@@ -18,6 +18,54 @@ app.post(
|
||||
),
|
||||
);
|
||||
|
||||
const serializableMemoryMap = new Map<string, any>();
|
||||
|
||||
app.post("/human-in-the-loop", async (ctx) => {
|
||||
const { workflow, stopEvent, startEvent, humanInteractionEvent } =
|
||||
await import("../workflows/human-in-the-loop");
|
||||
const json = await ctx.req.json();
|
||||
let context: ReturnType<typeof workflow.createContext>;
|
||||
if (json.requestId) {
|
||||
const data = json.data;
|
||||
const serializable = serializableMemoryMap.get(json.requestId);
|
||||
context = workflow.resume(data, serializable);
|
||||
} else {
|
||||
context = workflow.createContext();
|
||||
context.sendEvent(startEvent.with(json.data));
|
||||
}
|
||||
|
||||
const { onRequest, stream } = context;
|
||||
return new Promise<Response>((resolve) => {
|
||||
// listen to human interaction
|
||||
onRequest(humanInteractionEvent, async (reason) => {
|
||||
context.snapshot().then(([re, sd]) => {
|
||||
const requestId = crypto.randomUUID();
|
||||
serializableMemoryMap.set(requestId, sd);
|
||||
resolve(
|
||||
Response.json({
|
||||
requestId: requestId,
|
||||
reason: reason,
|
||||
data: re.map((r) =>
|
||||
r === humanInteractionEvent
|
||||
? "request human in the loop"
|
||||
: "UNKNOWN",
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// consume stream
|
||||
stream
|
||||
.until(stopEvent)
|
||||
.toArray()
|
||||
.then((events) => {
|
||||
const stopEvent = events.at(-1)!;
|
||||
resolve(Response.json(stopEvent.data));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
serve(app, ({ port }) => {
|
||||
console.log(`Server started at http://localhost:${port}`);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { input } from "@inquirer/prompts";
|
||||
import {
|
||||
workflow,
|
||||
stopEvent,
|
||||
startEvent,
|
||||
humanInteractionEvent,
|
||||
} from "../workflows/human-in-the-loop";
|
||||
|
||||
const name = await input({
|
||||
message: "What is your name?",
|
||||
});
|
||||
const { onRequest, stream, sendEvent } = workflow.createContext();
|
||||
|
||||
sendEvent(startEvent.with(name));
|
||||
|
||||
onRequest(humanInteractionEvent, async (reason) => {
|
||||
console.log("Requesting human interaction...");
|
||||
const name = await input({
|
||||
message: JSON.parse(reason).message,
|
||||
});
|
||||
console.log("Human interaction completed.");
|
||||
sendEvent(humanInteractionEvent.with(name));
|
||||
});
|
||||
|
||||
stream.on(stopEvent, ({ data }) => {
|
||||
console.log("AI analysis: ", data);
|
||||
});
|
||||
|
||||
await stream.until(stopEvent).toArray();
|
||||
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.14.1",
|
||||
"@inquirer/prompts": "^7.5.0",
|
||||
"@llama-flow/core": "latest",
|
||||
"@modelcontextprotocol/sdk": "^1.10.1",
|
||||
"hono": "^4.7.7",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { withSnapshot, request } from "@llama-flow/core/middleware/snapshot";
|
||||
import { createWorkflow, workflowEvent, getContext } from "@llama-flow/core";
|
||||
import { OpenAI } from "openai";
|
||||
|
||||
const openai = new OpenAI();
|
||||
|
||||
const workflow = withSnapshot(createWorkflow());
|
||||
|
||||
const startEvent = workflowEvent<string>({
|
||||
debugLabel: "start",
|
||||
});
|
||||
const humanInteractionEvent = workflowEvent<string>({
|
||||
debugLabel: "humanInteraction",
|
||||
});
|
||||
const stopEvent = workflowEvent<string>({
|
||||
debugLabel: "stop",
|
||||
});
|
||||
|
||||
workflow.handle([startEvent], async ({ data }) => {
|
||||
const response = await openai.chat.completions.create({
|
||||
stream: false,
|
||||
model: "gpt-4.1",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `You are a helpful assistant.
|
||||
If user doesn't provide his/her name, call ask_name tool to ask for user's name.
|
||||
Otherwise, analyze user's name with a good meaning and return the analysis.
|
||||
|
||||
For example, alex is from "Alexander the Great", who was a king of the ancient Greek kingdom of Macedon and one of history's greatest military minds.`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: data,
|
||||
},
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "ask_name",
|
||||
description: "Ask for user's name",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
message: {
|
||||
type: "string",
|
||||
description: "The message to ask for user's name",
|
||||
},
|
||||
},
|
||||
required: ["message"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const tools = response.choices[0].message.tool_calls;
|
||||
if (tools && tools.length > 0) {
|
||||
const askName = tools.find((tool) => tool.function.name === "ask_name");
|
||||
if (askName) {
|
||||
return request(humanInteractionEvent, askName.function.arguments);
|
||||
}
|
||||
}
|
||||
return stopEvent.with(response.choices[0].message.content!);
|
||||
});
|
||||
|
||||
workflow.handle([humanInteractionEvent], async ({ data }) => {
|
||||
const { sendEvent } = getContext();
|
||||
// going back to the start event
|
||||
sendEvent(startEvent.with(data));
|
||||
});
|
||||
|
||||
export { workflow, startEvent, humanInteractionEvent, stopEvent };
|
||||
@@ -68,6 +68,10 @@
|
||||
"types": "./middleware/validation.d.ts",
|
||||
"default": "./middleware/validation.js"
|
||||
},
|
||||
"./middleware/snapshot": {
|
||||
"types": "./middleware/snapshot.d.ts",
|
||||
"default": "./middleware/snapshot.js"
|
||||
},
|
||||
"./util/p-retry": {
|
||||
"types": "./util/p-retry.d.ts",
|
||||
"default": "./util/p-retry.js"
|
||||
|
||||
@@ -156,11 +156,11 @@ export const createContext = ({
|
||||
// return value is a special event
|
||||
if (isPromiseLike(result)) {
|
||||
(handlerContext as any).async = true;
|
||||
(handlerContext as any).pending = result;
|
||||
result.then((event) => {
|
||||
(handlerContext as any).pending = result.then((event) => {
|
||||
if (isEventData(event)) {
|
||||
workflowContext.sendEvent(event);
|
||||
}
|
||||
return event;
|
||||
});
|
||||
} else if (isEventData(result)) {
|
||||
workflowContext.sendEvent(result);
|
||||
@@ -196,44 +196,50 @@ export const createContext = ({
|
||||
};
|
||||
const createWorkflowContext = (
|
||||
handlerContext: HandlerContext,
|
||||
): WorkflowContext => ({
|
||||
get stream() {
|
||||
const subscribable = createSubscribable<
|
||||
[event: WorkflowEventData<any>],
|
||||
void
|
||||
>();
|
||||
rootWorkflowContext.__internal__call_send_event.subscribe(
|
||||
(newEvent: WorkflowEventData<any>) => {
|
||||
let currentEventContext = eventContextWeakMap.get(newEvent);
|
||||
while (currentEventContext) {
|
||||
if (currentEventContext === handlerContext) {
|
||||
subscribable.publish(newEvent);
|
||||
break;
|
||||
}
|
||||
currentEventContext = currentEventContext.prev;
|
||||
}
|
||||
},
|
||||
);
|
||||
return new WorkflowStream<WorkflowEventData<any>>(subscribable, null);
|
||||
},
|
||||
get signal() {
|
||||
return handlerContext.abortController.signal;
|
||||
},
|
||||
sendEvent: (...events) => {
|
||||
events.forEach((event) => {
|
||||
eventContextWeakMap.set(event, handlerContext);
|
||||
handlerContext.outputs.push(event);
|
||||
queue.push(event);
|
||||
rootWorkflowContext.__internal__call_send_event.publish(
|
||||
event,
|
||||
handlerContext,
|
||||
);
|
||||
queueUpdateCallback(handlerContext);
|
||||
});
|
||||
},
|
||||
__internal__call_context: createSubscribable(),
|
||||
__internal__call_send_event: createSubscribable(),
|
||||
});
|
||||
): WorkflowContext => {
|
||||
let lazyLoadStream: WorkflowStream | null = null;
|
||||
return {
|
||||
get stream() {
|
||||
if (!lazyLoadStream) {
|
||||
const subscribable = createSubscribable<
|
||||
[event: WorkflowEventData<any>],
|
||||
void
|
||||
>();
|
||||
rootWorkflowContext.__internal__call_send_event.subscribe(
|
||||
(newEvent: WorkflowEventData<any>) => {
|
||||
let currentEventContext = eventContextWeakMap.get(newEvent);
|
||||
while (currentEventContext) {
|
||||
if (currentEventContext === handlerContext) {
|
||||
subscribable.publish(newEvent);
|
||||
break;
|
||||
}
|
||||
currentEventContext = currentEventContext.prev;
|
||||
}
|
||||
},
|
||||
);
|
||||
lazyLoadStream = new WorkflowStream(subscribable, null);
|
||||
}
|
||||
return lazyLoadStream;
|
||||
},
|
||||
get signal() {
|
||||
return handlerContext.abortController.signal;
|
||||
},
|
||||
sendEvent: (...events) => {
|
||||
events.forEach((event) => {
|
||||
eventContextWeakMap.set(event, handlerContext);
|
||||
handlerContext.outputs.push(event);
|
||||
queue.push(event);
|
||||
rootWorkflowContext.__internal__call_send_event.publish(
|
||||
event,
|
||||
handlerContext,
|
||||
);
|
||||
queueUpdateCallback(handlerContext);
|
||||
});
|
||||
},
|
||||
__internal__call_context: createSubscribable(),
|
||||
__internal__call_send_event: createSubscribable(),
|
||||
};
|
||||
};
|
||||
|
||||
let rootAbortController = new AbortController();
|
||||
const handlerRootContext: HandlerContext = {
|
||||
|
||||
@@ -76,9 +76,9 @@ export class WorkflowStream<R = any>
|
||||
#stream: ReadableStream<R>;
|
||||
#subscribable: Subscribable<[data: R], void>;
|
||||
|
||||
on(
|
||||
event: WorkflowEvent<any>,
|
||||
handler: (event: WorkflowEventData<any>) => void,
|
||||
on<T>(
|
||||
event: WorkflowEvent<T>,
|
||||
handler: (event: WorkflowEventData<T>) => void,
|
||||
): () => void {
|
||||
return this.#subscribable.subscribe((ev) => {
|
||||
if (event.include(ev)) {
|
||||
|
||||
@@ -29,7 +29,7 @@ export function flattenEvents(
|
||||
|
||||
export type Subscribable<Args extends any[], R> = {
|
||||
subscribe: (callback: (...args: Args) => R) => () => void;
|
||||
publish: (...args: Args) => void;
|
||||
publish: (...args: Args) => unknown[];
|
||||
};
|
||||
|
||||
const __internal__subscribesSourcemap = new WeakMap<
|
||||
@@ -49,24 +49,30 @@ export function getSubscribers<Args extends any[], R>(
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function createSubscribable<Args extends any[], R>(): Subscribable<
|
||||
Args,
|
||||
R
|
||||
> {
|
||||
const subscribers = new Set<(...args: Args) => R>();
|
||||
export function createSubscribable<
|
||||
FnOrArgs extends ((...args: any[]) => any) | any[],
|
||||
R = unknown,
|
||||
>(): FnOrArgs extends (...args: any[]) => any
|
||||
? Subscribable<Parameters<FnOrArgs>, ReturnType<FnOrArgs>>
|
||||
: FnOrArgs extends any[]
|
||||
? Subscribable<FnOrArgs, R>
|
||||
: never {
|
||||
const subscribers = new Set<(...args: any) => any>();
|
||||
const obj = {
|
||||
subscribe: (callback: (...args: Args) => R) => {
|
||||
subscribe: (callback: (...args: any) => any) => {
|
||||
subscribers.add(callback);
|
||||
return () => {
|
||||
subscribers.delete(callback);
|
||||
};
|
||||
},
|
||||
publish: (...args: Args) => {
|
||||
publish: (...args: any) => {
|
||||
const results: unknown[] = [];
|
||||
for (const callback of subscribers) {
|
||||
callback(...args);
|
||||
results.push(callback(...args));
|
||||
}
|
||||
return results;
|
||||
},
|
||||
};
|
||||
__internal__subscribesSourcemap.set(obj, subscribers);
|
||||
return obj;
|
||||
return obj as any;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
import {
|
||||
eventSource,
|
||||
type Workflow as WorkflowCore,
|
||||
type WorkflowContext,
|
||||
type WorkflowEvent,
|
||||
workflowEvent,
|
||||
type WorkflowEventData,
|
||||
WorkflowStream,
|
||||
} from "@llama-flow/core";
|
||||
import type { HandlerContext } from "../core/context";
|
||||
import { createStableHash } from "./snapshot/stable-hash";
|
||||
import { createSubscribable, isPromiseLike } from "../core/utils";
|
||||
|
||||
/**
|
||||
* @internal We don't want to expose this special event to the user
|
||||
*/
|
||||
const snapshotEvent = workflowEvent<WorkflowEvent<any>>();
|
||||
const reasonWeakMap = new WeakMap<WorkflowEventData<any>, any>();
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export const request = <T>(
|
||||
event: WorkflowEvent<T>,
|
||||
reason?: any,
|
||||
): WorkflowEventData<WorkflowEvent<T>> => {
|
||||
const ev = snapshotEvent.with(event);
|
||||
reasonWeakMap.set(ev, reason);
|
||||
return ev;
|
||||
};
|
||||
|
||||
export type SnapshotFn = () => Promise<
|
||||
[requestEvents: WorkflowEvent<any>[], serializable: SnapshotData]
|
||||
>;
|
||||
|
||||
type SnapshotWorkflowContext<Workflow extends WorkflowCore> = ReturnType<
|
||||
Workflow["createContext"]
|
||||
> & {
|
||||
onRequest: <Event extends WorkflowEvent<any>>(
|
||||
event: Event,
|
||||
callback: (reason: any) => void | Promise<void>,
|
||||
) => () => void;
|
||||
/**
|
||||
* Snapshot will lock the context and wait for there is no pending event.
|
||||
*
|
||||
* This is useful when you want to take a current snapshot of the workflow
|
||||
*
|
||||
*/
|
||||
snapshot: SnapshotFn;
|
||||
};
|
||||
|
||||
type WithSnapshotWorkflow<Workflow extends WorkflowCore> = Omit<
|
||||
Workflow,
|
||||
"createContext"
|
||||
> & {
|
||||
createContext: () => SnapshotWorkflowContext<Workflow>;
|
||||
resume: (
|
||||
data: any[],
|
||||
serializable: Omit<SnapshotData, "unrecoverableQueue">,
|
||||
) => SnapshotWorkflowContext<Workflow>;
|
||||
};
|
||||
|
||||
interface SnapshotData {
|
||||
queue: [data: any, id: number][];
|
||||
/**
|
||||
* These events are not recoverable because they are not in any handler
|
||||
*
|
||||
* This is useful when you have `messageEvent` but you don't have any handler for it
|
||||
*/
|
||||
unrecoverableQueue: [data: any, id: number][];
|
||||
/**
|
||||
* This is the version of the snapshot
|
||||
*
|
||||
* Change any of the handlers will change the version
|
||||
*/
|
||||
version: string;
|
||||
missing: number[];
|
||||
}
|
||||
|
||||
type OnRequestFn<Event extends WorkflowEvent<any> = WorkflowEvent<any>> = (
|
||||
eventData: Event,
|
||||
reason: any,
|
||||
) => void | Promise<void>;
|
||||
|
||||
export function withSnapshot<Workflow extends WorkflowCore>(
|
||||
workflow: Workflow,
|
||||
): WithSnapshotWorkflow<Workflow> {
|
||||
const requests = createSubscribable<OnRequestFn>();
|
||||
const pendingRequestSetMap = new WeakMap<
|
||||
WorkflowContext,
|
||||
Set<PromiseLike<unknown>>
|
||||
>();
|
||||
const getPendingRequestSet = (context: WorkflowContext) => {
|
||||
if (!pendingRequestSetMap.has(context)) {
|
||||
pendingRequestSetMap.set(context, new Set());
|
||||
}
|
||||
return pendingRequestSetMap.get(context)!;
|
||||
};
|
||||
const stableHash = createStableHash();
|
||||
/**
|
||||
* This is to indicate the version of the snapshot
|
||||
*
|
||||
* It happens when you modify the workflow, all old snapshots should be invalidated
|
||||
*/
|
||||
const versionObj: [number[], Function][] = [];
|
||||
const getVersion = () => stableHash(versionObj);
|
||||
|
||||
const registeredEvents = new Set<WorkflowEvent<any>>();
|
||||
const isContextLockedWeakMap = new WeakMap<WorkflowContext, boolean>();
|
||||
const isContextLocked = (context: WorkflowContext): boolean => {
|
||||
return isContextLockedWeakMap.get(context) === true;
|
||||
};
|
||||
const isContextSnapshotReadyWeakSet = new WeakSet<WorkflowContext>();
|
||||
const isContextSnapshotReady = (context: WorkflowContext) => {
|
||||
return isContextSnapshotReadyWeakSet.has(context);
|
||||
};
|
||||
|
||||
const contextEventQueueWeakMap = new WeakMap<
|
||||
WorkflowContext,
|
||||
WorkflowEventData<any>[]
|
||||
>();
|
||||
const handlerContextSetWeakMap = new WeakMap<
|
||||
WorkflowContext,
|
||||
Set<HandlerContext>
|
||||
>();
|
||||
const collectedEventHandlerContextWeakMap = new WeakMap<
|
||||
WorkflowEventData<any>,
|
||||
Set<HandlerContext>
|
||||
>();
|
||||
|
||||
const createSnapshotFn = (context: WorkflowContext): SnapshotFn => {
|
||||
return async function snapshotHandler() {
|
||||
if (isContextLocked(context)) {
|
||||
throw new Error(
|
||||
"Context is already locked, you cannot snapshot a same context twice",
|
||||
);
|
||||
}
|
||||
isContextLockedWeakMap.set(context, true);
|
||||
|
||||
// 1. wait for all context is ready
|
||||
const handlerContexts = handlerContextSetWeakMap.get(context)!;
|
||||
|
||||
await Promise.all(
|
||||
[...handlerContexts]
|
||||
.filter((context) => context.async)
|
||||
.map((context) => context.pending),
|
||||
);
|
||||
// 2. collect all necessary data for a snapshot after lock
|
||||
const collectedEvents = contextEventQueueWeakMap.get(context)!;
|
||||
const requestEvents = collectedEvents
|
||||
.filter((event) => snapshotEvent.include(event))
|
||||
.map((event) => event.data);
|
||||
// there might have pending events in the queue, we need to collect them
|
||||
const queue = collectedEvents.filter(
|
||||
(event) => !snapshotEvent.include(event),
|
||||
);
|
||||
|
||||
// 3. serialize the data
|
||||
isContextSnapshotReadyWeakSet.add(context);
|
||||
|
||||
if (requestEvents.some((event) => !registeredEvents.has(event))) {
|
||||
console.warn("request event is not registered in the workflow");
|
||||
}
|
||||
|
||||
const serializable: SnapshotData = {
|
||||
queue: queue
|
||||
.filter((event) => eventCounterWeakMap.has(eventSource(event)!))
|
||||
.map((event) => [event.data, getEventCounter(eventSource(event)!)]),
|
||||
unrecoverableQueue: queue
|
||||
.filter((event) => !eventCounterWeakMap.has(eventSource(event)!))
|
||||
.map((event) => [event.data, getEventCounter(eventSource(event)!)]),
|
||||
version: getVersion(),
|
||||
missing: requestEvents
|
||||
// if you are request an event that is not in the handler, it's meaningless (from a logic perspective)
|
||||
.filter((event) => eventCounterWeakMap.has(event))
|
||||
.map((event) => getEventCounter(event)),
|
||||
};
|
||||
return [requestEvents, serializable];
|
||||
};
|
||||
};
|
||||
|
||||
let counter = 0;
|
||||
const eventCounterWeakMap = new WeakMap<WorkflowEvent<any>, number>();
|
||||
const counterEventMap = new Map<number, WorkflowEvent<any>>();
|
||||
const getEventCounter = (event: WorkflowEvent<any>) => {
|
||||
if (!eventCounterWeakMap.has(event)) {
|
||||
eventCounterWeakMap.set(event, counter++);
|
||||
}
|
||||
return eventCounterWeakMap.get(event)!;
|
||||
};
|
||||
const getCounterEvent = (counter: number) => {
|
||||
if (!counterEventMap.has(counter)) {
|
||||
throw new Error(`event counter ${counter} not found`);
|
||||
}
|
||||
return counterEventMap.get(counter)!;
|
||||
};
|
||||
|
||||
function initContext(context: WorkflowContext) {
|
||||
handlerContextSetWeakMap.set(context, new Set());
|
||||
contextEventQueueWeakMap.set(context, []);
|
||||
context.__internal__call_send_event.subscribe(
|
||||
(eventData, handlerContext) => {
|
||||
contextEventQueueWeakMap.get(context)!.push(eventData);
|
||||
if (isContextLocked(context)) {
|
||||
if (isContextSnapshotReady(context)) {
|
||||
console.warn(
|
||||
"snapshot is already ready, sendEvent after snapshot is not allowed",
|
||||
);
|
||||
}
|
||||
if (!collectedEventHandlerContextWeakMap.has(eventData)) {
|
||||
collectedEventHandlerContextWeakMap.set(eventData, new Set());
|
||||
}
|
||||
collectedEventHandlerContextWeakMap
|
||||
.get(eventData)!
|
||||
.add(handlerContext);
|
||||
}
|
||||
},
|
||||
);
|
||||
context.__internal__call_context.subscribe((handlerContext, next) => {
|
||||
if (isContextLocked(context)) {
|
||||
// replace it with noop, avoid calling the handler after snapshot
|
||||
handlerContext.handler = noop;
|
||||
next(handlerContext);
|
||||
} else {
|
||||
const queue = contextEventQueueWeakMap.get(context)!;
|
||||
handlerContext.inputs.forEach((input) => {
|
||||
queue.splice(queue.indexOf(input), 1);
|
||||
});
|
||||
const originalHandler = handlerContext.handler;
|
||||
const pendingRequests = getPendingRequestSet(context);
|
||||
const isPendingTask = pendingRequests.size !== 0;
|
||||
if (isPendingTask) {
|
||||
handlerContext.handler = async (...events) => {
|
||||
return Promise.all([...pendingRequests]).finally(() => {
|
||||
return originalHandler(...events);
|
||||
});
|
||||
};
|
||||
}
|
||||
handlerContextSetWeakMap.get(context)!.add(handlerContext);
|
||||
next(handlerContext);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...workflow,
|
||||
handle: (events: WorkflowEvent<any>[], handler: any) => {
|
||||
// version the snapshot based on the input events and function
|
||||
// I assume `uniqueId` is changeable
|
||||
versionObj.push([events.map(getEventCounter), handler]);
|
||||
|
||||
events.forEach((event) => {
|
||||
counterEventMap.set(getEventCounter(event), event);
|
||||
});
|
||||
|
||||
events.forEach((event) => {
|
||||
registeredEvents.add(event);
|
||||
});
|
||||
return workflow.handle(events, handler);
|
||||
},
|
||||
resume(
|
||||
data: any[],
|
||||
serializable: Omit<SnapshotData, "unrecoverableQueue">,
|
||||
): any {
|
||||
const events = data.map((d, i) =>
|
||||
getCounterEvent(serializable.missing[i]!).with(d),
|
||||
);
|
||||
const context = workflow.createContext();
|
||||
initContext(context);
|
||||
const stream = context.stream;
|
||||
context.sendEvent(
|
||||
...serializable.queue.map(([data, id]) => {
|
||||
const event = getCounterEvent(id);
|
||||
return event.with(data);
|
||||
}),
|
||||
);
|
||||
context.sendEvent(...events);
|
||||
|
||||
let lazyInitStream: WorkflowStream | null = null;
|
||||
const snapshotFn = createSnapshotFn(context);
|
||||
return {
|
||||
...context,
|
||||
snapshot: snapshotFn,
|
||||
onRequest: (
|
||||
event: WorkflowEvent<any>,
|
||||
callback: (reason: any) => void | Promise<void>,
|
||||
): (() => void) =>
|
||||
requests.subscribe((ev, reason) => {
|
||||
if (ev === event) {
|
||||
return callback(reason);
|
||||
}
|
||||
}),
|
||||
get stream() {
|
||||
if (!lazyInitStream) {
|
||||
lazyInitStream = stream.pipeThrough(
|
||||
new TransformStream({
|
||||
transform: (event, controller) => {
|
||||
if (snapshotEvent.include(event)) {
|
||||
const data = event.data;
|
||||
requests.publish(data, reasonWeakMap.get(event));
|
||||
} else {
|
||||
// ignore snapshot event from stream
|
||||
controller.enqueue(event);
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return lazyInitStream;
|
||||
},
|
||||
};
|
||||
},
|
||||
createContext(): any {
|
||||
const context = workflow.createContext();
|
||||
initContext(context);
|
||||
const stream = context.stream;
|
||||
let lazyInitStream: WorkflowStream | null = null;
|
||||
const snapshotFn = createSnapshotFn(context);
|
||||
return {
|
||||
...context,
|
||||
snapshot: snapshotFn,
|
||||
onRequest: (
|
||||
event: WorkflowEvent<any>,
|
||||
callback: (reason: any) => void | Promise<void>,
|
||||
): (() => void) =>
|
||||
requests.subscribe((ev, reason) => {
|
||||
if (ev === event) {
|
||||
return callback(reason);
|
||||
}
|
||||
}),
|
||||
get stream() {
|
||||
if (!lazyInitStream) {
|
||||
lazyInitStream = stream.pipeThrough(
|
||||
new TransformStream({
|
||||
transform: (event, controller) => {
|
||||
if (snapshotEvent.include(event)) {
|
||||
const data = event.data;
|
||||
const results = requests.publish(
|
||||
data,
|
||||
reasonWeakMap.get(event),
|
||||
);
|
||||
const pendingRequests = getPendingRequestSet(context);
|
||||
results.filter(isPromiseLike).forEach((promise) => {
|
||||
const task = promise.then(() => {
|
||||
pendingRequests.delete(task);
|
||||
});
|
||||
pendingRequests.add(task);
|
||||
});
|
||||
} else {
|
||||
// ignore snapshot event from stream
|
||||
controller.enqueue(event);
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return lazyInitStream;
|
||||
},
|
||||
};
|
||||
},
|
||||
} as unknown as WithSnapshotWorkflow<Workflow>;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Ref: https://github.com/shuding/stable-hash/blob/main/src/index.ts
|
||||
export function createStableHash() {
|
||||
// Use WeakMap to store the object-key mapping so the objects can still be
|
||||
// garbage collected. WeakMap uses a hashtable under the hood, so the lookup
|
||||
// complexity is almost O(1).
|
||||
const table = new WeakMap<object, string>();
|
||||
|
||||
// A counter of the key.
|
||||
let counter = 0;
|
||||
|
||||
// A stable hash implementation that supports:
|
||||
// - Fast and ensures unique hash properties
|
||||
// - Handles unserializable values
|
||||
// - Handles object key ordering
|
||||
// - Generates short results
|
||||
//
|
||||
// This is not a serialization function, and the result is not guaranteed to be
|
||||
// parsable.
|
||||
return function stableHash(arg: any): string {
|
||||
const type = typeof arg;
|
||||
const constructor = arg && arg.constructor;
|
||||
const isDate = constructor == Date;
|
||||
|
||||
if (Object(arg) === arg && !isDate && constructor != RegExp) {
|
||||
// Object/function, not null/date/regexp. Use WeakMap to store the id first.
|
||||
// If it's already hashed, directly return the result.
|
||||
let result = table.get(arg);
|
||||
if (result) return result;
|
||||
// Store the hash first for circular reference detection before entering the
|
||||
// recursive `stableHash` calls.
|
||||
// For other objects like set and map, we use this id directly as the hash.
|
||||
result = ++counter + "~";
|
||||
table.set(arg, result);
|
||||
let index: any;
|
||||
|
||||
if (constructor == Array) {
|
||||
// Array.
|
||||
result = "@";
|
||||
for (index = 0; index < arg.length; index++) {
|
||||
result += stableHash(arg[index]) + ",";
|
||||
}
|
||||
table.set(arg, result);
|
||||
} else if (constructor == Object) {
|
||||
// Object, sort keys.
|
||||
result = "#";
|
||||
const keys = Object.keys(arg).sort();
|
||||
while ((index = keys.pop() as string) !== undefined) {
|
||||
if (arg[index] !== undefined) {
|
||||
result += index + ":" + stableHash(arg[index]) + ",";
|
||||
}
|
||||
}
|
||||
table.set(arg, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (isDate) return arg.toJSON();
|
||||
if (type == "symbol") return arg.toString();
|
||||
return type == "string" ? JSON.stringify(arg) : "" + arg;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { withSnapshot, request } from "@llama-flow/core/middleware/snapshot";
|
||||
import {
|
||||
createWorkflow,
|
||||
eventSource,
|
||||
getContext,
|
||||
workflowEvent,
|
||||
} from "@llama-flow/core";
|
||||
|
||||
const startEvent = workflowEvent({
|
||||
debugLabel: "start",
|
||||
});
|
||||
const messageEvent = workflowEvent<string>({
|
||||
debugLabel: "message",
|
||||
});
|
||||
const humanResponseEvent = workflowEvent<string>({
|
||||
debugLabel: "human-response",
|
||||
});
|
||||
const stopEvent = workflowEvent({
|
||||
debugLabel: "stop",
|
||||
});
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
describe("with snapshot - snapshot API", () => {
|
||||
test("single handler (sync)", async () => {
|
||||
const workflow = withSnapshot(createWorkflow());
|
||||
workflow.handle([startEvent], () => {
|
||||
return request(humanResponseEvent);
|
||||
});
|
||||
|
||||
workflow.handle([humanResponseEvent], ({ data }) => {
|
||||
expect(data).toBe("hello world");
|
||||
return stopEvent.with();
|
||||
});
|
||||
|
||||
const { sendEvent, snapshot } = workflow.createContext();
|
||||
sendEvent(startEvent.with());
|
||||
const [req, sd] = await snapshot();
|
||||
expect(req.length).toBe(1);
|
||||
expect(req[0]!).toBe(humanResponseEvent);
|
||||
expect(sd).toMatchInlineSnapshot(`
|
||||
{
|
||||
"missing": [
|
||||
1,
|
||||
],
|
||||
"queue": [],
|
||||
"unrecoverableQueue": [],
|
||||
"version": "@@@0,,4~,,@@1,,7~,,",
|
||||
}
|
||||
`);
|
||||
|
||||
// recover
|
||||
const { stream } = workflow.resume(["hello world"], sd);
|
||||
const events = await stream.until(stopEvent).toArray();
|
||||
expect(events.length).toBe(2);
|
||||
expect(events.map(eventSource)).toEqual([humanResponseEvent, stopEvent]);
|
||||
});
|
||||
|
||||
test("single handler (async)", async () => {
|
||||
const workflow = withSnapshot(createWorkflow());
|
||||
workflow.handle([startEvent], async () => {
|
||||
return request(humanResponseEvent);
|
||||
});
|
||||
|
||||
workflow.handle([humanResponseEvent], ({ data }) => {
|
||||
expect(data).toBe("hello world");
|
||||
return stopEvent.with();
|
||||
});
|
||||
|
||||
const { sendEvent, snapshot } = workflow.createContext();
|
||||
sendEvent(startEvent.with());
|
||||
const [req, sd] = await snapshot();
|
||||
expect(req.length).toBe(1);
|
||||
expect(req[0]!).toBe(humanResponseEvent);
|
||||
expect(sd).toMatchInlineSnapshot(`
|
||||
{
|
||||
"missing": [
|
||||
1,
|
||||
],
|
||||
"queue": [],
|
||||
"unrecoverableQueue": [],
|
||||
"version": "@@@0,,4~,,@@1,,7~,,",
|
||||
}
|
||||
`);
|
||||
|
||||
// recover
|
||||
const { stream } = workflow.resume(["hello world"], sd);
|
||||
const events = await stream.until(stopEvent).toArray();
|
||||
expect(events.length).toBe(2);
|
||||
expect(events.map(eventSource)).toEqual([humanResponseEvent, stopEvent]);
|
||||
});
|
||||
|
||||
test("single handler (timer)", async () => {
|
||||
const workflow = withSnapshot(createWorkflow());
|
||||
workflow.handle([startEvent], async () => {
|
||||
await sleep(10);
|
||||
return request(humanResponseEvent);
|
||||
});
|
||||
|
||||
workflow.handle([humanResponseEvent], ({ data }) => {
|
||||
expect(data).toBe("hello world");
|
||||
return stopEvent.with();
|
||||
});
|
||||
|
||||
const { sendEvent, snapshot } = workflow.createContext();
|
||||
sendEvent(startEvent.with());
|
||||
const [req, sd] = await snapshot();
|
||||
expect(req.length).toBe(1);
|
||||
expect(req[0]!).toBe(humanResponseEvent);
|
||||
expect(sd).toMatchInlineSnapshot(`
|
||||
{
|
||||
"missing": [
|
||||
1,
|
||||
],
|
||||
"queue": [],
|
||||
"unrecoverableQueue": [],
|
||||
"version": "@@@0,,4~,,@@1,,7~,,",
|
||||
}
|
||||
`);
|
||||
|
||||
// recover
|
||||
const { stream } = workflow.resume(["hello world"], sd);
|
||||
const events = await stream.until(stopEvent).toArray();
|
||||
expect(events.length).toBe(2);
|
||||
expect(events.map(eventSource)).toEqual([humanResponseEvent, stopEvent]);
|
||||
});
|
||||
|
||||
test("multiple message in the queue", async () => {
|
||||
const workflow = withSnapshot(createWorkflow());
|
||||
workflow.handle([startEvent], async () => {
|
||||
const { sendEvent } = getContext();
|
||||
await sleep(10);
|
||||
sendEvent(messageEvent.with("1"));
|
||||
sendEvent(messageEvent.with("2"));
|
||||
return request(humanResponseEvent);
|
||||
});
|
||||
|
||||
workflow.handle([humanResponseEvent], ({ data }) => {
|
||||
expect(data).toBe("hello world");
|
||||
return stopEvent.with();
|
||||
});
|
||||
|
||||
const { sendEvent, snapshot } = workflow.createContext();
|
||||
sendEvent(startEvent.with());
|
||||
const [req, sd] = await snapshot();
|
||||
expect(req.length).toBe(1);
|
||||
expect(req[0]!).toBe(humanResponseEvent);
|
||||
// messageEvent is not in the queue, because it's not in any handler
|
||||
expect(sd).toMatchInlineSnapshot(`
|
||||
{
|
||||
"missing": [
|
||||
1,
|
||||
],
|
||||
"queue": [],
|
||||
"unrecoverableQueue": [
|
||||
[
|
||||
"1",
|
||||
2,
|
||||
],
|
||||
[
|
||||
"2",
|
||||
2,
|
||||
],
|
||||
],
|
||||
"version": "@@@0,,4~,,@@1,,7~,,",
|
||||
}
|
||||
`);
|
||||
|
||||
// recover
|
||||
const { stream } = workflow.resume(["hello world"], sd);
|
||||
const events = await stream.until(stopEvent).toArray();
|
||||
expect(events.length).toBe(2);
|
||||
expect(events.map(eventSource)).toEqual([humanResponseEvent, stopEvent]);
|
||||
});
|
||||
|
||||
test("multiple requests in the response", async () => {
|
||||
const workflow = withSnapshot(createWorkflow());
|
||||
workflow.handle([startEvent], async () => {
|
||||
return request(humanResponseEvent);
|
||||
});
|
||||
workflow.handle([startEvent], async () => {
|
||||
await sleep(10);
|
||||
return request(humanResponseEvent);
|
||||
});
|
||||
|
||||
workflow.handle([humanResponseEvent], ({ data }) => {
|
||||
expect(data).toBe("hello world");
|
||||
return stopEvent.with();
|
||||
});
|
||||
|
||||
const { sendEvent, snapshot } = workflow.createContext();
|
||||
sendEvent(startEvent.with());
|
||||
const [req, sd] = await snapshot();
|
||||
expect(req.length).toBe(2);
|
||||
expect(req[0]!).toBe(humanResponseEvent);
|
||||
expect(sd).toMatchInlineSnapshot(`
|
||||
{
|
||||
"missing": [
|
||||
1,
|
||||
1,
|
||||
],
|
||||
"queue": [],
|
||||
"unrecoverableQueue": [],
|
||||
"version": "@@@0,,4~,,@@0,,7~,,@@1,,10~,,",
|
||||
}
|
||||
`);
|
||||
// recover
|
||||
const { stream } = workflow.resume(["hello world"], sd);
|
||||
const events = await stream.until(stopEvent).toArray();
|
||||
expect(events.length).toBe(2);
|
||||
expect(events.map(eventSource)).toEqual([humanResponseEvent, stopEvent]);
|
||||
});
|
||||
|
||||
test("cannot handle setTimeout without ", async () => {
|
||||
const warn = vi.fn();
|
||||
vi.stubGlobal("console", {
|
||||
warn,
|
||||
});
|
||||
const workflow = withSnapshot(createWorkflow());
|
||||
workflow.handle([startEvent], async () => {
|
||||
const { sendEvent } = getContext();
|
||||
setTimeout(() => {
|
||||
sendEvent(request(humanResponseEvent));
|
||||
}, 100);
|
||||
});
|
||||
|
||||
const { sendEvent, snapshot } = workflow.createContext();
|
||||
sendEvent(startEvent.with());
|
||||
const [req, se] = await snapshot();
|
||||
expect(req.length).toBe(0);
|
||||
expect(se).toMatchInlineSnapshot(`
|
||||
{
|
||||
"missing": [],
|
||||
"queue": [],
|
||||
"unrecoverableQueue": [],
|
||||
"version": "@@@0,,4~,,",
|
||||
}
|
||||
`);
|
||||
await sleep(100);
|
||||
expect(warn.mock.calls.length).toBe(1);
|
||||
expect(warn.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
[
|
||||
"snapshot is already ready, sendEvent after snapshot is not allowed",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("onRequestEvent callback", async () => {
|
||||
const workflow = withSnapshot(createWorkflow());
|
||||
workflow.handle([startEvent], async () => {
|
||||
return request(humanResponseEvent, 1);
|
||||
});
|
||||
|
||||
workflow.handle([humanResponseEvent], ({ data }) => {
|
||||
expect(data).toBe("hello world");
|
||||
return stopEvent.with();
|
||||
});
|
||||
|
||||
const { sendEvent, stream, onRequest } = workflow.createContext();
|
||||
sendEvent(startEvent.with());
|
||||
|
||||
const onRequestCallback = vi.fn((reason) => {
|
||||
expect(reason).toBe(1);
|
||||
sendEvent(humanResponseEvent.with("hello world"));
|
||||
});
|
||||
onRequest(humanResponseEvent, onRequestCallback);
|
||||
|
||||
expect(onRequestCallback).toBeCalledTimes(0);
|
||||
const events = await stream.until(stopEvent).toArray();
|
||||
expect(onRequestCallback).toBeCalledTimes(1);
|
||||
expect(events.length).toBe(3);
|
||||
expect(events.map(eventSource)).toEqual([
|
||||
startEvent,
|
||||
humanResponseEvent,
|
||||
stopEvent,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -11,9 +11,9 @@ export default defineConfig({
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
exclude: ["**/dist/**", "**/lib/**"],
|
||||
name: "DOM",
|
||||
environment: "happy-dom",
|
||||
exclude: ["**/lib/**", "**/dist/**", "**/node_modules/**"],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -23,9 +23,9 @@ export default defineConfig({
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
exclude: ["**/dist/**", "**/lib/**"],
|
||||
name: "Node.js",
|
||||
environment: "node",
|
||||
exclude: ["**/lib/**", "**/dist/**", "**/node_modules/**"],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -35,9 +35,9 @@ export default defineConfig({
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
exclude: ["**/dist/**", "**/lib/**"],
|
||||
name: "Edge Runtime",
|
||||
environment: "edge-runtime",
|
||||
exclude: ["**/lib/**", "**/dist/**", "**/node_modules/**"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
Generated
+282
@@ -44,6 +44,9 @@ importers:
|
||||
'@hono/node-server':
|
||||
specifier: ^1.14.1
|
||||
version: 1.14.1(hono@4.7.7)
|
||||
'@inquirer/prompts':
|
||||
specifier: ^7.5.0
|
||||
version: 7.5.0(@types/node@22.14.1)
|
||||
'@llama-flow/core':
|
||||
specifier: latest
|
||||
version: link:../packages/core
|
||||
@@ -863,6 +866,127 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@inquirer/checkbox@4.1.5':
|
||||
resolution: {integrity: sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/confirm@5.1.9':
|
||||
resolution: {integrity: sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@10.1.10':
|
||||
resolution: {integrity: sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/editor@4.2.10':
|
||||
resolution: {integrity: sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/expand@4.0.12':
|
||||
resolution: {integrity: sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/figures@1.0.11':
|
||||
resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@inquirer/input@4.1.9':
|
||||
resolution: {integrity: sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/number@3.0.12':
|
||||
resolution: {integrity: sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/password@4.0.12':
|
||||
resolution: {integrity: sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/prompts@7.5.0':
|
||||
resolution: {integrity: sha512-tk8Bx7l5AX/CR0sVfGj3Xg6v7cYlFBkEahH+EgBB+cZib6Fc83dwerTbzj7f2+qKckjIUGsviWRI1d7lx6nqQA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/rawlist@4.1.0':
|
||||
resolution: {integrity: sha512-6ob45Oh9pXmfprKqUiEeMz/tjtVTFQTgDDz1xAMKMrIvyrYjAmRbQZjMJfsictlL4phgjLhdLu27IkHNnNjB7g==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/search@3.0.12':
|
||||
resolution: {integrity: sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/select@4.2.0':
|
||||
resolution: {integrity: sha512-KkXQ4aSySWimpV4V/TUJWdB3tdfENZUU765GjOIZ0uPwdbGIG6jrxD4dDf1w68uP+DVtfNhr1A92B+0mbTZ8FA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/type@3.0.6':
|
||||
resolution: {integrity: sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1563,6 +1687,10 @@ packages:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ansi-escapes@4.3.2:
|
||||
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-escapes@7.0.0:
|
||||
resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1737,6 +1865,10 @@ packages:
|
||||
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
cli-width@4.1.0:
|
||||
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
@@ -2587,6 +2719,10 @@ packages:
|
||||
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||
hasBin: true
|
||||
|
||||
mute-stream@2.0.0:
|
||||
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -3369,6 +3505,10 @@ packages:
|
||||
resolution: {integrity: sha512-PvSRruOsitjy6qdqwIIyolv99+fEn57gP6gn4zhsHTEcCYgXPhv6BAxzAjleS8XKpo+Y582vTTA9nuqYDmbRuA==}
|
||||
hasBin: true
|
||||
|
||||
type-fest@0.21.3:
|
||||
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
type-is@2.0.1:
|
||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -3574,6 +3714,10 @@ packages:
|
||||
'@cloudflare/workers-types':
|
||||
optional: true
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3624,6 +3768,10 @@ packages:
|
||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yoctocolors-cjs@2.1.2:
|
||||
resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
youch@3.3.4:
|
||||
resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==}
|
||||
|
||||
@@ -4227,6 +4375,122 @@ snapshots:
|
||||
'@img/sharp-win32-x64@0.34.1':
|
||||
optional: true
|
||||
|
||||
'@inquirer/checkbox@4.1.5(@types/node@22.14.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.10(@types/node@22.14.1)
|
||||
'@inquirer/figures': 1.0.11
|
||||
'@inquirer/type': 3.0.6(@types/node@22.14.1)
|
||||
ansi-escapes: 4.3.2
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@inquirer/confirm@5.1.9(@types/node@22.14.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.10(@types/node@22.14.1)
|
||||
'@inquirer/type': 3.0.6(@types/node@22.14.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@inquirer/core@10.1.10(@types/node@22.14.1)':
|
||||
dependencies:
|
||||
'@inquirer/figures': 1.0.11
|
||||
'@inquirer/type': 3.0.6(@types/node@22.14.1)
|
||||
ansi-escapes: 4.3.2
|
||||
cli-width: 4.1.0
|
||||
mute-stream: 2.0.0
|
||||
signal-exit: 4.1.0
|
||||
wrap-ansi: 6.2.0
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@inquirer/editor@4.2.10(@types/node@22.14.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.10(@types/node@22.14.1)
|
||||
'@inquirer/type': 3.0.6(@types/node@22.14.1)
|
||||
external-editor: 3.1.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@inquirer/expand@4.0.12(@types/node@22.14.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.10(@types/node@22.14.1)
|
||||
'@inquirer/type': 3.0.6(@types/node@22.14.1)
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@inquirer/figures@1.0.11': {}
|
||||
|
||||
'@inquirer/input@4.1.9(@types/node@22.14.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.10(@types/node@22.14.1)
|
||||
'@inquirer/type': 3.0.6(@types/node@22.14.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@inquirer/number@3.0.12(@types/node@22.14.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.10(@types/node@22.14.1)
|
||||
'@inquirer/type': 3.0.6(@types/node@22.14.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@inquirer/password@4.0.12(@types/node@22.14.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.10(@types/node@22.14.1)
|
||||
'@inquirer/type': 3.0.6(@types/node@22.14.1)
|
||||
ansi-escapes: 4.3.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@inquirer/prompts@7.5.0(@types/node@22.14.1)':
|
||||
dependencies:
|
||||
'@inquirer/checkbox': 4.1.5(@types/node@22.14.1)
|
||||
'@inquirer/confirm': 5.1.9(@types/node@22.14.1)
|
||||
'@inquirer/editor': 4.2.10(@types/node@22.14.1)
|
||||
'@inquirer/expand': 4.0.12(@types/node@22.14.1)
|
||||
'@inquirer/input': 4.1.9(@types/node@22.14.1)
|
||||
'@inquirer/number': 3.0.12(@types/node@22.14.1)
|
||||
'@inquirer/password': 4.0.12(@types/node@22.14.1)
|
||||
'@inquirer/rawlist': 4.1.0(@types/node@22.14.1)
|
||||
'@inquirer/search': 3.0.12(@types/node@22.14.1)
|
||||
'@inquirer/select': 4.2.0(@types/node@22.14.1)
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@inquirer/rawlist@4.1.0(@types/node@22.14.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.10(@types/node@22.14.1)
|
||||
'@inquirer/type': 3.0.6(@types/node@22.14.1)
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@inquirer/search@3.0.12(@types/node@22.14.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.10(@types/node@22.14.1)
|
||||
'@inquirer/figures': 1.0.11
|
||||
'@inquirer/type': 3.0.6(@types/node@22.14.1)
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@inquirer/select@4.2.0(@types/node@22.14.1)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.10(@types/node@22.14.1)
|
||||
'@inquirer/figures': 1.0.11
|
||||
'@inquirer/type': 3.0.6(@types/node@22.14.1)
|
||||
ansi-escapes: 4.3.2
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@inquirer/type@3.0.6(@types/node@22.14.1)':
|
||||
optionalDependencies:
|
||||
'@types/node': 22.14.1
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
@@ -4879,6 +5143,10 @@ snapshots:
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-escapes@4.3.2:
|
||||
dependencies:
|
||||
type-fest: 0.21.3
|
||||
|
||||
ansi-escapes@7.0.0:
|
||||
dependencies:
|
||||
environment: 1.1.0
|
||||
@@ -5062,6 +5330,8 @@ snapshots:
|
||||
slice-ansi: 5.0.0
|
||||
string-width: 7.2.0
|
||||
|
||||
cli-width@4.1.0: {}
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
cliui@8.0.1:
|
||||
@@ -5850,6 +6120,8 @@ snapshots:
|
||||
|
||||
mustache@4.2.0: {}
|
||||
|
||||
mute-stream@2.0.0: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
@@ -6642,6 +6914,8 @@ snapshots:
|
||||
turbo-windows-64: 2.5.0
|
||||
turbo-windows-arm64: 2.5.0
|
||||
|
||||
type-fest@0.21.3: {}
|
||||
|
||||
type-is@2.0.1:
|
||||
dependencies:
|
||||
content-type: 1.0.5
|
||||
@@ -6895,6 +7169,12 @@ snapshots:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -6937,6 +7217,8 @@ snapshots:
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
yoctocolors-cjs@2.1.2: {}
|
||||
|
||||
youch@3.3.4:
|
||||
dependencies:
|
||||
cookie: 0.7.2
|
||||
|
||||
Reference in New Issue
Block a user