首页
/ Cline SDK Agent 运行时(@cline/agents)实战指南:状态无关 Agent 循环、工具调用与事件流

Cline SDK Agent 运行时(@cline/agents)实战指南:状态无关 Agent 循环、工具调用与事件流

2026-09-03 15:29:45作者:庞队千Virginia

本篇基于 Cline 仓库中的官方 Agent 参考文档(REFERENCE.md)及配套 API、模式、避坑文档,系统讲解 @cline/agents 包中 Agent 类(别名 AgentRuntime)的核心循环、配置体系、事件流与状态管理。读完本文,你可以独立搭建一个自定义工具的无状态 Agent、实现多轮会话与流式 UI,并借助仓库源码(agent-runtime.ts)理解工具策略、完成工具、上下文溢出恢复等底层机制。

一、Agent 是什么:轻量、无状态的 Agent 循环

Agent 类是 @cline/agents 包导出的轻量、无状态(stateless)Agent 循环,负责最核心的迭代过程:向 LLM 发送消息 → 执行模型返回的工具调用 → 收集结果 → 重复,直到任务完成

从源码结构看,AgentAgentRuntime 是同一个类的两个名字,定义见 AgentRuntime 类别名声明

// sdk/packages/agents/src/agent-runtime.ts
export const Agent = AgentRuntime;
export type Agent = AgentRuntime;

export function createAgent(config: AgentRuntimeConfig): AgentRuntime {
	return new AgentRuntime(config);
}

包入口 index.ts 的注释说明了命名约定:当你提供 provider/model ID 时使用 Agent,当你提供预构建的 AgentModel@cline/core 的做法)时使用 AgentRuntime。此外还导出 createAgent / createAgentRuntime 工厂函数、判别式配置联合类型,以及从 @cline/shared 再导出的 createTool(用于编写工具)。共享类型(AgentMessageAgentRunResult 等)则建议直接从 @cline/shared 导入。

二、何时选 Agent,何时选 ClineCore

官方文档给出了一张清晰的选型对照表,核心分界线是:是否需要内置工具、会话持久化、配置发现等"开箱即用"能力

使用 Agent 的场景 应改用 ClineCore 的场景
想做一个带自定义工具的简单 Agent 需要内置工具(bash、编辑器等)
希望依赖最少 需要会话持久化
需要浏览器兼容 需要 .cline/ 目录的配置发现
构建无状态 worker 需要多进程共享会话
想完全控制运行时 想要开箱即用的完整方案

这一点在包级 README 中也有印证:@cline/agents(即 Agent 类)本身浏览器安全、无 Node.js 依赖;而 @cline/coreClineCore 需要 Node.js 22+。若从 @cline/sdk 统一导入,会拿到包含 Node-only 代码的全部内容;纯浏览器场景应直接从 @cline/agents 导入。

三、快速上手与配置解析

3.1 最小示例

import { Agent } from "@cline/sdk"

const agent = new Agent({
  providerId: "anthropic",
  modelId: "claude-sonnet-4-6",
  apiKey: process.env.ANTHROPIC_API_KEY,
  systemPrompt: "You are a helpful assistant.",
  tools: [],
})

const result = await agent.run("What is the capital of France?")
console.log(result.outputText)

除构造函数外,等价工厂函数同样可用:

import { createAgent } from "@cline/sdk"
const agent = createAgent(config)

3.2 两种配置形态:判别式联合类型

AgentRuntimeConfig 是一个判别式联合(discriminated union),源码定义见 配置类型声明。判别依据是配置中是否提供了预构建的 model 字段。

形态一:Provider ID 形态(推荐,大多数独立使用者)

interface AgentRuntimeConfigWithProvider {
  providerId: string          // 如 "anthropic"、"openai"、"gemini"
  modelId: string             // 如 "claude-sonnet-4-6"、"gpt-5.5"
  apiKey?: string             // provider API key
  baseUrl?: string            // 自定义端点
  headers?: Record<string, string>
  options?: GatewayProviderSettings["options"] // provider 级 gateway 选项

