Files
nanoclaw/docs/zh/agent-runner-details.md
2026-05-12 13:14:17 +00:00

28 KiB
Raw Permalink Blame History

NanoClaw Agent-Runner 详解

容器内 agent-runner代理运行器的实现级细节。高层设计请参见 architecture.md

关注点分离

agent-runner 分为两层:

  1. Agent-runner 核心 — 负责 poll loop轮询循环、消息格式化、数据库读写、MCP toolMCP 工具)实现、路由、状态管理、媒体处理。此部分是 NanoClaw 专有的,跨所有 provider提供器共享。

  2. Agent provider代理提供器 — 负责 SDK 交互。接收格式化后的提示词,将其推送到 SDK并回传事件。主干代码内置 claude provider其他 providerOpenCode、Codex 等)通过 /add-<provider> 技能从 providers 分支安装。

边界划分agent-runner 决定发送什么以及如何处理结果。provider 决定如何与 SDK 通信

AgentProvider 接口

interface AgentProvider {
  /** 启动一个新的查询。返回一个用于流式输入和输出的句柄。 */
  query(input: QueryInput): AgentQuery;
}

interface QueryInput {
  /** 初始提示词(已由 agent-runner 格式化)。
   *  纯文本时使用 String。多模态图片、PDF、音频时使用 ContentBlock[]。 */
  prompt: string | ContentBlock[];

  /** 要恢复的会话 ID如有 */
  sessionId?: string;

  /** 从会话的特定位置恢复provider 特定字段,可能被忽略) */
  resumeAt?: string;

  /** 容器内的工作目录 */
  cwd: string;

  /** MCP serverMCP 服务器)配置(标准化格式 — provider 负责转换) */
  mcpServers: Record<string, McpServerConfig>;

  /** 系统提示词 / 开发者指令 */
  systemPrompt?: string;

  /** SDK 进程的环境变量 */
  env: Record<string, string | undefined>;

  /** agent 可访问的额外目录 */
  additionalDirectories?: string[];
}

interface McpServerConfig {
  command: string;
  args: string[];
  env: Record<string, string>;
}

interface AgentQuery {
  /** 将一条后续消息推送到活动查询中 */
  push(message: string): void;

  /** 表示不再发送更多输入 */
  end(): void;

  /** 输出事件流 */
  events: AsyncIterable<ProviderEvent>;

  /** 强制停止查询(例如容器正在关闭) */
  abort(): void;
}

type ProviderEvent =
  | { type: 'init'; sessionId: string }
  | { type: 'result'; text: string | null }
  | { type: 'error'; message: string; retryable: boolean; classification?: string }
  | { type: 'progress'; message: string };

接口不包含的内容

  • 消息格式化 — agent-runner 在传递给 provider 之前格式化消息。provider 接收的是可直接发送的提示词字符串。
  • Hooks钩子 — Claude 特有功能。Claude provider 在内部注册 hooksPreCompact、PreToolUse 等)。其他 provider 不需要。
  • 工具许可列表 — Claude 使用 allowedTools。Codex 使用 approvalPolicy。OpenCode 使用 permission。各 provider 基于相同的意图("允许一切,无需提示")在内部自行配置。
  • 会话持久化 — Claude 自动将会话持久化到磁盘。Codex 和 OpenCode 管理各自的 session 状态。agent-runner 不控制这一点 — 它只传递 sessionIdresumeAt
  • 沙箱配置 — provider 特定。各 provider 在内部配置自己的沙箱。

