跳到主要内容

Harness:会话树、系统提示、Skills 与上下文压缩

30 秒导读: 第 2 章讲的 agent 循环只解决"这一回合怎么跑完"。但真实的编码 agent 要跨几十上百回合工作,还要在上下文窗口撑爆前不崩。本章讲的 AgentHarness 就是循环之上那一层——它把"循环、会话记忆、模型、上下文压缩"编排到一起,负责记住、装配、瘦身

本章聚焦 packages/agent/src/harness/ 这个目录。它是 pi-agent-core 里"有状态"的那半边;工具集、CLI 那些具体接线属于 第 4 章


1. 这是什么(零基础也能懂)

一句话定义: AgentHarness 是一台长会话编排器——它反复调用 agent 循环,把每一步都记进一棵可分叉的会话树,每回合重新拼系统提示,并在历史太长时自动把老对话压成摘要。

为什么需要它(问题场景):

假设你在终端里让 AI 改一个大项目,聊了两小时。这中间会发生一堆循环本身不管的事:

  • 你关掉终端明天再来——对话得存下来、能恢复
  • 你想回到半小时前那个岔路口试另一种改法——历史得能分叉、能回溯
  • 聊到第 80 回合,token 堆到把 20 万的上下文窗口塞满——老历史得自动压缩,否则模型直接拒绝请求
  • 你中途想切个更强的模型、或临时启用某个技能(Skill)——这些变更也得被记住、下回合生效

循环不管这些。Harness 就是专门管这些的那一层。

它由哪几块组成:

组成职责文件
编排器 AgentHarness驱动一个个回合、管状态机、发事件/钩子harness/agent-harness.ts
会话 Session + 存储把每一步落成一棵 append-only 的树,可存可读可分叉harness/session/
系统提示 + 消息装配每回合把提示、历史、Skills 拼成模型能吃的格式harness/system-prompt.tsmessages.ts
Skills从目录扫技能文件,注入提示harness/skills.ts
压缩 compaction历史太长时压成结构化摘要harness/compaction/

一句话直觉: 把 agent 循环当成 CPU 的一个指令周期,那么 Harness 就是操作系统——它管进程调度(回合)、管磁盘(会话持久化)、管内存回收(压缩)。


2. 顶层全景(它大概怎么转)

2.1 分层:循环之上的"状态与记忆"层

怎么读这张图: 从上到下是"控制流谁调用谁";Harness 夹在应用和循环之间,右侧三根柱子是它依赖的资源。

应用 (pi-coding-agent / 你的代码)
│ harness.prompt("改这个 bug")

┌─────────────────────────────────┐ ┌──────────────┐
│ AgentHarness 编排器 │────▶│ 会话树/存储 │ 记住每一步
│ · phase 状态机(idle/turn/…) │ ├──────────────┤
│ · 每回合装配提示+历史+工具 │────▶│ Models 层 │ 发请求(见 01)
│ · 事件广播 + 钩子拦截 │ ├──────────────┤
│ · 撑爆前触发 compaction │────▶│ ExecutionEnv │ 读文件/跑命令
└─────────────────────────────────┘ └──────────────┘
│ 每回合调用一次

runAgentLoop(...) ← 第 2 章的循环

Harness 自己不跑模型、不执行工具——那是 Models 层(见 01)和循环(见 02)的活。它只做"编排 + 记忆 + 瘦身"。

2.2 各部件一句话职责

部件干什么关键符号 (file:line)
AgentHarness顶层类,持有全部可变状态,对外暴露 prompt/skill/compact/navigateTreeagent-harness.ts:157 class AgentHarness
createTurnState每回合开始时快照一份"提示+历史+模型+工具"agent-harness.ts:314
Session会话的领域对象:往树上 append 各类条目、读分支session/session.ts:82
buildSessionContext把树的一条分支重放成模型要的 messages[]session/session.ts:22
JsonlSessionStorage落盘:一行一条 JSON(JSONL)的 append-only 日志session/jsonl-storage.ts:161
convertToLlm把内部 AgentMessage 转成 provider 的 Messagemessages.ts:120
loadSkills / formatSkillInvocation扫技能目录、生成注入块skills.ts:49 / skills.ts:38
prepareCompaction / compact选切割点、生成结构化摘要compaction/compaction.ts:545 / :630

