weifuwu 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,7 @@ Features like `tsx()`, WebSocket, GraphQL, and AI streaming all follow the same
18
18
  - **WebSocket** — `router.ws()` with upgrade middleware (auth before connect)
19
19
  - **GraphQL** — `router.graphql()` with GraphiQL IDE
20
20
  - **AI streaming** — `router.ai()` via Vercel AI SDK
21
+ - **AI workflows** — `router.workflow()` — intent-to-execution pipelines with `tool()` + SSE
21
22
  - **Static files** — `serveStatic()` with ETag, 304, MIME, directory index
22
23
  - **Request validation** — `validate()` with Zod (body / query / params / headers)
23
24
  - **File upload** — `upload()` multipart parser with disk save, size & type limits
@@ -430,6 +431,35 @@ const app = new Router()
430
431
  serve(app.handler(), { port: 3000 })
431
432
  ```
432
433
 
434
+ ## Workflow
435
+
436
+ Define tools (business capabilities) and let AI generate and execute multi-step workflows.
437
+
438
+ ```ts
439
+ import { Router, tool, createWorkflowEngine } from 'weifuwu'
440
+ import { z } from 'zod'
441
+
442
+ const tools = {
443
+ queryUser: tool({
444
+ description: '查询用户信息,返回 email, name',
445
+ inputSchema: z.object({ userId: z.string() }),
446
+ execute: async ({ userId }) => ({ id: userId, email: 'user@test.com', name: 'Test' }),
447
+ }),
448
+ }
449
+
450
+ const app = new Router()
451
+
452
+ // Router method — accepts { nodes } or { goal } with model
453
+ app.workflow('/api/agent', { tools })
454
+
455
+ // Or manual execution
456
+ const engine = createWorkflowEngine({ tools })
457
+ const result = await engine.execute({ nodes: [
458
+ { id: 's1', tool: 'set', input: { name: 'msg', value: 'hello' } },
459
+ { id: 'g1', tool: 'get', input: { name: 'msg' } },
460
+ ]})
461
+ ```
462
+
433
463
  ## Graceful shutdown
434
464
 
435
465
  ```ts
package/dist/index.d.ts CHANGED
@@ -19,3 +19,5 @@ export { rateLimit } from './rate-limit.ts';
19
19
  export type { RateLimitOptions } from './rate-limit.ts';
20
20
  export { compress } from './compress.ts';
21
21
  export type { CompressOptions } from './compress.ts';
22
+ export { tool, createWorkflowEngine, createSSEManager, generateWorkflow } from './workflow/index.ts';
23
+ export type { Tool, Workflow, WorkflowEngine, WorkflowState, SSEEvent } from './workflow/types.ts';
package/dist/index.js CHANGED
@@ -110,6 +110,490 @@ import { WebSocketServer } from "ws";
110
110
  import { buildSchema, graphql } from "graphql";
111
111
  import { makeExecutableSchema } from "@graphql-tools/schema";
112
112
  import { streamText } from "ai";
