Files
united-workforce/docs/plans/2026-05-12-react-agent.md
T
xingyue 5970456a54
CI / check (pull_request) Failing after 8m30s
refactor: align package folder names with npm package names
Rename packages/ subdirectories to match their @united-workforce/* scope:
  cli-workflow → cli
  workflow-agent-builtin → agent-builtin
  workflow-agent-claude-code → agent-claude-code
  workflow-agent-hermes → agent-hermes
  workflow-dashboard → dashboard
  workflow-protocol → protocol
  workflow-util-agent → util-agent
  workflow-util → util

Updated all tsconfig references, scripts, and active docs.
Historical docs (docs/plans/, docs/superpowers/) left as-is.

Closes #21
2026-06-02 23:45:45 +08:00

6.3 KiB

workflow-agent-react — ReAct Agent Package

Status: RFC v3 Author: 小橘 🍊

Problem

现有的 agent 包都依赖外部 CLI 进程:

Package 机制 能力
agent-hermes spawn hermes chat 完整工具链(文件、终端、浏览器…)
workflow-agent-cursor spawn cursor-agent IDE 级别代码编辑
workflow-agent-llm 单轮 chat completion 纯文本,无工具

缺少一个 内置 ReAct agent:用 LLM + tool calling 循环执行任务,不依赖外部 CLI,工具集由调用方注入。

核心设计变更:AdapterFn 替代 AgentFn

现状的问题

当前 AgentFn 返回 string,engine 再用额外一轮 LLM 调用 extract meta:

Agent(ctx) → string → Extract(string, schema) → meta   // 浪费一轮 LLM

新抽象:AdapterFn

type RoleFn<T> = (ctx: ThreadContext) => Promise<T>;

type AdapterFn = <T>(prompt: string, schema: z.ZodType<T>) => RoleFn<T>;
  • prompt — role 的 system prompt,描述角色职责和输出要求
  • schema — role 的 meta schema,定义输出格式
  • ThreadContext — threadId, depth, bundleHash, start, steps

prompt 和 schema 是一对:prompt 说"你要输出什么",schema 定义"输出的格式"。它们属于 role definition,由 createWorkflow 在每个 role 执行时传给 adapter。

AgentContext 不再需要

AgentContextThreadContext 上扩展了 currentRole: { name, systemPrompt }。prompt 现在直接传给 adapter,AgentContext 可以删除。

createWorkflow 签名变更

// Before
type AgentBinding = {
  agent: AgentFn;
  overrides: Partial<Record<string, AgentFn>> | null;
};

// After
type AdapterBinding = {
  adapter: AdapterFn;
  overrides: Partial<Record<string, AdapterFn>> | null;
};

engine 对每个 role 的执行逻辑:

// Before
const result = await agent({ ...threadCtx, currentRole: { name, systemPrompt } });
const meta = await extract(result, role.metaSchema, provider);  // 额外一轮 LLM

// After
const roleFn = adapter(role.systemPrompt, role.metaSchema);
const meta = await roleFn(threadCtx);  // 直接拿到类型安全的 T

createReactAdapter — 复用 workflow-reactor

AdapterFn 的终止条件是"拿到符合 schema 的 T"——和 workflow-reactorThreadReactorFn 完全一致。因此 react adapter 是对 reactor 的薄包装,不需要自己实现 ReAct 循环。

import { createLlmFn, createThreadReactor } from "@uncaged/workflow-reactor";
import type { ThreadContext, LlmProvider } from "@uncaged/protocol";
import type { ToolDefinition } from "@uncaged/workflow-reactor";

type ReactToolHandler = (name: string, args: string) => Promise<string>;

type ReactAdapterConfig = {
  provider: LlmProvider;
  tools: readonly ToolDefinition[];
  toolHandler: ReactToolHandler;
  maxRounds: number;
};

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 输入构造

agentToAdapter — 向后兼容

把现有 AgentFn(hermes/cursor)包装成 AdapterFn

function agentToAdapter(agent: AgentFn, extractProvider: LlmProvider): AdapterFn {
  return <T>(prompt: string, schema: z.ZodType<T>): RoleFn<T> => {
    return async (ctx: ThreadContext): Promise<T> => {
      const agentCtx = { ...ctx, currentRole: { name: "agent", systemPrompt: prompt } };
      const result = await agent(agentCtx);
      const output = typeof result === "string" ? result : result.output;
      return extract(output, schema, extractProvider);
    };
  };
}

hermes/cursor agent 内部不改,bundle-entry 层多包一层即可。

包结构

packages/workflow-agent-react/
  src/
    types.ts                 # ReactAdapterConfig, ReactToolHandler
    create-react-adapter.ts  # AdapterFn 工厂(包装 reactor)
    thread-input.ts          # ThreadContext → user message string
    index.ts
  __tests__/
    create-react-adapter.test.ts
  package.json

依赖:

  • @uncaged/protocolThreadContext, LlmProvider
  • @uncaged/workflow-reactorcreateLlmFn, createThreadReactor, types

影响范围

Breaking Changes

改动 影响
AgentBindingAdapterBinding createWorkflow 调用方(所有 bundle-entry)
AgentContext 删除 buildAgentPrompt(util-agent)改为接收 ThreadContext
extract 从 engine 下沉到 adapter workflow-execute 简化

需修改的包

  1. protocol — 删除 AgentContext/AgentFn/AgentFnResult/AgentBinding,新增 AdapterFn/RoleFn/AdapterBinding
  2. workflow-runtime — 更新 re-export
  3. workflow-execute — engine 调用 adapter(prompt, schema) 替代 agent(ctx) + extract
  4. util-agentbuildAgentPromptbuildThreadInput,接收 ThreadContext
  5. 所有 bundle-entry — agent:adapter:

不受影响

  • workflow-cas / workflow-register / workflow-reactor / dashboard
  • agent-hermes / workflow-agent-cursor(内部不改,外部用 agentToAdapter 包装)

Phases

  1. Phase 1: protocol 类型 + createWorkflow 签名变更 + agentToAdapter
  2. Phase 2: workflow-agent-react 包(包装 reactor)
  3. Phase 3: 工具集实现(read/write/patch/shell) + smoke test 闭环

工具集(后续讨论)

工具 说明 优先级
read_file 读文件 P0
write_file 写文件 P0
patch_file find-and-replace 编辑 P0
shell_exec 执行 shell 命令 P0
search_files grep / find P1
list_files ls P1