docs: RFC v3 — react adapter as thin wrapper over reactor

小橘 🍊
This commit is contained in:
2026-05-13 02:19:12 +00:00
parent 730340d123
commit 11ba185fef
+47 -101
View File
@@ -1,6 +1,6 @@
# workflow-agent-react — ReAct Agent Package # workflow-agent-react — ReAct Agent Package
**Status**: RFC v2 **Status**: RFC v3
**Author**: 小橘 🍊 **Author**: 小橘 🍊
## Problem ## Problem
@@ -25,8 +25,6 @@
Agent(ctx) → string → Extract(string, schema) → meta // 浪费一轮 LLM Agent(ctx) → string → Extract(string, schema) → meta // 浪费一轮 LLM
``` ```
对于内置 ReAct agent,我们完全可以把 schema 作为 resolve tool 注入循环,agent 直接按 schema 输出结构化结果,**零额外 LLM 调用**。
### 新抽象:AdapterFn ### 新抽象:AdapterFn
```typescript ```typescript
@@ -43,7 +41,7 @@ prompt 和 schema 是一对:prompt 说"你要输出什么",schema 定义"输
### AgentContext 不再需要 ### AgentContext 不再需要
现有 `AgentContext``ThreadContext` 上扩展了 `currentRole: { name, systemPrompt }`。prompt 现在直接传给 adapter,context 只需要 thread 信息,因此 `AgentContext` 可以删除。 `AgentContext``ThreadContext` 上扩展了 `currentRole: { name, systemPrompt }`。prompt 现在直接传给 adapter,`AgentContext` 可以删除。
### createWorkflow 签名变更 ### createWorkflow 签名变更
@@ -53,17 +51,15 @@ type AgentBinding = {
agent: AgentFn; agent: AgentFn;
overrides: Partial<Record<string, AgentFn>> | null; overrides: Partial<Record<string, AgentFn>> | null;
}; };
function createWorkflow(def, binding: AgentBinding): WorkflowFn;
// After // After
type AdapterBinding = { type AdapterBinding = {
adapter: AdapterFn; adapter: AdapterFn;
overrides: Partial<Record<string, AdapterFn>> | null; overrides: Partial<Record<string, AdapterFn>> | null;
}; };
function createWorkflow(def, binding: AdapterBinding): WorkflowFn;
``` ```
`createWorkflow` 对每个 role 的执行逻辑: engine 对每个 role 的执行逻辑:
```typescript ```typescript
// Before // Before
@@ -75,11 +71,15 @@ const roleFn = adapter(role.systemPrompt, role.metaSchema);
const meta = await roleFn(threadCtx); // 直接拿到类型安全的 T const meta = await roleFn(threadCtx); // 直接拿到类型安全的 T
``` ```
## AdapterFn 实现 ## `createReactAdapter` — 复用 workflow-reactor
### 1. `createReactAdapter`(本 RFC 核心) AdapterFn 的终止条件是"拿到符合 schema 的 T"——和 `workflow-reactor``ThreadReactorFn` 完全一致。因此 react adapter 是对 reactor 的**薄包装**,不需要自己实现 ReAct 循环。
```typescript ```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<string>; type ReactToolHandler = (name: string, args: string) => Promise<string>;
type ReactAdapterConfig = { type ReactAdapterConfig = {
@@ -89,19 +89,31 @@ type ReactAdapterConfig = {
maxRounds: number; maxRounds: number;
}; };
function createReactAdapter(config: ReactAdapterConfig): AdapterFn; function createReactAdapter(config: ReactAdapterConfig): AdapterFn {
return <T>(prompt: string, schema: z.ZodType<T>) => {
const reactor = createThreadReactor<ThreadContext>({
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<T> => {
const input = buildThreadInput(ctx);
const result = await reactor({ thread: ctx, input, schema });
if (!result.ok) throw new Error(result.error);
return result.value;
};
};
}
``` ```
内部实现: 整个包就是:**一个工厂函数 + 类型定义 + thread 输入构造**。
1. 接收 `(prompt, schema)` → 生成 resolve tool(schema → JSON Schema → tool definition)
2. 返回 `RoleFn<T>`,执行时:
-`prompt` 作为 system message
- 用 thread history 构造 user message
- 进入 ReAct 循环:LLM 调用工具 → 执行 → 继续
- 当 LLM 调用 resolve tool → 校验 schema → 返回 `T`
- 纯文本回复视为错误(prompt 要求 agent 最终调用 resolve)
### 2. `agentToAdapter`向后兼容包装器) ## `agentToAdapter`向后兼容
把现有 `AgentFn`(hermes/cursor)包装成 `AdapterFn` 把现有 `AgentFn`(hermes/cursor)包装成 `AdapterFn`
@@ -109,98 +121,34 @@ function createReactAdapter(config: ReactAdapterConfig): AdapterFn;
function agentToAdapter(agent: AgentFn, extractProvider: LlmProvider): AdapterFn { function agentToAdapter(agent: AgentFn, extractProvider: LlmProvider): AdapterFn {
return <T>(prompt: string, schema: z.ZodType<T>): RoleFn<T> => { return <T>(prompt: string, schema: z.ZodType<T>): RoleFn<T> => {
return async (ctx: ThreadContext): Promise<T> => { return async (ctx: ThreadContext): Promise<T> => {
// 重建 AgentContext 给旧 agent 用
const agentCtx = { ...ctx, currentRole: { name: "agent", systemPrompt: prompt } }; const agentCtx = { ...ctx, currentRole: { name: "agent", systemPrompt: prompt } };
const output = typeof result === "string" ? result : result.output;
const result = await agent(agentCtx); const result = await agent(agentCtx);
// 走 extract 流程(保持现有行为) const output = typeof result === "string" ? result : result.output;
return extract(output, schema, extractProvider); return extract(output, schema, extractProvider);
}; };
}; };
} }
``` ```
这样 hermes/cursor agent 无需改动,只是在 bundle-entry 层多包一层。 hermes/cursor agent 内部不改,bundle-entry 层多包一层即可
### 3. `createLlmAdapter`(单轮 chat)
```typescript
function createLlmAdapter(provider: LlmProvider): AdapterFn {
return <T>(prompt: string, schema: z.ZodType<T>): RoleFn<T> => {
return async (ctx: ThreadContext): Promise<T> => {
// 单轮 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<T>(schema: z.ZodType<T>): 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
## 包结构 ## 包结构
``` ```
packages/workflow-agent-react/ packages/workflow-agent-react/
src/ src/
types.ts # ReactAdapterConfig, ReactToolHandler types.ts # ReactAdapterConfig, ReactToolHandler
resolve-tool.ts # buildResolveTool (zod → tool definition) create-react-adapter.ts # AdapterFn 工厂(包装 reactor)
parse-assistant.ts # assistant message 解析 thread-input.ts # ThreadContext → user message string
react-loop.ts # ReAct 循环核心
create-react-adapter.ts # AdapterFn 工厂
index.ts index.ts
__tests__/ __tests__/
react-loop.test.ts create-react-adapter.test.ts
package.json package.json
``` ```
依赖: 依赖:
- `@uncaged/workflow-protocol``ThreadContext`, `LlmProvider` - `@uncaged/workflow-protocol``ThreadContext`, `LlmProvider`
- `@uncaged/workflow-reactor``LlmFn`, `createLlmFn`, `ChatMessage`, `ToolDefinition`, `ToolCall` - `@uncaged/workflow-reactor``createLlmFn`, `createThreadReactor`, types
- `zod` — schema
## 影响范围 ## 影响范围
@@ -209,32 +157,30 @@ packages/workflow-agent-react/
| 改动 | 影响 | | 改动 | 影响 |
|------|------| |------|------|
| `AgentBinding``AdapterBinding` | `createWorkflow` 调用方(所有 bundle-entry) | | `AgentBinding``AdapterBinding` | `createWorkflow` 调用方(所有 bundle-entry) |
| `AgentContext` 删除 | `buildAgentPrompt`(util-agent)改为接收 `ThreadContext` | | `AgentContext` 删除 | `buildAgentPrompt`(util-agent)改为接收 `ThreadContext` |
| extract 从 engine 下沉到 adapter | `workflow-execute` 的 engine 简化 | | 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 2. `workflow-runtime` — 更新 re-export
3. `workflow-execute` — engine 调用 `adapter(prompt, schema)` 替代 `agent(ctx)` + `extract` 3. `workflow-execute` — engine 调用 `adapter(prompt, schema)` 替代 `agent(ctx) + extract`
4. `workflow-util-agent``buildAgentPrompt` 改为接收 `ThreadContext` 4. `workflow-util-agent``buildAgentPrompt` `buildThreadInput`接收 `ThreadContext`
5. `workflow-agent-hermes` / `workflow-agent-cursor` — 不改内部,在 util 层提供 `agentToAdapter` 5. 所有 bundle-entry — `agent:``adapter:`
6. 所有 bundle-entry — `agent:``adapter:`
### 不受影响 ### 不受影响
- `workflow-cas` / `workflow-register` / `workflow-reactor` / `workflow-dashboard` - `workflow-cas` / `workflow-register` / `workflow-reactor` / `workflow-dashboard`
- `workflow-agent-hermes` / `workflow-agent-cursor`(内部不改,外部用 `agentToAdapter` 包装)
## Phases ## Phases
1. **Phase 1**: protocol 类型定义 + `createWorkflow` 签名变更 + `agentToAdapter` 兼容包装 1. **Phase 1**: protocol 类型 + `createWorkflow` 签名变更 + `agentToAdapter`
2. **Phase 2**: `workflow-agent-react` — ReAct 循环 + resolve tool + 测试 2. **Phase 2**: `workflow-agent-react`(包装 reactor)
3. **Phase 3**: 工具集实现(read/write/patch/shell) + smoke test 闭环 3. **Phase 3**: 工具集实现(read/write/patch/shell) + smoke test 闭环
## 工具集(后续讨论) ## 工具集(后续讨论)
最小闭环候选,参考 hermes builtin:
| 工具 | 说明 | 优先级 | | 工具 | 说明 | 优先级 |
|------|------|--------| |------|------|--------|
| `read_file` | 读文件 | P0 | | `read_file` | 读文件 | P0 |