ezharness
简 单 ,交 给 用 户 。

ezharness

系统架构 · session 树 · 上下文全动作 —— 每条消息从哪来、何时插入、长什么样,全部在此
第一章 · 系统整体架构
双仓库分界、三层结构(desktop 壳 × core sidecar × 前端)、三通道通信、数据目录与换代生命周期
第二章 · session 树
线与世代、compact 换代、fork 分叉、内存↔磁盘同源、恢复流程与跨轮不变量
第三章 · 上下文全动作
一轮 chat 的完整时序、16 个 hook 逐一详解、8 种注入消息的确切格式与场景演练
▼ SCROLL

第一章系统整体架构

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)
hook 分界判据:依赖 session 域状态(Store / 资源基线 / 落盘)的 hook 留 ezharness(remind · trim · sessionstore…); 纯工具组件下沉 ezloop(filetools · skilltool · mcp · approve…)。skill/mcp 的变更检测因此归 remind——若长在 skilltool/mcp 自己身上,它们就耦合了 session 基线,无法成为 ezloop 公共组件。 开发期 go.mod 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
分层链路:controller(表现)→ service(用例)→ domain(会话聚合与事件);tools / hooks 为领域扩展,osfs / config 为基础设施,main 只做装配。

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
桌面壳 spawn core sidecar,健康就绪后开窗加载 core 伺服的页面(同源访问,前端 API 零适配);业务全在 core,desktop 只做壳与桌面资产。共享浏览器是 desktop 的资产:core 的 browser_* 工具经 WS 桥转发到壳的 WebContentsView 执行器,桥未连接(web 端访问)时工具报「仅桌面端可用」。

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 的相对路径存储自动落此。

数据目录(进程 cwd)
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 应用生命周期

startup
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()

1.7 开发与发布

script
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>\ 安装包 + 绿色版

第二章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
线内 compact 换代链(换代不换线名);fork@anchor 复制前缀开新线;task 分身不开线,answer 汇回主循环。

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 / CompactSummarycompress 边:上一世代 ID 与摘要(前端上翻链)
TargetID / Anchor / SeedKind / ForkedFromSnapEdge(创建时写死):向上边目标 / 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

compact-flow
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
与 trim 的分工:trim 是接力棒(模型侧:水位自动 + trim_context 主动,四节结构化摘要 + progress.md,就地截断不换库);compact 是总结沉淀(用户侧:固定三记忆文件合并重写 + 精炼交接摘要换代)。详见 docs/md/compaction.md。

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。

三种「分叉」勿混淆:① fork 分叉(copy 语义,复制前缀开新线);② task 分身(轮内子循环,事件带 ForkID,存档在 forks/ 只存增量,前端分身抽屉展示); ③ compact 换代(归档封存,同线换叶)。

2.5 生命周期与恢复

lifecycle
创建: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 请求
一致性不变量:sessionstore 落盘的与内存 history 必须一致,否则同进程续聊丢上下文(重启反而恢复)。 FinishRun: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)
装配顺序即执行顺序:startHooks 数组一次、每轮迭代内的模型/工具调用、endHooks 收尾落盘。

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 落盘在最后
fork(task 分身)的差异:浅拷贝 Agent 后 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 实时
summarycompact 摘要段(换代时注入 / 热更)compact 时

此外各 hook 的 OnStart 会向 system 追加说明段(幂等):filetools 的 系统环境段(GOOS + shell 语法提示)与 5 个 <tool-guide>(approve 审批 / ask_user 提问 / task 分身 / mcp_router / load_skill / trim_context)。