Provider 事件语义

  • init — 每次查询当 provider 建立或恢复 session 时发送一次。agent-runner 捕获 sessionId 用于后续恢复。
  • result — 当 agent 生成完整响应时发送。每次查询可发送多次(例如 Claude 的多轮子 agent 交互。agent-runner 将每个 result 写入 messages_out
  • error — 失败时发送。retryable 指示 agent-runner 是否应重试。classification 是可选的详细分类(如 'quota''auth''transport')。
  • progress — 可选用于日志记录。agent-runner 记录这些事件但不据此作出行动。

Provider 实现

主干代码仅内置 claude provider。下文的 Codex 和 OpenCode 章节记录了 provider 接口以供参考,以及供安装额外 provider 的技能使用 — 它们并非核心镜像的内置部分。

Claude Provider

封装 @anthropic-ai/claude-agent-sdkquery()

class ClaudeProvider implements AgentProvider {
  query(input: QueryInput): AgentQuery {
    const stream = new MessageStream();  // AsyncIterable<SDKUserMessage>
    stream.push(input.prompt);

    const sdkQuery = query({
      prompt: stream,
      options: {
        cwd: input.cwd,
        resume: input.sessionId,
        resumeSessionAt: input.resumeAt,
        systemPrompt: input.systemPrompt
          ? { type: 'preset', preset: 'claude_code', append: input.systemPrompt }
          : undefined,
        mcpServers: input.mcpServers,  // 已是正确格式
        additionalDirectories: input.additionalDirectories,
        env: input.env,
        allowedTools: NANOCLAW_TOOL_ALLOWLIST,
        permissionMode: 'bypassPermissions',
        allowDangerouslySkipPermissions: true,
        hooks: {
          PreCompact: [{ hooks: [preCompactHook] }],
          PreToolUse: [{ matcher: 'Bash', hooks: [sanitizeBashHook] }],
        },
      },
    });

    return {
      push: (msg) => stream.push(msg),
      end: () => stream.end(),
      abort: () => sdkQuery.close(),
      events: translateClaudeEvents(sdkQuery),
    };
  }
}

translateClaudeEvents 是一个异步生成器,将 SDK 消息映射为 ProviderEvent

  • message.type === 'system' && message.subtype === 'init'{ type: 'init', sessionId }
  • message.type === 'result'{ type: 'result', text }
  • message.type === 'system' && message.subtype === 'api_retry'{ type: 'error', retryable: true }
  • message.type === 'system' && message.subtype === 'rate_limit_event'{ type: 'error', retryable: false, classification: 'quota' }
  • message.type === 'system' && message.subtype === 'task_notification'{ type: 'progress', message }
  • 其他一切 → 仅记录日志,不发送事件

在 provider 内部保留的 Claude 特有功能:

  • MessageStream 用于异步可迭代输入(基于推送模式)
  • resumeSessionAt 用于在特定消息 UUID 处恢复
  • PreCompact hook 用于对话转录归档
  • PreToolUse hook 用于清理 bash 环境变量
  • 完整的工具许可列表
  • additionalDirectories 用于多目录访问

Codex Provider

封装 @openai/codex-sdk

class CodexProvider implements AgentProvider {
  query(input: QueryInput): AgentQuery {
    const codex = new Codex(this.buildOptions(input));
    const thread = input.sessionId
      ? codex.resumeThread(input.sessionId, this.threadOptions(input))
      : codex.startThread(this.threadOptions(input));

    const abortController = new AbortController();
    let pendingFollowUp: string | null = null;

    return {
      push: (msg) => {
        // Codex 不支持流式输入。
        // 存储后续消息并中止当前轮次。
        pendingFollowUp = msg;
        abortController.abort();
      },
      end: () => { /* 无操作 — Codex 轮次自然结束 */ },
      abort: () => abortController.abort(),
      events: this.run(thread, input.prompt, abortController, () => pendingFollowUp),
    };
  }

  private async *run(thread, prompt, abortController, getPendingFollowUp): AsyncIterable<ProviderEvent> {
    let currentPrompt = prompt;

    while (true) {
      try {
        const streamed = await thread.runStreamed(currentPrompt, {
          signal: abortController.signal,
        });

        let sessionId: string | undefined;
        let resultText = '';

        for await (const event of streamed.events) {
          if (event.type === 'thread.started') {
            sessionId = event.thread_id;
            yield { type: 'init', sessionId };
          }
          if (event.type === 'item.completed' && event.item.type === 'agent_message') {
            resultText = event.item.text || resultText;
          }
          if (event.type === 'turn.failed') {
            yield { type: 'error', message: event.error.message, retryable: false };
            return;
          }
        }

        yield { type: 'result', text: resultText || null };

        // 检查本轮次中是否有后续消息排入队列
        const followUp = getPendingFollowUp();
        if (followUp) {
          currentPrompt = followUp;
          // 为下一次迭代重置
          continue;
        }

        return;
      } catch (err) {
        if (abortController.signal.aborted && getPendingFollowUp()) {
          // 因后续消息被中止 — 使用新提示词重启
          currentPrompt = getPendingFollowUp();
          abortController = new AbortController();
          continue;
        }
        throw err;
      }
    }
  }
}

在 provider 内部保留的 Codex 特有行为:

  • developer_instructions 用于系统提示词(从 CLAUDE.md 加载)
  • 工作区中执行 git initCodex 需要 git 仓库)
  • 中止+重启模式处理后续消息
  • 从环境变量中读取 sandboxModeapprovalPolicynetworkAccessEnabled
  • 对话归档Codex 没有 PreCompact

