weifuwu 0.9.6 → 0.11.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.
Files changed (57) hide show
  1. package/README.md +265 -164
  2. package/dist/agent/migrate.d.ts +1 -1
  3. package/dist/agent/rest.d.ts +1 -1
  4. package/dist/agent/run.d.ts +2 -2
  5. package/dist/agent/types.d.ts +2 -2
  6. package/dist/ai/workflow.d.ts +14 -0
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +2292 -11035
  9. package/dist/messager/migrate.d.ts +1 -1
  10. package/dist/messager/rest.d.ts +1 -1
  11. package/dist/messager/types.d.ts +2 -1
  12. package/dist/messager/ws.d.ts +1 -1
  13. package/dist/opencode/client.d.ts +2 -0
  14. package/dist/opencode/index.d.ts +2 -0
  15. package/dist/opencode/migrate.d.ts +2 -0
  16. package/dist/opencode/permissions.d.ts +5 -0
  17. package/dist/opencode/prompt.d.ts +8 -0
  18. package/dist/opencode/rest.d.ts +15 -0
  19. package/dist/opencode/run.d.ts +13 -0
  20. package/dist/opencode/session.d.ts +26 -0
  21. package/dist/opencode/skills.d.ts +4 -0
  22. package/dist/opencode/tools/bash.d.ts +6 -0
  23. package/dist/opencode/tools/edit.d.ts +19 -0
  24. package/dist/opencode/tools/glob.d.ts +9 -0
  25. package/dist/opencode/tools/grep.d.ts +17 -0
  26. package/dist/opencode/tools/index.d.ts +12 -0
  27. package/dist/opencode/tools/question.d.ts +5 -0
  28. package/dist/opencode/tools/read.d.ts +16 -0
  29. package/dist/opencode/tools/skill.d.ts +18 -0
  30. package/dist/opencode/tools/web.d.ts +18 -0
  31. package/dist/opencode/tools/write.d.ts +13 -0
  32. package/dist/opencode/types.d.ts +90 -0
  33. package/dist/opencode/ws.d.ts +22 -0
  34. package/dist/postgres/index.d.ts +3 -1
  35. package/dist/postgres/module.d.ts +9 -0
  36. package/dist/postgres/schema/columns.d.ts +34 -0
  37. package/dist/postgres/schema/index.d.ts +4 -0
  38. package/dist/postgres/schema/sql.d.ts +7 -0
  39. package/dist/postgres/schema/table.d.ts +49 -0
  40. package/dist/postgres/types.d.ts +11 -35
  41. package/dist/queue/types.d.ts +1 -1
  42. package/dist/redis/types.d.ts +1 -1
  43. package/dist/router.d.ts +1 -1
  44. package/dist/sse.d.ts +10 -0
  45. package/dist/tenant/graphql.d.ts +1 -1
  46. package/dist/tenant/migrate.d.ts +1 -1
  47. package/dist/tenant/rest.d.ts +1 -1
  48. package/dist/tenant/types.d.ts +2 -1
  49. package/dist/tsx-context.d.ts +12 -0
  50. package/dist/tsx-instance.d.ts +32 -0
  51. package/dist/tsx.d.ts +5 -16
  52. package/dist/user/oauth2.d.ts +2 -1
  53. package/dist/user/types.d.ts +2 -1
  54. package/dist/vendor.d.ts +3 -0
  55. package/dist/workflow/engine.d.ts +1 -1
  56. package/dist/workflow/route.d.ts +1 -1
  57. package/package.json +5 -4
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  weifuwu doesn't invent its own request/response abstraction. `Request` and `Response` are the same objects you use in `fetch()` — what you learn in the browser applies directly on the server. `ctx` is the only framework object, and it only carries what the router parsed for you (`params`, `query`).
8
8
 
9
- Everything follows the same `(req, ctx) => Response` contract. The Router handles HTTP routing and WebSocket. All other features — auth, validation, database, GraphQL, AI, workflow — are standalone modules you import and mount with `app.use()`.
9
+ Everything follows the same `(req, ctx) => Response` contract. The Router handles HTTP routing and WebSocket. All other features — auth, validation, database, GraphQL, AI — are standalone modules you import and mount with `app.use()`.
10
10
 
11
11
  ## Features
12
12
 
@@ -18,8 +18,8 @@ Everything follows the same `(req, ctx) => Response` contract. The Router handle
18
18
  - **WebSocket** — `router.ws()` with upgrade middleware (auth before connect)
19
19
  - **GraphQL** — `graphql(handler)` sub-Router with GraphiQL IDE