  systemPrompt?: string
  tools?: AgentTool[]
  initialMessages?: AgentMessage[]
  toolPolicies?: Record<string, ToolPolicy>
  hooks?: Partial<AgentRuntimeHooks>
  plugins?: AgentPlugin[]
}

形态二:预构建 Model 形态(进阶)

interface AgentRuntimeConfigWithModel {
  model: AgentModel            // 从 gateway 预构建的 model
  systemPrompt?: string
  tools?: AgentTool[]
  initialMessages?: AgentMessage[]
  toolPolicies?: Record<string, ToolPolicy>
  hooks?: Partial<AgentRuntimeHooks>
  plugins?: AgentPlugin[]
}

底层解析逻辑:当使用形态一时,运行时会在 resolveRuntimeConfig 中通过 @cline/llmscreateGateway 内部构建 AgentModel,并把 messageModelInfo{ id: modelId, provider: providerId })写入配置,用于给后续的 assistant 消息打模型标签;调用方显式提供的 messageModelInfo 始终优先。构造函数还会为 Agent 生成默认 agentIdagent_${nanoid} 形式),并把 initialMessages 深拷贝进内部状态。

另外注意:toolExecution 选项在构造时默认解析为 "sequential"(见 构造函数toolExecution: resolved.toolExecution ?? "sequential")。

重要提示AgentRuntimeConfig 没有顶层 onEvent 字段。事件流请使用 agent.subscribe()hooks.onEvent(见第五节)。

四、核心概念:Agent 循环如何运转

官方文档将 Agent 的循环概括为 6 步:

  1. 接收用户输入(字符串、单条消息或消息数组均可);
  2. 构建回合上下文(system prompt、消息历史、工具);
  3. 调用 LLM provider;
  4. 若模型返回工具调用,执行它们并回到第 3 步;
  5. 若模型返回不含工具调用的文本,本次运行完成;
  6. 全程发出事件以支持流式输出。

"无状态"的含义是:Agent 不向磁盘持久化任何东西,对话历史保存在内存中,可通过 snapshot() 访问。

对照源码,循环主体在 execute 私有方法 中:

  • 若状态已经是 "running",直接抛出 "Agent runtime is already running" 错误——同一 Agent 实例不支持并发 run
  • 每次执行会生成新的 runId,把 iterationusagelastError 等运行期状态清零,并重建 AbortController
  • 输入经 normalizeInput 归一化后逐条压入 state.messages 并发出 message-added 事件;
  • 主循环受 maxIterations 约束:源码中 while (this.config.maxIterations === undefined || this.state.iteration < this.config.maxIterations),即未配置时无迭代上限,配置后到达上限即停止。

完成工具(completesRun)机制

当你的 Agent 配有工具时,循环何时"收尾"由完成工具决定:标记了 lifecycle: { completesRun: true } 的工具一旦成功执行,运行即告完成。源码中还有配套的"完成策略":当 completionPolicy.requireCompletionTool === true 时,运行开始会向对话中注入一条系统提醒消息(见 getCompletionToolReminderMessage):

[SYSTEM] This run is not complete until you call one of these terminal completion tools: ... Continue working if requirements are not met. If the task is complete, call the appropriate terminal completion tool now.

这意味着:没有工具时,模型返回纯文本即结束;有工具时,务必在 system prompt 中引导模型在完成任务后调用完成工具,否则循环可能不停——这是官方避坑文档列出的第一类问题(详见第七节)。

五、关键 API 详解

以下 API 清单来自官方参考文档,类型细节与 api.md 一致,源码行为以 agent-runtime.ts 为准。

5.1 run / continue / abort

  • agent.run(input):用用户输入启动一次运行,返回 Promise<AgentRunResult>input 可以是 string、单条 AgentMessageAgentMessage[](源码类型 AgentRunInput = string | AgentMessage | readonly AgentMessage[])。
  • agent.continue(input?):在既有会话上继续,可带可不带新输入。
  • agent.abort(reason?):取消当前运行。源码实现(abort 方法)会将 reason 包装成 AgentRuntimeAbortError,写入 lastError,发出任务取消遥测事件,并触发内部 AbortController.abort()run() 将以 status: "aborted" 的结果返回。