OpenCode Provider

封装 @opencode-ai/sdk

class OpenCodeProvider implements AgentProvider {
  query(input: QueryInput): AgentQuery {
    // OpenCode 运行本地服务器 — 创建一次,跨查询复用
    const { client, server } = await createOpencode({ config: this.buildConfig(input) });
    const { stream } = await client.event.subscribe();

    let aborted = false;
    let pendingFollowUp: string | null = null;

    return {
      push: (msg) => {
        pendingFollowUp = msg;
        server.close();  // 中断当前查询
      },
      end: () => { /* 无操作 */ },
      abort: () => { aborted = true; server.close(); },
      events: this.run(client, server, stream, input, () => pendingFollowUp),
    };
  }

  private async *run(client, server, stream, input, getPendingFollowUp): AsyncIterable<ProviderEvent> {
    const session = await client.session.create();
    yield { type: 'init', sessionId: session.data.id };

    await client.session.promptAsync({
      path: { id: session.data.id },
      body: { parts: [{ type: 'text', text: input.prompt }] },
    });

    for await (const event of stream) {
      if (event.type === 'session.idle') {
        // 从累积的消息部分中收集结果文本
        const resultText = this.extractResult(event);
        yield { type: 'result', text: resultText };

        const followUp = getPendingFollowUp();
        if (followUp) {
          await client.session.promptAsync({
            path: { id: session.data.id },
            body: { parts: [{ type: 'text', text: followUp }] },
          });
          continue;
        }

        return;
      }

      if (event.type === 'session.error') {
        yield { type: 'error', message: event.properties?.error?.data?.message, retryable: false };
        return;
      }
    }
  }
}