113
+
114
+ // workflow/tool.ts
115
+ function tool(def) {
116
+ return {
117
+ name: def.name ?? "",
118
+ description: def.description,
119
+ inputSchema: def.inputSchema,
120
+ execute: def.execute
121
+ };
122
+ }
123
+
124
+ // workflow/reference.ts
125
+ function getByPath(obj, path) {
126
+ let current = obj;
127
+ for (const key of path) {
128
+ if (current === null || current === void 0) return void 0;
129
+ if (typeof current === "object" && key in current) {
130
+ current = current[key];
131
+ } else {
132
+ return void 0;
133
+ }
134
+ }
135
+ return current;
136
+ }
137
+ function resolveRef(path, ctx) {
138
+ if (path.startsWith("$nodes.")) {
139
+ const afterNodes = path.slice(7);
140
+ const dotIdx = afterNodes.indexOf(".");
141
+ if (dotIdx === -1) {
142
+ return ctx.nodeOutputs.get(afterNodes);
143
+ }
144
+ const id2 = afterNodes.slice(0, dotIdx);
145
+ const propPath = afterNodes.slice(dotIdx + 1);
146
+ const output = ctx.nodeOutputs.get(id2);
147
+ if (output === void 0) {
148
+ throw new Error(`Node "${id2}" has no output yet`);
149
+ }
150
+ if (propPath.startsWith("output")) {
151
+ return getByPath(output, propPath.slice(7).split(".").filter(Boolean));
152
+ }
153
+ return getByPath(output, propPath.split("."));
154
+ }
155
+ if (path.startsWith("$var.")) {
156
+ const name = path.slice(5);
157
+ if (!ctx.variables.has(name)) {
158
+ throw new Error(`Variable "${name}" is not defined`);
159
+ }
160
+ return ctx.variables.get(name);
161
+ }
162
+ if (path.startsWith("$input.")) {
163
+ const key = path.slice(7);
164
+ return ctx.input[key];
165
+ }
166
+ if (path === "true") return true;
167
+ if (path === "false") return false;
168
+ if (path === "null") return null;
169
+ const num = Number(path);
170
+ if (!isNaN(num) && path.trim() !== "") return num;
171
+ return path;
172
+ }
173
+ function resolveValue(v, ctx) {
174
+ if (typeof v === "string" && v.startsWith("$")) {
175
+ return resolveRef(v, ctx);
176
+ }
177
+ if (Array.isArray(v)) {
178
+ return v.map((item) => resolveValue(item, ctx));
179
+ }
180
+ if (typeof v === "object" && v !== null) {
181
+ const result = {};
182
+ for (const [k, val] of Object.entries(v)) {
183
+ result[k] = resolveValue(val, ctx);
184
+ }
185
+ return result;
186
+ }
187
+ return v;
188
+ }
189
+
190
+ // workflow/nodes.ts
191
+ function evaluateExpression(expr, ctx) {
192
+ const operators = [
193
+ { op: "===", fn: (a, b) => a === b },
194
+ { op: "!==", fn: (a, b) => a !== b },
195
+ { op: ">=", fn: (a, b) => Number(a) >= Number(b) },
196
+ { op: "<=", fn: (a, b) => Number(a) <= Number(b) },
197
+ { op: ">", fn: (a, b) => Number(a) > Number(b) },
198
+ { op: "<", fn: (a, b) => Number(a) < Number(b) },
199
+ { op: "==", fn: (a, b) => a == b },
200
+ { op: "!=", fn: (a, b) => a != b },
201
+ { op: "+", fn: (a, b) => Number(a) + Number(b) },
202
+ { op: "-", fn: (a, b) => Number(a) - Number(b) },
203
+ { op: "*", fn: (a, b) => Number(a) * Number(b) },
204
+ { op: "/", fn: (a, b) => Number(a) / Number(b) },
205
+ { op: "%", fn: (a, b) => Number(a) % Number(b) },
206
+ { op: "&&", fn: (a, b) => Boolean(a) && Boolean(b) },
207
+ { op: "||", fn: (a, b) => Boolean(a) || Boolean(b) }
208
+ ];
209
+ for (const { op, fn } of operators) {
210
+ const idx = expr.indexOf(op);
211
+ if (idx > 0) {
212
+ const leftRaw = expr.slice(0, idx).trim();
213
+ const rightRaw = expr.slice(idx + op.length).trim();
214
+ const left = resolveValue(leftRaw, ctx);
215
+ const right = resolveValue(rightRaw, ctx);
216
+ return fn(left, right);
217
+ }
218
+ }
219
+ const trimmed = expr.trim();
220
+ if (trimmed === "true") return true;
221
+ if (trimmed === "false") return false;
222
+ if (trimmed === "null") return null;
223
+ const num = Number(trimmed);
224
+ if (!isNaN(num) && trimmed !== "") return num;
225
+ return resolveValue(expr, ctx);
226
+ }
227
+ async function executeEval(node, ctx) {
228
+ const expression = node.input.expression;
229
+ if (!expression) throw new Error('eval node requires "expression" field');
230
+ const result = evaluateExpression(expression, ctx);
231
+ return { result };
232
+ }
233
+ async function executeSet(node, ctx) {
234
+ const name = node.input.name;
235
+ const value = node.input.value;
236
+ if (!name) throw new Error('set node requires "name" field');
237
+ let resolved;
238
+ if (typeof value === "string") {
239
+ resolved = evaluateExpression(value, ctx);
240
+ } else {
241
+ resolved = resolveValue(value ?? null, ctx);
242
+ }
243
+ ctx.variables.set(name, resolved);
244
+ return resolved;
245
+ }
246
+ async function executeGet(node, ctx) {
247
+ const name = node.input.name;
248
+ if (!name) throw new Error('get node requires "name" field');
249
+ if (!ctx.variables.has(name)) {
250
+ throw new Error(`Variable "${name}" is not defined`);
251
+ }
252
+ return ctx.variables.get(name);
253
+ }
254
+ async function executeIf(node, ctx) {
255
+ const conditions = node.conditions ?? [];
256
+ for (const condition of conditions) {
257
+ const test = typeof condition.test === "string" ? Boolean(resolveValue(condition.test, ctx)) : condition.test;
258
+ if (test && condition.body) {
259
+ let lastOutput = void 0;
260
+ for (const bodyNode of condition.body) {
261
+ lastOutput = await executeNode(bodyNode, ctx);
262
+ }
263
+ return lastOutput;
264
+ }
265
+ }
266
+ return void 0;
267
+ }
268
+ async function executeWhile(node, ctx) {
269
+ const conditionExpr = node.input.condition;
270
+ if (!conditionExpr) throw new Error('while node requires "condition" field');
271
+ let lastOutput = void 0;
272
+ let iterations = 0;
273
+ const maxIterations = 1e3;
274
+ while (iterations < maxIterations) {
275
+ iterations++;
276
+ ctx.stepCount++;
277
+ if (ctx.stepCount > ctx.maxSteps) {
278
+ throw new Error(`Step limit exceeded (${ctx.maxSteps})`);
279
+ }
280
+ const condition = Boolean(evaluateExpression(conditionExpr, ctx));
281
+ if (!condition) break;
282
+ for (const bodyNode of node.body ?? []) {
283
+ lastOutput = await executeNode(bodyNode, ctx);
284
+ }
285
+ }
286
+ return lastOutput;
287
+ }
288
+ async function executeCall(node, ctx) {
289
+ const toolName = node.input.tool;
290
+ const args = node.input.args ?? {};
291
+ if (toolName && ctx.toolRegistry.has(toolName)) {
292
+ const tool3 = ctx.toolRegistry.get(toolName);
293
+ const resolvedInput = resolveValue(args, ctx);
294
+ const parsed = tool3.inputSchema.parse(resolvedInput);
295
+ return tool3.execute(parsed, {
296
+ nodeId: node.id,
297
+ workflowId: ctx.workflowId,
298
+ onStream: async (event) => {
299
+ if (ctx.sseManager && ctx.workflowId) {
300
+ ctx.sseManager.send(ctx.workflowId, { event: "llm-stream", data: { nodeId: node.id, ...event } });
301
+ }
302
+ }
303
+ });
304
+ }
305
+ const functionName = node.input.function;
306
+ if (functionName && ctx.functions[functionName]) {
307
+ const fn = ctx.functions[functionName];
308
+ const prevFunctions = ctx.functions;
309
+ const prevInput = ctx.input;
310
+ ctx.input = resolveValue(args, ctx);
311
+ let lastOutput = void 0;
312
+ for (const bodyNode of fn.workflow.nodes) {
313
+ lastOutput = await executeNode(bodyNode, ctx);
314
+ }
315
+ ctx.input = prevInput;
316
+ return lastOutput;
317
+ }
318
+ throw new Error(`call node: tool "${toolName ?? functionName}" not found`);
319
+ }
320
+ async function executeHttp(node, ctx) {
321
+ const input = resolveValue(node.input, ctx);
322
+ const url = input.url;
323
+ if (!url) throw new Error('http node requires "url" field');
324
+ const controller = new AbortController();
325
+ const timeout = input.timeout ?? 3e4;
326
+ const timer = setTimeout(() => controller.abort(), timeout);
327
+ try {
328
+ const fetchInit = {
329
+ method: input.method ?? "GET",
330
+ headers: input.headers ?? {},
331
+ signal: controller.signal
332
+ };
333
+ if (input.body && fetchInit.method !== "GET") {
334
+ fetchInit.body = JSON.stringify(input.body);
335
+ }
336
+ const response = await fetch(url, fetchInit);
337
+ const contentType = response.headers.get("content-type") ?? "";
338
+ const body = contentType.includes("application/json") ? await response.json() : await response.text();
339
+ return {
340
+ status: response.status,
341
+ statusText: response.statusText,
342
+ headers: Object.fromEntries(response.headers.entries()),
343
+ body
344
+ };
345
+ } finally {
346
+ clearTimeout(timer);
347
+ }
348
+ }
349
+ var executors = {
350
+ eval: executeEval,
351
+ set: executeSet,
352
+ get: executeGet,
353
+ if: executeIf,
354
+ while: executeWhile,
355
+ call: executeCall,
356
+ http: executeHttp
357
+ };
358
+ async function executeNode(node, ctx) {
359
+ const executor = executors[node.tool];
360
+ if (!executor) {
361
+ throw new Error(`Unknown node type: "${node.tool}"`);
362
+ }
363
+ return executor(node, ctx);
364
+ }
365
+
366
+ // workflow/llm.ts
367
+ function buildToolsDescription(tools) {
368
+ return Object.entries(tools).map(([key, t]) => {
369
+ const name = t.name || key;
370
+ const schema = t.inputSchema;
371
+ return `- ${name}: ${t.description}
372
+ Input schema: describe as JSON object fields`;
373
+ }).join("\n");
374
+ }
375
+ var SYSTEM_PROMPT_TEMPLATE = `You are a workflow generator. Given a user goal and available tools, output a workflow JSON.
376
+
377
+ Available tools:
378
+ {{TOOLS}}
379
+
380
+ Workflow format:
381
+ {
382
+ "name": "workflow name",
383
+ "nodes": [
384
+ {
385
+ "id": "step1",
386
+ "tool": "set",
387
+ "input": { "name": "varName", "value": "initialValue" }
388
+ },
389
+ {
390
+ "id": "step2",
391
+ "tool": "call",
392
+ "input": { "tool": "toolName", "args": { "param1": "$var.varName" } }
393
+ },
394
+ {
395
+ "id": "step3",
396
+ "tool": "if",
397
+ "input": {},
398
+ "conditions": [
399
+ { "test": "$nodes.step2.output.someField", "body": [
400
+ { "id": "step4", "tool": "call", "input": { "tool": "toolName", "args": {} } }
401
+ ]}
402
+ ]
403
+ }
404
+ ]
405
+ }
406
+
407
+ Node types:
408
+ - eval: evaluate an expression. input: { expression: "..." }
409
+ - set: assign a variable. input: { name, value }
410
+ - get: read a variable. input: { name }
411
+ - if: conditional branch. input: {}, conditions: [{ test, body }]
412
+ - while: loop. input: { condition }, body: [nodes]
413
+ - call: call a registered tool. input: { tool, args }
414
+ - http: HTTP request. input: { url, method?, headers?, body? }
415
+
416
+ Reference syntax:
417
+ - $var.name - read a variable
418
+ - $nodes.id.output - output of a previous node
419
+ - $nodes.id.output.field - specific field of a node's output
420
+ - $input.field - workflow input parameter
421
+
422
+ Output ONLY valid JSON. No explanation, no markdown.`;
423
+ async function generateWorkflow(goal, tools, generateFn) {
424
+ const toolsDesc = buildToolsDescription(tools);
425
+ const system = SYSTEM_PROMPT_TEMPLATE.replace("{{TOOLS}}", toolsDesc);
426
+ const result = await generateFn({
427
+ system,
428
+ messages: [{ role: "user", content: goal }]
429
+ });
430
+ const text = result.text.trim();
431
+ const jsonStart = text.indexOf("{");
432
+ const jsonEnd = text.lastIndexOf("}");
433
+ if (jsonStart === -1 || jsonEnd === -1) {
434
+ throw new Error(`LLM output is not valid JSON: ${text.slice(0, 200)}`);
435
+ }
436
+ const jsonStr = text.slice(jsonStart, jsonEnd + 1);
437
+ try {
438
+ const workflow = JSON.parse(jsonStr);
439
+ if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
440
+ throw new Error("Generated workflow has no nodes array");
441
+ }
442
+ return workflow;
443
+ } catch (err) {
444
+ if (err instanceof SyntaxError) {
445
+ throw new Error(`Failed to parse LLM output as JSON: ${err.message}`);
446
+ }
447
+ throw err;
448
+ }
449
+ }
450
+
451
+ // workflow/engine.ts
452
+ import { generateText } from "ai";
453
+ function createWorkflowEngine(options) {
454
+ const toolRegistry = /* @__PURE__ */ new Map();
455
+ for (const [key, t] of Object.entries(options.tools)) {
456
+ t.name = t.name || key;
457
+ toolRegistry.set(t.name, t);
458
+ }
459
+ const states = /* @__PURE__ */ new Map();
460
+ async function execute(workflow, opts) {
461
+ const ctx = {
462
+ variables: /* @__PURE__ */ new Map(),
463
+ nodeOutputs: /* @__PURE__ */ new Map(),
464
+ functions: workflow.functions ?? {},
465
+ stepCount: 0,
466
+ maxSteps: opts?.maxSteps ?? 1e3,
467
+ input: opts?.initialInput ?? {},
468
+ toolRegistry,
469
+ sseManager: options.sseManager,
470
+ workflowId: opts?.workflowId
471
+ };
472
+ let lastOutput = void 0;
473
+ for (const node of workflow.nodes) {
474
+ ctx.stepCount++;
475
+ if (ctx.stepCount > ctx.maxSteps) {
476
+ throw new Error(`Step limit exceeded (${ctx.maxSteps})`);
477
+ }
478
+ options.sseManager?.send(ctx.workflowId ?? "", { event: "node-start", data: { nodeId: node.id, tool: node.tool, input: node.input } });
479
+ const output = await executeNode(node, ctx);
480
+ ctx.nodeOutputs.set(node.id, output);
481
+ lastOutput = output;
482
+ options.sseManager?.send(ctx.workflowId ?? "", { event: "node-end", data: { nodeId: node.id, output } });
483
+ }
484
+ return lastOutput;
485
+ }
486
+ async function runAsync(workflowId, workflow, opts) {
487
+ const state = {
488
+ workflowId,
489
+ status: "running",
490
+ goal: workflow.name ?? "",
491
+ startTime: Date.now()
492
+ };
493
+ states.set(workflowId, state);
494
+ const sse = options.sseManager;
495
+ sse?.send(workflowId, { event: "workflow-start", data: { workflowId, goal: state.goal } });
496
+ try {
497
+ const result = await execute(workflow, { ...opts, workflowId });
498
+ state.status = "completed";
499
+ state.result = result;
500
+ state.endTime = Date.now();
501
+ sse?.send(workflowId, { event: "complete", data: { result, duration: state.endTime - state.startTime } });
502
+ } catch (err) {
503
+ state.status = "error";
504
+ state.error = err instanceof Error ? err.message : String(err);
505
+ state.endTime = Date.now();
506
+ sse?.send(workflowId, { event: "error", data: { error: state.error } });
507
+ } finally {
508
+ sse?.close(workflowId);
509
+ }
510
+ }
511
+ async function generateWorkflow2(goal) {
512
+ if (!options.model) {
513
+ throw new Error('LLM model is required for generateWorkflow. Pass "model" to createWorkflowEngine.');
514
+ }
515
+ return generateWorkflow(goal, options.tools, async (prompt) => {
516
+ const result = await generateText({
517
+ model: options.model,
518
+ system: prompt.system,
519
+ messages: prompt.messages
520
+ });
521
+ return { text: result.text };
522
+ });
523
+ }
524
+ return {
525
+ execute,
526
+ runAsync,
527
+ generateWorkflow: generateWorkflow2,
528
+ getState(workflowId) {
529
+ return states.get(workflowId);
530
+ }
531
+ };
532
+ }
533
+
534
+ // workflow/sse.ts
535
+ function createSSEManager() {
536
+ const streams = /* @__PURE__ */ new Map();
537
+ const encoder = new TextEncoder();
538
+ function createStream(workflowId) {
539
+ const state = {
540
+ controller: null,
541
+ encoder,
542
+ closed: false,
543
+ buffer: []
544
+ };
545
+ const stream = new ReadableStream({
546
+ start(controller) {
547
+ state.controller = controller;
548
+ streams.set(workflowId, state);
549
+ for (const event of state.buffer) {
550
+ try {
551
+ controller.enqueue(encoder.encode(event));
552
+ } catch {
553
+ break;
554
+ }
555
+ }
556
+ state.buffer = [];
557
+ },
558
+ cancel() {
559
+ state.closed = true;
560
+ streams.delete(workflowId);
561
+ }
562
+ });
563
+ return stream;
564
+ }
565
+ function send(workflowId, event) {
566
+ const state = streams.get(workflowId);
567
+ if (!state || state.closed) return;
568
+ const data = `event: ${event.event}
569
+ data: ${JSON.stringify(event.data)}
570
+
571
+ `;
572
+ if (state.controller) {
573
+ try {
574
+ state.controller.enqueue(encoder.encode(data));
575
+ } catch {
576
+ state.closed = true;
577
+ streams.delete(workflowId);
578
+ }
579
+ } else {
580
+ state.buffer.push(data);
581
+ }
582
+ }
583
+ function close(workflowId) {
584
+ const state = streams.get(workflowId);
585
+ if (!state) return;
586
+ state.closed = true;
587
+ streams.delete(workflowId);
588
+ try {
589
+ state.controller?.close();
590
+ } catch {
591
+ }
592
+ }
593
+ return { createStream, send, close };
594
+ }
595
+
596
+ // router.ts
113
597
  var createTrieNode = () => ({
114
598
  children: /* @__PURE__ */ new Map(),
115
599
  handlers: /* @__PURE__ */ new Map(),
@@ -302,6 +786,50 @@ var Router = class _Router {
302
786
  };
303
787
  return this.post(path, ...middlewares, routeHandler);
304
788
  }
789
+ workflow(path, options) {
790
+ const sseManager = options.stream ? createSSEManager() : void 0;
791
+ if (options.stream && sseManager) {
792
+ this.get(`${path}/:workflowId/events`, async (req, ctx) => {
793
+ const stream = sseManager.createStream(ctx.params.workflowId);
794
+ return new Response(stream, {
795
+ headers: {
796
+ "Content-Type": "text/event-stream",
797
+ "Cache-Control": "no-cache",
798
+ "Connection": "keep-alive"
799
+ }
800
+ });
801
+ });
802
+ }
803
+ this.post(path, async (req) => {
804
+ const body = await req.json();
805
+ const engine = createWorkflowEngine({
806
+ tools: options.tools,
807
+ model: options.model,
808
+ sseManager
809
+ });
810
+ let wf;
811
+ if (body.goal && options.model) {
812
+ wf = await engine.generateWorkflow(body.goal);
813
+ } else if (body.workflow) {
814
+ wf = body.workflow;
815
+ } else if (body.nodes) {
816
+ wf = { nodes: body.nodes };
817
+ } else {
818
+ return Response.json(
819
+ { error: 'Provide "goal" (with model) or "workflow"/"nodes"' },
820
+ { status: 400 }
821
+ );
822
+ }
823
+ if (options.stream && sseManager) {
824
+ const workflowId = crypto.randomUUID();
825
+ engine.runAsync(workflowId, wf);
826
+ return Response.json({ workflowId, eventsUrl: `${path}/${workflowId}/events` });
827
+ }
828
+ const result = await engine.execute(wf);
829
+ return Response.json({ workflow: wf, result });
830
+ });
831
+ return this;
832
+ }
305
833
  handler() {
306
834
  return (req, ctx) => {
307
835
  const url = new URL(req.url);
@@ -1472,13 +2000,17 @@ export {
1472
2000
  auth,
1473
2001
  compress,
1474
2002
  cors,
2003
+ createSSEManager,
2004
+ createWorkflowEngine,
1475
2005
  deleteCookie,
2006
+ generateWorkflow,
1476
2007
  getCookies,
1477
2008
  logger,
1478
2009
  rateLimit,
1479
2010
  serve,
1480
2011
  serveStatic,
1481
2012
  setCookie,
2013
+ tool,
1482
2014
  tsx,
1483
2015
  upload,
1484
2016
  useTsx,
package/dist/router.d.ts CHANGED
@@ -2,8 +2,9 @@ import { type WebSocket } from 'ws';
2
2
  import type { IncomingMessage } from 'node:http';
3
3
  import type { Duplex } from 'node:stream';
4
4
  import { type GraphQLSchema } from 'graphql';
5
- import { streamText } from 'ai';
5
+ import { streamText, generateText } from 'ai';
6
6
  import type { Context, Handler, Middleware, ErrorHandler } from './types.ts';
7
+ import type { Tool as WfTool } from './workflow/types.ts';
7
8
  type StreamTextParams = Parameters<typeof streamText>[0];
8
9
  export type WebSocketHandler = {
9
10
  open?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
@@ -44,6 +45,11 @@ export declare class Router {
44
45
  ws(path: string, ...args: [...Middleware[], WebSocketHandler]): this;
45
46
  graphql(path: string, ...args: [...Middleware[], GraphQLOptions]): this;
46
47
  ai(path: string, ...args: [...Middleware[], AIHandler]): this;
48
+ workflow(path: string, options: {
49
+ tools: Record<string, WfTool>;
50
+ model?: Parameters<typeof generateText>[0]['model'];
51
+ stream?: boolean;
52
+ }): this;
47
53
  handler(): Handler;
48
54
  websocketHandler(): WsUpgradeHandler;
49
55
  private splitPath;
@@ -0,0 +1,7 @@
1
+ import type { Tool, WorkflowEngine, SSEManager } from './types.ts';
2
+ import { type LanguageModel } from 'ai';
3
+ export declare function createWorkflowEngine(options: {
4
+ tools: Record<string, Tool>;
5
+ sseManager?: SSEManager;
6
+ model?: LanguageModel;
7
+ }): WorkflowEngine;
@@ -0,0 +1,6 @@
1
+ export { tool } from './tool.ts';
2
+ export { createWorkflowEngine } from './engine.ts';
3
+ export { createSSEManager } from './sse.ts';
4
+ export { resolveRef, resolveValue } from './reference.ts';
5
+ export { generateWorkflow } from './llm.ts';
6
+ export type { Tool, ToolContext, Workflow, Node, WorkflowState, SSEEvent, SSEManager, WorkflowEngine, ExecuteOptions, StreamEvent, Condition, SubWorkflow } from './types.ts';
@@ -0,0 +1,10 @@
1
+ import type { Tool, Workflow } from './types.ts';
2
+ export declare function generateWorkflow(goal: string, tools: Record<string, Tool>, generateFn: (prompt: {
3
+ system: string;
4
+ messages: {
5
+ role: string;
6
+ content: string;
7
+ }[];
8
+ }) => Promise<{
9
+ text: string;
10
+ }>): Promise<Workflow>;
@@ -0,0 +1,10 @@
1
+ import type { Node, WorkflowContext } from './types.ts';
2
+ export type NodeExecutor = (node: Node, ctx: WorkflowContext) => Promise<unknown>;
3
+ export declare function executeEval(node: Node, ctx: WorkflowContext): Promise<unknown>;
4
+ export declare function executeSet(node: Node, ctx: WorkflowContext): Promise<unknown>;
5
+ export declare function executeGet(node: Node, ctx: WorkflowContext): Promise<unknown>;
6
+ export declare function executeIf(node: Node, ctx: WorkflowContext): Promise<unknown>;
7
+ export declare function executeWhile(node: Node, ctx: WorkflowContext): Promise<unknown>;
8
+ export declare function executeCall(node: Node, ctx: WorkflowContext): Promise<unknown>;
9
+ export declare function executeHttp(node: Node, ctx: WorkflowContext): Promise<unknown>;
10
+ export declare function executeNode(node: Node, ctx: WorkflowContext): Promise<unknown>;
@@ -0,0 +1,3 @@
1
+ import type { WorkflowContext } from './types.ts';
2
+ export declare function resolveRef(path: string, ctx: WorkflowContext): unknown;
3
+ export declare function resolveValue(v: unknown, ctx: WorkflowContext): unknown;
@@ -0,0 +1,2 @@
1
+ import type { SSEManager } from './types.ts';
2
+ export declare function createSSEManager(): SSEManager;
@@ -0,0 +1,8 @@
1
+ import type { z } from 'zod';
2
+ import type { Tool, ToolContext } from './types.ts';
3
+ export declare function tool<TInput = unknown, TOutput = unknown>(def: {
4
+ name?: string;
5
+ description: string;
6
+ inputSchema: z.ZodSchema<TInput>;
7
+ execute: (input: TInput, ctx: ToolContext) => Promise<TOutput>;
8
+ }): Tool<TInput, TOutput>;
@@ -0,0 +1,86 @@
1
+ import type { z } from 'zod';
2
+ export type NodeType = 'eval' | 'set' | 'get' | 'if' | 'while' | 'call' | 'http';
3
+ export interface Tool<TInput = unknown, TOutput = unknown> {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodSchema<TInput>;
7
+ execute: (input: TInput, ctx: ToolContext) => Promise<TOutput>;
8
+ }
9
+ export interface ToolContext {
10
+ workflowId?: string;
11
+ nodeId: string;
12
+ onStream?: (event: StreamEvent) => Promise<void>;
13
+ }
14
+ export interface StreamEvent {
15
+ type: string;
16
+ chunk?: string;
17
+ accumulated?: string;
18
+ [key: string]: unknown;
19
+ }
20
+ export interface Node {
21
+ id: string;
22
+ tool: NodeType | string;
23
+ input: Record<string, unknown>;
24
+ conditions?: Condition[];
25
+ body?: Node[];
26
+ }
27
+ export interface Condition {
28
+ test: string | boolean;
29
+ body: Node[];
30
+ }
31
+ export interface Workflow {
32
+ name?: string;
33
+ nodes: Node[];
34
+ functions?: Record<string, SubWorkflow>;
35
+ }
36
+ export interface SubWorkflow {
37
+ inputSchema: Record<string, unknown>;
38
+ workflow: {
39
+ nodes: Node[];
40
+ };
41
+ }
42
+ export interface WorkflowState {
43
+ workflowId: string;
44
+ status: 'running' | 'completed' | 'error';
45
+ goal: string;
46
+ result?: unknown;
47
+ error?: string;
48
+ startTime: number;
49
+ endTime?: number;
50
+ }
51
+ export interface SSEEvent {
52
+ event: string;
53
+ data: unknown;
54
+ }
55
+ export interface ExecuteOptions {
56
+ initialInput?: Record<string, unknown>;
57
+ maxSteps?: number;
58
+ }
59
+ export interface WorkflowContext {
60
+ variables: Map<string, unknown>;
61
+ nodeOutputs: Map<string, unknown>;
62
+ functions: Record<string, SubWorkflow>;
63
+ stepCount: number;
64
+ maxSteps: number;
65
+ input: Record<string, unknown>;
66
+ toolRegistry: Map<string, Tool>;
67
+ sseManager?: SSEManager;
68
+ workflowId?: string;
69
+ onNodeEvent?: (event: SSEEvent) => void;
70
+ }
71
+ export interface SSEManager {
72
+ createStream: (workflowId: string) => ReadableStream<Uint8Array>;
73
+ send: (workflowId: string, event: SSEEvent) => void;
74
+ close: (workflowId: string) => void;
75
+ }
76
+ export interface EngineOptions {
77
+ tools: Record<string, Tool>;
78
+ model?: unknown;
79
+ sseManager?: SSEManager;
80
+ }
81
+ export interface WorkflowEngine {
82
+ execute: (workflow: Workflow, options?: ExecuteOptions) => Promise<unknown>;
83
+ getState: (workflowId: string) => WorkflowState | undefined;
84
+ runAsync: (workflowId: string, workflow: Workflow, options?: ExecuteOptions) => Promise<void>;
85
+ generateWorkflow: (goal: string) => Promise<Workflow>;
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",