上下文会满
子代理把大段阅读留在自己的上下文里,父会话只收一条回执;主会话的历史只压缩“这一次发出去的副本”,磁盘上的对话始终完整。
Local-first agent workspace · Electron
一个本地 Electron 客户端:让模型在你选定的项目目录里跑几十分钟的长任务——读写文件、跑 PowerShell、联网搜索,必要时把独立的活派给子代理。写操作按轮确认,这一轮随时可停;每次请求实际发出的上下文都留在磁盘上,事后能对上账。
一次回答几秒钟,长任务要跑几十分钟。三件事会先出问题:上下文会满、逐条确认太烦、出了问题说不清。这个客户端围绕这三件事设计,而不是在聊天框外面加几个按钮。
子代理把大段阅读留在自己的上下文里,父会话只收一条回执;主会话的历史只压缩“这一次发出去的副本”,磁盘上的对话始终完整。
工具调用分成 read、write、blocked 三档。写操作停一次,“允许这一轮”覆盖这一轮剩下的写;危险命令点过允许也不执行。
三份只追加的 JSONL:一份对话给模型看,一份事件做审计,一份记下每次请求实际发出的上下文,用 id 互相指认。
能停下来,能停得住,事后能对上账。
左边是工作区和会话,中间是对话,右边可以展开「答案解释」。工具调用默认折叠成一行,需要时再展开——长任务的第一屏仍然读得下去。
Reached max tool steps (50). Stopped.,把决定权交回给人,而不是悄悄继续。
ExplainRuntime:拖入选中的文字,200 字以内作答,最多 3 条来源;不读技能,也不读任务清单。
所有会改变磁盘的动作都要经过一道确认门;需要你拿主意时,模型也会停下来问。确认和提问都发生在同一轮里,不会另起一段对话。
write 停下来等确认。“允许这一轮”覆盖这一轮剩下的写操作;“停止”打断这一轮,但不会取消已经受理的后台子代理。
ask_user 提问卡。可以点选项,也可以自己输入;一次只问一个问题,同一回复里排在它后面的工具调用会被跳过。
/ 直接调技能;技能正文只附在这次发送上。
Ctrl+F 只在当前视图里搜索:对话、解释侧栏或日志页,各自独立。
config.json;正在跑的那一轮不会换配置。截图来自本机实际运行的真实窗口(1100 × 720,2× 缩放);交互演示使用隔离的工作区副本与本地 mock 模型,详见页脚说明。
渲染进程只画界面,所有决策都在主进程里的 @agent/core。这个核心库不依赖 Electron,可以脱离窗口单独测试;窗口只负责把它的状态画出来。
← 左右滑动查看整张示意图 →
turn_id,用户消息先追加进 messages.jsonl,再开始这一轮。assembleContext 在副本上做省略与压缩,磁盘上的消息一条不动。request_id、context_id、tools_sha256 写进 trace.jsonl。tool 消息写回,回到组装,进入下一批。done(reason);如果到了第 51 批,则跳过执行、给每个调用补一条合成结果,记 done(max_steps)。← 左右滑动查看整张示意图 →
done(max_steps) 结束。
停止按钮打断的是当前这一轮:还在队列里的工具调用不会被直接丢掉,而是补上合成结果,让历史仍然可以继续。已经在后台跑的 deferred 子代理不受影响,它们完成时照样把回执投进收件箱。
并发由 SessionRuntimePool 兜底:最多 5 条会话同时运行,第 6 条等前面的结束,而不是把内存堆满。每条会话的账本互相独立,停掉一条不会动到别的会话。
工具调用在真正执行之前先过一遍 policy。分类只有三档,决定它要不要停下来问人。这不是给界面加一个弹窗,而是执行路径上的一道岔口:被归到 write 的调用拿不到执行权,除非人点了允许。
← 左右滑动查看整张示意图 →
write 处理,宁可多问一次。
每个会话目录里有三份 JSONL。它们回答三个不同的问题:模型当时看到了什么、系统当时做了什么、那一次请求实际发出去的又是什么。三份都只追加,不回头改写。
← 左右滑动查看整张示意图 →
trace_id 指回触发它的事件,事件用 context_id 指回当时真正发出去的快照。想知道「它为什么这么说」,沿着 id 走一遍就够了。
llm_request 事件:context_id 指向对应的快照,approx_chars 记录这一次请求的规模。
context.jsonl 的一次快照:tools_sha256 说明这一轮带的是哪一版工具定义,系统提示与消息逐条可见。
meta.json:标题、工作区、模型,以及这个会话的来处。
messages.jsonl:按发生顺序排列,工具结果也是一条消息。
id、turn_id、trace_id、is_error——复盘时要的就是这些。
长会话会撑爆上下文。这里的做法不是「删掉旧消息」,而是每次发送前复制一份,在副本上做五种机械变换;磁盘上的 messages.jsonl 永远完整。下一次请求,再从完整的原稿重新复制一份。
← 左右滑动查看整张示意图 →
context.jsonl 里对照结果。没有隐形的向量检索,也没有「模型自己决定忘掉什么」。
100000 是给历史正文的字符预算,不是整个请求的 token 上限。系统提示、工具定义另算;字符也不等于 token。它保证的是「发出去的东西有边界」,不是「一定塞得进模型」。
子代理不是「另一个通用 Agent」。只有两种固定类型,能力写死在白名单里;它们唯一能改的地方,是调用时指定的 output_dir。类型少,权限才说得清。
← 左右滑动查看整张示意图 →
delivery 决定父回合等不等。写死四种禁止能力,是为了让「一个子代理能做什么」在调用参数里就能读完。
Tool rounds left: 15 of 16,以及它实际写了什么。
deferred 子代理跑完后不会直接往对话里塞东西。它先往 inbox.jsonl 追加一条 enqueue,再由会话在合适的时机领取、注入、标记消费。三个动作都是追加写,所以中途崩溃也不会把一条回执变成两条或零条。
← 左右滑动查看整张示意图 →
blocked_by 没完成时,这条任务不会出现在可领取的列表里。
一个本地工具最重要的不是功能列表,而是它明确不做什么。下面是默认预算、文件落点,以及三条值得在开始前就知道的限制。
| 配置项 | 默认 | 它约束什么 |
|---|---|---|
| maxSteps | 50 | 一轮最多执行多少批工具。第 51 批不再执行,补合成结果后结束。 |
| contextCharLimit | 100000 | 历史正文的字符预算。先扣系统提示与最后一条消息,保底 1000。 |
| maxToolOutputChars | 20000 | 单个工具结果写回上下文的上限,超出部分不进对话。 |
| maxReadChars | 50000 | 单次读文件的上限,防止一次把整份大文件塞进来。 |
| maxSearchResults | 50 | 单次搜索返回的条数上限。 |
| bashTimeoutMs | 60000 | 单条命令的超时;超时按失败返回,不会挂住整轮。 |
← 左右滑动查看完整表格 →
{workspace}/ └─ .agent/ └─ sessions/ └─ sess_a5de054c90fd4882b32d22c9aecf53f3/ ├─ meta.json 标题、工作区、模型 ├─ messages.jsonl 模型看到的消息,只追加 ├─ trace.jsonl 审计事件:请求、工具、权限、结束 ├─ context.jsonl 每次请求实际发出的快照 ├─ tasks.jsonl 任务清单,原子领取 └─ inbox.jsonl 后台回执:enqueue / claim / consumed ~/.long-task-agent/ ├─ config.json 模型、API Key、工作区、预算 └─ skills/ 已安装技能,每个目录一个 SKILL.md
← 左右滑动查看完整目录树 →
确认门挡的是写。它可以把读到的内容放进模型请求;工作区里的密钥、令牌、私人笔记,它都读得到。工作区是目录边界,不是沙箱。
config.json 里存着 API Key;会话目录里有完整对话、路径和写入内容。这些文件不加密,靠的是这台机器的账户权限。
没有多用户、没有远程鉴权。能读到这些目录的人,就能读到完整账本——这既是可审计性,也是它的暴露面。
看不明白的命令语法直接归到 write;只读判定要求 bash 与 PowerShell 两套规则同时同意。代价是偶尔多一次确认,换来的是不会把一次写操作当成读放过去。
省略与压缩的规则是确定的,结果能在 context.jsonl 里逐字对照。代价是它不会像记忆系统那样「理解并保留重点」;换来的是每一次请求都能解释。