在 provider 内部保留的 OpenCode 特有行为:

  • 本地 gRPC/HTTP 服务器生命周期(server.close()
  • SSE 事件流用于输出
  • 通过配置选择 provider/modelOPENCODE_PROVIDEROPENCODE_MODEL
  • MCP config 格式转换(type: 'local'command: [cmd, ...args]environment
  • 通过提示词文本中以 <system> 前缀注入系统提示词
  • 不支持恢复session 始终是新建的或按 ID 复用的)

Agent-Runner 核心

以下所有内容均由 agent-runner 处理,而非 provider。

轮询循环

┌─────────────────────────────────────────┐
│                                         │
│  1. 查询 messages_in 中待处理的行        │
│     WHERE status = 'pending'            │
│     AND (process_after IS NULL          │
│          OR process_after <= now())     │
│                                         │
│  2. 如果找到行:                         │
│     a. 设置 status = 'processing'       │
│     b. 按 kind 格式化消息               │
│     c. 剥离路由字段                     │
│     d. 调用 provider.query(prompt)      │
│     e. 处理 provider 事件               │
│     f. 将结果写入 messages_out          │
│     g. 设置 status = 'completed'        │
│                                         │
│  3. 当查询处于活动状态时:               │
│     - 继续轮询 messages_in              │
│     - 新消息 → provider.push()          │
│                                         │
│  4. 查询结束时:                         │
│     - 回到步骤 1                        │
│     - 若无消息,休眠并重新轮询           │
│                                         │
└─────────────────────────────────────────┘

活动查询期间的并发轮询: 当 provider 正在运行查询时agent-runner 以短间隔(约 500ms持续轮询 messages_in。新的待处理消息被格式化并通过 provider.push() 推送到活动查询中。这使得在 agent 处理过程中后续消息可以到达 — Claude 原生支持此方式Codex/OpenCode 通过内部的中止+重启来处理。

空闲行为: 当没有待处理消息且没有活动查询时agent-runner 短暂休眠1s并重新轮询。容器保持运行状态直到 host主机将其终止空闲超时

空闲检测的例外情况: 在以下情况下容器不应被视为空闲:

  • 一个 ask_user_question 工具调用正在等待(等待用户在 messages_in 中的响应)
  • agent 正在活跃工作中(工具调用进行中、子 agent 运行中)

agent-runner 向 host 发送"忙碌"状态信号。具体机制因 provider 而异 — 对于 Claude查询 AsyncGenerator 仍在产出事件。对于其他 provideragent-runner 可以将心跳或状态指示器写入 session DB会话数据库host 在终止前会检查该状态。

消息格式化

agent-runner 将 messages_in 行转换为提示词字符串。provider 接收的是可直接发送的字符串 — 它不知道消息的种类或路由信息。

路由字段剥离: platform_idchannel_typethread_id 永远不会包含在提示词中。它们作为上下文存储,用于写入 messages_out

按 kind 分类的单条消息格式化:

  • chat — 格式化为消息 XML

    <message sender="John" time="2024-01-01 10:00">
      Check this PR
    </message>
    
  • chat-sdk — 从序列化的 Chat SDK 消息中提取字段:

    <message sender="John (john@slack)" time="2024-01-01 10:00">
      Check this PR
      [image: screenshot.png — https://signed-url...]
    </message>
    

    附件以内联方式列出。Claude 原生支持的图片/PDF 以 content block内容块方式传递参见下文的媒体处理部分

  • task — 任务提示词,可选择附带脚本输出:

    [SCHEDULED TASK]
    
    Script output:
    {"data": ...}
    
    Instructions:
    Review open PRs
    
  • webhook — webhook 负载:

    [WEBHOOK: github/pull_request]
    
    {"action": "opened", "pull_request": {...}}
    
  • system — host 操作结果(对先前系统请求的响应):

    [SYSTEM RESPONSE]
    
    Action: register_agent_group
    Status: success
    Result: {"agent_group_id": "ag-456"}
    

批量格式化: 多条待处理消息合并为一个提示词:

<context timezone="America/Los_Angeles">
<messages>
<message sender="John" time="10:00">Check this PR</message>
<message sender="Jane" time="10:01">Already on it</message>
</messages>

混合 kind例如一条 chat 消息 + 一条 system 响应)用清晰的分隔符组合。每个部分按 kind 标注。

命令检测:/ 开头的消息会与命令列表进行匹配。识别到的命令会绕过格式化,原样传递给 provider用于 Claude 的斜杠命令处理),或被 agent-runner 拦截(用于会话重置等 NanoClaw 级别的命令)。

路由

当 agent-runner 拾取 messages_in 行时,它会从批次中捕获路由字段:

interface RoutingContext {
  platformId: string | null;
  channelType: string | null;
  threadId: string | null;
  inReplyTo: string | null;  // 触发消息的 messages_in.id
}

写入 messages_out(无论是来自 provider 结果还是 MCP tool 调用agent-runner 默认会复制此路由上下文。agent 永远不会看到路由字段 — 它只生成文本。路由是隐式的:"回复发送消息的人。"

目标为其他目的地的 MCP tool例如带显式 channel 参数的 send_to_agentsend_message)会覆盖该特定 messages_out 行的路由上下文。

状态管理

agent-runner 管理 messages_in 上的 statusstatus_changed 字段:

pending → processing → completed
                     → failed (若 provider 返回错误且已达到最大重试次数)
  • 拾取: UPDATE messages_in SET status = 'processing', status_changed = now(), tries = tries + 1 WHERE id IN (...)
  • 完成: UPDATE messages_in SET status = 'completed', status_changed = now() WHERE id IN (...)
  • 错误: agent-runner 不设置 failed — 它将消息保持为 processing。host 通过 status_changed 检测过时的 processing 状态并处理重试逻辑(重置为 pending带退避策略。这样将重试策略保留在 host 端。

MCP 工具

agent-runner 运行一个 MCP serverMCP 服务器),向 agent 暴露 NanoClaw 工具。所有工具都写入 session DB。

数据库路径: MCP server 通过环境变量接收 session 数据库路径。它打开第二个连接到同一个 SQLite 文件WAL 模式允许并发访问)。

