第一章系统整体架构
ezharness = 基于 ezloop 引擎的桌面 agent harness。启动即对本机全权(文件不限目录 + 原生 shell)。三层结构:desktop(Electron 壳:窗口 / 托盘 / 内嵌浏览器)拉起 core(Go sidecar,gin 三层 MVC + 内嵌前端)伺服 frontend(端无关页面,单入口 SPA);core 也可直接运行——浏览器访问同一份页面(浏览器控制等桌面专属能力自动降级)。
1.1 双仓库:引擎 × 宿主
| 仓库 | 角色 |
|---|---|
ezloop../ezloop 本地 replace |
agent 引擎:core 循环(loop / agent / fork)、provider 三协议(anthropic / openai / responses)、hook / warp 扩展点、ext 通用组件(filetools · skilltool · mcp · approve · askuser · task · offload · contextfix) |
| ezharness 本仓库 |
宿主应用:装配(选 hook、传参数)+ 产品域(会话树 / 设置 / 终端 / 快应用 / 前端)+ 自有 hooks(remind / trim / sessionstore / sysprompt / reference_file / guard / trace) |
replace 指向本地 ezloop;发布前升版本、移除 replace。
1.2 分层总图
flowchart TB
UI["🖥 前端 SPA(Svelte 5 runes)<br/>store.svelte · api.ts · term.ts"]:::ui
subgraph shell["core 装配 · core/main.go / app.go"]
CFG["config · ezharness.json(应用根)<br/>chdir 数据目录 · 换代 restart · boot 代际"]:::layer
end
subgraph EZH["ezharness 宿主 · 装配 + 产品域"]
C["controller 表现层<br/>10 组 gin handler"]:::layer
S["service 用例层<br/>Chat / Session / Topic / Settings / Terminal / Agent…"]:::layer
D["domain · Session 聚合 + Hub<br/>零 HTTP 依赖"]:::layer
H["internal/hooks(宿主侧)<br/>sysprompt · remind · trim · sessionstore<br/>reference_file · guard · trace"]:::layer
TL["internal/tools<br/>save_app · term_* · browser_*"]:::layer
WP["internal/warp<br/>modeldump · visionguard · toolarg"]:::layer
end
subgraph EZL["ezloop 引擎 · 本地 replace"]
CORE["core · loop / agent / fork"]:::engine
EXT["ext 组件 · filetools · skilltool · mcp<br/>approve · askuser · task · offload · contextfix"]:::engine
PROV["provider · anthropic / openai / responses"]:::engine
end
DISK[("数据目录 = 进程 cwd<br/>sessions/ · memory/ · workspace/ · apps/")]
UI -- "HTTP REST(用例)" --> C
UI -- "SSE /events(对话事件流)" --> C
UI -- "WS /terminal/ws(共享终端)" --> C
C --> S --> D
D --> H
S --> TL
D --> WP
H --> EZL
WP --> PROV
CORE --> PROV
CORE -.->|hook 调用| H
H --> DISK
classDef ui fill:#0b1322,stroke:#33496e,color:#c4cede
classDef layer fill:#072331,stroke:#00add8,color:#9be5f7
classDef engine fill:#07281b,stroke:#34d399,color:#a7f3d0
style shell fill:#0a101d,stroke:#33496e,stroke-dasharray:6 4
style EZH fill:#0a101d,stroke:#00add8,stroke-dasharray:6 4
style EZL fill:#0a101d,stroke:#34d399,stroke-dasharray:6 4
1.3 三层结构:desktop 壳 × core sidecar × 前端页面
flowchart TB
subgraph DESK["desktop/ · ezharness.exe · Electron 壳"]
MW["主窗口 / 托盘 / 快应用子窗<br/>无边框主窗(前端 TitleBar 自绘)"]:::role
BW["内嵌浏览器窗口<br/>WebContentsView 标签页(desktop 资产)"]:::role
end
subgraph COREP["core/ · ezharness-core.exe · Go sidecar"]
G["gin server<br/>前端页面 · /api/* · SSE 事件流 · /apps/*"]:::layer
A["agent 引擎<br/>ezloop core · 随会话装配"]:::engine2
end
UI["🖥 前端页面 · Svelte 单入口 SPA<br/>主窗 / 抽屉弹窗 / 浏览器抽屉同一份页面"]:::ui
BR["🌐 浏览器(web 端形态)"]:::misc
DESK -. "spawn + health 轮询<br/>退出随壳 kill" .-> COREP
MW -- "loadURL 同源页面" --> G
BW -- "承载 SPA 的浏览器抽屉/弹窗<br/>(标签条/地址栏 UI,?popout=browser)" --> G
BW -. "browser_* 工具<br/>WS 桥 /api/browser/bridge" .-> G
BR --> G
G --> A
classDef role fill:#072331,stroke:#00add8,color:#9be5f7
classDef layer fill:#072331,stroke:#00add8,color:#9be5f7
classDef engine2 fill:#07281b,stroke:#34d399,color:#a7f3d0
classDef ui fill:#0b1322,stroke:#33496e,color:#c4cede
classDef misc fill:#0b1322,stroke:#33496e,color:#c4cede
style DESK fill:#0a101d,stroke:#33496e,stroke-dasharray:6 4
style COREP fill:#0a101d,stroke:#00add8,stroke-dasharray:6 4
1.4 前后端三通道
| 通道 | 端点 | 承担 |
|---|---|---|
| HTTP REST | 127.0.0.1:5260(可配) |
全部用例操作:会话 / 设置 / 模型 / 分支 / mcp / apps / 工作目录文件预览。错误形态 400: {"error":"xx"} |
| SSE | GET /api/sessions/:id/events |
对话事件流({type, ts, iter, forkId?, data?} 帧)。建连重放整轮聚合帧(断线恢复),首帧 replay.sync 权威复位 busy |
| WS | GET /api/terminal/ws |
共享终端:单连接多路复用全部终端(帧带 id 路由),hello 帧带各终端快照,重连即恢复 |
| WS | GET /api/browser/bridge |
共享浏览器桥:desktop 壳连入,browser_* 工具以 JSON-RPC 转发到壳的 WebContentsView 执行器;桥未连接时工具报「仅桌面端可用」 |
全部 /api/* 经鉴权中间件:X-EZ-Token 头或 ?token=(设置页可查看复制;desktop 每次启动随机生成注入 core 与页面,web 直跑模式打印在启动 URL);同源页面(前端与 /apps/* 快应用)Origin/Referer 与 Host 匹配即放行——快应用调 MCP 等 API 无需感知 token。
前端手写 fetch 层(端无关,无桌面绑定);core 经 go:embed 内嵌前端产物(dist 缺失则 go build 直接报错);桌面壳经 preload IPC 暴露 window.ez(窗口控制 / 浏览器窗口),web 端无此对象即自动降级。
1.5 数据目录布局
应用根 = exe 所在目录(config.Root() 锚定,不受 chdir 影响);进程 cwd = 数据目录(启动时 chdir),ezloop 各 hook 的相对路径存储自动落此。
D:\ezharness\ # 应用根 = exe 所在目录(portable 绿色版经 --root 锚定到 exe 旁) ├─ ezharness.exe # desktop 壳(安装版/绿色版;core 内嵌于其 resources\) ├─ ezharness.json # 端口 / 监听 / dataDir / 窗口尺寸(结构配置) └─ data\ # 数据目录(dataDir 空 = 应用根下 data/) ├─ settings.json # WorkDir / TrimPercent / MaxIterations / DisabledSkills ├─ models.json # 模型三槽 main/image/audio + 用量累计 ├─ toolRules.json # 工具审批四档策略 ├─ topics.json # 分支(线)索引 ├─ stats.json # 跨会话生命体征 ├─ mcp.json # MCP server 配置(热加载) ├─ sessions\<id>\ # session.json(状态)· trace.jsonl(消息行+span,追加式)· progress.md · decisions.jsonl · forks/ ├─ memory\longterm\ # harness.md 索引 + 记忆文件 user/soul/project.md ├─ memory\skills\ # 技能库 SKILL.md + scripts/ ├─ apps\ # 快应用 html(save_app 生成,静态服务 /apps/*) ├─ workspace\tmp\ # 上传附件暂存(base64 不入上下文) └─ .ezloop\offload\ # 大工具结果卸载区
system prompt 的 <workspace> 段只把三个可写位置(workspace / memory/longterm / memory/skills,绝对路径)告知模型,其余目录行为规则化不诱导翻阅——详见第三章 system 组装;压缩与归档的记忆方案见 docs/md/compaction.md。
1.6 应用生命周期
desktop 壳(ezharness.exe · Electron 主进程) ├─ 读 ezharness.json # 应用根(portable 用 PORTABLE_EXECUTABLE_DIR) ├─ spawn ezharness-core.exe # core 退出 → 壳随之退出 └─ health 轮询就绪 # /api/app/health(30s 超时)→ 主窗 loadURL core 地址 core(ezharness-core.exe · Go sidecar) ├─ config.Load() # 读应用根 ezharness.json(缺失建默认;--root 可覆盖根) ├─ newApp(cfg) # MkdirAll → os.Chdir(数据目录) ├─ a.start() # net.Listen 预占端口 → buildRouter() → srv.Serve └─ 信号等待(Interrupt/SIGTERM)→ stop()
- buildRouter 换代重建:
domain.NewHub()(含 bootstrap 恢复)→ TerminalService → AgentService.Assemble → 全部 controller。设置页改端口 / 数据目录触发换代:shutdownGeneration(杀终端 shell → 取消运行轮并等落盘 → 关 server)→ syncDrained → chdir → buildRouter;boot代际计数跨代共享,前端据此判断新服务就绪 - 退出顺序同理:轮内历史只在 OnEnd 落盘,不等落盘直接退出会丢整轮;SSE 长连直接
srv.Close()(优雅 Shutdown 会拖满超时)
1.7 开发与发布
script\version.yaml # 版本单源:frontend 页脚 ezharness-v<version> · desktop 打包版本(release.bat 同步) script\dev.bat # 调试:前端构建 + core 编译(bin\)→ npm run start 前台跑 Electron(无打包) script\release.bat # 发布打包:三段链 + NSIS → release\v<version>\ 安装包 + 绿色版
- 构建链:前端 vite build(单入口 SPA)→ 拷
frontend/dist → core/web/dist→go build出bin\ezharness-core.exe→ electron-builder(extraResources 内嵌 core,产物直出release\v<version>\);dev.bat 只走前两段后直接跑壳,release.bat 走完整链打包 - 验证链:
cd core && go build ./... && go vet ./...+cd frontend && npm run build;desktop 主进程 JS 用node --check过一遍 go run ./core会把 exe 放临时目录导致 config 找不到应用根——验证须go build -o xxx.exe ./core后在配置所在目录运行
第二章session 树
一条 session = 一次会话聚合:内存运行时(domain.Session)+ 磁盘存档(sessions/<id>/session.json)。多个 session 世代构成 session 树(线 → compact 换代链 → fork 分叉),用户在"分支"(线)粒度上切换对话。
2.1 核心概念
| 概念 | 含义 |
|---|---|
| session 世代 | 一次 compact 归档产生一个新世代;每世代一个 <id>,各自有存档目录 |
| ID(叶) | 当前叶世代 ID,compact 换代随之更新 |
| RootID(线根) | 所属分支的稳定身份——Hub.branches 注册表键、前端路由、API :id 参数全用它;换代不换线 |
| 线(branch/topic) | 同一 RootID 下的世代链 + 侧栏条目(topics.json 的 TopicEntry) |
2.2 树形结构图
flowchart TD
subgraph line1["线 root-1(TopicEntry:ID=root-1,LeafID=gen-3)"]
gen1["gen-1 世代<br/>Archived=true 封存"]:::gen -- "compact(用户手动)<br/>摘要归档 + PrevSession 边" --> gen2["gen-2 世代<br/>Archived=true"]:::gen
gen2 -- "compact" --> gen3["gen-3 当前叶<br/>(Active,ID 随换代更新)"]:::leaf
end
gen1 -. "fork@anchor<br/>携带 0,anchor 完整副本" .-> line2root
subgraph line2["线 root-2(Kind=fork,自成新根)"]
line2root["root-2 世代 0<br/>标题 = 锚点消息内容"]:::fork
end
taskFork["轮内 task 分身(ForkID ≠ '')<br/>并行子循环 · 增量存 forks/<fid>/<br/>不开线,answer 汇回主循环"]:::task
gen3 -. "task 工具" .-> taskFork
prev["上翻链:PrevSession<br/>(前端懒加载旧世代全文)"]:::note
gen2 --- prev
classDef gen fill:#072331,stroke:#00add8,color:#9be5f7
classDef leaf fill:#0b2e40,stroke:#38bdf8,color:#bae6fd
classDef fork fill:#1d1636,stroke:#a78bfa,color:#ddd3fc
classDef task fill:#07281b,stroke:#34d399,color:#a7f3d0
classDef note fill:#0b1322,stroke:#33496e,color:#8b98b0
2.3 内存 ↔ 磁盘:同源两份
flowchart LR
subgraph mem["内存 · domain.Session"]
HIST["history []Message<br/>全量(含 marker 前档案)"]:::m
VIEW["modelView()<br/>= 最后 context_trim marker 之后"]:::m
WIRED["wiring:Agent / Provider<br/>ApproveCh / AnswerCh / Trace"]:::m
end
subgraph disk["磁盘 sessions/<id>/"]
SNAP["session.json(状态)<br/>SystemPrompt / Snapshot 基线<br/>Edge / Usage / Archived…"]:::d
TRC["trace.jsonl<br/>消息行 + span(追加式)"]:::d
DEC["decisions.jsonl"]:::d
FK["forks/<fid>/session.json"]:::d
end
HIST -- "sessionstore.OnEnd 每轮落盘:<br/>新增消息追加 trace.jsonl;<br/>状态覆盖写 session.json" --> SNAP
SNAP -- "bootstrap restoreFrom<br/>(history / 基线 / 水位 / LineRoot)" --> HIST
WIRED -.-> TRC
classDef m fill:#072331,stroke:#00add8,color:#9be5f7
classDef d fill:#0b1322,stroke:#33496e,color:#c4cede
style mem fill:#0a101d,stroke:#00add8,stroke-dasharray:6 4
style disk fill:#0a101d,stroke:#33496e,stroke-dasharray:6 4
SessionSnap 关键字段
| 字段 | 作用 |
|---|---|
Messages(json:"-") | 不落 session.json——消息以追加行存 trace.jsonl(LoadMessages 读回合成) |
SystemPrompt / SystemBase / SummaryBlock | 渲染后完整 system(恢复零逻辑)/ 基础段(人格+记忆+清单)/ compact 摘要段 |
PrevSession / CompactSummary | compress 边:上一世代 ID 与摘要(前端上翻链) |
TargetID / Anchor / SeedKind / ForkedFrom | SnapEdge(创建时写死):向上边目标 / fork 复制的前缀长度 / new·fork·compress / 分叉出处 |
LineRoot | 所属线根(冗余存档,链操作 O(1);换代 SetID 不清它) |
Snapshot *ResSnapshot | 资源基线 {Skills, Mcps, Terms}——remind 变更段的对比基准,重启不重复报 |
LastOutputAt / Usage / CtxTokens / CtxWindow | "距上次输出" / 本代用量 / 水位恢复 |
Title / Archived | 世代名(本代首条 user)/ compact 后封存位(不再作恢复候选) |
落盘时机 = OnEnd 每轮结束(sessionstore 是 hooks 数组最后一位;失败不阻断,错误记日志)。落盘为增量追加:盘上消息(LoadMessages 合并主 trace 与 fork 增量)→ MergeFull(prev, state) 求全量 → stripSystem → fork 剥 seed → 只把 msgs[len(prev):] 追加进 trace.jsonl(trim 折叠不重写盘上档案);session.json 只写状态(原子覆盖)。
2.4 session 树的三种演进
compact 换代 仅用户手动
沉淀式归档:先把值得长期保留的内容合并进记忆文件,再精炼交接摘要封存旧库、新库带摘要继续。POST /api/topics/compact
BeginArchive 占锁(沉淀+摘要各 5 分钟;锁发消息/切分支/二次归档)
→ ① distillToMemory(沉淀步,失败静默):会话 view + user/soul/project 三文件现内容
→ 模型合并重写(去重/纠偏/每文件 ≤30 行,UNCHANGED 段跳写)→ 整体覆盖写回
→ ② summarizeMsgs(交接摘要:总体目标/未完成与下一步/关键决定/交付物/待办,300 字内)
→ 旧库翻 Archived=true 封存
→ sys.Set(base, summaryBlock) # 新 system 两段生效
→ 新库初始快照先落盘(空 messages + compress 边——任一时刻重启新库都可恢复)
→ SetID(newID)(清基线/用量,LineRoot 留)→ SetPrev → topics.UpdateLeaf(换代不换线名)
→ RotateTo(内存换代:清历史/水位)→ Publish session.compact
fork 分叉 copy 语义
POST /api/sessions/:id/fork body {anchor}——锚点消息序号(含选中消息)。
- 源是"未产生新消息的 fork"时沿 fork 边上溯到实际内容源(空叉链折叠)
- 新 session 体内携带 [0, anchor] 完整副本(快照隔离;system / 水位承源)→ topics.Add(Kind:"fork") → LoadBranch + SetActive
- fork 线标题 = 锚点消息内容(TitleFromMsg);有自己的首条新增 user 后显式切换
- 命名规则:new 线 = 首条真实 user(FirstUserTitle 跳过 agent_status / resource_change / end_reason / trim marker 等系统注入,40 字截断);世代名 = 本代首条 user,命名后固定
归档 预留
POST /sessions/:id/archive——仅翻 snap.Archived 位;活动分支当前叶或运行中拒绝(标记会被下次落盘覆盖)。归档世代继续聊走 fork@tip。
2.5 生命周期与恢复
创建:newSession(rootID 空 → 自成一根)→ Assemble(装配 wiring;snap 有则还原 SysPrompt)
├─ 新线:首条发言时 topics.Add(延迟建索引防空线粉尘)
└─ fork:创建即 Add
运行:StartRun ⇄ FinishRun 每轮;sessionstore.OnEnd 落盘
恢复(NewHub → bootstrap):
ListMain 列目录项(排除 -task-)→ mtime 降序 → LoadSnap 逐个
→ 跳过 Archived → root = snap.LineRoot(空则 RootOf 沿边上溯兜底)
→ newSession + restoreFrom + Register;第一个(最近未封存)为 Active
restoreFrom 还原:history / Title / ResSnap 基线 / LastOutputAt / Usage / 水位
(CtxTokens 缺省从历史尾部 agent_status 兜底 LastCtxTokens)/ LineRoot / Edge
systemPrompt 不重组:快照 base / summary 直接注入(记忆/skill/mcp 变更等下个 session)
切换:Switch 按索引 LeafID 现读快照(按需恢复);SetActive 不取消旧分支运行轮(后台跑完)
2.6 视图三层与跨轮不变量
| 视图 | 定义 | 谁消费 |
|---|---|---|
| history(全量) | 内存与磁盘的全量消息(含 trim marker 前的档案段) | 落盘 / 时间线渲染 / fork 副本 |
| modelView() | 最后 <context_trim> marker 之后(ViewStart 回溯 kept 属性);system 由 sysprompt hook 每轮重注 | StartRun → WithHistory → 模型 |
| 请求副本 | visionguard 对 modelView 剥图后的副本(只改请求,历史不动) | 无视觉模型的实际 API 请求 |
s.history = state.Messages(trim 轮内截断过则 MergeFull = 盘上档案前缀 + 折叠段 + 本轮消息);
ViewStart 是 modelView / MergeFull / 落盘三处共用的同一套判断——"marker 前档案从未进过本轮视图的必须保留,跨轮覆盖不丢档"。历史以引擎 state 为准(含取消/出错轮;每个已入史 tool_call 必有结果消息)。
第三章上下文全动作
一个 session / 分支的上下文(模型看到的消息序列)由引擎 + 16 个 hook + 2 条 warp 链共同塑造。本章逐一回答:每个组件在什么时机触发、何时插入消息、插入的内容长什么样。
3.1 一轮 chat 的完整时序
sequenceDiagram
autonumber
participant FE as 前端
participant CS as ChatService/Session
participant E as ezloop 引擎
participant HK as hooks(startHooks/endHooks)
participant P as Provider(模型 warp 链)
participant T as 工具(工具 warp 链)
FE->>CS: POST /messages {text, files}
CS->>CS: 附件落盘 workspace/tmp/(base64 不入上下文)
CS->>E: RunAsync(WithHistory(modelView), WithUploadFiles)
E->>E: AppendMessage(input) ← 用户输入入史
E->>HK: startHooks[].OnStart(按序)
Note over HK: sys(system) → trace(观测层,须在会 Skip 短路的 hook 之前)<br/>→ loopguard(护栏) → contextfix(修理) → filetools(注册工具) → skilltool<br/>→ remind(变更段 resource_change? + 快照段 agent_status) → reference_file(路径告知?)<br/>→ approver/asker/task/mcp(注册) → offload/guard/trim/sessionstore(就位)
loop 迭代(≤MaxIterations)
E->>P: 模型调用
Note over P: modeldump(打印) → modelretry(退避)<br/>→ visionguard(无视觉剥图) → 真实 API
P-->>E: 流式响应 / tool_calls
E->>FE: SSE model_chunk / tool_chunk …
alt 有工具调用
E->>HK: OnToolStart(approve 审批 / skilltool·asker·task·trim 拦截,可 Skip)
E->>T: toolarg(参数展开) → limit(并发) → safetool → 工具本体
T-->>E: 结果
E->>HK: OnToolEnd(offload 卸载 → guard 兜底 → trace)
E->>HK: OnLoop(filetools 图片标记→图片消息+事件 → trim 水位 → mcp 热加载)
else 纯文本
E-->>E: break
end
end
E->>HK: endHooks[].OnEnd
Note over HK: remind(LastOutputAt + end_reason) → trace<br/>→ sessionstore 落盘(MergeFull)
CS-->>FE: SSE turn_end(stopReason/usage)
3.2 引擎时序基石:消息何时入史
以下 6 条是理解一切 hook 行为的前提(来自 ezloop core/loop.go,是文档易错点):
| 时刻 | 引擎动作 |
|---|---|
| Run 开始 | 构造 LoopState → 应用 RunOption(WithHistory 把 modelView 放进来;WithUploadFiles 写 Metadata)→ 发 loop_start 事件 |
| ① input 先入史 | 用户输入 AppendMessage(user) ——先于 startHooks。因此 startHooks 运行时末条必为本轮 input,remind / reference_file 都靠"插在末条 user 之前"定位轮首 |
| ② startHooks 后跑 | 16 个 hook 的 OnStart 按装配序执行:system 注入(sysprompt,ezharness 不用引擎的 systemPrompt 字段)、历史修理、工具注册、轮首消息插入都发生在「system, [history], input」已就位的序列上 |
| ③ assistant 入史 | 每次模型响应完成(含 ToolCalls)→ assistant 消息入史 → 发 model_end |
| ④ tool 消息批末入史 | 不是即时入史:同批全部调用执行完后,严格按调用顺序统一写入(Skip 短路的调用也占位)。补全规则:未执行的补 "not executed: <name>";hook Skip 无文案时引擎默认 "skipped by tool-start hook: <name>" |
| ⑤ OnLoop 回边 | 工具批入史后 → 下次模型调用前。此刻本轮全部工具结果已按序在史、消息序列协议完整——filetools 在这里插图片消息、trim 在这里截断(引擎串行区,无并发竞争) |
| ⑥ OnEnd 收尾 | defer 语义:成功 / 失败 / 取消 / panic 恢复路径都跑(context.WithoutCancel)→ 发 loop_end。sessionstore 落盘在最后 |
startHooks = nil(组装期 hook 不重跑,防重复注入)、systemPrompt = ""(seed 已含完整 system)、toolWarps = nil(传入工具已带主循环 warp 壳);
继承全部运行期 hook(OnModel* / OnToolStart / OnToolEnd / OnLoop / OnEnd)——审批照拦、session 照存。子 state 设 ForkID(如 task-1)与 SeedLen。
3.3 system prompt:三段式 + 运行时追加
每 session 组装一次并固定(重启从快照还原不重组;skill / mcp / 记忆变更等下个 session 生效,过渡期靠 agent_status 告知模型)。sysprompt hook 是 system 的唯一来源,必须在 startHooks 首位。
| 段 | 内容 | 时机 |
|---|---|---|
| base | 人格 + SystemExtra(设置页)+ <workspace> 三个可写位置与行为规则 + <memory>(记忆结构 + harness.md 索引全文)+ <skills> 清单? + <mcp> 清单? + <action> 行动准则 + <output> 输出规范(supper_url 正误示例) | session 创建时一次 |
| identity | <session> 会话 ID + 存档与进度档案绝对路径(trim 折叠后的回忆入口 + 恢复现场);render 时求值——换代后 ID 永远正确 | 每轮 render 实时 |
| summary | compact 摘要段(换代时注入 / 热更) | compact 时 |
此外各 hook 的 OnStart 会向 system 追加说明段(幂等):filetools 的 系统环境段(GOOS + shell 语法提示)与 5 个 <tool-guide>(approve 审批 / ask_user 提问 / task 分身 / mcp_router / load_skill / trim_context)。
你是 ezharness——一个持续陪伴用户的设备级 agent,可全权操作本机文件与命令。能用工具就用工具,回答简洁。……(SystemExtra) <workspace> # 工作区:三个可写位置(下列均为完整绝对路径,直接使用,不要自行拼接) # D:/ezharness/data/workspace 工作目录,草稿/脚本/命令产物一律放这里;tmp/ 附件暂存(read_file 读,图片自动进上下文) # D:/ezharness/data/memory/longterm 长期记忆:harness.md 索引(已注入),user/soul/project 三个记忆文件;归档时系统自动合并更新 # D:/ezharness/data/memory/skills 技能库:每技能一个子目录,新建后下个 session 进清单 # 行为规则(不需要记路径):技能/MCP 清单以 <skills>/<mcp> 段为准不读目录发现;快应用由 save_app 生成; # 存档与进度档案见 <session> 块,历史会话不主动翻阅;超长工具结果按提示 read_file 取回;配置文件不改写…… </workspace> <memory> # 长期记忆……- 索引 …/longterm/harness.md:长期记忆入口,全文见下方…… # - 记忆文件 …/longterm/{user,soul,project}.md:用户信息/工作习惯/项目索引,不进上下文,归档时自动合并…… # 索引 harness.md 全文 (harness.md 全文) </memory> <skills>(本清单由系统运行时生成……使用前先调用 load_skill 获取完整指令与脚本路径) - 技能名: 描述 </skills> <mcp>(经 mcp_router 工具调用,先用 mcp_list/tool_list 发现服务与工具) - server名: 描述 (同源页面直调:fetch('/api/mcp/call',POST {"server","tool","args"};构建可观测页面等快应用时把 MCP 工具当本地接口直接用,同源免 token) </mcp> <action> # 行动准则:1 接任务先匹配技能;2 多步/有风险先 ask_user 提交计划获批;3 工具选择(terminal / term_* / browser_*); # 4 MCP 先发现再调用;5 独立子任务交 task 分身(描述自包含,无依赖并行);6 交付入口用 <$supper_url>,整理后从进度档案恢复现场…… </action> <output> # 输出规范:<$supper_url> 整段闭合、标签紧贴地址、地址内无空格文字(正确示例×5 + 错误示例×3); # <@toolArg>绝对路径</@toolArg> 执行时展开为文件内容;回复风格:结论先行…… </output> <session> 当前会话 ID:a1b2c3d4 本会话存档:D:/ezharness/data/sessions/a1b2c3d4/session.json (这是本会话的完整历史档案:上下文整理折叠掉的早期对话仍完整保留在此文件中,需要回忆细节时读取它。) 进度档案:D:/ezharness/data/sessions/a1b2c3d4/progress.md (每次上下文整理自动重写:已完成/正在做/待办/关键事实。恢复任务现场或阶段性节点先读写它。) </session> (compact 摘要段,仅换代会话有:存档路径 + 沉淀去向说明 + 交接摘要) ——以下为各 hook OnStart 每轮幂等追加—— # 系统环境 当前操作系统:windows(terminal 工具用系统原生 shell 执行,Windows 请写 cmd 语法 dir/type/findstr,类 Unix 请写 POSIX 语法 ls/cat/grep) <tool-guide>审批:写操作等有副作用的工具调用会先请用户批准再执行,等待是正常流程,不躲避审批变相绕行;被拒先读理由——调整方案/澄清/停手……</tool-guide> <tool-guide>ask_user:缺少必要信息、需要澄清或确认方向时向用户提问,不替用户假设关键参数;options 给候选;问题具体到一句话能答……</tool-guide> <tool-guide>task:可并行、相互独立、或会产生大量中间输出的子任务交分身隔离执行,主上下文只留结论;描述自包含;同批无依赖并行发……</tool-guide> <tool-guide>mcp_router:访问外部能力的统一入口。两步:mcp_list 看清单、tool_list 拉参数 schema,再 tool_call——不凭记忆猜……</tool-guide> <tool-guide>load_skill:接任务的第一个动作是对照技能清单,命中就先加载(全文+脚本路径)后严格按指引执行;改过技能要重新加载……</tool-guide> <tool-guide>trim_context:把早期对话就地折叠为结构化摘要(已完成/正在做/待办/关键事实),并写入进度档案 progress.md……</tool-guide>
3.4 hook 全景表(16 个,装配序即执行序)
| # | hook | 来源 | hook 点 | 职责一句话 |
|---|---|---|---|---|
| 1 | sysprompt | ezharness | OnStart(必须首位) | system 唯一来源:base + identity + summary |
| 2 | trace | ezharness | OnModel* / OnTool* / OnEnd | otel 风格调用链(trace.jsonl);toolStart 队列必须最前,否则被 Skip 短路的调用无 span |
| 3 | loopguard | ezharness | OnToolStart | 循环护栏:被拒后反复重试同样计数,重复触发 <loop_guard> 提醒(不拦调用) |
| 4 | contextfix | ezloop | OnStart | 修理残缺历史(孤儿 tool_call / tool 结果) |
| 5 | filetools | ezloop | OnStart + OnLoop | 注册 read/write/edit/terminal + OS 环境段;OnLoop 图片标记→图片消息 |
| 6 | skilltool | ezloop | OnStart | 注册 load_skill(工具 Invoke 闭环:渲染 SKILL.md 全文 + 目录树) |
| 7 | remind | ezharness | OnStart + OnEnd | 系统提醒三段:变更段 / 快照段 / 收尾段(见 3.5 详解) |
| 8 | reference_file | ezharness | OnStart | 带引用轮次插 <reference_file>(附件=路径;标注=路径+片段行号+备注),无引用零消息 |
| 9 | approve | ezloop | OnStart + OnToolStart | 人机审批:规则匹配挂起等决策,拒绝即 Skip |
| 10 | askuser | ezloop | OnStart + OnToolStart | ask_user:模型向用户提问,阻塞等回答 |
| 11 | task | ezloop | OnStart + OnToolStart | task:fork 分身子循环,answer 汇回 |
| 12 | mcp | ezloop 包装 | OnStart + OnLoop + OnEnd | 注册 mcp_router;配置热加载 |
| 13 | offload | ezloop | OnToolEnd | >4096 字节工具结果卸载 .ezloop/offload/ |
| 14 | guard | ezharness | OnToolEnd | 窗口余量兜底:豁免名单的大结果放不下时强制卸载 |
| 15 | trim | ezharness | OnLoop + OnToolStart | 上下文整理:水位自动 + trim_context 主动 |
| 16 | sessionstore | ezharness | OnStart + OnEnd(最后) | 持久化:MergeFull 落 session.json |
ezloop 引擎的 7 个扩展点:OnStart(Run 组装期一次)· OnModelStart / OnModelEnd(每次模型调用前后)· OnToolStart(可短路:Proceed / Skip(result) / Abort)· OnToolEnd(结果入史前可改写)· OnLoop(迭代回边)· OnEnd(defer 语义)。并发契约:除 OnToolStart / OnToolEnd 在同轮多工具调用间并发外,其余 hook 点引擎串行;LoopState.Metadata 是 Run 内跨 hook 共享通道。
3.5 逐 hook 详解
每个面板:触发时机 → 动作 → 对上下文的影响 → 确切的消息示例(与源码格式化字符串一字不差)。
1 · sysprompt hook ezharness
动作:消息序列缺 system 时在最前注入当前 prompt;已存在则原地重写(全程唯一一条)。替代引擎的 WithSystemPrompt——compact 需要运行中换 system,agent 构建后只读是引擎契约。
上下文影响:system = base + identity(render 时实时求值)+ summary 三段拼接;完整结构与示例见 3.3。fork 不重跑(seed 已含 system)。
2 · trace hook ezharness
位置有语义:必须在 toolStart 队列最前——approve / askuser / task / skilltool 在 OnToolStart 里返回 Skip 会短路后续 hook,观测层排在它们之后,这些调用(含 task 分身)就留不下 span。
动作:otel 风格 span 调用链(turn / model / tool / fork / compact),span 关闭即追加进 sessions/<id>/trace.jsonl(与消息行同文件:每行 JSON 的 kind 字段区分 span 与 message;fork 子循环落 forks/<fid>/trace.jsonl);读取时合并两份并把 fork 根挂到发起它的 tool.task span 下(跨文件没有可对上的字段,按开始时间序号配对)。不碰消息、不碰 lastOutputAt——纯观测,前端「调用链」页画瀑布图。
3 · loopguard hook ezharness
动作:同一工具被拒(审批拒绝 / 策略 Skip)后反复重试同样调用即计数;重复触发插一条 <loop_guard> 提醒消息劝阻,不拦调用(模型仍有最终决定权)。排在 Skip 型 hook 之前,保证被短路路径也计入。
4 · contextfix hook ezloop
动作:双向修理 state.Messages,保证发给 API 的序列协议完整(残缺会被 API 直接拒绝):
- 补缺:assistant 发起的 tool_call 缺对应结果 → 在该 assistant 后补一条 tool 消息,占位文案
(missing tool result) - 删孤:tool 消息的 ToolCallID 没有任何 assistant 发起过(孤儿结果)→ 整条删除
[tool_use read_file call_x7]
(missing tool result) ← 上轮取消/崩溃留下的残缺,contextfix 补齐
5 · filetools hook ezloop
OnStart:注册 read_file / write_file / edit_file / terminal 四工具(ezharness 经 WithImageHandler 注入图片分支裁决)+ system 追加「# 系统环境」段(GOOS + shell 语法提示)。
read_file 遇到图片(魔数判定 jpeg/png/gif/webp)
| 情形 | 工具返回(tool 消息内容) |
|---|---|
| 主模型开视觉 | <image_loaded path="C:/…/img.png"/> ——机器标记,保留不改写,前端工具卡原文显示 |
| 超 8MiB | [图片 X 大小 N 字节,超出 8388608 字节上限,无法加载进上下文;请改用文件路径引用或外部识别工具] |
| 无视觉 | [当前模型无多模态能力,无法读取图片 X;如需识别图片请在设置·模型为主模型勾选"支持视觉"] |
OnLoop:标记 → 持久化图片消息(核心机制)
时机:工具批完成后、下次模型调用前。从消息尾部向前回扫本轮 tool 消息至第一条 assistant 止(批边界天然划定,免状态),扫描其中的 <image_loaded path="…"/> 标记——已入史的 tool 结果不改写,标记只是批末转换的信号。读盘成功(≤8MiB + 魔数有效)的合并为一条 user 消息插在批内最后一条 tool 之后:
我来看看这两张截图
<image_loaded path="D:/…/tmp/att-1694-a.png"/>
<image_loaded path="D:/…/tmp/att-1694-b.png"/>
<image_loaded> D:/…/tmp/att-1694-a.png D:/…/tmp/att-1694-b.png </image_loaded> ← 消息 Images 字段带两张图的 base64(全协议通用通道)
同时发 SSE 事件 filetools.image_loaded(data = 路径数组),前端据此实时渲染缩略图行——事件在 OnLoop 发而非 tool_end,因为"加载进上下文"发生在此刻。
6 · skilltool hook ezloop
OnStart:注册 load_skill 壳工具 + system 追加 tool-guide("接任务先扫技能清单,命中就加载后按其指引执行")。技能身份用目录名(skill.DirOf),frontmatter name 仅作显示。
工具 Invoke 闭环:调用时实时列技能(按 disabled 名单过滤),命中 → 返回渲染结果(SKILL.md 全文 + 目录树;免 offload 豁免——指令集是后续行动依据,卸载再回读纯浪费);未命中 → skill "X" 不存在。可用技能:a, b, c
# skill: release-publish
路径: D:/…/memory/skills/release-publish/SKILL.md
目录内容:
release-publish/
├─ SKILL.md
└─ scripts/
└─ do-release.bat
脚本可用 bash 执行、资料可用 read_file 读取(路径相对工作目录)。
--- SKILL.md ---
(SKILL.md 全文)7 · remind hook ezharness
系统→模型的全部单向旁路提醒归这一个 hook,内部分三段独立成模块。变更检测留在此处的分界判据见 1.1。
变更段(OnStart,有变化才插——零噪音)
对比 session 资源基线 ResSnapshot{Skills, Mcps}(持久化随 session,重启不重复报)。首轮只建基线不 diff(新建/迁移不算变更)。条目为中文完整短语(模型与前端变更卡同源):
| 操作 | 条目文案 |
|---|---|
| 启用新技能 | 新增技能 release-publish |
| 禁用/删除技能 | 移除技能 release-publish |
| mcp.json 加 server | 新增MCP 服务 time |
| mcp.json 删 server | 移除MCP 服务 time |
有变化才插一条 user 消息(位于 agent_status 前、本轮 input 前)+ 发 resource.change 事件:
<resource_change> (系统检测到的本轮资源变更,非用户发言,无需回应,无需回溯处理) - 新增技能 release-publish 变更后完整清单(技能与 MCP 服务以此为准,不要读取 memory/skills 目录或 mcp.json 来发现技能与服务): available_skill: release-publish - 把当前分支发布为 release available_mcp: context7 - 查询库文档 </resource_change>
available_ 清单行是给模型的即时资源发现(前端变更卡不展示,只渲染 - 变更条目行)。
快照段(OnStart,每轮一条)
水位/时间快照,插在本轮 input 前(无变更段时它就是轮首第一条)+ 发 status.snapshot 事件(前端右上角水位条):
<agent_status> 当前时间:2026-09-08 14:30 上下文水位:45210 / 200000 tokens ← CtxWindow>0 才有此行 当前运行中终端:build、deploy ← 有运行中终端才有(每轮现查) 开启中浏览器标签:文档站(docs.example.com) ← 同上 距上次输出:37 分钟 ← LastOutputAt 有记录才有 </agent_status>
水位超窗口 70% 时追加:(已超窗口 70%,建议调用 trim_context 整理上下文)——注意「整理上下文」四字是前端历史重建的识别关键词(硬契约)。
收尾段(OnEnd)
先无条件记录 LastOutputAt(下轮"距上次输出"用,fork 也记);ForkID ≠ "" 直接返回(分身历史只存增量不记收尾);主循环追加轮末收尾(位于消息序列尾部):
<end_reason> (系统自动记录的轮次收尾信息,非用户发言,无需回应) 运行轮次:3 运行时长:1 分 24 秒 结束时间:2026-09-08 14:31:24 结束原因:正常结束 ← 手动停止/达到迭代上限/策略中止/执行出错 错误详情:… ← 仅出错轮有(压平单行、300 字截断) 话题压缩:本轮结束时上下文水位达到阈值,上一会话已压缩归档并开启新会话,… ← 仅 compact 轮有 </end_reason>
8 · reference_file hook ezharness
动作:引用统一为路径:附件先落盘 workspace/tmp/att-<ts>-<序号>-<净化名>(base64 不入上下文),文件页标注带 sel/note/from/to;路径经 WithRefFiles 写入 LoopState.Metadata,hook 在本轮 input 前插一条记录(agent_status 之后、input 之前),标签体是纯 JSON 载荷(前端 JSON.parse 重建 chips)。一切皆资源:文件本体不进上下文,模型按需 read_file 读真身——图片仍会经 filetools OnLoop 转成图片消息直接进上下文。
<reference_file> {"hint":"用户本轮引用了以下本地文件,可用 read_file 读取","refs":[{"path":"D:/…/workspace/tmp/att-1694165400-1-报错截图.png"},{"path":"D:/…/src/main.ts","items":[{"sel":"timeout: 300","note":"这里太短了","from":118,"to":131}]}]} </reference_file>
看下这两个文件,帮我定位问题
9 · approve hook ezloop
判定(ezharness 闭包注入 needsApprove):ask_user / load_skill / trim_context 恒免审;mcp_router 的 mcp_list / tool_list 只读免审,tool_call 按 server.server.tool 名单走四档(ask / black / white / auto);未知工具默认审批。运行时读设置快照,改策略即时生效。
拦截流程:发 approve.request 事件(data = ToolCall,前端时间线出决策卡)→ goroutine 阻塞在 channel 上等决策(引擎无感知)→ 批准 Proceed / 拒绝 Skip。
[tool_use terminal call_q2] command: "del /s build"]
denied by user: 不要动 build 目录 ← 拒绝理由原样成为工具结果,模型据此调整方案
10 · askuser hook ezloop
拦截流程:发 askuser.request 事件(Args.question / Args.options,前端出提问卡可点选)→ 阻塞等 Answer{CallID, Input} → 用户的回答原样作为工具结果入史(Skip 通道):
[tool_use ask_user] question: "删哪条线?" options: ["旧测试线","演示线"]
旧测试线 ← 用户回答即结果;空回答为 (user gave no input)11 · task hook ezloop
拦截流程:单层检查(子循环内再调 task → task: nested fork is not allowed (single level);空描述 → task: empty task description)→ agent.Fork 开分身子循环:独立 LoopState(ForkID 如 task-1)、seed = 截至本轮 assistant 之前且剔除触发指令那条 user 的快照、工具集剔除 task 自身、startHooks 不重跑(见 3.2 fork 差异)。
子循环 input 加自包含前缀(分身看不到本轮对话之外的语境):
以下是你独立负责的子任务,请完成它;你的最后一条回复将作为结果直接交付,须自包含、简洁: (模型给的 task 描述)
answer 汇回主循环:取子循环最后一条 assistant 的 Content 作为 Skip 结果(即 task 的工具结果);异常附加后缀——无输出 (subtask completed with no output)、迭代耗尽 [subtask reached max iterations, result may be incomplete]、中止 [subtask stopped: aborted];子循环出错不终止主循环 task failed: …。子循环 Usage 累加回父 state。事件 task.start / task.end 带 forkId(前端进分身抽屉,不入主线);增量存档 sessions/<id>/forks/<fid>/session.json(剥离 seed)。
12 · mcp hook ezloop(ezharness 包装)
动作:注册唯一工具 mcp_router(三 action:mcp_list 列 server · tool_list 列某 server 工具+schema(ACL 过滤)· tool_call 调用;错误统一 JSON {"error":…,"hint":…})。OnLoop cfg.Reload 热加载 mcp.json 变更(即时生效);OnEnd 关闭连接。审批语义见 hook 7。
13 · offload hook ezloop
动作:结果超 4096 字节 → 全文写 .ezloop/offload/<工具名>-<sha256 前 6>.txt → 结果改写为「头部 512 字节 + 卸载说明」。豁免名单:ask_user / task / load_skill(人机与指令结果);read_file 是 ReplayTool(说明文案不同)。写入失败降级透传原文。
(前 512 字节原文)
[输出共 83210 字节,超出 4096 字节阈值,已卸载到 .ezloop/offload/terminal-a1b2c3d4e5.txt,可使用 tool read_file 进行全量内容回放]14 · guard hook ezharness
动作:窗口余量动态兜底——估算「当前历史 tokens(provider 精确值 + 末次响应后追加消息按 1.2 字符/token 补估)+ 本次结果」,放不下则卸载同目录。专堵 offload 豁免漏网(如 read_file 读单行超长文件)。写入失败时硬截断丢弃尾部——此处不能像 offload 降级透传,透传即撑爆上下文,保会话优先于保内容。
(前 512 字符原文)
[输出共 96000 字符,当前上下文约 195000/200000 tokens,余量放不下完整结果,已卸载到 .ezloop/offload/read_file-9f8e7d.txt;如需查看请用 read_file 按行分段读取(offset/limit),勿整文件回放]15 · trim hook ezharness
路径一(水位自动):每次迭代回边检查 PromptTokens > 窗口 × TrimPercent%(默认 75,0 禁用),先发 session.trimming 事件("上下文水位 156000 / 200000 tokens(78%),达到阈值,正在整理…")。
路径二(模型主动):模型调 trim_context → OnToolStart 只登记 pending(并发回调区不写 state),Skip 安抚模型继续任务:
已安排整理:本批工具调用完成后立即折叠早期上下文,请继续当前任务,不要重述将被折叠的过程细节。执行(OnLoop,本批工具结果已全部按序入史——消息序列协议完整):摘要折叠段(独立 2 分钟预算,prompt:"保留关键事实/已达成决定/未完成待办/近期文件操作,200 字以内")→ 截断为 [head, tail, marker]:head = system(fork 则完整 seed);tail = 末 4 条(孤儿 tool 前移保证配对完整);marker 尾插。折叠段挂 Metadata(追加式档案),立即生效——下次模型调用即新上下文。发 session.trim 事件 {tokens, folded, kept, fork}。
(不动)
(保留的末 4 条近期消息,含配对完整的 assistant/tool)
<context_trim kept="4"> (系统自动整理,非用户发言,无需回应) 此前的早期对话已折叠出模型上下文,整理结果已写入进度档案 sessions/…/progress.md(路径见块)。当前任务衔接摘要: 【已完成】导出方案已定(CSV、/api/export),exporter.py 草稿已写入 workspace 【正在做】导出接口联调 【待办】订单明细列对齐 【关键事实】用户要求含订单明细;草稿在 workspace/exporter.py </context_trim>
kept 属性是视图起点回溯的依据:ViewStart(marker 的 kept 回溯 + clamp 到上一 marker)是 modelView / MergeFull / 落盘三处共用的同一套判断——"marker 前档案从未进过本轮视图的必须保留,跨轮覆盖不丢档"(见 2.6)。
16 · sessionstore hook ezharness
动作(OnEnd 落盘):盘上旧全量 MergeFull(old.Messages, state)(trim 轮:盘上档案前缀 + 本轮折叠段 + 本轮消息)→ stripSystem → fork 剥 seed → 写 sessions/<id>/session.json + 基线 + 用量。失败不阻断(错误记 Metadata)。内存与磁盘同源的关键:必须排在 remind 之后(end_reason 已入史)——见 2.6 不变量。
3.6 上下文注入消息全表
模型视角的全部消息类型(8 种注入 + 3 种基本)。所有注入均为 role=user(全协议通用),用包裹标签自我说明。
| 消息 | 生成者 | 插入时机 | 条件 |
|---|---|---|---|
system | sysprompt + 各 hook 追加 | 每轮首位(重注入) | 恒有 |
<context_trim> | trim | 整理时尾插 | 水位达阈 / 模型主动 |
<resource_change> | remind 变更段 | 轮首、agent_status 前 | 有变更才插(附 available 清单) |
<agent_status> | remind 快照段 | 轮首、reference_file 前 | 每轮 |
<reference_file> | reference_file | 本轮 input 前 | 有引用才插(纯 JSON 载荷) |
<loop_guard> | loopguard | 被拒调用重试时 | 重复触发才插(提醒不拦截) |
| 用户真实输入 | 引擎 | 轮首入史(startHooks 前) | 每轮 |
<image_loaded> 包裹 | filetools OnLoop | 工具批末、最后一条 tool 后 | 读图才插(Images 带 base64) |
| assistant | 模型 | 每次响应完成 | 迭代内 |
| tool | 引擎(批末按序) | 工具批执行完 | 迭代内 |
<end_reason> | remind 收尾段 | 序列尾部 | 每轮末 |
3.7 场景演练:进行了 xx 操作后,消息长这样
四个典型场景的完整消息序列(模型实际看到的顺序,逐条对应 3.6 全表)。
场景 A · 普通一轮(无附件、无变更)
(base + identity + summary? + 环境段 + tool-guides)
(此前轮次的消息……)
<agent_status>\n当前时间:2026-09-08 14:30\n上下文水位:45210 / 200000 tokens\n</agent_status>
工作目录里有哪些文件?
[tool_use terminal] command: "dir /b"
apps\nmemory\nworkspace\n…
工作目录下有 apps、memory、workspace 三个目录……
<end_reason>\n(系统自动记录的轮次收尾信息,非用户发言,无需回应)\n运行轮次:2\n运行时长:8 秒\n结束时间:…\n结束原因:正常结束\n</end_reason>
场景 B · 带附件 + 读图(消息类型最全的一轮)
用户上传两张截图 + 一个日志文件,问"帮我定位问题"。
(……)
(历史……)
<reference_file>\n{"hint":"用户本轮引用了以下本地文件,可用 read_file 读取","refs":[\n {"path":"…/tmp/att-1694165400-1-报错截图.png"},\n {"path":"…/tmp/att-1694165400-2-界面异常.png"},\n {"path":"…/tmp/att-1694165400-3-错误日志.txt"}]}\n</reference_file><agent_status>…</agent_status>
看下这几个文件,帮我定位问题
[tool_use read_file ×3] path: …/报错截图.png / …/界面异常.png / …/错误日志.txt
<image_loaded path="…/att-1694165400-1-报错截图.png"/>
<image_loaded path="…/att-1694165400-2-界面异常.png"/>
Traceback (most recent call last):\n File "main.py", line 88…\nValueError: invalid literal…
<image_loaded>\n…/att-1694165400-1-报错截图.png\n…/att-1694165400-2-界面异常.png\n</image_loaded> ← 批末一条合并图片消息,Images 带两张 base64;文本日志正常返回原文
两张截图显示列表页空白,日志第 88 行在解析分页参数——根因是页码参数未做空值防御……
<end_reason>…运行轮次:2…结束原因:正常结束…</end_reason>
场景 C · 技能启停后的下一轮
上一轮结束后:用户在设置里启用了技能 release-publish,然后让模型发版。变更段只在下一个拥有它的轮次出现一次,再下轮消失(基线已推进)。
<resource_change>\n(系统检测到的本轮资源变更,非用户发言,无需回应,无需回溯处理)\n- 新增技能 release-publish\n变更后完整清单(技能与 MCP 服务以此为准,不要读取 memory/skills 目录或 mcp.json 来发现技能与服务):\navailable_skill: release-publish - 把当前分支发布为 release\n</resource_change>
<agent_status>…</agent_status>
构建刚跑完,帮我按流程发个版本
好,先加载发布技能 [tool_use load_skill] name: "release-publish"
# skill: release-publish\n路径: …\n--- SKILL.md ---\n(全文)
(按技能指引执行发布流程……)
场景 D · 水位达阈的 trim 轮
长任务进行中,OnLoop 检查水位 156000/200000(78% > 75% 阈值)→ 发 session.trimming 事件 → 就地折叠。下图为整理后的消息序列(立即生效,下次模型调用即新上下文):
(不动)
(保留的末 4 条:孤儿 tool 已前移配对,协议完整)
<context_trim kept="4">\n(系统自动整理,非用户发言,无需回应)\n此前的早期对话已折叠出模型上下文,整理结果已写入进度档案 sessions/…/progress.md。当前任务衔接摘要:\n【已完成】…\n【正在做】…\n【待办】…\n【关键事实】…\n</context_trim>
会话身份不变(不换库不切线);被折叠的早期消息进入 Metadata 折叠段,sessionstore 落盘时经 MergeFull 合成全量——前端时间线上整理位置仍可见(note 分割线),只是模型视图不再包含。
3.8 warp 双链(先注册 = 外层)
flowchart LR
subgraph 模型链
MD["modeldump<br/>调试打印(最外,重试不重打)"]:::m --> MR["modelretry<br/>指数退避(流式已出 chunk 不重试)"]:::m --> VG["visionguard<br/>无视觉剥请求图"]:::m --> API["provider 真实 API"]:::m
end
flowchart LR
subgraph 工具链
TA["toolarg<br/><@toolArg> 执行前展开"]:::t --> LM["limit(4)<br/>同轮并发上限"]:::t --> ST["safetool<br/>panic 恢复为 error"]:::t --> TOOL["工具本体"]:::t
end
| warp | 行为 | 关键细节 |
|---|---|---|
| modeldump | 全量输入打印 | 打印前副本截断(每字段 48 字符 + …(N chars)),头部附 img-chars 统计——防止 base64 刷屏日志 |
| modelretry | 指数退避重试 | 流式已出 chunk 不重试(前端已有半截输出);模型层错误必须在 provider 装饰器处理——引擎在模型节点失败即终止,hook 拦不到 |
| visionguard | 无视觉模型剥图 | 每次实际请求(含 retry)剥请求视图里全部 user Images,<image_loaded> 开标签注入 omitted="当前模型可能已切换,不支持图片输入"——只改请求副本,落盘历史不动,换回多模态图片自动恢复 |
| toolarg | 参数语法糖 | 两遍遍历(收集验证 → 展开替换);入史参数保持原文(时间线所见即模型所说),展开只发生在执行侧;单文件上限 200000 字符 |
| limit(4) | 并发闸 | 同批工具调用的信号量 |
| safetool | panic 恢复 | 工具 panic 转为 error 结果,不炸整轮 |
warp 实例 per-Run 独立(fork 复刻 warp 链,分身输入同样可见)。
3.9 特殊 tag 契约速查(前后端硬契约)
| tag | 生成者 | 模型可见 | 前端实时路径 | 前端历史路径 |
|---|---|---|---|---|
<agent_status> | remind 快照段 | ✓ | status.snapshot 事件 | 关键词「整理上下文」识别进时间线 |
<resource_change> | remind 变更段 | ✓ | resource.change 事件 | 标签 + - 行(变更卡;available_ 行只给模型) |
<reference_file> | reference_file | ✓ | 发送时本地 push user 块 | 解析 JSON 载荷 → files/fileRefs chips |
<image_loaded> 包裹 | filetools OnLoop | ✓(Images base64) | filetools.image_loaded 事件 | 标签体 + m.images |
<image_loaded path=…/> 自闭合 | read_file 工具结果 | ✓(保留不改写) | 工具卡结果原文 | 同左(不解析) |
<end_reason> | remind 收尾段 | ✓ | turn_end 合成 endtick | 正则提取字段 |
<context_trim kept="N"> | trim | ✓ | session.trim 事件(note 块) | 标签 → note 分割线(【已完成】锚点截 160 字;同内容落 progress.md) |
<$supper_url>… | 模型输出占位语法 | ✓(正文) | marked extension 实时解析:https:// 外链 / term://终端id 拉开终端抽屉定位 / app://快应用名 开子窗 / browser://标签id 聚焦共享浏览器窗口 | 同左(同一渲染管线) |
<@toolArg>… | 模型工具参数 | ✓(入史保持原文) | 工具卡显示原文 | 同左 |
- 与关键词「整理上下文」是硬契约。改事件协议检查三处:后端 MapEvent(event.go)、replayable 名单(session.go)、前端 apply case(含幂等性——所有可回放帧的 apply 必须幂等)。
第四章产品视角 · 人机协同交互
agent 的能力边界再大,落到用户手里的只是几个交互面。ezharness 把人机协同交互工具外置——不塞进对话流硬凑,做成独立的产品面,让用户在 agent 工作时能看、能改、能答。核心理念:为用户提供最简单的交互方案——上下文工程、工具封装、人机交互三支柱,能力越复杂,落到用户手里的越要简单。
决策链路:四档审批策略
| 档位 | 行为 |
|---|---|
ask 每次审批 | 所有工具调用都出决策卡等用户 |
black 黑名单 | 名单内的出决策卡,其余放行 |
white 白名单 | 名单内的放行,其余出决策卡 |
auto 全部免审 | 全自动(人机与内部工具恒免审不受档位影响) |
- 审批 / 询问两型 DecisionCard 嵌在时间线,设置页即时生效
- 悬置请求登记 pending——断线重连可重放;刷新后由 decisions.jsonl 重建
- 轮已结束的过期决策自动丢弃
全局通知栏:跨分支找到你
- 汇总所有分支(含后台分支与分身)的未决请求——无论用户停在哪个分支哪个页面,agent 需要人时一定能找到人
- 通知内联直接决策(乐观移除);「查看」跳转:跨分支先切线,分身请求开分身抽屉定位决策卡
- 服务端为唯一真相,内容不变不换引用,防前端抖动
共享终端:人机共用同一个真实 shell
flowchart LR
subgraph TERM["ConPTY 全局公共池 · 跨会话共享"]
PT["真实 shell<br/>环形缓冲 256KB"]:::core
end
AG["🤖 agent<br/>term_action send / read"]:::side --> PT
US["👤 用户<br/>xterm 直接敲"]:::side --> PT
PT -- "readMark 单游标<br/>读即消费" --> AG
classDef core fill:#072331,stroke:#00add8,color:#9be5f7
classDef side fill:#0b1322,stroke:#33496e,color:#c4cede
style TERM fill:#0a101d,stroke:#33496e,stroke-dasharray:6 4
- 单条 WebSocket 多路复用全部终端(帧带 id 路由,断线指数退避重连,环形缓冲恢复快照)
- 终端全局共享(个人助手语义);各会话的终端基线独立——A 会话首轮见到 B 会话开的终端报"新增"是设计(各会话模型知悉全局水位)
- readMark 单游标:term_action 的 send / read 共用,read 游标式消费(读一次推进一次);send 只写入、不等输出,不推进游标
共享浏览器:AI 操作、用户共见
flowchart LR
subgraph COREP["core · 桥服务端"]
BT["browser_* 工具两件套(browser_tab / browser_action)<br/>定义/审批在 core(tools 层不感知实现)"]:::core
end
subgraph DESK["desktop · 浏览器窗口 = 壳的资产"]
EXE["执行器<br/>WebContentsView.webContents 直接操作"]:::side
VIEW["👤 用户共见<br/>原生交互(点击 / IME / 视频)"]:::side
end
AI["🤖 agent"]:::side -- "工具调用" --> BT
BT -- "WS 桥 /api/browser/bridge<br/>JSON-RPC 转发 + 回执" --> EXE
EXE --- VIEW
classDef core fill:#072331,stroke:#00add8,color:#9be5f7
classDef side fill:#0b1322,stroke:#33496e,color:#c4cede
style COREP fill:#0a101d,stroke:#00add8,stroke-dasharray:6 4
style DESK fill:#0a101d,stroke:#33496e,stroke-dasharray:6 4
- AI 开始操作 → 浏览器窗口自动弹出置前(共见);用户关窗 = 隐藏保留(标签后台存活,AI 可能正在用)
- 核心链路:AI 调
browser_action 的 navigate / click / type / key / scroll / screenshot / read / eval / console / network等 → core 经 WS 桥转发 → 壳执行器直接操作对应标签的 webContents → 回执(截图 base64 回传落盘,多模态可读) - 桥未连接(web 端访问)时工具报「仅桌面端可用」——主应用无浏览器残缺 UI,端无关原则
http://127.0.0.1:<port>——与桌面端同一份页面(浏览器控制等桌面专属能力自动降级)。
附录深读文档索引
本页是全景与速查;细节文档(md/ 目录)与各主题一一对应,改代码前先扫对应篇。
| 文档 | 内容 |
|---|---|
| architecture.md | 系统整体架构:分层、双仓库、数据目录、三通道(第一章底稿) |
| session.md | session 设计:树、SessionSnap、三种演进、恢复、不变量(第二章底稿) |
| runtime.md | 运行时:hook 组成与一轮 chat 全流程、上下文消息全表(第三章底稿) |
| frontend.md | 前端:SSE 事件全集与归约、历史重载解析、Block 类型、tag 契约 |
| hooks.md | 封装原则:四步范式与 ezloop / ezharness 分界判据 |
| context.md | 上下文工程:system 组装、trim 双触发、agent_status 快照、guard 兜底 |
| compaction.md | trim 与 compact 分工:模型侧折叠 vs 用户侧沉淀换代 |
| build.md · interaction.md | 构建方案 / 人机交互设计 |
AGENTS.md(仓库根) | 开发备忘:变更连带检查清单与易踩的坑——改 remind / 工具面 / skill / 终端 / SSE / warp 前必扫 |