2.3 主线走一遍(一次 prompt 的高层旅程)

harness.prompt(text)
1. 检查 phase===idle,否则抛 "busy" [agent-harness.ts:609]
2. createTurnState():读会话分支 → buildContext()
装配 systemPrompt(可含 Skills 清单) [agent-harness.ts:314]
3. executeTurn():把 text 包成 UserMessage,
调 runAgentLoop(第 2 章) [agent-harness.ts:531]
4. 循环每发一个事件 → handleAgentEvent()
· message_end → 立刻 append 进会话树 [agent-harness.ts:489]
· turn_end → flush 待写、发 save_point
· agent_end → phase 回 idle
5. 返回最后一条 assistant 消息

压缩(compact)和回溯(navigateTree)是另外两个入口,不在 prompt 主线里——它们由应用在合适时机主动调用(比如检测到 token 快满了)。


3. 核心原理(逐个机制,由浅入深)

3.1 编排器 AgentHarness:一台带记忆的状态机

它要解决的小问题

一个 harness 实例在整个会话生命周期里被反复调用。它必须:同一时刻只跑一个回合(否则会话树会写乱)、把回合中途产生的状态变更安全落盘、还要让应用能观察/干预每一步。

思路:一个 phase 互斥锁 + 事件/钩子系统

① phase 是一把状态锁。 所有"重活入口"进来第一件事就是检查 phase !== "idle" 就抛 busy:

// 示意,非源码
async prompt(text) {
if (this.phase !== "idle") throw new AgentHarnessError("busy", "…");
this.phase = "turn"; // 上锁
try { /* 跑一个回合 */ }
finally { /* 结束回 idle */ }
}

phase 一共 5 个取值,真实定义在 types.ts:492 AgentHarnessPhase:

phase含义由谁进入
idle空闲,可接新活初始 / 回合结束
turn正在跑一个 agent 回合prompt / skill / promptFromTemplate
compaction正在压缩历史compact()
branch_summary正在回溯分支并总结navigateTree()
retry(保留)重试

② 事件用两套机制。 subscribe() 广播式订阅所有事件(只读观察);on(type, handler) 注册钩子,能改变行为——返回值会被采纳。真实的钩子结果类型表在 types.ts:704 AgentHarnessEventResultMap:

钩子事件能干什么
before_agent_start追加消息、替换本回合系统提示
context发给模型前改写整个消息列表
tool_call拦截某次工具调用(block: true)
tool_result改写工具结果、或 terminate 提前结束
session_before_compact取消压缩、或直接提供现成摘要
before_provider_request打补丁改 headers/超时/重试等请求参数

这套钩子就是 第 4 章编码 agent 接线"权限确认、工具审计"的挂载点。

关键细节:回合中途的状态变更走"待写队列"

回合跑到一半时,你可能通过钩子切了模型、或改了激活工具。这些变更不能立刻插进会话树(会打断正在进行的回合语义),于是被塞进 pendingSessionWrites 队列,等到安全点(turn_end)再统一 flush:

// 示意,非源码
async setModel(model) {
if (this.phase === "idle") await this.session.appendModelChange(...); // 直接写
else this.pendingSessionWrites.push({ type: "model_change", ... }); // 排队
}

真实逻辑见 agent-harness.ts:833 setModelagent-harness.ts:462 flushPendingSessionWrites。这个"idle 直写、忙时排队"的模式贯穿所有 set* 方法。

每回合重新装配:createTurnState

一个容易忽略的点:系统提示、历史、激活工具都是每回合重新算的,不是构造时定死的。createTurnState(agent-harness.ts:314)每次调 session.buildContext() 拉最新历史,并在 systemPrompt 是函数时重新执行它:

