diff --git a/docs/plans/2026-05-12-react-agent.md b/docs/plans/2026-05-12-react-agent.md index e15c96d..96c933e 100644 --- a/docs/plans/2026-05-12-react-agent.md +++ b/docs/plans/2026-05-12-react-agent.md @@ -1,6 +1,6 @@ # workflow-agent-react — ReAct Agent Package -**Status**: RFC v2 +**Status**: RFC v3 **Author**: 小橘 🍊 ## Problem @@ -25,8 +25,6 @@ Agent(ctx) → string → Extract(string, schema) → meta // 浪费一轮 LLM ``` -对于内置 ReAct agent,我们完全可以把 schema 作为 resolve tool 注入循环,agent 直接按 schema 输出结构化结果,**零额外 LLM 调用**。 - ### 新抽象:AdapterFn ```typescript @@ -43,7 +41,7 @@ prompt 和 schema 是一对:prompt 说"你要输出什么",schema 定义"输 ### AgentContext 不再需要 -现有 `AgentContext` 在 `ThreadContext` 上扩展了 `currentRole: { name, systemPrompt }`。prompt 现在直接传给 adapter,context 只需要 thread 信息,因此 `AgentContext` 可以删除。 +`AgentContext` 在 `ThreadContext` 上扩展了 `currentRole: { name, systemPrompt }`。prompt 现在直接传给 adapter,`AgentContext` 可以删除。 ### createWorkflow 签名变更 @@ -53,17 +51,15 @@ type AgentBinding = { agent: AgentFn; overrides: Partial> | null; }; -function createWorkflow(def, binding: AgentBinding): WorkflowFn; // After type AdapterBinding = { adapter: AdapterFn; overrides: Partial> | null; }; -function createWorkflow(def, binding: AdapterBinding): WorkflowFn; ``` -`createWorkflow` 对每个 role 的执行逻辑: +engine 对每个 role 的执行逻辑: ```typescript // Before @@ -75,11 +71,15 @@ const roleFn = adapter(role.systemPrompt, role.metaSchema); const meta = await roleFn(threadCtx); // 直接拿到类型安全的 T ``` -## AdapterFn 实现 +## `createReactAdapter` — 复用 workflow-reactor -### 1. `createReactAdapter`(本 RFC 核心) +AdapterFn 的终止条件是"拿到符合 schema 的 T"——和 `workflow-reactor` 的 `ThreadReactorFn` 完全一致。因此 react adapter 是对 reactor 的**薄包装**,不需要自己实现 ReAct 循环。 ```typescript +import { createLlmFn, createThreadReactor } from "@uncaged/workflow-reactor"; +import type { ThreadContext, LlmProvider } from "@uncaged/workflow-protocol"; +import type { ToolDefinition } from "@uncaged/workflow-reactor"; + type ReactToolHandler = (name: string, args: string) => Promise; type ReactAdapterConfig = { @@ -89,19 +89,31 @@ type ReactAdapterConfig = { maxRounds: number; }; -function createReactAdapter(config: ReactAdapterConfig): AdapterFn; +function createReactAdapter(config: ReactAdapterConfig): AdapterFn { + return (prompt: string, schema: z.ZodType) => { + const reactor = createThreadReactor({ + llm: createLlmFn(config.provider), + staticTools: config.tools, + structuredToolFromSchema: (s) => buildStructuredTool(s), + systemPromptForStructuredTool: () => prompt, + toolHandler: (call, ctx) => + config.toolHandler(call.function.name, call.function.arguments), + maxRounds: config.maxRounds, + }); + + return async (ctx: ThreadContext): Promise => { + const input = buildThreadInput(ctx); + const result = await reactor({ thread: ctx, input, schema }); + if (!result.ok) throw new Error(result.error); + return result.value; + }; + }; +} ``` -内部实现: -1. 接收 `(prompt, schema)` → 生成 resolve tool(schema → JSON Schema → tool definition) -2. 返回 `RoleFn`,执行时: - - 用 `prompt` 作为 system message - - 用 thread history 构造 user message - - 进入 ReAct 循环:LLM 调用工具 → 执行 → 继续 - - 当 LLM 调用 resolve tool → 校验 schema → 返回 `T` - - 纯文本回复视为错误(prompt 要求 agent 最终调用 resolve) +整个包就是:**一个工厂函数 + 类型定义 + thread 输入构造**。 -### 2. `agentToAdapter`(向后兼容包装器) +## `agentToAdapter` — 向后兼容 把现有 `AgentFn`(hermes/cursor)包装成 `AdapterFn`: @@ -109,98 +121,34 @@ function createReactAdapter(config: ReactAdapterConfig): AdapterFn; function agentToAdapter(agent: AgentFn, extractProvider: LlmProvider): AdapterFn { return (prompt: string, schema: z.ZodType): RoleFn => { return async (ctx: ThreadContext): Promise => { - // 重建 AgentContext 给旧 agent 用 const agentCtx = { ...ctx, currentRole: { name: "agent", systemPrompt: prompt } }; - const output = typeof result === "string" ? result : result.output; const result = await agent(agentCtx); - // 走 extract 流程(保持现有行为) + const output = typeof result === "string" ? result : result.output; return extract(output, schema, extractProvider); }; }; } ``` -这样 hermes/cursor agent 无需改动,只是在 bundle-entry 层多包一层。 - -### 3. `createLlmAdapter`(单轮 chat) - -```typescript -function createLlmAdapter(provider: LlmProvider): AdapterFn { - return (prompt: string, schema: z.ZodType): RoleFn => { - return async (ctx: ThreadContext): Promise => { - // 单轮 chat,要求 JSON output - // 用 schema 做 response_format 或 parse 校验 - }; - }; -} -``` - -## ReAct 循环细节 - -### 终止条件 - -与 reactor(structured extraction)不同,react adapter 的终止条件是 **agent 调用 resolve tool**: - -``` -messages = [system(prompt), user(threadHistory)] -for round in 0..maxRounds: - response = llm({ messages, tools: [...userTools, resolveTool] }) - assistant = parseAssistantMessage(response) - for each tool_call in assistant.tool_calls: - if tool_call.name == "resolve": - validate(tool_call.arguments, schema) - if valid: return parsed_value - else: push error feedback, continue loop - else: - result = toolHandler(name, arguments) - push tool result - if no tool_calls: - // 纯文本回复 → 提醒 agent 必须调用 resolve - push correction message, continue loop -throw Error("max rounds exceeded") -``` - -### resolve tool 生成 - -```typescript -function buildResolveTool(schema: z.ZodType): ToolDefinition { - return { - type: "function", - function: { - name: "resolve", - description: "Submit the final structured output for this role. Call this when the task is complete.", - parameters: zodToJsonSchema(schema), - }, - }; -} -``` - -### 与 reactor 的关系 - -- **复用**:`LlmFn` / `createLlmFn`、`ToolDefinition` / `ToolCall` / `ChatMessage` 类型 -- **不复用**:reactor 的 ReAct 循环(终止条件不同)、assistant 消息解析(reactor 有 plain JSON fallback 等多余逻辑) -- **不修改 reactor**:react-agent 自己实现解析(~30 行),保持 reactor 专注 structured extraction +hermes/cursor agent 内部不改,bundle-entry 层多包一层即可。 ## 包结构 ``` packages/workflow-agent-react/ src/ - types.ts # ReactAdapterConfig, ReactToolHandler - resolve-tool.ts # buildResolveTool (zod → tool definition) - parse-assistant.ts # assistant message 解析 - react-loop.ts # ReAct 循环核心 - create-react-adapter.ts # AdapterFn 工厂 + types.ts # ReactAdapterConfig, ReactToolHandler + create-react-adapter.ts # AdapterFn 工厂(包装 reactor) + thread-input.ts # ThreadContext → user message string index.ts __tests__/ - react-loop.test.ts + create-react-adapter.test.ts package.json ``` 依赖: - `@uncaged/workflow-protocol` — `ThreadContext`, `LlmProvider` -- `@uncaged/workflow-reactor` — `LlmFn`, `createLlmFn`, `ChatMessage`, `ToolDefinition`, `ToolCall` -- `zod` — schema +- `@uncaged/workflow-reactor` — `createLlmFn`, `createThreadReactor`, types ## 影响范围 @@ -209,32 +157,30 @@ packages/workflow-agent-react/ | 改动 | 影响 | |------|------| | `AgentBinding` → `AdapterBinding` | `createWorkflow` 调用方(所有 bundle-entry) | -| `AgentContext` 删除 | `buildAgentPrompt`(util-agent)需改为接收 `ThreadContext` | -| extract 从 engine 下沉到 adapter | `workflow-execute` 的 engine 简化 | +| `AgentContext` 删除 | `buildAgentPrompt`(util-agent)改为接收 `ThreadContext` | +| extract 从 engine 下沉到 adapter | `workflow-execute` 简化 | ### 需修改的包 -1. `workflow-protocol` — 删除 `AgentContext`,新增 `AdapterFn` / `RoleFn` / `AdapterBinding` +1. `workflow-protocol` — 删除 `AgentContext`/`AgentFn`/`AgentFnResult`/`AgentBinding`,新增 `AdapterFn`/`RoleFn`/`AdapterBinding` 2. `workflow-runtime` — 更新 re-export -3. `workflow-execute` — engine 调用 `adapter(prompt, schema)` 替代 `agent(ctx)` + `extract` -4. `workflow-util-agent` — `buildAgentPrompt` 改为接收 `ThreadContext` -5. `workflow-agent-hermes` / `workflow-agent-cursor` — 不改内部,在 util 层提供 `agentToAdapter` -6. 所有 bundle-entry — `agent:` → `adapter:` +3. `workflow-execute` — engine 调用 `adapter(prompt, schema)` 替代 `agent(ctx) + extract` +4. `workflow-util-agent` — `buildAgentPrompt` → `buildThreadInput`,接收 `ThreadContext` +5. 所有 bundle-entry — `agent:` → `adapter:` ### 不受影响 - `workflow-cas` / `workflow-register` / `workflow-reactor` / `workflow-dashboard` +- `workflow-agent-hermes` / `workflow-agent-cursor`(内部不改,外部用 `agentToAdapter` 包装) ## Phases -1. **Phase 1**: protocol 层类型定义 + `createWorkflow` 签名变更 + `agentToAdapter` 兼容包装 -2. **Phase 2**: `workflow-agent-react` 包 — ReAct 循环 + resolve tool + 测试 +1. **Phase 1**: protocol 类型 + `createWorkflow` 签名变更 + `agentToAdapter` +2. **Phase 2**: `workflow-agent-react` 包(包装 reactor) 3. **Phase 3**: 工具集实现(read/write/patch/shell) + smoke test 闭环 ## 工具集(后续讨论) -最小闭环候选,参考 hermes builtin: - | 工具 | 说明 | 优先级 | |------|------|--------| | `read_file` | 读文件 | P0 |