Cline SDK Agent 类完全解析:配置、run/continue 生命周期与 Hook 拦截机制
本篇以 Cline SDK 中独立运行的 Agent 类(@cline/agents 包,经 @cline/sdk 统一导出)为对象,完整覆盖其构造函数与 AgentRuntimeConfig 两种配置形态、run/continue/abort/subscribe/snapshot/restore 全量方法、AgentRunResult/AgentMessage/AgentUsage 等结果类型,以及 AgentRuntimeHooks 七种生命周期钩子的拦截语义;并深入 agent-runtime.ts 源码,印证事件发射顺序、工具策略(toolPolicies)的合并规则、上下文窗口溢出的自动恢复流程等底层行为,读完即可独立搭建并控制一个可流式、可中止、可持久化恢复的无状态 Agent 循环。
一、Agent 定位与创建方式
Agent 类(同时以 AgentRuntime 名导出,二者是同一个类)是 @cline/agents 包提供的轻量、无状态 Agent 循环:发送消息给 LLM → 执行工具调用 → 收集结果 → 重复,直到任务完成。它不向磁盘持久化任何内容,会话历史保存在内存中,可通过 snapshot() 获取。适用于需要自定义工具、最小依赖、浏览器兼容或无状态 worker 的场景;需要内置工具(bash、editor)、会话持久化或 .cline/ 配置发现时,应改用 ClineCore(参见 Agent Runtime 概览)。
构造函数与工厂函数
import { Agent } from "@cline/sdk"
const agent = new Agent(config: AgentRuntimeConfig)
等价工厂函数两种:
import { createAgent } from "@cline/sdk"
const agent = createAgent(config)
更底层的 createAgentRuntime 返回的也是同一个 Agent 类(源码中 createAgentRuntime 与 createAgent 都只是 new AgentRuntime(config),见 agent-runtime.ts#L2169-L2188):
import { createAgentRuntime } from "@cline/sdk"
const runtime = createAgentRuntime(config)
从 index.ts 的导出注释可以看到命名约定:Agent 面向传入 provider/model ID 的独立调用者,AgentRuntime 面向传入预构建 AgentModel 的 @cline/core;共享类型(AgentMessage、AgentRunResult 等)应从 @cline/shared 直接导入,createTool 也从 @cline/shared 再导出。
二、AgentRuntimeConfig:两种判别联合配置
配置是判别联合(discriminated union),按“是否携带预构建 model”分两种形态(源码定义见 agent-runtime.ts#L106-L139)。
形态一:Provider ID 形式(推荐)
interface AgentRuntimeConfigWithProvider {
providerId: string // e.g. "anthropic", "openai", "gemini"
modelId: string // e.g. "claude-sonnet-4-6", "gpt-5.5"
apiKey?: string // provider API key
baseUrl?: string // custom endpoint
headers?: Record<string, string>
systemPrompt?: string
tools?: AgentTool[]
initialMessages?: AgentMessage[]
toolPolicies?: Record<string, ToolPolicy>
hooks?: Partial<AgentRuntimeHooks>
plugins?: AgentPlugin[]
}
运行时收到这种配置后,resolveRuntimeConfig() 会用 @cline/llms 的 createGateway 内部构建出 AgentModel(见 agent-runtime.ts#L147-L168),同时把 messageModelInfo 默认设为 { id: modelId, provider: providerId },用于给 assistant 消息打 modelInfo 标签(调用方显式提供的值优先)。因此大多数独立用户只需关心 providerId、modelId、apiKey 三个字段。
形态二:预构建 Model 形式
interface AgentRuntimeConfigWithModel {
model: AgentModel // pre-built model from gateway
systemPrompt?: string
tools?: AgentTool[]
initialMessages?: AgentMessage[]
toolPolicies?: Record<string, ToolPolicy>
hooks?: Partial<AgentRuntimeHooks>
plugins?: AgentPlugin[]
}
这是高级形态:调用方自建 AgentModel(通常通过 @cline/llms 的 gateway),以便复用同一个 gateway 的遥测与配置接线。典型场景是多 provider 网关:
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: [],
})
运行时通过 hasPrebuiltModel()(即 config.model !== undefined)判断走哪条分支,因此两种形态不可混用:一旦传了 model,providerId 等字段就不再起模型构建作用。
一个容易踩的坑:配置上没有顶层 onEvent
AgentRuntimeConfig 上不存在顶层 onEvent 字段。事件订阅只有两条路:agent.subscribe()(同步回调,适合 UI 流式渲染)或 hooks.onEvent(可异步,被 await 等待,适合写外部日志服务等副作用)。hooks.onEvent 收到的 AgentRuntimeEvent 类型与 subscribe() 完全相同。完整事件目录见 事件参考。
三、核心方法逐个解析
run(input)
启动一次运行。输入可以是 string、单个 AgentMessage 或 AgentMessage[](源码中类型别名 AgentRunInput = string | AgentMessage | readonly AgentMessage[],见 agent-runtime.ts#L98)。字符串会被包装为 user 角色的 text 消息;消息数组则深拷贝后入栈。
const result: AgentRunResult = await agent.run("Build a REST API")
从源码的 execute() 主循环看(agent-runtime.ts#L690-L938),run() 的完整流程是:
- 确保完成惰性初始化(注册 hooks、工具表、插件
setup());若当前状态是running则直接抛错,防止重入; - 新建
AbortController、生成新runId,重置 iteration、usage、错误状态; - 依次调用
beforeRun钩子 → 发射run-started事件; - 输入消息逐条入栈并各发一个
message-added事件; - 进入迭代循环:发射
turn-started→ 请求模型(含上下文溢出自动恢复)→ 若模型返回tool-call部件则执行工具(发射tool-started/tool-finished,工具结果作为tool角色消息入栈)并继续下一轮;若返回纯文本(无工具调用)则结束,发射turn-finished与run-finished; - 捕获任何异常后统一组装
AgentRunResult返回(注意:异常不向外抛,而是以status: "aborted" | "failed"的 result 形式返回,error字段仅failed时携带)。
continue(input?)
继续既有会话,输入可选(不传输入也可以,例如仅消费排队消息)。它与 run() 在源码里都委托给同一个 execute()(agent-runtime.ts#L536-L542),差异仅在于语义:continue 不重置会话历史,直接在当前 messages 之上再跑一个 run(新 runId、usage 从零累计)。
const result = await agent.continue("Now add authentication")
配合 hasRun 属性(布尔值,表示 run() 是否至少被调用过一次)做入口分派是标准写法:
if (agent.hasRun) {
await agent.continue(input)
} else {
await agent.run(input)
}
abort(reason?)
取消当前正在进行的运行。reason 可以是任意值:若传入的就是 AgentRuntimeAbortError 则原样使用,否则运行时包装成一个(agent-runtime.ts#L544-L560)。中止后主循环在下一个检查点抛出中止错误,run()/continue() 的 Promise 正常 resolve,返回 status: "aborted" 的 result(而非 reject),因此超时保护可以这样写:
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)
}
subscribe(listener)
注册流式事件监听器,返回取消函数:
const unsubscribe = agent.subscribe((event: AgentRuntimeEvent) => {
// handle event
})
// Later: stop listening
unsubscribe()
实现上就是一个 Set<AgentEventListener>(agent-runtime.ts#L562-L567),emit() 先同步调用所有 listener,再 await 所有 hooks.onEvent。因此务必在 run() 之前注册以免错过 run-started 等早期事件;assistant-text-delta 等逐 chunk 事件频率极高,listener 应保持轻量。
snapshot() 与 restore(messages)
snapshot() 返回当前运行时状态的深拷贝(messages 经 cloneMessages 逐项浅克隆,防止外部修改污染内部状态)。AgentRuntimeStateSnapshot 的完整字段(agent.ts#L151-L165)比 API 文档简表更丰富:
interface AgentRuntimeStateSnapshot {
agentId: string
agentRole?: string
parentAgentId?: string | null
conversationId?: string
runId?: string
status: "idle" | "running" | "completed" | "aborted" | "failed"
iteration: number
messages: readonly AgentMessage[]
pendingToolCalls: readonly string[]
usage: AgentUsage
lastError?: string
lastErrorClass?: "context_window_exceeded" | "auth" | "unknown"
}
restore(messages) 用于整体替换会话历史:它会先 abort("Agent state restored") 丢弃进行中的运行与 usage 状态,然后保留 model、tools、hooks、plugins 与已注册的事件订阅者——这正好服务于“外部持久化会话、跨进程恢复”的诉求(agent-runtime.ts#L577-L595):
// Save state
const snapshot = agent.snapshot()
const serialized = JSON.stringify(snapshot.messages)
// Later: restore
const agent2 = new Agent({ ...config })
const messages = JSON.parse(serialized)
agent2.restore(messages)
const result = await agent2.continue("Continue where we left off")
如果需要自动持久化(checkpoint、.cline/ 会话管理),应使用 ClineCore。
四、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"— 正常结束。两种触发路径:模型返回无工具调用的文本;或成功调用了带lifecycle.completesRun: true的“终结工具”(此时outputText取该工具的输出文本而非 assistant 消息文本,见finishRun调用处 agent-runtime.ts#L850-L867);"aborted"— 经abort()或 hook 返回stop: true的受控停止(ControlledStopError)导致;"failed"— 不可恢复错误,error字段携带该Error实例,同时发射run-failed事件。
outputText 取最后一条 assistant 消息中的 text 部件拼接;iterations 即本次 run 消耗的模型轮次数。注意 messages 与 usage 都是拷贝,可安全持有。
五、AgentMessage 与 AgentUsage 数据模型
AgentMessage
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
}
}
补充两个源码层面的事实:消息 id 由 createUID("msg") 生成(msg_ 前缀 + nanoid);metrics 是运行时用 usageDelta() 计算的单条消息增量(本轮模型调用前后 usage 之差,见 agent-runtime.ts#L375-L420),当所有项均为 0 时该字段省略——所以按消息统计 token 成本时应以 metrics 为准,而 usage 是 run 级累计。
content 是部件数组(AgentMessagePart),包含 text、reasoning、media、file、tool-call、tool-result 等类型;assistant 消息可能同时携带文本与多个 tool-call 部件,工具结果则单独作为 role: "tool" 的消息入栈。
AgentUsage
interface AgentUsage {
inputTokens: number
outputTokens: number
cacheReadTokens: number
cacheWriteTokens: number
totalInputTokens: number
totalOutputTokens: number
totalCost?: number
}
totalInputTokens/totalOutputTokens 为跨 run 累计总量。usage 由模型流的 usage 事件增量累加(updateUsage(),agent-runtime.ts#L1610-L1628),每次更新都会发射 usage-updated 事件,因此 UI 可以实时显示成本:
agent.subscribe((event) => {
if (event.type === "usage-updated" && event.usage.totalCost) {
console.log(`Running cost: $${event.usage.totalCost.toFixed(4)}`)
}
})
六、AgentRuntimeHooks:七种生命周期钩子
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>
}
钩子可以在每个阶段拦截并修改行为。结合 shared/agent.ts 中各 Result 类型的完整字段,各钩子的能力边界是:
| 钩子 | 可返回的关键字段 | 能力 |
|---|---|---|
beforeRun |
stop、reason |
运行开始前否决整次 run |
afterRun |
无(void) | 拿到最终 result 做收尾、上报 |
beforeModel |
stop、reason、messages、tools、options |
每个模型请求前改写消息、工具集与模型选项 |
afterModel |
stop、reason |
模型返回后审查 assistant 消息并终止 |
beforeTool |
skip、stop、reason、input、policy、appendContext |
拦截/改写工具输入、覆盖工具策略、注入上下文 |
afterTool |
stop、reason、result、appendContext |
改写工具结果、注入上下文 |
onEvent |
无 | 同 subscribe() 的事件流,但会被 await,可异步 |
几个值得注意的源码行为:
- 停止语义:
beforeRun/afterModel/beforeTool返回{ stop: true, reason }时,运行时抛出内部ControlledStopError,run 以status: "aborted"结束,reason写入snapshot().lastError(agent-runtime.ts#L2044-L2054); - 钩子可叠加:
registerHooks()把 config hooks 与各插件 hooks 追加进同一个HookBag数组,按注册顺序逐个执行(agent-runtime.ts#L637-L648); - appendContext 注入:
beforeTool/afterTool返回的appendContext文本会被包装为带source/tool_name/tool_call_id属性的<hook_context>块,在该轮所有工具结果之后合并为一条用户消息追加进会话(displayRole: "system",不出现在面向用户的 transcript 中),模型在下一轮请求即可看到(agent-runtime.ts#L826-L843); - subscribe 与 onEvent 的分工:
subscribe()listener 同步调用、适合 UI 流式渲染;hooks.onEvent在emit()中被逐个await、适合异步副作用(如上报外部服务),但注意它会阻塞主循环推进,应保持快速。
插件形态的 hooks 写法(插件可额外通过 setup() 返回工具与 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],
})
七、工具策略 toolPolicies 与审批
toolPolicies 是 Record<string, ToolPolicy>,ToolPolicy 定义在 shared/llms/tools.ts:
interface ToolPolicy {
/** 工具是否允许执行,默认 true */
enabled?: boolean
/** 是否无需客户端审批即可运行,默认 true */
autoApprove?: boolean
}
源码中 resolveToolPolicy() 的合并规则(agent-runtime.ts#L170-L178):通配符 "*" 的策略先展开,再被同名工具的策略覆盖,因此可以写“全局默认 + 个别例外”:
const agent = new Agent({
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
tools: [readFile, writeFile, runShell],
toolPolicies: {
"*": { autoApprove: false }, // 默认全部需要审批
read_file: { autoApprove: true }, // 只读工具放行
run_shell: { enabled: false }, // 直接禁用
},
})
执行链上的判定顺序(prepareToolExecution,agent-runtime.ts#L1674-L1771):先跑 beforeTool 钩子(可改写 input、覆盖 policy、返回 skip)→ 合并 config 策略与钩子策略 → enabled === false 则记为跳过 → autoApprove === false 则调用配置的 requestToolApproval 回调请求审批,未配置回调时视为拒绝。被跳过/拒绝的工具不会抛出,而是产出 isError: true 的工具结果消息(附带 -- 拒绝后缀),让模型看到失败原因后自行调整。
八、迭代上限与上下文溢出的自动恢复
两个超出 API 文档范围、但直接影响生产可用性的运行时机制:
- maxIterations:
config.maxIterations未设置时无上限;设置后循环条件为iteration < maxIterations,超出即抛出 “Agent runtime exceeded maxIterations” 错误并以status: "failed"返回(agent-runtime.ts#L727-L730)。此外completionPolicy.requireCompletionTool: true时,若模型只返回文本,运行时会注入“必须调用终结完成工具”的提醒消息并继续循环,直到成功调用completesRun: true的工具。 - 上下文窗口溢出恢复:当 provider 拒绝请求且错误被分类为
context_window_exceeded时,运行时每个 run 自动尝试一次恢复:通过prepareTurn强制压缩(compaction)后重试,并先发射status-notice事件(“context window exceeded — compacting and retrying”)。若重试后消息未实际变小、或重试仍溢出,则以带可操作建议的终态消息失败(如 “there is no conversation history to compact…”),这些常量在 agent-runtime.ts#L52-L75 中定义,可直接用于错误文案匹配。
九、典型完整用法
把前述 API 组合起来的流式多轮 Agent:
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. Keep responses concise.",
tools: [],
hooks: {
onEvent: async (event) => {
if (event.type === "run-failed") {
await logToExternalService(event.error.message)
}
},
},
})
agent.subscribe((event) => {
if (event.type === "assistant-text-delta") {
process.stdout.write(event.text)
}
})
const first = await agent.run("What is 2 + 2?")
const second = await agent.continue("Now multiply that by 3")
console.log(second.outputText)
console.log(first.usage, second.iterations)
更多模式(交互式 CLI、按线程维护 agent 实例的聊天机器人、completesRun 结构化工具、插件)见 patterns.md;常见陷阱与调试见 gotchas.md。
十、延伸阅读(仓库内路径)
- Agent Runtime 概览与快速上手
- Agent 常见模式、陷阱与调试
- 事件类型完整目录
- 工具创建参考、Provider 配置参考
- 源码:agent-runtime.ts(主循环、工具执行、溢出恢复)、agents/src/index.ts(导出清单)、shared/src/agent.ts(
AgentMessage/AgentRuntimeHooks/AgentRunResult等类型定义)、shared/src/llms/tools.ts(ToolPolicy) - 测试:agent-runtime.test.ts、agent-runtime.provider-form.test.ts
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00