// 示意,非源码 —— createTurnState 的核心
const context = await this.session.buildContext(); // 最新历史
let systemPrompt = "You are a helpful assistant."; // 默认兜底
if (typeof this.systemPrompt === "function")
systemPrompt = await this.systemPrompt({ env, session, model, activeTools, resources });

这意味着系统提示可以动态反映"当前用了哪个模型、激活了哪些工具、有哪些 Skills"。


3.2 会话持久化与分支树:一切都是 append-only

它要解决的小问题

要能"关掉再恢复""回到岔路口试另一条路",会话就不能是一根线性数组,而得是一棵;而且为了健壮,写入最好是只追加、不修改

思路:每一步是树上一个节点,靠 parentId 串成链

会话里发生的每件事——一条消息、一次改模型、一次压缩——都是一个 SessionTreeEntry,带 idparentId。类型联合见 types.ts:409:

message → message → model_change → message ← 主分支(当前 leaf)

message → message ← 回溯后另开的分支

"当前在哪"由一个 leaf 指针决定。 存储里最新的 leaf 条目指向当前叶子;顺着 parentId 一路走到根,就是"当前这条对话"。取分支的核心是 getPathToRoot(jsonl-storage.ts:275):

// 示意,非源码 —— 从叶子回溯到根
let current = byId.get(leafId);
while (current) {
path.unshift(current); // 头插,最后得到根→叶顺序
if (!current.parentId) break;
current = byId.get(current.parentId);
}

落盘:JSONL,一行一条 JSON

JsonlSessionStorage(jsonl-storage.ts:161)把会话存成一个 .jsonl 文件:第一行是 session 头(version 3、id、cwd),之后每行一个条目。写入永远是 appendFile(jsonl-storage.ts:250)——从不改已有行。这带来两个好处:崩溃安全(半行坏了不影响前面),以及天然的审计日志。

一个精巧处:leaf 也是一条追加的日志。切换当前叶子不是改指针,而是 append 一条 leaf 条目;重放时 leafIdAfterEntry(jsonl-storage.ts:109)决定"读完这行后当前叶子是谁":

// 示意,非源码
function leafIdAfterEntry(entry) {
return entry.type === "leaf" ? entry.targetId : entry.id; // leaf 条目跳转,其它条目自身成为叶子
}

重放:buildSessionContext 把树变回 messages[]

树是给"记录/回溯"用的;模型要的是一个线性 messages[]buildSessionContext(session/session.ts:22)负责这次转换。它扫一遍分支,顺手把 model_changethinking_level_changeactive_tools_change 折叠成"当前有效值",再把 message / custom_message / branch_summary 拼成消息列表。

压缩感知是它最关键的一段。 如果分支里有 compaction 条目,它不重放被压掉的老历史,而是先放一条摘要消息,再只接上 firstKeptEntryId 之后的条目:

// 示意,非源码 —— buildSessionContext 里的压缩分支
if (compaction) {
messages.push(createCompactionSummaryMessage(compaction.summary,)); // 摘要顶替老历史
// 只重放 firstKeptEntryId 及其之后的条目,前面的全跳过
}

这是全章一个核心洞见:压缩不删任何东西。 JSONL 里老条目还在(append-only),compaction 只是一条"从这里起用摘要代替前文"的标记;瘦身发生在重放时,而非写入时。所以压缩可回溯、可审计。

回溯与分叉:navigateTreefork

  • navigateTree(targetId)(agent-harness.ts:732):把 leaf 移到树上另一个节点。如果你回到某条旧的 user 消息,它会把 leaf 设到那条消息的上、并把该消息文本作为 editorText 吐回来(供你编辑重发);沿途"被抛弃的分支"可选地生成一段 branch summary(见 3.5)存下来,免得白探索一场。
  • fork(session/jsonl-repo.ts:133):把一个会话的前 N 条复制成一个新文件,parentSession 指回原会话。会话文件按 cwd 分目录存放(encodeCwd,jsonl-repo.ts:34)。