send_message

向当前对话(或指定 destination目的地发送聊天消息。

{
  name: 'send_message',
  params: {
    text: string,          // 消息内容
    channel?: string,      // 可选:目标 channel 类型(默认:回复来源)
    platformId?: string,   // 可选:目标 platform ID
    threadId?: string,     // 可选:目标 thread ID
  }
}

实现:写入一个 messages_out 行,kind: 'chat'。如果提供了 channel/platformId/threadId则使用这些作为路由。否则从当前路由上下文中复制。

send_file

向当前对话发送文件。

{
  name: 'send_file',
  params: {
    path: string,          // 文件路径(相对于 /workspace/agent/ 或绝对路径)
    text?: string,         // 可选:附带消息
    filename?: string,     // 显示名称默认path 的 basename
  }
}

实现:

  1. 生成消息 ID
  2. 创建 outbox/{messageId}/ 目录
  3. 将文件复制到 outbox 目录中
  4. 写入一个 messages_out 行,内容中包含 files: [filename]

send_card

发送结构化卡片(交互式或仅展示)。

{
  name: 'send_card',
  params: {
    card: CardElement,     // 卡片结构title、children、actions
    fallbackText?: string, // 不支持卡片的平台使用的文本回退
  }
}

实现:写入一个 messages_out 行,kind: 'chat-sdk',内容中包含卡片结构。

ask_user_question

发送一个交互式问题并等待用户响应。这是一个阻塞式工具调用 — 直到用户响应后工具才会返回。

{
  name: 'ask_user_question',
  params: {
    title: string,         // 简短卡片标题,例如 "Confirm deletion"
    question: string,
    options: (string | { label: string; selectedLabel?: string; value?: string })[],
    timeout?: number,      // 秒默认300
  }
}

实现:

  1. 生成 questionId
  2. 写入一个 messages_out 行,包含 operation: 'ask_question'、问题、选项和 questionId
  3. 轮询 messages_in 中是否有内容匹配的 questionId 的行
  4. 找到后,将 selectedOption 作为工具结果返回
  5. 如果超时到期,将超时错误作为工具结果返回

agent 的执行在此工具调用处暂停。provider 的查询继续运行Claude 保持工具调用打开。agent-runner 在单独循环中轮询响应。

edit_message

编辑先前发送的消息。

{
  name: 'edit_message',
  params: {
    messageId: string,     // 显示给 agent 的整数 ID
    text: string,          // 新内容
  }
}

实现:写入一个 messages_out 行,包含 operation: 'edit'、消息 ID 和新文本。

add_reaction

向消息添加表情反应。

{
  name: 'add_reaction',
  params: {
    messageId: string,     // 显示给 agent 的整数 ID
    emoji: string,         // 表情名称(如 'thumbs_up'
  }
}

实现:写入一个 messages_out 行,包含 operation: 'reaction'

send_to_agent

向另一个 agent group代理组发送消息。

{
  name: 'send_to_agent',
  params: {
    agentGroupId: string,  // 目标 agent group
    text: string,          // 消息内容
    sessionId?: string,    // 可选:目标特定 session
  }
}

实现:写入一个 messages_out 行,包含 channel_type: 'agent'platform_id: agentGroupIdthread_id: sessionId

schedule_task

安排一次性或 recurring task周期性任务

{
  name: 'schedule_task',
  params: {
    prompt: string,             // 任务提示词
    processAfter: string,       // 首次运行的 ISO 时间戳
    recurrence?: string,        // cron 表达式(可选)
    script?: string,            // 前置脚本(可选)
  }
}

实现:写入一个 messages_in 行(发给自己),包含 kind: 'task'process_after 和可选的 recurrence。host sweep 在到期时拾取。

list_tasks

列出活跃的 scheduled/recurring 任务。

{
  name: 'list_tasks',
  params: {}
}

实现:查询 messages_in WHERE recurrence IS NOT NULL AND status != 'failed'

cancel_task / pause_task / resume_task / update_task

修改已安排的任务。

{
  name: 'cancel_task',
  params: { taskId: string }
}
// pause_task: 设置 status = 'paused'(周期性任务的新状态值)
// resume_task: 设置 status = 'pending'
// update_task: 将 { prompt?, recurrence?, processAfter?, script? } 合并到活动行中

实现cancel/pause/resume 直接更新活动行。update_task 作为 system action 发送 — host 读取当前内容,合并提供的字段,并写回。所有四个操作均通过 (id = ? OR series_id = ?) AND kind='task' AND status IN ('pending','paused') 匹配,因此即使 agent 传入原始现已完成的id也能触及周期性任务的下一个活动实例。

register_agent_group

注册一个新的 agent group仅限 admin

{
  name: 'register_agent_group',
  params: {
    name: string,
    folder: string,
    platformId: string,        // 要连接的 messaging group
    channelType: string,
    triggerRules?: object,
    sessionMode?: 'shared' | 'per-thread',
  }
}

实现:写入一个 messages_out 行,包含 kind: 'system'action: 'register_agent_group'。host 读取、验证 admin 权限、在 central DB 中创建实体行,并写入一个 system 类型的 messages_in 响应。

媒体处理

入站messages_in → agent 提示词)