system 结构示例(拼装后)
你是 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 点职责一句话
1syspromptezharnessOnStart(必须首位)system 唯一来源:base + identity + summary
2traceezharnessOnModel* / OnTool* / OnEndotel 风格调用链(trace.jsonl);toolStart 队列必须最前,否则被 Skip 短路的调用无 span
3loopguardezharnessOnToolStart循环护栏:被拒后反复重试同样计数,重复触发 <loop_guard> 提醒(不拦调用)
4contextfixezloopOnStart修理残缺历史(孤儿 tool_call / tool 结果)
5filetoolsezloopOnStart + OnLoop注册 read/write/edit/terminal + OS 环境段;OnLoop 图片标记→图片消息
6skilltoolezloopOnStart注册 load_skill(工具 Invoke 闭环:渲染 SKILL.md 全文 + 目录树)
7remindezharnessOnStart + OnEnd系统提醒三段:变更段 / 快照段 / 收尾段(见 3.5 详解)
8reference_fileezharnessOnStart带引用轮次插 <reference_file>(附件=路径;标注=路径+片段行号+备注),无引用零消息
9approveezloopOnStart + OnToolStart人机审批:规则匹配挂起等决策,拒绝即 Skip
10askuserezloopOnStart + OnToolStartask_user:模型向用户提问,阻塞等回答
11taskezloopOnStart + OnToolStarttask:fork 分身子循环,answer 汇回
12mcpezloop 包装OnStart + OnLoop + OnEnd注册 mcp_router;配置热加载
13offloadezloopOnToolEnd>4096 字节工具结果卸载 .ezloop/offload/
14guardezharnessOnToolEnd窗口余量兜底:豁免名单的大结果放不下时强制卸载
15trimezharnessOnLoop + OnToolStart上下文整理:水位自动 + trim_context 主动
16sessionstoreezharnessOnStart + 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

hook 点:OnStart(必须首位——先有 system,后续 hook 才能追加 tool-guide)触发:每次 Run 开始

动作:消息序列缺 system 时在最前注入当前 prompt;已存在则原地重写(全程唯一一条)。替代引擎的 WithSystemPrompt——compact 需要运行中换 system,agent 构建后只读是引擎契约。

上下文影响:system = base + identity(render 时实时求值)+ summary 三段拼接;完整结构与示例见 3.3。fork 不重跑(seed 已含 system)。

2 · trace hook ezharness

hook 点:OnStart / OnModelStart / OnModelEnd / OnToolStart / OnToolEnd / OnEnd触发:全程观测

位置有语义:必须在 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

hook 点:OnToolStart触发:每个工具调用前(装配在 trace 后、Skip 型 hook 前)

动作:同一工具被拒(审批拒绝 / 策略 Skip)后反复重试同样调用即计数;重复触发插一条 <loop_guard> 提醒消息劝阻,不拦调用(模型仍有最终决定权)。排在 Skip 型 hook 之前,保证被短路路径也计入。

4 · contextfix hook ezloop

hook 点:OnStart触发:每轮,input 入史之后

动作:双向修理 state.Messages,保证发给 API 的序列协议完整(残缺会被 API 直接拒绝):

  • 补缺:assistant 发起的 tool_call 缺对应结果 → 在该 assistant 后补一条 tool 消息,占位文案 (missing tool result)
  • 删孤:tool 消息的 ToolCallID 没有任何 assistant 发起过(孤儿结果)→ 整条删除
assistant
[tool_use read_file  call_x7]
tool
(missing tool result)   ← 上轮取消/崩溃留下的残缺,contextfix 补齐

5 · filetools hook ezloop

hook 点:OnStart + OnLoop触发:轮首注册 / 每次迭代回边

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 之后:

assistant
我来看看这两张截图
tool
<image_loaded path="D:/…/tmp/att-1694-a.png"/>
tool
<image_loaded path="D:/…/tmp/att-1694-b.png"/>
user · 注入
<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

hook 点:OnStart + OnToolStart触发:轮首注册 / 模型调用 load_skill 时

OnStart:注册 load_skill 壳工具 + system 追加 tool-guide("接任务先扫技能清单,命中就加载后按其指引执行")。技能身份用目录名(skill.DirOf),frontmatter name 仅作显示。

工具 Invoke 闭环:调用时实时列技能(按 disabled 名单过滤),命中 → 返回渲染结果(SKILL.md 全文 + 目录树;免 offload 豁免——指令集是后续行动依据,卸载再回读纯浪费);未命中 → skill "X" 不存在。可用技能:a, b, c

tool
# 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 点:OnStart + OnEnd触发:轮首(变更段? + 快照段)/ 轮末(收尾段)

系统→模型的全部单向旁路提醒归这一个 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 事件:

user · 注入
<resource_change>
(系统检测到的本轮资源变更,非用户发言,无需回应,无需回溯处理)
- 新增技能 release-publish
变更后完整清单(技能与 MCP 服务以此为准,不要读取 memory/skills 目录或 mcp.json 来发现技能与服务):
available_skill: release-publish - 把当前分支发布为 release
available_mcp: context7 - 查询库文档
</resource_change>

available_ 清单行是给模型的即时资源发现(前端变更卡不展示,只渲染 - 变更条目行)。

快照段(OnStart,每轮一条)

水位/时间快照,插在本轮 input 前(无变更段时它就是轮首第一条)+ 发 status.snapshot 事件(前端右上角水位条):

user · 注入
<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 ≠ "" 直接返回(分身历史只存增量不记收尾);主循环追加轮末收尾(位于消息序列尾部):

user · 注入
<end_reason>
(系统自动记录的轮次收尾信息,非用户发言,无需回应)
运行轮次:3
运行时长:1 分 24 秒
结束时间:2026-09-08 14:31:24
结束原因:正常结束                              ← 手动停止/达到迭代上限/策略中止/执行出错
错误详情:…                                   ← 仅出错轮有(压平单行、300 字截断)
话题压缩:本轮结束时上下文水位达到阈值,上一会话已压缩归档并开启新会话,…  ← 仅 compact 轮有
</end_reason>

8 · reference_file hook ezharness

hook 点:OnStart触发:仅带引用的轮次(无引用零开销)

动作:引用统一为路径:附件先落盘 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 转成图片消息直接进上下文。

user · 注入
<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>
user
看下这两个文件,帮我定位问题

9 · approve hook ezloop

hook 点:OnStart + OnToolStart触发:轮首 tool-guide / 每个需审批的工具调用

判定(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。

assistant
[tool_use terminal  call_q2]  command: "del /s build"]
tool
denied by user: 不要动 build 目录   ← 拒绝理由原样成为工具结果,模型据此调整方案

10 · askuser hook ezloop

hook 点:OnStart + OnToolStart触发:轮首注册 / 模型调用 ask_user 时

拦截流程:发 askuser.request 事件(Args.question / Args.options,前端出提问卡可点选)→ 阻塞等 Answer{CallID, Input} → 用户的回答原样作为工具结果入史(Skip 通道):

assistant
[tool_use ask_user]  question: "删哪条线?"  options: ["旧测试线","演示线"]
tool
旧测试线                                 ← 用户回答即结果;空回答为 (user gave no input)

11 · task hook ezloop

hook 点:OnStart + OnToolStart触发:轮首注册 / 模型调用 task 时

拦截流程:单层检查(子循环内再调 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 加自包含前缀(分身看不到本轮对话之外的语境):

分身 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 包装)

hook 点:OnStart + OnLoop + OnEnd触发:轮首注册 / 每次回边热加载 / 轮末关连接

动作:注册唯一工具 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

hook 点:OnToolEnd触发:每个工具结果入史前

动作:结果超 4096 字节 → 全文写 .ezloop/offload/<工具名>-<sha256 前 6>.txt → 结果改写为「头部 512 字节 + 卸载说明」。豁免名单:ask_user / task / load_skill(人机与指令结果);read_file 是 ReplayTool(说明文案不同)。写入失败降级透传原文。

tool
(前 512 字节原文)
[输出共 83210 字节,超出 4096 字节阈值,已卸载到 .ezloop/offload/terminal-a1b2c3d4e5.txt,可使用 tool read_file 进行全量内容回放]

14 · guard hook ezharness

hook 点:OnToolEnd(装配在 offload 之后)触发:每个工具结果(offload 改写后的最终 Content)

动作:窗口余量动态兜底——估算「当前历史 tokens(provider 精确值 + 末次响应后追加消息按 1.2 字符/token 补估)+ 本次结果」,放不下则卸载同目录。专堵 offload 豁免漏网(如 read_file 读单行超长文件)。写入失败时硬截断丢弃尾部——此处不能像 offload 降级透传,透传即撑爆上下文,保会话优先于保内容。