3.3 系统提示与消息装配:内部格式 ↔ 模型格式

它要解决的小问题

Harness 内部用一套富类型的 AgentMessage(除了 user/assistant/toolResult,还有 bashExecutioncustombranchSummarycompactionSummary 等)。但 provider 只认标准的 user/assistant/toolResult。中间需要一层翻译。

convertToLlm:把"非标准角色"降维成 user 文本

convertToLlm(messages.ts:120)逐条转换。标准三类原样透传;特殊角色则渲染成一段 user 文本:

内部角色转成什么渲染方式
bashExecutionuser 文本bashExecutionToText——命令+输出+退出码
customuser 文本/图直接取 content
branchSummaryuser 文本包在 BRANCH_SUMMARY_PREFIX/SUFFIX
compactionSummaryuser 文本包在 COMPACTION_SUMMARY_PREFIX/SUFFIX

那些 create*Message(messages.ts:81 起)是反方向的构造器:会话树里存的是紧凑条目(如 compaction 只存 summary 字符串),重放时用它们造出带完整前后缀的消息对象。前后缀常量如 messages.ts:4 COMPACTION_SUMMARY_PREFIX——摘要外面裹一句"此点之前的历史已被压缩成如下摘要"。

一个细节:bashExecution 上有 excludeFromContext 开关(messages.ts:125),置真时该条不进模型上下文,但仍留在会话树里可见——用于"我看得到但不喂给模型"的输出。

Skills 如何进系统提示

Skills 有两种入场方式,别混淆:

  1. 清单注入(让模型知道有哪些技能):formatSkillsForSystemPrompt(system-prompt.ts:3)把每个技能的 name/description/location 拼成 <available_skills> XML 块,放进系统提示,并叮嘱模型"任务匹配时去读完整技能文件"。disableModelInvocation 的技能会被过滤掉、不出现在清单里。
  2. 显式调用(把整个技能塞进来执行):见 3.4 的 formatSkillInvocation

3.4 Skills 机制:目录里的 Markdown 就是能力

它要解决的小问题

想让 agent 可扩展——用户丢一个 Markdown 文件进某个目录,就多一项能力,不用改代码。这就要有一套"扫目录 → 校验 frontmatter → 变成可注入的技能"的加载器。

思路:递归扫目录,认 SKILL.md 与根级 .md

loadSkills(skills.ts:49)遍历给定目录:每个子目录里若有 SKILL.md 就当一个技能;根目录下的直接 .md 文件也各算一个技能。它还尊重 .gitignore / .ignore / .fdignore(skills.ts:7),跳过被忽略的路径。

每个技能文件的头部是 YAML frontmatter,parseFrontmatter(skills.ts:303)解析:

---
name: code-review # 必须等于父目录名,且只能 a-z0-9-
description: 审查代码的 diff # 必填,进系统提示清单;≤1024 字符
disable-model-invocation: false # 置真则从模型可见清单里隐藏
---
(下面正文就是技能的完整指令 content)

校验很较真:validateName(skills.ts:281)要求 name 与父目录同名、全小写连字符、不能首尾连字符或双连字符;description 为空的技能直接被丢弃(skills.ts:265)。校验失败不抛错,而是收集成 SkillDiagnostic 警告返回——一个坏技能不该拖垮整批加载

显式调用:formatSkillInvocation

当应用直接 harness.skill("code-review")(agent-harness.ts:623),会用 formatSkillInvocation(skills.ts:38)把技能正文包成一个 <skill> 块作为 user 消息发进去:

// 示意,非源码
`<skill name="code-review" location="/abs/path/SKILL.md">
References are relative to /abs/path.

<这里是技能正文全文>
</skill>`

注意它显式告诉模型"技能里的相对路径以技能目录为基准"——因为技能常引用同目录的脚本/模板。