20
20
  - **AI streaming** — `ai(handler)` sub-Router via Vercel AI SDK
21
- - **AI workflows** — `workflow(handler)` sub-Router intent-to-execution pipelines with `tool()` + SSE
22
- - **AI Agent** — `agent()` — server-side AI agents with chat/workflow/knowledge types, OpenAI-compatible, Ollama-ready
21
+ - **DAG workflow tool** — `runWorkflow()` — multi-step execution engine as a single AI SDK `Tool`
22
+ - **AI Agent** — `agent()` — server-side AI agents with chat/tool-use/knowledge types, OpenAI-compatible, Ollama-ready
23
23
  - **Messaging** — `messager()` — real-time chat with channels, WebSocket, agent routing, webhook support
24
24
  - **Tenant BaaS** — `tenant()` — multi-tenant dynamic tables, auto REST + GraphQL, row-level isolation, pgvector/HNSW
25
25
  - **Redis** — `redis()` — ioredis client, `ctx.redis`, middleware
@@ -221,83 +221,247 @@ Features: MIME type detection (20+ types), ETag + If-None-Match (304), directory
221
221
 
222
222
  ## PostgreSQL
223
223
 
224
- Built-in PostgreSQL — zero config, zero ORM, zero migration files.
224
+ Built-in PostgreSQL client connection management, type-safe DDL, transactions, and module lifecycle.
225
225
 
226
226
  ```ts
227
227
  import { serve, Router, postgres } from 'weifuwu'
228
- import { z } from 'zod'
229
228
 
230
229
  const app = new Router()
231
- const pg = postgres()
230
+ const pg = postgres() // reads DATABASE_URL
231
+ app.use(pg) // injects ctx.sql into handlers
232
+ ```
233
+
234
+ ### Type-safe DDL with schema builder
232
235
 