tool
(前 512 字符原文)
[输出共 96000 字符,当前上下文约 195000/200000 tokens,余量放不下完整结果,已卸载到 .ezloop/offload/read_file-9f8e7d.txt;如需查看请用 read_file 按行分段读取(offset/limit),勿整文件回放]

15 · trim hook ezharness

hook 点:OnStart + OnToolStart + OnLoop触发:两条路径,统一在 OnLoop 串行区执行

路径一(水位自动):每次迭代回边检查 PromptTokens > 窗口 × TrimPercent%(默认 75,0 禁用),先发 session.trimming 事件("上下文水位 156000 / 200000 tokens(78%),达到阈值,正在整理…")。

路径二(模型主动):模型调 trim_context → OnToolStart 只登记 pending(并发回调区不写 state),Skip 安抚模型继续任务:

tool
已安排整理:本批工具调用完成后立即折叠早期上下文,请继续当前任务,不要重述将被折叠的过程细节。

执行(OnLoop,本批工具结果已全部按序入史——消息序列协议完整):摘要折叠段(独立 2 分钟预算,prompt:"保留关键事实/已达成决定/未完成待办/近期文件操作,200 字以内")→ 截断为 [head, tail, marker]:head = system(fork 则完整 seed);tail = 末 4 条(孤儿 tool 前移保证配对完整);marker 尾插。折叠段挂 Metadata(追加式档案),立即生效——下次模型调用即新上下文。发 session.trim 事件 {tokens, folded, kept, fork}。

system
(不动)
user × N
(保留的末 4 条近期消息,含配对完整的 assistant/tool)
user · 注入
<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

hook 点:OnStart + OnEnd(数组最后一位)触发:轮首就位 / 每轮结束

动作(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(全协议通用),用包裹标签自我说明。

消息生成者插入时机条件
systemsysprompt + 各 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 · 普通一轮(无附件、无变更)

system
(base + identity + summary? + 环境段 + tool-guides)
user × N
(此前轮次的消息……)
user · 注入
<agent_status>\n当前时间:2026-09-08 14:30\n上下文水位:45210 / 200000 tokens\n</agent_status>
user
工作目录里有哪些文件?
assistant
[tool_use terminal]  command: "dir /b"
tool
apps\nmemory\nworkspace\n…
assistant
工作目录下有 apps、memory、workspace 三个目录……
user · 注入
<end_reason>\n(系统自动记录的轮次收尾信息,非用户发言,无需回应)\n运行轮次:2\n运行时长:8 秒\n结束时间:…\n结束原因:正常结束\n</end_reason>

场景 B · 带附件 + 读图(消息类型最全的一轮)

用户上传两张截图 + 一个日志文件,问"帮我定位问题"。

system
(……)
user × N
(历史……)
user · 注入
<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>
user · 注入
<agent_status>…</agent_status>
user
看下这几个文件,帮我定位问题
assistant
[tool_use read_file ×3]  path: …/报错截图.png / …/界面异常.png / …/错误日志.txt
tool
<image_loaded path="…/att-1694165400-1-报错截图.png"/>
tool
<image_loaded path="…/att-1694165400-2-界面异常.png"/>
tool
Traceback (most recent call last):\n  File "main.py", line 88…\nValueError: invalid literal…
user · 注入
<image_loaded>\n…/att-1694165400-1-报错截图.png\n…/att-1694165400-2-界面异常.png\n</image_loaded>   ← 批末一条合并图片消息,Images 带两张 base64;文本日志正常返回原文
assistant
两张截图显示列表页空白,日志第 88 行在解析分页参数——根因是页码参数未做空值防御……
user · 注入
<end_reason>…运行轮次:2…结束原因:正常结束…</end_reason>

场景 C · 技能启停后的下一轮

上一轮结束后:用户在设置里启用了技能 release-publish,然后让模型发版。变更段只在下一个拥有它的轮次出现一次,再下轮消失(基线已推进)。

user · 注入
<resource_change>\n(系统检测到的本轮资源变更,非用户发言,无需回应,无需回溯处理)\n- 新增技能 release-publish\n变更后完整清单(技能与 MCP 服务以此为准,不要读取 memory/skills 目录或 mcp.json 来发现技能与服务):\navailable_skill: release-publish - 把当前分支发布为 release\n</resource_change>
user · 注入
<agent_status>…</agent_status>
user
构建刚跑完,帮我按流程发个版本
assistant
好,先加载发布技能 [tool_use load_skill]  name: "release-publish"
tool
# skill: release-publish\n路径: …\n--- SKILL.md ---\n(全文)
assistant
(按技能指引执行发布流程……)

场景 D · 水位达阈的 trim 轮

长任务进行中,OnLoop 检查水位 156000/200000(78% > 75% 阈值)→ 发 session.trimming 事件 → 就地折叠。下图为整理后的消息序列(立即生效,下次模型调用即新上下文):

system
(不动)
user × 4
(保留的末 4 条:孤儿 tool 已前移配对,协议完整)
user · 注入
<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)并发闸同批工具调用的信号量
safetoolpanic 恢复工具 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>…模型工具参数✓(入史保持原文)工具卡显示原文同左
改任一 tag 格式的连带清单见 AGENTS.md;行前缀 - 与关键词「整理上下文」是硬契约。改事件协议检查三处:后端 MapEvent(event.go)、replayable 名单(session.go)、前端 apply case(含幂等性——所有可回放帧的 apply 必须幂等)。