ExecutionEnv:文件与命令的可替换抽象

Skills 加载器从不直接调 node:fs,而是通过 ExecutionEnv(types.ts:331,由 FileSystem + Shell 拼成)。默认实现是 NodeExecutionEnv(env/nodejs.ts:230)。这层抽象有两个用意:

  • 可移植:换个后端(远程沙箱、内存 FS)就换个 ExecutionEnv,加载器代码不动。
  • 永不抛错:FileSystem 契约(types.ts:249 注释)规定所有文件操作必须把失败编码进 Result<T, FileError> 返回,而不是抛异常。所以加载器里满是 if (!result.ok) 而非 try/catch——错误是值,不是控制流。

Prompt 模板(prompt-templates.ts:30 loadPromptTemplates)是 Skills 的姊妹机制:同样扫 .md、解析 frontmatter,但支持 $1/$@/$ARGUMENTS 占位符替换(substituteArgs,prompt-templates.ts:248),用于"斜杠命令"式的参数化提示。


3.5 【重点】上下文压缩 compaction:撑爆前把老历史压成摘要

这是本章工程含量最高的一支,也是长会话能活下去的关键。

它要解决的小问题

上下文窗口是有限的(比如 20 万 token)。聊得越久,历史越长,总有一刻请求会被 provider 拒绝。压缩要做的是:在撑爆前,把远端的老历史换成一段浓缩摘要,同时原封不动保留最近的工作

三个问题定义了压缩:何时触发、保留什么、丢弃什么

何时触发。 判断函数是 shouldCompact(compaction.ts:200):

// 示意,非源码
shouldCompact = contextTokens > contextWindow - reserveTokens;

即"已用 token 逼近(窗口 − 预留)"就该压。默认 reserveTokens 16384(给摘要提示和输出留地),定义在 DEFAULT_COMPACTION_SETTINGS(compaction.ts:111)。token 估算优先信任 provider 回报的真实 usage,只对最后一条 usage 之后的消息用"字符数 ÷ 4"的启发式补估(estimateContextTokens,compaction.ts:169)。

注意:shouldCompact 只是个判断器,harness 的 compact() 自己不查它。触发时机由调用方(编码 agent)决定,这条接线归 第 4 章;本章讲清压缩机制本身

保留什么(切割点选择)。 目标是保住大约 keepRecentTokens(默认 20000)的最近历史。findCutPoint(compaction.ts:333)从最新往回累加 token,一过阈值就找一个合法切点落刀:

← 越老 越新 →
[msg][msg][msg][msg] | [msg][msg][msg][msg]
└──── 压成摘要 ────┘ ↑ └──── 原样保留 ────┘
切割点(必须落在回合边界)

合法切点不能乱选——findValidCutPoints(compaction.ts:265)只允许切在 user/assistant/bashExecution/摘要类条目上,绝不切在 toolResult(否则会留下一个没有对应 toolCall 的孤儿结果,模型会报错)。

处理"切到回合中间"的巧劲(split turn)。 有时最近 20000 token 装不下一整个回合,切点落在了某回合内部。这时 pi 不粗暴截断,而是把这个回合劈成两半:前缀(prefix)另外用一个更省字的提示单独总结(generateTurnPrefixSummary,compaction.ts:699),后缀(recent)原样保留,两段摘要拼在一起。判定见 prepareCompaction(compaction.ts:545)里的 isSplitTurn

丢弃什么,但留下什么线索。 切点之前的历史被摘要顶替。但有一样东西不能丢:这段历史里读过/改过哪些文件。extractFileOpsFromMessage(compaction/utils.ts:24)扫 assistant 的 toolCall,按工具名分类:

工具名归入
read读过的文件
write整文件写过
edit编辑过

computeFileLists(utils.ts:54)最后算出"只读过"与"改过"两张清单,formatFileOperations 把它们作为 <read-files> / <modified-files> 标签追加进摘要。这样即便对话被压掉,模型仍知道"我之前动过这些文件",不会重复劳动。