agent-runner 检查 chat/chat-sdk 消息中的附件attachment并根据类型和 provider 能力进行处理:

Provider 原生的 content block

类型 Claude Codex / OpenCode
图片JPEG、PNG、GIF、WebP 原生图片 content block 保存到磁盘
PDF 原生文档 content block 保存到磁盘
音频 原生音频 content block 保存到磁盘
其他文件(代码、数据、视频、存档) 保存到磁盘 保存到磁盘

"保存到磁盘" 的含义是:下载到 /workspace/downloads/{messageId}/,在提示词文本中引用:

<message sender="John" time="10:00">
  Check this spreadsheet
  [file available at: /workspace/downloads/msg-123/data.xlsx]
</message>

agent 可以使用工具Read、Bash访问保存的文件。

对于无法直接下载的 channel例如 WhatsApp 缓冲流channel adapter 通过本地 URL 提供媒体内容。agent-runner 从该 URL 下载。

Content block 构建Claude agent-runner 构建多部分 MessageParam 内容:[{ type: 'image', source: { type: 'base64', media_type, data } }, { type: 'text', text: '...' }]。此时传递给 provider 的提示词不是纯字符串 — QueryInput.prompt 字段需要支持 Claude 的结构化内容。provider 的 query() 方法处理特定格式的构建。