第四章产品视角 · 人机协同交互

agent 的能力边界再大,落到用户手里的只是几个交互面。ezharness 把人机协同交互工具外置——不塞进对话流硬凑,做成独立的产品面,让用户在 agent 工作时能看、能改、能答。核心理念:为用户提供最简单的交互方案——上下文工程、工具封装、人机交互三支柱,能力越复杂,落到用户手里的越要简单。

决策链路:四档审批策略

档位行为
ask 每次审批所有工具调用都出决策卡等用户
black 黑名单名单内的出决策卡,其余放行
white 白名单名单内的放行,其余出决策卡
auto 全部免审全自动(人机与内部工具恒免审不受档位影响)

全局通知栏:跨分支找到你

共享终端:人机共用同一个真实 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
不是 agent 私有的副本:agent 的工作目录与终端一致,用户随时接管、演示、纠错;输出进同一环形缓冲,双方读的是同一条流。

共享浏览器: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
真实浏览器 = Electron 主进程的 WebContentsView(每标签一个,partition persist:ezbrowser 登录态共享);浏览器抽屉只画 UI 框架(标签条 / 地址栏,同一份 SPA 的 ?popout=browser 形态),内容区不挂 DOM——由渲染层上报 rect、主进程 setBounds 贴靠。
快速上手:Releases 下载绿色版 / 安装包——exe 在哪运行,配置与数据就在哪生成(ezharness.json + data\ 就地创建,零配置可用)。「模型」页填 apiKey 即可对话; 纯 server 形态:直接运行 ezharness-core.exe,浏览器访问 http://127.0.0.1:<port>——与桌面端同一份页面(浏览器控制等桌面专属能力自动降级)。

附录深读文档索引

本页是全景与速查;细节文档(md/ 目录)与各主题一一对应,改代码前先扫对应篇。

文档内容
architecture.md系统整体架构:分层、双仓库、数据目录、三通道(第一章底稿)
session.mdsession 设计:树、SessionSnap、三种演进、恢复、不变量(第二章底稿)
runtime.md运行时:hook 组成与一轮 chat 全流程、上下文消息全表(第三章底稿)
frontend.md前端:SSE 事件全集与归约、历史重载解析、Block 类型、tag 契约
hooks.md封装原则:四步范式与 ezloop / ezharness 分界判据
context.md上下文工程:system 组装、trim 双触发、agent_status 快照、guard 兜底
compaction.mdtrim 与 compact 分工:模型侧折叠 vs 用户侧沉淀换代
build.md · interaction.md构建方案 / 人机交互设计
AGENTS.md(仓库根)开发备忘:变更连带检查清单与易踩的坑——改 remind / 工具面 / skill / 终端 / SSE / warp 前必扫
三大原则回顾:① 对话不是唯一的通道(决策、观察、操作各有专门产品面); ② 状态即消息(全部状态是可落盘的消息库与 system,重启即恢复,折叠不丢档); ③ 一切面向用户简化(零配置、文件夹即存储、桌面与浏览器同一份页面)。