摘要长什么样:一份结构化 checkpoint

压缩不是让模型随口概括,而是逼它填一个固定模板(SUMMARIZATION_PROMPT,compaction.ts:387):Goal / Constraints / Progress(Done·In Progress·Blocked)/ Key Decisions / Next Steps / Critical Context。系统提示(compaction.ts:383)还反复强调"只输出摘要,别接着聊、别回答里面的问题"——防止总结模型被历史里的指令带跑。

增量压缩:第二次压缩不重头再来

如果分支里已经有过一次压缩,prepareCompaction 会找到上一条 compaction,把它的 summary 作为 previousSummary 传下去,并只summarize"上次切点之后的新消息"。这时改用 UPDATE_SUMMARIZATION_PROMPT(compaction.ts:420),规则是"保留旧摘要的一切,把新进展并进去、把 In Progress 挪到 Done"。摘要是迭代累积的,不是每次推倒重写。

整条压缩流水线

compact(customInstructions?) [agent-harness.ts:686]
1. 取当前分支所有条目 session.getBranch()
2. prepareCompaction():定切点、分 history/prefix、抽 fileOps
3. session_before_compact 钩子(可取消 / 可直接给摘要)
4. compact():调模型生成结构化摘要(+ 文件清单标签)
5. session.appendCompaction():把摘要作为一条 compaction 追加进树
→ 下回合 buildSessionContext 就会用这条摘要顶替老历史

分支摘要 branch summary:压缩的近亲

navigateTree 回溯时,"被离开的那条分支"可选地被总结掉,机制几乎和压缩同源。collectEntriesForBranchSummary(compaction/branch-summarization.ts:67)先算老叶子和目标的最近公共祖先,只收集"公共祖先之后、要被抛弃的那段",再由 generateBranchSummary(branch-summarization.ts:199)按一个类似的结构化模板总结。区别在于:

compactionbranch summary
触发历史太长,当前分支瘦身回溯到别的节点,总结被离开的分支
存成compaction 条目branch_summary 条目
目的省当前上下文别让探索白费,回来还看得到
提示SUMMARIZATION_PROMPT(含 Critical Context)BRANCH_SUMMARY_PROMPT(略简)

两者复用同一套文件操作追踪(utils.ts)和 serializeConversation(utils.ts:91,把消息拍平成 [User]: … [Assistant tool calls]: … 的纯文本喂给总结模型,单条 toolResult 超 2000 字符会截断)。


4. 巧妙之处(可借鉴的技术)

  • 压缩即"重放时跳过",而非"写入时删除"。 append-only 日志 + compaction 标记,让瘦身可回溯、可审计,还天然崩溃安全。见 session/session.ts:61 的重放分支。
  • leaf 也是一条日志条目。 把"当前在哪"做成可追加的 leaf 条目而非可变指针,整份会话史(包括每次跳转)都能忠实回放。jsonl-storage.ts:109 leafIdAfterEntry
  • 回合中途变更走待写队列,turn_end 才 flush。 干净地把"用户/钩子随时改状态"和"回合语义完整性"解耦。agent-harness.ts:462
  • 切点绝不切在 toolResult 上。 一个不起眼但致命的正确性约束——避免产生孤儿工具结果。compaction.ts:265 findValidCutPoints
  • split turn 双段摘要。 单个回合都塞不下时,前缀用更省 token 的提示单独总结,而不是硬截断丢信息。compaction.ts:655 起。
  • 文件操作清单穿透压缩。 把"读过/改过哪些文件"作为结构化标签保进摘要,让模型跨压缩仍有工作记忆。utils.ts:54
  • 错误是值不是异常。 FileSystem 契约强制所有文件操作返回 Result,加载器逻辑因此线性、无 try/catch 噪音。types.ts:249