Content block 构建Codex/OpenCode 一切皆为文本。文件引用以内联方式出现在提示词字符串中。provider 接收的是纯字符串提示词。

出站agent → messages_out

通过 send_file MCP tool 处理参见上文。agent 显式决定发送文件 — agent-runner 不会扫描输出中的文件引用。

任务前置脚本

对于 kindtask 且内容中包含 script 字段的消息:

  1. agent-runner 将脚本写入临时文件
  2. 使用 bash 执行30s 超时)
  3. 将 stdout 最后一行解析为 JSON{ wakeAgent: boolean, data?: unknown }
  4. 如果 wakeAgent === false:将消息标记为已完成,不调用 provider
  5. 如果 wakeAgent === true:用脚本输出丰富提示词,然后调用 provider

对话转录归档

agent-runner 在上下文压缩前归档对话转录。对于 Claude这通过 PreCompact hook 处理provider 内部)。对于其他没有 hooks 的 provideragent-runner 在每次查询完成后基于 provider 的输出进行归档。

归档位置:/workspace/agent/conversations/{date}-{summary}.md

会话恢复

agent-runner 在查询之间跟踪 sessionIdresumeAt

  • sessionId — 从 ProviderEvent { type: 'init' } 中捕获。在下一次查询时传回 QueryInput.sessionId
  • resumeAt — Claude 特有(最后一条 assistant 消息的 UUID。由 agent-runner 存储,传递给 QueryInput.resumeAt。不支持的 provider 会忽略它。

这些是容器生命周期的临时数据。当容器被终止并重启时host 会传递从 central DB sessions 表中存储的 sessionIdresumeAt 在容器重启时丢失provider 从 session 末尾恢复)。

容器启动

agent-runner 通过以下方式接收配置:

  • 环境变量: AGENT_PROVIDERclaude/codex/opencodeNANOCLAW_ADMIN_USER_ID、provider 特定变量API 密钥、模型覆盖)、TZ
  • 固定挂载路径: Session DB 位于 /workspace/session.db。Agent group 文件夹位于 /workspace/agent/。系统提示词来自 /workspace/agent/CLAUDE.md/workspace/global/CLAUDE.md
  • 可选启动配置: 部分配置可能以 JSON 文件形式传递到固定路径(例如 /workspace/config.json),用于诸如要恢复的 session ID、assistant 名称和 admin user ID 等内容。这避免了对环境变量的过度使用。

agent-runner 读取配置,创建 provider并进入轮询循环。无标准输入无初始提示词 — 消息已经存在于 session DB 中。

Provider 工厂

type ProviderName = 'claude' | string;

function createProvider(name: ProviderName, config: ProviderConfig): AgentProvider {
  // 主干代码注册 'claude';其他 provider 在通过技能安装时自行注册。
  const factory = providerRegistry.get(name);
  if (!factory) throw new Error(`Unknown provider: ${name}`);
  return factory(config);
}

provider 名称来自容器的环境变量(AGENT_PROVIDER env var由 host 根据 agent_groups.agent_providersessions.agent_provider 设置。

ProviderConfig 包含 provider 特定设置API 密钥、模型覆盖等),通过环境变量传递 — 而非通过接口。每个 provider 根据自身需要从 env 中读取。

Agent-Runner 属性

  • MCP server 是由 provider通过 mcpServers 配置)启动的独立 Node 进程
  • MCP server 二进制文件跨 provider 共享 — 相同的工具、相同的数据库访问
  • CLAUDE.md 加载(全局 + 每组) — agent-runner 读取并作为 systemPrompt 传递
  • 额外目录发现(/workspace/extra/*
  • 通过 stderr 进行日志记录([agent-runner] ...

相关文档

  • architecture.md — 高层架构session DB schema、central DB、channel adapter、消息流
  • api-details.md — Channel adapter 接口、消息内容示例、host delivery 逻辑