run()continue() 的关系(易错点):run() 用于第一次交互、建立会话;continue() 追加到既有会话;第二次调用 run() 会重置对话历史。实践中用 agent.hasRun 布尔属性判断该走哪个分支(hasRun 表示 run() 是否至少被调用过一次):

if (agent.hasRun) {
  await agent.continue(input)
} else {
  await agent.run(input)
}

5.2 subscribe:流式事件监听

const unsubscribe = agent.subscribe((event: AgentRuntimeEvent) => {
  // 处理事件
})
// 之后停止监听
unsubscribe()

源码中 subscribe 将监听器加入 Set,返回的闭包用于移除(subscribe 实现)。务必在调用 run() 之前注册监听器,否则会丢失早期事件(run-started 与首个 message-added 在 run 一开始就已发出)。

5.3 snapshot 与 restore

  • snapshot():返回当前运行时状态。实际返回的 AgentRuntimeStateSnapshot 比简化描述更丰富,源码可见其包含 agentIdagentRoleparentAgentIdconversationIdrunIdstatusiterationmessages(深拷贝)、pendingToolCallsusagelastErrorlastErrorClasssnapshot 实现)。
  • restore(messages):整体替换消息历史。从源码注释与实现看(restore 方法),它会先 abort 掉进行中的运行,重置 runId/迭代/用量/错误状态,但保留模型、工具、hooks、插件与已注册的事件订阅者——专为"外部持久化会话、重新注入运行时"的场景设计。

典型的手动持久化模式:

// 保存状态
const snapshot = agent.snapshot()
const serialized = JSON.stringify(snapshot.messages)

// 之后恢复
const agent2 = new Agent({ ...config })
agent2.restore(JSON.parse(serialized))
const result = await agent2.continue("Continue where we left off")

若需要自动持久化,请改用 ClineCore

5.4 AgentRunResult:运行结果

run() / continue() 的返回值:

interface AgentRunResult {
  agentId: string
  agentRole?: string
  runId: string
  status: "completed" | "aborted" | "failed"
  iterations: number
  outputText: string
  messages: readonly AgentMessage[]
  usage: AgentUsage
  error?: Error
}

状态取值语义:"completed" 正常结束;"aborted"abort() 取消;"failed" 不可恢复错误。

5.5 消息与用量结构

interface AgentMessage {
  id: string
  role: "user" | "assistant" | "tool"
  content: AgentMessagePart[]
  createdAt: number
  metadata?: Record<string, unknown>
  modelInfo?: { id: string; provider: string; family?: string }
  metrics?: {
    inputTokens: number
    outputTokens: number
    cacheReadTokens?: number
    cacheWriteTokens?: number
    cost?: number
  }
}

interface AgentUsage {
  inputTokens: number
  outputTokens: number
  cacheReadTokens: number
  cacheWriteTokens: number
  totalInputTokens: number
  totalOutputTokens: number
  totalCost?: number
}

modelInfo 即前文提到的模型标签来源;每条消息的 metrics 是相对上一状态的用量增量(源码中由 usageDelta 计算,取各计数的非负差值)。

5.6 Hooks:贯穿生命周期的拦截点

interface AgentRuntimeHooks {
  beforeRun?(context): AgentStopControl | undefined
  afterRun?(context): void
  beforeModel?(context): AgentBeforeModelResult | undefined
  afterModel?(context): AgentStopControl | undefined
  beforeTool?(context): AgentBeforeToolResult | undefined
  afterTool?(context): AgentAfterToolResult | undefined
  onEvent?(event: AgentRuntimeEvent): void | Promise<void>
}

Hooks 可以在每个阶段拦截并修改行为;从 beforeRunafterModelbeforeTool 返回一个 stop control 即可中止 Agent 循环。

hooks.onEventagent.subscribe() 收到相同AgentRuntimeEvent,区别在于:hook 回调会被 await(可异步),subscribe() 监听器同步调用。官方建议:UI 流式渲染用 subscribe(),异步副作用(如写外部日志服务)用 hooks.onEvent