5. 边界与局限

  • 压缩靠"再调一次模型"。 生成摘要本身是一次 LLM 请求,有延迟、有成本、可能失败(CompactionError)。它假设你有一个可用的总结模型。
  • token 估算是启发式的。 最后一条 usage 之后的消息按"字符 ÷ 4"估,图片按固定 4800 字符估(compaction.ts:205)。对非英文/代码密集内容会有偏差,reserveTokens 的余量就是为吸收这种误差留的。
  • 摘要有损。 压缩后被丢的细节只剩摘要里那几条;模板逼它保留文件路径/函数名/错误信息,但仍可能漏掉当时没判断为"关键"的东西。
  • harness 不自动决定何时压缩。 compact() 是手动入口,shouldCompact 只是给调用方的判断器——触发策略在应用层(第 4 章)。
  • 单会话单文件、按 cwd 分目录。 会话文件路径由 cwd 编码(jsonl-repo.ts:34),跨机器/跨路径迁移会话需要额外处理。
  • JSONL 全量载入内存。 loadJsonlStorage(jsonl-storage.ts:136)把整个文件读进来建 byId 索引;超长会话的内存占用随条目数线性增长。

6. 横向对比(本组其它章)

  • 想知道 Harness 每回合调用的那个循环内部怎么跑(工具调用、steering、事件流)?看 02-agent-loop.md
  • 想知道压缩何时被触发、Skills 目录从哪来、这些钩子被接到哪些真实工具上?看 04-coding-agent.md
  • 想知道 Models 层怎么把摘要请求真正发给某个 provider?看 01-unified-llm.md
  • 全景与阅读地图见 index.md

7. 代码地图(导航索引)

主题文件路径符号名
顶层编排器packages/agent/src/harness/agent-harness.tsAgentHarness
回合装配packages/agent/src/harness/agent-harness.tscreateTurnState
待写队列 flushpackages/agent/src/harness/agent-harness.tsflushPendingSessionWrites
事件路由packages/agent/src/harness/agent-harness.tshandleAgentEvent
手动压缩入口packages/agent/src/harness/agent-harness.tscompact
树回溯入口packages/agent/src/harness/agent-harness.tsnavigateTree
phase / options / 事件表packages/agent/src/harness/types.tsAgentHarnessPhaseAgentHarnessOptionsAgentHarnessEventResultMap
会话领域对象packages/agent/src/harness/session/session.tsSession
分支重放为消息packages/agent/src/harness/session/session.tsbuildSessionContext
JSONL 存储packages/agent/src/harness/session/jsonl-storage.tsJsonlSessionStoragegetPathToRootleafIdAfterEntry
会话仓库/分叉packages/agent/src/harness/session/jsonl-repo.tsJsonlSessionRepoforkencodeCwd
消息格式转换packages/agent/src/harness/messages.tsconvertToLlmbashExecutionToText
Skills 清单注入packages/agent/src/harness/system-prompt.tsformatSkillsForSystemPrompt
Skills 加载/校验/调用packages/agent/src/harness/skills.tsloadSkillsloadSkillFromFilevalidateNameformatSkillInvocation
Prompt 模板packages/agent/src/harness/prompt-templates.tsloadPromptTemplatessubstituteArgs
压缩阈值/触发判断packages/agent/src/harness/compaction/compaction.tsDEFAULT_COMPACTION_SETTINGSshouldCompactestimateContextTokens
切点选择packages/agent/src/harness/compaction/compaction.tsfindCutPointfindValidCutPointsfindTurnStartIndex
压缩准备/执行packages/agent/src/harness/compaction/compaction.tsprepareCompactioncompactgenerateSummary
文件操作追踪/序列化packages/agent/src/harness/compaction/utils.tsextractFileOpsFromMessagecomputeFileListsserializeConversation
分支摘要packages/agent/src/harness/compaction/branch-summarization.tscollectEntriesForBranchSummarygenerateBranchSummary
执行环境抽象packages/agent/src/harness/env/nodejs.tsNodeExecutionEnv(实现 ExecutionEnv)