233
- const User = pg.table('users', {
234
- id: z.number().optional(), // → SERIAL PRIMARY KEY
235
- name: z.string().min(1), // → TEXT NOT NULL
236
- email: z.string().email(), // TEXT NOT NULL
237
- age: z.number().optional(), // → INTEGER
236
+ Define tables declaratively with type inference — no raw SQL for common operations, no Zod needed:
237
+
238
+ ```ts
239
+ import { pgTable, serial, uuid, text, integer, boolean, timestamptz, jsonb, sql } from 'weifuwu'
240
+
241
+ const users = pgTable('_users', {
242
+ id: serial('id').primaryKey(),
243
+ name: text('name').notNull(),
244
+ email: text('email').unique().notNull(),
245
+ age: integer('age'),
246
+ active: boolean('active').default(true),
247
+ createdAt: timestamptz('created_at').default(sql`NOW()`),
248
+ metadata: jsonb<{ role: string }>('metadata'),
238
249
  })
250
+ ```
239
251
 
240
- await pg.migrate()
241
- // Auto-creates tables / adds missing columns via information_schema
242
- app.use(pg) // injects ctx.sql into handlers
252
+ Supports 10 column types:
253
+ | Builder | DDL | TS Type |
254
+ |---------|-----|---------|
255
+ | `serial()` | `SERIAL` | `number` |
256
+ | `uuid()` | `UUID` | `string` |
257
+ | `text()` | `TEXT` | `string` |
258
+ | `integer()` | `INTEGER` | `number` |
259
+ | `boolean()` | `BOOLEAN` | `boolean` |
260
+ | `timestamptz()` | `TIMESTAMPTZ` | `string` |
261
+ | `jsonb<T>()` | `JSONB` | `T` |
262
+ | `textArray()` | `TEXT[]` | `string[]` |
263
+ | `vector(name, dims)` | `vector(N)` | `number[]` |
264
+
265
+ Column constraints chainable: `.primaryKey()`, `.notNull()`, `.nullable()`, `.default(value | sql\`...\`)`, `.unique()`, `.references(table, column?, onDelete?)`.
266
+
267
+ ### DDL execution
268
+
269
+ ```ts
270
+ await users.create() // CREATE TABLE IF NOT EXISTS
271
+ await users.createIndex('email') // CREATE INDEX
272
+ await users.createUniqueIndex('slug') // CREATE UNIQUE INDEX
273
+ await users.createIndex('created_at', { desc: true })
274
+ await users.createIndex(['a', 'b']) // multi-column
275
+ await users.createIndex('embedding', { // pgvector HNSW
276
+ type: 'hnsw', operator: 'vector_cosine_ops',
277
+ })
278
+ await users.drop({ cascade: true })
243
279
  ```
244
280
 
245
- ### 6 methods HTTP semantics
281
+ ### Type-safe CRUD with BoundTable
282
+
283
+ Two usage paths — use `pg.table()` when you have a `pg` handle, or `pgTable()` with explicit `sql`:
246
284
 
247
285
  ```ts
248
- User.get(1) // GET /users/:id
249
- User.list({ name: 'a' }, // GET /users?name=a
250
- { limit: 10, offset: 0, sort: { id: 'desc' } })
251
- // → { rows: User[], count: number }
286
+ // pg.table() — auto-binds sql, no need to pass it
287
+ const users = pg.table('_users', {
288
+ id: serial('id').primaryKey(),
289
+ name: text('name').notNull(),
290
+ email: text('email').unique(),
291
+ active: boolean('active').default(true),
292
+ createdAt: timestamptz('created_at').default(sql`NOW()`),
293
+ })
252
294
 
253
- User.create({ name: 'A', email: 'a@b.com' }) // POST /users
254
- User.patch(1, { name: 'B' }) // PATCH /users/:id
255
- User.remove(1) // DELETE /users/:id
295
+ // INSERT ... RETURNING * auto-strips serial id
296
+ const user = await users.insert({ name: 'Alice', email: 'alice@test.com' })
297
+ // { id: 1, name: 'Alice', email: 'alice@test.com', active: true, ... }
298
+
299
+ // SELECT ... WHERE id = ? LIMIT 1
300
+ const found = await users.findById(1)
301
+
302
+ // SELECT ... WHERE ... [ORDER BY ...] [LIMIT ...] [OFFSET ...]
303
+ const admins = await users.find({ role: 'admin' })
304
+ const sorted = await users.find({ active: true }, { orderBy: { name: 'asc' } })
305
+ const page = await users.find(undefined, { limit: 10, offset: 0 })
306
+ const filtered = await users.find({ role: 'admin' }, { orderBy: { name: 'desc' }, limit: 5 })
307
+
308
+ // UPDATE ... SET ... WHERE ... RETURNING *
309
+ const updated = await users.update({ id: 1 }, { name: 'Bob' })
310
+ // With SQL expressions:
311
+ await users.update({ id: 1 }, { name: 'Bob', updated_at: sql`NOW()` })
312
+
313
+ // DELETE ... WHERE ... RETURNING 1
314
+ const ok = await users.delete({ id: 1 })
315
+ ```
316
+
317
+ When using `pgTable()` directly (without `pg`), pass `sql` as the first argument:
318
+
319
+ ```ts
320
+ const t = pgTable('_users', { ... })
321
+ await t.insert(ctx.sql, { name: 'Alice' })
322
+ await t.find(ctx.sql, { role: 'admin' }, { orderBy: { name: 'asc' } })
256
323
  ```
257
324
 
258
- Every method validates input against your zod schema automatically. Complex queries use `ctx.sql`:
325
+ ### Complex queries use raw SQL
259
326
 
260
327
  ```ts
261
328
  app.get('/users/stats', async (req, ctx) => {
262
329
  const rows = await ctx.sql`
263
330
  SELECT u.*, count(p.id) as posts
264
- FROM users u LEFT JOIN posts p ON p.user_id = u.id
331
+ FROM ${users} u LEFT JOIN posts p ON p.user_id = u.id
265
332
  GROUP BY u.id
266
333
  `
267
334
  return Response.json(rows)
268
335
  })
269
336
  ```
270
337
 
271
- ### Migration-free sync
338
+ ### Transactions
272
339
 
273
- `pg.migrate()` queries `information_schema.columns` and only runs the DDL needed:
340
+ ```ts
341
+ const result = await pg.transaction(async (tx) => {
342
+ const [user] = await tx`INSERT INTO "_users" (...) VALUES (...) RETURNING *`
343
+ const [wallet] = await tx`INSERT INTO "_wallets" ("user_id") VALUES (${user.id}) RETURNING *`
344
+ return { user, wallet }
345
+ })
346
+ ```
274
347
 
275
- - **Table missing** → `CREATE TABLE IF NOT EXISTS`
276
- - **Column missing** → `ALTER TABLE ADD COLUMN IF NOT EXISTS`
277
- - **Existing** → no-op
348
+ ### Connection lifecycle
278
349
 
279
- Safe for production: never drops or alters existing columns. Destructive operations (rename, type change, drop) are done via `ctx.sql`.
350
+ ```ts
351
+ const pg = postgres() // reads DATABASE_URL
352
+ const pg = postgres('postgres://...') // explicit connection
353
+ const pg = postgres({
354
+ connection: 'postgres://...',
355
+ max: 10, // pool size
356
+ ssl: { rejectUnauthorized: false }, // SSL options
357
+ idle_timeout: 30, // idle timeout (s)
358
+ connect_timeout: 10, // connection timeout (s)
359
+ closeTimeout: 5, // close grace period (s)
360
+ signal: ac.signal, // abort → sql.end()
361
+ })
362
+ await pg.close()
363
+ ```
280
364
 
281
- ### Connection lifecycle
365
+ ### Module base class
366
+
367
+ Every database module (`opencode`, `messager`, `tenant`, `agent`, `user`) extends `PgModule`:
368
+
369
+ ```ts
370
+ import { PgModule } from 'weifuwu'
371
+
372
+ class MyModule extends PgModule {
373
+ constructor(pg: PostgresClient) {
374
+ super(pg) // sets this.sql = pg.sql
375
+ }
376
+ async migrate() { /* override */ }
377
+ // close() inherited — calls pg.close() automatically
378
+ }
379
+ ```
380
+
381
+ ## Opencode
382
+
383
+ AI programming assistant — chat with LLM agents that have access to filesystem tools, skills, and isolated session workspaces.
282
384
 
283
385
  ```ts
284
- const pg = postgres() // reads DATABASE_URL
285
- const pg = postgres('postgres://...') // explicit connection
286
- const pg = postgres({ signal: ac.signal }) // abort → sql.end()
287
- await pg.close() // explicit close
386
+ import { serve, Router, postgres, opencode } from 'weifuwu'
387
+
388
+ const app = new Router()
389
+ const pg = postgres()
390
+ const oc = await opencode({ pg, permissions: { ... } })
391
+
392
+ await oc.migrate()
393
+ app.use('/opencode', await oc.router())
394
+ app.ws('/opencode', oc.wsHandler())
395
+
396
+ serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
397
+ ```
398
+
399
+ ### Session-isolated workspaces
400
+
401
+ Each session gets its own sandbox directory — tools operate within it, files cannot escape:
402
+
288
403
  ```
404
+ cwd/.sessions/opencode/1/ ← session 1's workspace
405
+ cwd/.sessions/opencode/2/ ← session 2's workspace
406
+ cwd/.sessions/chat/3/ ← different mount point
407
+ ```
408
+
409
+ Workspaces are computed from `cwd { ctx.mountPath } { sessionId }`. The system prompt shows the session's workspace so the LLM knows where it is.
289
410
 
290
- ### Primary keys
411
+ ### Tools
291
412
 
292
- | zod field | PostgreSQL |
293
- |-----------|-----------|
294
- | `id: z.number().optional()` | `SERIAL PRIMARY KEY` |
295
- | `id: z.string().uuid().optional()` | `UUID PRIMARY KEY DEFAULT gen_random_uuid()` |
296
- | `id: z.string()` | `TEXT PRIMARY KEY` (you pass the value) |
413
+ | Tool | Description |
414
+ |------|-------------|
415
+ | `bash` | Execute shell commands in the workspace |
416
+ | `read` | Read files with offset/limit |
417
+ | `write` | Create or overwrite files |
418
+ | `edit` | Exact string replacements |
419
+ | `grep` | Regex content search |
420
+ | `glob` | Glob pattern file search |
421
+ | `web` | Fetch URL content |
422
+ | `question` | Ask the user for input |
423
+ | `skill` | Load a skill on demand |
297
424
 
298
- ## Authentication
425
+ ### Skills
299
426
 
300
- Built-in user management password login, JWT, and OAuth2 Server. Zero config beyond PostgreSQL and a secret key.
427
+ Skills are discovered from filesystem and loaded on demand via the `skill` tool no system prompt bloat:
428
+
429
+ - Project: `.opencode/skills/{name}/SKILL.md`
430
+ - Global: `~/.config/opencode/skills/{name}/SKILL.md`
431
+ - Also reads: `.claude/skills/`, `.agents/skills/` (project + global)
432
+
433
+ ```ts
434
+ const oc = await opencode({
435
+ pg,
436
+ skills: [{ name: 'git', description: 'Git workflow', content: '...' }],
437
+ })
438
+ ```
439
+
440
+ ### Permissions
441
+
442
+ Control tool access per conversation:
443
+
444
+ ```ts
445
+ const oc = await opencode({
446
+ pg,
447
+ permissions: {
448
+ bash: { allow: true },
449
+ read: { allow: true },
450
+ write: { allow: false },
451
+ edit: { allow: false },
452
+ skill: { '*': { allow: true }, 'internal-*': { allow: false } },
453
+ },
454
+ })
455
+ ```
456
+
457
+ ### Workspace isolation
458
+
459
+ ```ts
460
+ const oc = await opencode({ pg, permissions })
461
+ // All sessions inherit the instance's workspace (default: process.cwd())
462
+ // Sessions cannot override their workspace
463
+ // Different mount points = different opencode() instances = isolated workspaces
464
+ ```
301
465
 
302
466
  ```ts
303
467
  import { serve, Router, postgres, user } from 'weifuwu'
@@ -570,7 +734,7 @@ await fetch('http://localhost/api/sys/tenants/invite', {
570
734
 
571
735
  ## AI Agent
572
736
 
573
- Server-side AI agents with OpenAI-compatible API. Built-in chat, workflow (tool-calling), and knowledge (RAG) types. Works out of the box with Ollama or any OpenAI-compatible provider.
737
+ Server-side AI agents with OpenAI-compatible API. Built-in chat, tool-use (tool-calling), and knowledge (RAG) types. Works out of the box with Ollama or any OpenAI-compatible provider.
574
738
 
575
739
  ```ts
576
740
  import { agent } from 'weifuwu'
@@ -584,7 +748,7 @@ app.use('/api', agents.router())
584
748
  | Type | Description | Execution |
585
749
  |------|-------------|-----------|
586
750
  | `chat` | Pure conversation | `streamText()` / `generateText()` |
587
- | `workflow` | Tool-calling agent | `streamText({ tools })` |
751
+ | `tool-use` | Tool-calling agent | `streamText({ tools })` |
588
752
 
589
753
  ### Knowledge (RAG)
590
754
 
@@ -721,18 +885,18 @@ app.use('/chat', ai(async (req, ctx) => {
721
885
  serve(app.handler(), { port: 3000 })
722
886
  ```
723
887
 
724
- ## Workflow
888
+ ## runWorkflow
725
889
 
726
- Define business capabilities as **Tools** (`tool()`), then chain them into **workflows** for AI-driven multi-step execution. Works with or without an LLM hand-write the workflow JSON or let AI generate it from a goal.
890
+ Multi-step DAG execution engine packaged as a single AI SDK `Tool`. Use it with `streamText()` or `generateText()` when the LLM needs conditional logic, loops, or multi-step tool orchestration.
727
891
 
728
892
  ```ts
729
- import { Router, tool, workflow } from 'weifuwu'
893
+ import { tool, streamText } from 'ai'
894
+ import { runWorkflow } from 'weifuwu'
730
895
  import { z } from 'zod'
731
896
 
732
- // 1. Define tools (business capabilities)
733
897
  const tools = {
734
898
  queryUser: tool({
735
- description: 'Query user info, returns email, name',
899
+ description: 'Query user info',
736
900
  inputSchema: z.object({ userId: z.string() }),
737
901
  execute: async ({ userId }) => ({ id: userId, email: 'user@test.com', name: 'Test' }),
738
902
  }),
@@ -741,139 +905,57 @@ const tools = {
741
905
  inputSchema: z.object({ to: z.string(), subject: z.string() }),
742
906
  execute: async ({ to, subject }) => ({ sent: true }),
743
907
  }),
908
+ runWF: runWorkflow({ tools: { queryUser, sendEmail } }),
744
909
  }
745
910
 
746
- // 2. Mount workflow sub-router
747
- const app = new Router()
748
- app.use('/agent', workflow(() => ({ tools })))
749
- // POST /agent { nodes: [...] } → 200 { workflow: {...}, result: ... }
750
-
751
- // With SSE streaming:
752
- app.use('/agent-stream', workflow(() => ({ tools, stream: true })))
753
- // POST /agent-stream { nodes: [...] }
754
- // → 200 { workflowId: "xxx", eventsUrl: "/xxx/events" }
755
- // GET /agent-stream/:workflowId/events
756
- // → SSE: workflow-start → node-start → node-end → complete
757
-
758
- // With LLM model (generates workflow from goal):
759
- app.use('/agent-llm', workflow(() => ({
911
+ // Use in any streamText call — the LLM can decide when to trigger a workflow
912
+ const result = await streamText({
913
+ model,
760
914
  tools,
761
- model: openai('gpt-4o'),
762
- })))
763
- // POST /agent-llm { goal: "给用户123发欢迎邮件" }
764
- // ← LLM generates → executes → returns result
765
- ```
766
-
767
- ### Tool
768
-
769
- ```ts
770
- import { tool } from 'weifuwu'
771
- import { z } from 'zod'
772
-
773
- const myTool = tool({
774
- description: '做什么的,返回什么',
775
- inputSchema: z.object({ key: z.string() }),
776
- execute: async (input, ctx) => {
777
- return { result: input.key }
778
- },
779
- })
780
- ```
781
-
782
- `ctx.onStream` 用于流式推送(如 LLM token 输出):
783
-
784
- ```ts
785
- const llmTool = tool({
786
- description: '生成文本',
787
- inputSchema: z.object({ prompt: z.string() }),
788
- execute: async (input, ctx) => {
789
- const stream = await openai.chat.completions.create({ ... })
790
- let full = ''
791
- for await (const chunk of stream) {
792
- full += chunk.choices[0]?.delta?.content || ''
793
- ctx.onStream?.({ type: 'llm-stream', chunk, accumulated: full })
794
- }
795
- return { text: full }
796
- },
915
+ messages: [{ role: 'user', content: '查询用户123,如果存在则发送欢迎邮件' }],
797
916
  })
798
917
  ```
799
918
 
800
- ### Core Nodes
919
+ ### Node types
801
920
 
802
- 7 built-in node types:
921
+ 7 built-in node types for defining the execution graph:
803
922
 
804
923
  | Node | Purpose | Input |
805
924
  |------|---------|-------|
806
- | `call` | Call a tool or sub-workflow | `{ tool: "name", args: {...} }` or `{ function: "name", args: {...} }` |
807
- | `set` | Declare or assign a variable | `{ name: "x", value: 42 }` |
925
+ | `call` | Call a registered AI SDK Tool | `{ tool: "name", args: {...} }` |
926
+ | `set` | Assign a variable | `{ name: "x", value: 42 }` |
808
927
  | `get` | Read a variable | `{ name: "x" }` |
809
928
  | `eval` | Evaluate an expression | `{ expression: "$var.x + 1" }` |
810
929
  | `if` | Conditional branch | `{ conditions: [{ test: ..., body: [nodes] }] }` |
811
930
  | `while` | Loop | `{ condition: "$var.i < 5" }, body: [nodes]` |
812
931
  | `http` | HTTP request | `{ url: "https://...", method: "GET" }` |
813
932
 
814
- ### Variable Reference Syntax
933
+ ### Reference syntax
815
934
 
816
935
  | Pattern | Meaning | Example |
817
936
  |---------|---------|---------|
818
937
  | `$var.x` | Variable `x` | `$var.counter` |
819
938
  | `$nodes.u.output` | Full output of node `u` | `$nodes.u.output` |
820
939
  | `$nodes.u.output.field` | Specific field | `$nodes.u.output.email` |
821
- | `$input.userId` | Workflow input param | `$input.userId` |
822
- | `42`, `true`, `"hello"` | Literal values | Passed as-is |
823
-
824
- ### Engine API
825
-
826
- For programmatic use outside of Router:
827
-
828
- ```ts
829
- import { createWorkflowEngine, createSSEManager } from 'weifuwu'
830
-
831
- const sse = createSSEManager()
832
- const engine = createWorkflowEngine({ tools, sseManager: sse })
940
+ | `$input.userId` | Input param | `$input.userId` |
833
941
 
834
- // Sync execution
835
- const result = await engine.execute({ nodes: [...] })
942
+ ### LLM generation
836
943
 
837
- // Async execution with SSE
838
- engine.runAsync('wf-1', { nodes: [...] })
839
- ```
840
-
841
- ### SSE Events
944
+ Pass a `model` to `runWorkflow` — the LLM generates the workflow JSON from a goal:
842
945
 
843
946
  ```ts
844
- const sse = createSSEManager()
845
- const stream = sse.createStream('wf-1')
947
+ const runWF = runWorkflow({
948
+ tools: { queryUser, sendEmail },
949
+ model: openai('gpt-4o'),
950
+ })
846
951
 
847
- const reader = stream.getReader()
848
- // event: workflow-start — { workflowId, goal }
849
- // event: node-start — { nodeId, tool, input }
850
- // event: node-end — { nodeId, output }
851
- // event: llm-stream — { nodeId, chunk, accumulated }
852
- // event: complete — { result, duration }
853
- // event: error — { error }
952
+ const result = await streamText({
953
+ model,
954
+ tools: { runWF },
955
+ })
854
956
  ```
855
957
 
856
- ### Sub-workflows
857
-
858
- Define reusable sub-workflows in the `functions` field:
859
-
860
- ```json
861
- {
862
- "functions": {
863
- "double": {
864
- "inputSchema": { "type": "object", "properties": { "x": { "type": "number" } } },
865
- "workflow": {
866
- "nodes": [
867
- { "id": "calc", "tool": "eval", "input": { "expression": "$input.x * 2" } }
868
- ]
869
- }
870
- }
871
- },
872
- "nodes": [
873
- { "id": "call_double", "tool": "call", "input": { "function": "double", "args": { "x": 21 } } }
874
- ]
875
- }
876
- ```
958
+ The LLM calls `runWF` with a goal, and `runWorkflow` internally calls `generateText` to produce the workflow nodes, then executes them.
877
959
 
878
960
  ## React pages with tsx()
879
961
 
@@ -1074,14 +1156,12 @@ export default function NotFound() {
1074
1156
  ## Usage within a full app
1075
1157
 
1076
1158
  ```ts
1077
- import { serve, Router, ai, graphql, workflow } from 'weifuwu'
1078
- import { tsx } from 'weifuwu/tsx'
1159
+ import { serve, Router, ai, graphql } from 'weifuwu'
1079
1160
 
1080
1161
  const app = new Router()
1081
1162
  app.use('/', await tsx({ dir: './pages/' }))
1082
1163
  app.use('/chat', ai(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages })))
1083
1164
  app.use('/graphql', graphql(() => ({ schema: `type Query { hello: String }`, resolvers: { Query: { hello: () => 'world' } } })))
1084
- app.use('/agent', workflow(() => ({ tools: myTools, stream: true })))
1085
1165
  app.ws('/chat', { message(ws, _, data) { ws.send(data) } })
1086
1166
 
1087
1167
  serve(app.handler(), { websocket: app.websocketHandler() })
@@ -1214,10 +1294,25 @@ Returns `TenantModule` — `{ migrate, middleware, router, graphql, close }`.
1214
1294
  | `model` | env `OPENAI_MODEL` → Ollama | `LanguageModel` from ai SDK |
1215
1295
  | `embeddingModel` | env `OPENAI_EMBEDDING_MODEL` → Ollama | `EmbeddingModel` for knowledge RAG |
1216
1296
  | `embeddingDimension` | `1024` | Vector dimension for pgvector |
1217
- | `tools` | — | Tools for workflow-type agents (ai SDK `Tool` objects) |
1297
+ | `tools` | — | Tools for tool-use agents (ai SDK `Tool` objects) |
1218
1298
 
1219
1299
  Returns `AgentModule` — `{ migrate, router, run, addKnowledge, close }`.
1220
1300
 
1301
+ ### `opencode(options)`
1302
+
1303
+ | Option | Default | Description |
1304
+ |--------|---------|-------------|
1305
+ | `pg` | — | PostgreSQL client from `postgres()` |
1306
+ | `workspace` | `process.cwd()` | Base directory for `.sessions` |
1307
+ | `model` | `'deepseek-v4-flash'` | LLM model name |
1308
+ | `baseURL` | env `DEEPSEEK_BASE_URL` | API base URL |
1309
+ | `apiKey` | env `DEEPSEEK_API_KEY` | API key |
1310
+ | `systemPrompt` | — | Custom system prompt |
1311
+ | `skills` | `[]` | Static skill definitions |
1312
+ | `permissions` | — | Tool permission config |
1313
+
1314
+ Returns `OpencodeModule` — `{ migrate, router, wsHandler, close }`.
1315
+
1221
1316
  ### `messager(options)`
1222
1317
 
1223
1318
  | Option | Default | Description |
@@ -1278,16 +1373,17 @@ serve(app.handler(), { websocket: app.websocketHandler() })
1278
1373
 
1279
1374
  | Import | Description |
1280
1375
  |--------|-------------|
1281
- | `postgres(options?)` | PostgreSQL connection + auto-migration + 6 CRUD methods |
1376
+ | `postgres(options?)` | PostgreSQL connection + DDL schema builder + transactions + module lifecycle |
1282
1377
  | `redis(options?)` | Redis client (ioredis) — injects `ctx.redis` |
1283
1378
  | `queue(options?)` | Redis-backed job queue — immediate, delayed, cron scheduling |
1284
1379
  | `user(options)` | Built-in authentication (password + OAuth2 Server + JWT, middleware) |
1285
1380
  | `tenant(options)` | Multi-tenant BaaS — dynamic tables, REST + GraphQL auto-generation, row-level isolation |
1286
- | `agent(options)` | AI Agent — chat/workflow/knowledge, Ollama-ready, programmatic API |
1381
+ | `agent(options)` | AI Agent — chat/tool-use/knowledge, Ollama-ready, programmatic API |
1287
1382
  | `messager(options)` | Real-time messaging — channels, WebSocket, agent routing, webhooks |
1383
+ | `opencode(options)` | AI programming assistant — chat agents with tools, skills, permissions, isolated workspaces |
1288
1384
  | `graphql(handler)` | GraphQL endpoint (GET/POST + GraphiQL) |
1289
1385
  | `ai(handler)` | AI streaming endpoint (POST) |
1290
- | `workflow(handler)` | Workflow engine (POST + SSE) |
1386
+ | `runWorkflow(options)` | DAG execution engine as an AI SDK `Tool` — use with `streamText()` |
1291
1387
 
1292
1388
  ### Deploy
1293
1389
 
@@ -1305,9 +1401,14 @@ serve(app.handler(), { websocket: app.websocketHandler() })
1305
1401
  | `setCookie(res, name, value, options?)` | Set cookie (returns new Response) |
1306
1402
  | `deleteCookie(res, name)` | Delete cookie (returns new Response) |
1307
1403
  | `useTsx()` | Hook returning `{ params, query, user, parsed }` from `TsxContext` |
1308
- | `createWorkflowEngine(options)` | Programmatic workflow engine |
1309
- | `createSSEManager()` | SSE event manager for workflows |
1310
- | `tool(def)` | Define a workflow tool |
1404
+ | `runWorkflow(options)` | Create a DAG execution AI SDK `Tool` — `{ tools?, model?, maxSteps? }` |
1405
+ | `pgTable(name, columns)` | Type-safe table schema definition with DDL + CRUD |
1406
+ | `pg.table(name, columns)` | Pre-bound table (no `sql` parameter needed for CRUD) |
1407
+ | `serial()`, `uuid()`, `text()`, `integer()`, `boolean()`, `timestamptz()`, `jsonb()`, `textArray()`, `vector()` | Column type builders |
1408
+ | `sql(strings, ...)` | SQL expression literal for defaults and SET values (e.g. `sql\`NOW()\``) |
1409
+ | `PgModule` | Base class for database-backed modules (provides `sql`, `close()`) |
1410
+ | `BoundTable` | Table with pre-bound `sql` — returned by `pg.table()` |
1411
+ | `FindOptions` | Query options: `{ orderBy?, limit?, offset? }` for `find()` |
1311
1412
 
1312
1413
  Import `useTsx` and `TsxContext` from `'weifuwu'`.
1313
1414
 
@@ -1,4 +1,4 @@
1
- import type { Sql } from 'postgres';
1
+ import type { Sql } from '../vendor.ts';
2
2
  export interface MigrateOptions {
3
3
  sql: Sql<{}>;
4
4
  embeddingDimension: number;
@@ -1,4 +1,4 @@
1
- import type { Sql } from 'postgres';
1
+ import type { Sql } from '../vendor.ts';
2
2
  import { Router } from '../router.ts';
3
3
  import type { RunParams } from './types.ts';
4
4
  interface RestDeps {
@@ -1,5 +1,5 @@
1
- import type { LanguageModel, EmbeddingModel, Tool } from 'ai';
2
- import type { Sql } from 'postgres';
1
+ import { type LanguageModel, type EmbeddingModel, type Tool } from 'ai';
2
+ import type { Sql } from '../vendor.ts';
3
3
  import type { RunParams, RunResult, KnowledgeDoc } from './types.ts';
4
4
  interface RunnerDeps {
5
5
  sql: Sql<{}>;
@@ -4,7 +4,7 @@ export interface AgentConfig {
4
4
  tenant_id: string | null;
5
5
  name: string;
6
6
  description: string;
7
- type: 'chat' | 'workflow';
7
+ type: 'chat' | 'tool-use';
8
8
  model: string;
9
9
  system_prompt: string;
10
10
  owner_id: number;
@@ -36,7 +36,7 @@ export type RunResult = {
36
36
  stream: ReadableStream<Uint8Array>;
37
37
  };
38
38
  export interface AgentOptions {
39
- pg: any;
39
+ pg: import('../postgres/types.ts').PostgresClient;
40
40
  model?: LanguageModel;
41
41
  embeddingModel?: EmbeddingModel;
42
42
  embeddingDimension?: number;
@@ -0,0 +1,14 @@
1
+ import type { LanguageModel } from 'ai';
2
+ export declare function runWorkflow(opts?: {
3
+ tools?: Record<string, any>;
4
+ model?: LanguageModel;
5
+ maxSteps?: number;
6
+ }): import("ai").Tool<{
7
+ goal: string;
8
+ nodes?: any[];
9
+ }, {
10
+ result: unknown;
11
+ nodeOutputs: {
12
+ [k: string]: unknown;
13
+ };
14
+ }>;