5.7 工具策略(toolPolicies)

配置中的 toolPolicies 按工具名映射策略,支持通配符 * 作为默认策略。源码中的 resolveToolPolicy 展示了合并规则:先展开 policies["*"],再叠加 policies[toolName],同名键后者覆盖前者——即具体工具的策略优先于通配策略。

六、多轮对话与事件流实战

6.1 多轮对话

const agent = new Agent({
  providerId: "anthropic",
  modelId: "claude-sonnet-4-6",
  systemPrompt: "You are a helpful assistant.",
  tools: [],
})

const first = await agent.run("What is 2 + 2?")
console.log(first.outputText)

const second = await agent.continue("Now multiply that by 3")
console.log(second.outputText)

6.2 事件流:可用的事件类型

Agent 运行时发出的事件(AgentRuntimeEvent)在 events/REFERENCE.md 有完整目录。按类别归纳:

类别 事件类型 关键字段
运行生命周期 run-started / run-finished / run-failed snapshot;完成时带 result: AgentRunResult,失败时带 error
回合 turn-started / turn-finished iteration;完成时带 toolCallCount
文本流式 assistant-text-delta textaccumulatedText(另有 assistant-reasoning-delta 用于扩展思考)
助手消息 assistant-message message: AgentMessagefinishReason
消息变更 message-added message(用户或助手消息入历史时触发)
工具 tool-started / tool-updated / tool-finished toolCall(含 toolNametoolCallIdinput
用量 usage-updated usage(含 totalCost 等)
提示 status-notice messagemetadata

每个事件都带 snapshot 字段(当前 AgentRuntimeStateSnapshot)。注意区分:ClineCore 走的是另一套 CoreSessionEvent(文本流事件是 chunk),不要与 Agent 的事件类型混用

最小流式示例(来自官方参考文档):

const agent = new Agent({
  providerId: "anthropic",
  modelId: "claude-sonnet-4-6",
  systemPrompt: "You are a helpful assistant.",
  tools: [],
})

agent.subscribe((event) => {
  if (event.type === "assistant-text-delta") {
    process.stdout.write(event.text)
  }
})

const result = await agent.run("What is the capital of France?")

更完整的 UI 流式模式(区分文本增量、回合计数、工具提示与成本):

agent.subscribe((event) => {
  switch (event.type) {
    case "assistant-text-delta":
      ui.appendText(event.text)
      break
    case "assistant-message":
      ui.endText()
      break
    case "turn-started":
      ui.startTurn(event.iteration)
      break
    case "turn-finished":
      if (event.toolCallCount > 0) ui.showToolCount(event.toolCallCount)
      break
    case "usage-updated":
      ui.updateUsage(event.usage.inputTokens, event.usage.outputTokens)
      break
  }
})

七、常见模式(官方 patterns 全集)

以下模式来自 patterns.md,均为可直接落地的参考实现。

7.1 交互式 CLI Agent

终端多轮对话 + 流式输出,核心是 hasRun 分支与 subscribe 前置:

import { Agent } from "@cline/sdk"
import * as readline from "node:readline"

const agent = new Agent({
  providerId: "anthropic",
  modelId: "claude-sonnet-4-6",
  apiKey: process.env.ANTHROPIC_API_KEY,
  systemPrompt: "You are a helpful assistant. Keep responses concise.",
  tools: [],
})

agent.subscribe((event) => {
  if (event.type === "assistant-text-delta") {
    process.stdout.write(event.text)
  }
})

const rl = readline.createInterface({ input: process.stdin, output: process.stdout })

function prompt(): void {
  rl.question("\nYou: ", async (input) => {
    const trimmed = input.trim()
    if (!trimmed || trimmed === "exit") {
      rl.close()
      return
    }
    process.stdout.write("\nAssistant: ")
    if (agent.hasRun) {
      await agent.continue(trimmed)
    } else {
      await agent.run(trimmed)
    }
    process.stdout.write("\n")
    prompt()
  })
}

prompt()

7.2 会话型 Agent(Slack Bot / 聊天应用)

每个线程一个 Agent 实例,用 Map 维护会话记忆(这也是 SDK README 的官方示例形态):

const agents = new Map<string, Agent>()

async function handleMessage(threadId: string, message: string) {
  let agent = agents.get(threadId)
  if (!agent) {
    agent = new Agent({
      providerId: "anthropic",
      modelId: "claude-sonnet-4-6",
      systemPrompt: "You are a concise assistant.",
      tools: [],
    })
    agents.set(threadId, agent)
  }

  const result = agent.hasRun
    ? await agent.continue(message)
    : await agent.run(message)

  return result.outputText
}

7.3 结构化输出:完成工具提取结构化数据

利用 completesRun: true 的工具做"收口",把最终结果约束成结构化 JSON:

import { Agent, createTool } from "@cline/sdk"
import { z } from "zod"

const submitReview = createTool({
  name: "submit_review",
  description: "Submit the final code review with structured feedback.",
  inputSchema: z.object({
    summary: z.string(),
    issues: z.array(z.object({
      file: z.string(),
      line: z.number(),
      severity: z.enum(["error", "warning", "info"]),
      message: z.string(),
    })),
    approved: z.boolean(),
  }),
  lifecycle: { completesRun: true },
  execute: async (input) => input,
})

const agent = new Agent({
  providerId: "anthropic",
  modelId: "claude-sonnet-4-6",
  systemPrompt: "Review the code diff and submit structured feedback.",
  tools: [submitReview],
})

const result = await agent.run(diffContent)
const review = result.toolCalls.find(tc => tc.name === "submit_review")
console.log(review?.output)

7.4 超时与中止

const timeout = setTimeout(() => agent.abort("Timeout"), 30_000)
try {
  const result = await agent.run(data)
  if (result.status === "aborted") {
    console.log("Agent was aborted")
  } else {
    console.log(result.outputText)
  }
} finally {
  clearTimeout(timeout)
}

7.5 插件(Plugins)

插件通过 plugins 配置注入,可自带 hooks 与工具:

import { Agent } from "@cline/sdk"
import type { AgentPlugin } from "@cline/sdk"

const loggingPlugin: AgentPlugin = {
  name: "logging",
  manifest: { capabilities: ["hooks"] },
  setup() {},
  hooks: {
    beforeTool({ toolCall }) {
      console.log(`Calling tool: ${toolCall.toolName}`)
    },
    afterRun({ result }) {
      console.log(`Completed in ${result.iterations} iterations`)
    },
  },
}

const agent = new Agent({
  providerId: "anthropic",
  modelId: "claude-sonnet-4-6",
  systemPrompt: "You are a helpful assistant.",
  tools: [myTool],
  plugins: [loggingPlugin],
})

从源码看,插件在初始化阶段(initialize 方法)被处理:setup() 返回的 tools 会被注册进工具表(可能与配置工具同名覆盖),返回的 hooks 会被注册进 hook 链——即插件是"工具 + hooks"的组合扩展点。

7.6 预构建 Model(Gateway 形态)

进阶 provider 配置时,可以自带 AgentModel

import { Agent } from "@cline/sdk"
import { createGateway } from "@cline/llms"

const gateway = createGateway({
  providerConfigs: [
    { providerId: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY },
    { providerId: "openai", apiKey: process.env.OPENAI_API_KEY },
  ],
})

const model = gateway.createAgentModel({
  providerId: "anthropic",
  modelId: "claude-opus-4-7",
})

const agent = new Agent({ model, systemPrompt: "You are a helpful assistant.", tools: [] })

这正是 AgentRuntimeConfigWithModel 形态的用法,也是 @cline/core 内部复用 gateway/遥测接线的方式。

八、避坑清单(gotchas 全解)

来自 gotchas.md,每一条都有源码对应物:

  1. Agent 循环不停止。希望显式收尾时,确保至少一个工具带 lifecycle: { completesRun: true };无工具时模型返回纯文本即结束;有工具时在 system prompt 中引导模型在任务完成后调用完成工具;并确认完成工具正常返回(不抛错)。源码中,仅当 completionPolicy.requireCompletionTool === true 时会自动注入提醒消息(getRequiredCompletionToolNames),否则完全依赖模型自觉。

  2. 工具抛错会被计为"失误"。工具的 execute 抛异常会被 SDK 计为 mistake,失误过多会以 mistake_limit 原因停止。正确姿势是返回结构化错误数据:

    // 错误示范:throw
    execute: async (input) => { throw new Error("File not found") }
    // 正确示范:返回错误数据
    execute: async (input) => { return { error: "File not found", path: input.path } }
    
  3. run()continue() 的语义run() 首次建会话;continue() 追加;第二次 run() 会重置历史;用 agent.hasRun 判断走哪个分支。

  4. 浏览器兼容@cline/agents 无 Node 依赖,浏览器环境请直接 import { Agent } from "@cline/agents";从 @cline/sdk 导入会连带 Node-only 代码。

  5. 配置没有顶层 onEventnew Agent({ onEvent: ... }) 不生效。两个正确入口是 subscribe()(同步,适合 UI 流式)与 hooks.onEvent(被 await,适合异步副作用,如 await logToService(event.text))。

  6. 监听器时机subscribe(handler) 必须在 run() 之前;若在 const promise = agent.run(input) 之后再注册,可能丢失早期事件。

  7. 工具 inputSchema 决定模型行为:模型依据 inputSchema 决定传参——固定取值用 z.enum() 而非自由字符串;Zod 里每个属性加 .describe()、JSON Schema 里加 description;把速率限制、上限等约束写进工具 description。

  8. 长对话内存:Agent 所有消息驻留内存,轮次越多占用越大。可考虑:改用带 compaction 的 ClineCore;定期用摘要重建新 Agent;监控 result.usage.totalInputTokens 观察上下文增长。

  9. 长任务工具要响应中止信号

    execute: async (input, context) => {
      for (const item of items) {
        if (context.abortSignal?.aborted) {
          return { partial: results, aborted: true }
        }
        results.push(await process(item))
      }
      return { results }
    }
    
  10. Provider 密钥问题:确认 apiKey 已设置(配置或环境变量)、key 与 providerId 匹配(如 anthropic 的 key 配 providerId: "anthropic")、OpenAI 兼容 provider 需同时设置 apiKeybaseUrl。更多 provider 细节见 providers 参考

九、源码纵深:上下文溢出自动恢复与错误处理

文档未展开、但源码中值得了解的两个健壮性机制:

上下文窗口溢出恢复。源码顶部定义了三种终态错误文案(溢出错误常量):无历史可压缩(system prompt + 工具 + 当前输入本身超窗)、压缩重试一次后仍超窗、以及无压缩管线可用。运行时维护 overflowRecoveryAttempted 标记(每个 run 只允许一次自动恢复尝试,见 状态字段):检测到超窗后先尝试压缩对话再重试,失败则抛出携带可操作建议的 ContextWindowOverflowError。这对长会话 Agent 是重要的运维信号:收到"please start a new session or switch to a larger-window model"类错误时,应按提示处理而非盲目重试。

输出截断识别。循环中对 finishReason === "max-tokens" 且有工具调用的情况有特殊处理(循环主体),配合 MAX_TOKENS_INCOMPLETE_TURN_MESSAGE 常量,用于识别"模型在输出上限内未完成本轮"的场景,避免把截断的工具调用参数误当作有效 JSON。工具调用参数解析失败时,也会返回明确的诊断信息("Tool call arguments could not be parsed as JSON...",见 JSON 解析校验),方便排查模型输出格式问题。

十、延伸阅读

仓库内与本文配套的完整资料:

适用前提:本文所有 API 描述以当前仓库中 .agents/skills/cline-sdk 技能文档与 sdk/packages/agents 源码为准;示例中的模型 ID(如 claude-sonnet-4-6)为文档演示值,实际使用时请以你所用 provider 的可用模型列表为准。

登录后查看全文
热门项目推荐
相关项目推荐