长任务桌面 Agent
Long-Task Desktop Agent
GitHub ↗ 产品说明 / 2026

Local-first agent workspace · Electron

长任务,跑在
你自己的目录里。

一个本地 Electron 客户端:让模型在你选定的项目目录里跑几十分钟的长任务——读写文件、跑 PowerShell、联网搜索,必要时把独立的活派给子代理。写操作按轮确认,这一轮随时可停;每次请求实际发出的上下文都留在磁盘上,事后能对上账。

Electron 33 · Windows 优先 OpenAI 兼容接口 文件与日志都在本地
长任务桌面 Agent 主界面:左侧工作区与会话树,中间是调研 Orca 项目的对话,右侧展开答案解释侧栏
5条会话并行运行
50一轮工具批次上限
100k历史正文字符预算
8 / 16子代理回合计上限
3份只追加的 JSONL
01 · Why

长任务,不是一次问答

一次回答几秒钟,长任务要跑几十分钟。三件事会先出问题:上下文会满、逐条确认太烦、出了问题说不清。这个客户端围绕这三件事设计,而不是在聊天框外面加几个按钮。

CONTEXT

上下文会满

子代理把大段阅读留在自己的上下文里,父会话只收一条回执;主会话的历史只压缩“这一次发出去的副本”,磁盘上的对话始终完整。

PERMISSION

逐条确认会把人钉住

工具调用分成 read、write、blocked 三档。写操作停一次,“允许这一轮”覆盖这一轮剩下的写;危险命令点过允许也不执行。

AUDIT

出了问题要能复盘

三份只追加的 JSONL:一份对话给模型看,一份事件做审计,一份记下每次请求实际发出的上下文,用 id 互相指认。

Design stance

能停下来,能停得住,事后能对上账。

02 · Interface

一个窗口,三块地方

左边是工作区和会话,中间是对话,右边可以展开「答案解释」。工具调用默认折叠成一行,需要时再展开——长任务的第一屏仍然读得下去。

会话开头:用户用斜杠技能发起 Orca 项目调研,助手返回多轮工具调用摘要,页面提示达到 50 步上限
FIG 01 同一个会话里的四次续跑。工具调用被折成「77 个工具 · 14 个失败」这样一行;到 50 步会明确写下 Reached max tool steps (50). Stopped.,把决定权交回给人,而不是悄悄继续。
右侧答案解释侧栏,展示 Orca 项目的简答与要点列表
FIG 02 「答案解释」是独立的 ExplainRuntime:拖入选中的文字,200 字以内作答,最多 3 条来源;不读技能,也不读任务清单。
只看问题模式:隐藏助手回复和工具行,只保留用户消息与右侧问题刻度
FIG 03 「只看问题」把助手回复和工具行收起,右侧刻度保留跳转。长会话里,先找回自己说过的那句话。
Why it stays readable 工具行是缩进的,失败的一行标红;子代理的调用缩进在受理它的那一行下面。所有过程都在对话里,但都不挡路。
03 · Interaction

该停的时候停,该放的放

所有会改变磁盘的动作都要经过一道确认门;需要你拿主意时,模型也会停下来问。确认和提问都发生在同一轮里,不会另起一段对话。

写文件前的权限确认卡:需要确认才能执行,提供允许这一轮、拒绝、停止三个按钮
FIG 04 write 停下来等确认。“允许这一轮”覆盖这一轮剩下的写操作;“停止”打断这一轮,但不会取消已经受理的后台子代理。
ask_user 提问卡:问题为报告还差一节先补哪个方向,提供三个选项和自由输入框
FIG 05 ask_user 提问卡。可以点选项,也可以自己输入;一次只问一个问题,同一回复里排在它后面的工具调用会被跳过。
任务面板:被挡住的任务 3 依赖任务 1,下面列出待做的任务 1 和任务 2
FIG 06 任务清单记在会话目录里,不放在项目根。编号删掉也不复用;领取是原子的,两个子代理同时领同一条,只有一条会成功。
输入斜杠唤起的技能菜单,列出 github-project-research、web-research、weekly-report
FIG 07输入 / 直接调技能;技能正文只附在这次发送上。
查找栏搜索 daemon,显示 1/613 命中,会话中大量工具行
FIG 08Ctrl+F 只在当前视图里搜索:对话、解释侧栏或日志页,各自独立。
模型配置对话框叠加在对话之上,包含 Base URL、模型和 API Key
FIG 09模型配置只写 config.json;正在跑的那一轮不会换配置。

截图来自本机实际运行的真实窗口(1100 × 720,2× 缩放);交互演示使用隔离的工作区副本与本地 mock 模型,详见页脚说明。

04 · Architecture

主进程里的一套循环

渲染进程只画界面,所有决策都在主进程里的 @agent/core。这个核心库不依赖 Electron,可以脱离窗口单独测试;窗口只负责把它的状态画出来。

渲染进程 renderer · 1100 × 720 对话与工具行 三份日志的视图 答案解释侧栏 只看问题 · Ctrl+F 无 Node · contextIsolation nodeIntegration:关 preload window.agent 只暴露具名方法 会话 · 日志 配置 · 窗口 没有通用 IPC main 主进程 · @agent/core Electron 只管窗口;核心库不依赖 Electron AgentRuntime 一轮 ReAct 的循环 工具批次 · 步数上限 50 SessionRuntimePool 最多 5 条并发运行 多出来的排队等位 ToolHost + policy 工具调度与确认门 read / write / blocked ExplainRuntime 独立的解释会话 200 字 · 最多 3 条来源 Stores 会话 / 追踪 / 上下文 任务 / 收件箱 · 只追加 Provider OpenAI 兼容流式接口 只和 baseUrl 通信 追加写 JSONL {workspace}/.agent/sessions/{id}/ messages.jsonl · trace.jsonl · context.jsonl · meta.json ~/.long-task-agent/config.json 模型 · API Key · 工作区 · 各种预算

← 左右滑动查看整张示意图 →

D1 渲染进程没有 Node 能力,只能通过 preload 暴露的具名方法说话。模型请求、工具执行、日志落盘都发生在主进程;每个会话的账本放在工作区目录里,而不是应用安装目录。

一轮 ReAct 怎么走

  1. 发送。生成一个新的 turn_id,用户消息先追加进 messages.jsonl,再开始这一轮。
  2. 换气。收件箱里已 enqueue、未投递的回执,在这个点以固定前缀注入对话——它明确写着「这是完成回执,不是用户的新指令」。
  3. 组装。assembleContext 在副本上做省略与压缩,磁盘上的消息一条不动。
  4. 请求。流式调用模型;请求前把 request_id、context_id、tools_sha256 写进 trace.jsonl。
  5. 返回。有工具调用就交给 ToolHost 逐条分类;没有就收尾。
  6. 执行。一批工具执行完,结果作为 tool 消息写回,回到组装,进入下一批。
  7. 收尾。写最终回答,trace 记 done(reason);如果到了第 51 批,则跳过执行、给每个调用补一条合成结果,记 done(max_steps)。
用户发送消息 turn_id = t_… 投递收件箱 有未消费的回执就注入 组装上下文 assembleContext · 只改副本 请求模型(流式) llm_request · context_id 模型返回 文本?还是工具调用? ToolHost 执行一批 分类 → 授权 → 结果回填 收尾 写最终回答 · done(reason) 到达上限 50 这一批不执行 · done(max_steps) 有工具调用 step++ · 下一批 没有工具调用 换气点 回合开始 每批工具执行完之后 收尾之前 空闲且被观察的会话开一轮唤醒 一轮 = 一个 turn_id 步数按工具批次计 不是 HTTP 请求数 50 是默认值,可配置

← 左右滑动查看整张示意图 →

D2 一批里十个工具调用只算一步;模型每次返回都从「组装」重新开始。到了第 51 批,工具不执行,每个调用补一条合成结果,让对话保持合法,然后以 done(max_steps) 结束。

停下来,停得住

停止按钮打断的是当前这一轮:还在队列里的工具调用不会被直接丢掉,而是补上合成结果,让历史仍然可以继续。已经在后台跑的 deferred 子代理不受影响,它们完成时照样把回执投进收件箱。

并发由 SessionRuntimePool 兜底:最多 5 条会话同时运行,第 6 条等前面的结束,而不是把内存堆满。每条会话的账本互相独立,停掉一条不会动到别的会话。

确认门在系统里做什么

工具调用在真正执行之前先过一遍 policy。分类只有三档,决定它要不要停下来问人。这不是给界面加一个弹窗,而是执行路径上的一道岔口:被归到 write 的调用拿不到执行权,除非人点了允许。

ToolHost · 执行前先分类 name + args → policy read 不用确认 读文件 / 列目录 / 搜索 只读命令:bash 与 PowerShell 两套规则都判只读 直接执行 · 结果回填 不打断这一轮 write 停下来问 写 / 改 / 删文件 非只读命令 看不懂的语法也走到这里 允许这一轮 拒绝 停止 允许这一轮覆盖剩下所有写;下一轮重新问 blocked 永不执行 格式化磁盘 删根目录 其它明确危险的命令 直接拒绝 连确认都不给 允许这一轮 = 覆盖这一轮剩下的写操作,直到这一轮结束;下一轮从零开始。 停止 = 打断这一轮;已经受理的后台子代理继续跑完,完成后照常回执。 确认门挡的是「改」,不是「泄露」:read 不需要确认,读到什么、发去哪里,仍由模型决定。

← 左右滑动查看整张示意图 →

D3 只读与写入的判定发生在执行前,而不是界面层。命令是否只读,要 bash 与 PowerShell 两套规则同时同意;任何看不明白的语法都按 write 处理,宁可多问一次。
05 · Records

三份只追加的账

每个会话目录里有三份 JSONL。它们回答三个不同的问题:模型当时看到了什么、系统当时做了什么、那一次请求实际发出去的又是什么。三份都只追加,不回头改写。

messages.jsonl 模型看到的内容 id · session_id · turn_id role · content tool_calls[] · tool_call_id trace_id 用户消息、助手消息、工具结果 按发生顺序追加 trace.jsonl 系统做了什么 event: llm_request tool_call · permission subagent · task · done message_id · context_id 不参与上下文组装 只用来复盘与审计 context.jsonl 实际发出去的请求 request_id · context_id messages[](副本) tools_sha256 approx_chars 这一轮为什么这么长 打开就能看到,不用猜 trace_id context_id 三者只追加;一个会话目录就是一份可以回放的账本,删除会话即删除整份账本。

← 左右滑动查看整张示意图 →

D4 消息用 trace_id 指回触发它的事件,事件用 context_id 指回当时真正发出去的快照。想知道「它为什么这么说」,沿着 id 走一遍就够了。
日志页的 trace.jsonl 视图,展开一条 llm_request,显示 context_id、model 与 approx_chars
FIG 10 llm_request 事件:context_id 指向对应的快照,approx_chars 记录这一次请求的规模。
日志页的 context.jsonl 视图,展开一次请求,显示 tools_sha256 与完整的 system 提示
FIG 11 context.jsonl 的一次快照:tools_sha256 说明这一轮带的是哪一版工具定义,系统提示与消息逐条可见。
日志页的 meta 视图,显示会话标题、工作区与模型
FIG 12meta.json:标题、工作区、模型,以及这个会话的来处。
messages.jsonl 列表,按顺序展开每一条消息
FIG 13messages.jsonl:按发生顺序排列,工具结果也是一条消息。
展开的消息条目,显示 id、session_id、turn_id、role、trace_id、tool_call_id 与 is_error 字段
FIG 14展开后能看到 id、turn_id、trace_id、is_error——复盘时要的就是这些。

上下文只在这份副本上改

长会话会撑爆上下文。这里的做法不是「删掉旧消息」,而是每次发送前复制一份,在副本上做五种机械变换;磁盘上的 messages.jsonl 永远完整。下一次请求,再从完整的原稿重新复制一份。

01 复制 原消息一条不改。 所有省略和压缩 都发生在这个 临时副本上。 02 省略大段写入 超过 500 字符的 持久化写入, 只留一条摘要。 03 省略旧技能 技能正文只附在 调用它的那一轮, 之后不再重复。 04 压缩早期轮次 用户轮次超过 10, 保留前 3 + 后 10; 中间截成 200 字, 摘要不超过 4000 字。 05 字符预算 默认 10 万字符, 先扣系统提示与 最后一条消息, 保底 1000。 悬空的工具调用 → 补合成结果 · 这是机械汇总,不是模型总结 · 最后一条消息永不截断

← 左右滑动查看整张示意图 →

D5 五种变换都能用一句话说清,也都能在 context.jsonl 里对照结果。没有隐形的向量检索,也没有「模型自己决定忘掉什么」。
A number, read carefully 100000 是给历史正文的字符预算,不是整个请求的 token 上限。系统提示、工具定义另算;字符也不等于 token。它保证的是「发出去的东西有边界」,不是「一定塞得进模型」。
06 · Background

把独立的活派出去

子代理不是「另一个通用 Agent」。只有两种固定类型,能力写死在白名单里;它们唯一能改的地方,是调用时指定的 output_dir。类型少,权限才说得清。

父会话 一次 subagent 调用 type · delivery output_dir · task 子代理不能 跑 shell 命令 向用户提问(ask_user) 再派一个子代理 执行技能脚本 写到 output_dir 之外 explore 8 轮 只读:读文件、搜内容、看目录 不能写文件,也没有 output_dir 适合把大段阅读留在自己身上 回执封顶 6000 字符 worker 16 轮 只能在 output_dir 下写 目录在调用时锁定,大小写不敏感 适合把一段独立产出落成文件 回执封顶 6000 字符 joined 同一回复里的多个子代理 在第一次 ask_user 之前 用 Promise.all 一起跑 父回合等它们回来 deferred 父回合不等;完成时 写一条回执进收件箱 在下一个换气点出现 子代理只在内存里,重启即失 回执 6000 字符封顶,投给用户的通知 24000 字符封顶。 长内容应该落成文件,让回执只留路径与摘要——这是设计意图,不是限制。

← 左右滑动查看整张示意图 →

D6 类型决定回合上限与写权限,delivery 决定父回合等不等。写死四种禁止能力,是为了让「一个子代理能做什么」在调用参数里就能读完。
派发 worker 子代理时的权限确认卡,写明 worker writes under project-research/orca/demo-notes
FIG 15 派 worker 也要过确认门:允许的是这个子代理在指定目录里的写。目录之外的路径它拿不到。
展开的子代理调用,显示 write_file 步骤与剩余回合数 15 of 16
FIG 16 展开调用能看到它的工具行缩进在下层:Tool rounds left: 15 of 16,以及它实际写了什么。

完成回执怎么回来

deferred 子代理跑完后不会直接往对话里塞东西。它先往 inbox.jsonl 追加一条 enqueue,再由会话在合适的时机领取、注入、标记消费。三个动作都是追加写,所以中途崩溃也不会把一条回执变成两条或零条。

worker 子代理 会话 runtime inbox.jsonl enqueue(subagent_id, payload) · 只追加 轻通知:有回执了 claim(id) · 原子领取 在换气点注入对话(固定前缀) consumed(id) · 标记已投递 崩溃安全:已 enqueue、未 consumed 的回执,重启后按 claim 状态重投。 重复投递是幂等的;已经写进 inbox 的回执不受重启影响,还在跑的 deferred 子代理才会丢。

← 左右滑动查看整张示意图 →

D7 投递时机是「换气点」:回合开始、每批工具之后、收尾之前;空闲且正被观察的会话会因此开一轮唤醒。注入的回执带固定前缀,模型不会把它当成用户的新指令。
子代理完成后的后台完成回执卡,列出已结束的子代理与写入的文件,下面是模型接着汇总的回复
FIG 17 回执卡写着「后台任务完成。这是完成回执,不是用户的新指令」,并列出 subagent id、写入路径与字符数。下面那条回复,就是会话被唤醒后接着做的汇总。
Tasks, in the same directory 任务清单也放在会话目录里,不落到项目根。编号删掉也不复用;领取是原子的——两个子代理同时领同一条,只有一条会成功。blocked_by 没完成时,这条任务不会出现在可领取的列表里。
07 · Limits

它把边界画在哪

一个本地工具最重要的不是功能列表,而是它明确不做什么。下面是默认预算、文件落点,以及三条值得在开始前就知道的限制。

默认预算

配置项默认它约束什么
maxSteps50一轮最多执行多少批工具。第 51 批不再执行,补合成结果后结束。
contextCharLimit100000历史正文的字符预算。先扣系统提示与最后一条消息,保底 1000。
maxToolOutputChars20000单个工具结果写回上下文的上限,超出部分不进对话。
maxReadChars50000单次读文件的上限,防止一次把整份大文件塞进来。
maxSearchResults50单次搜索返回的条数上限。
bashTimeoutMs60000单条命令的超时;超时按失败返回,不会挂住整轮。

← 左右滑动查看完整表格 →

文件落在哪

← 左右滑动查看完整目录树 →

三条限制

NOT A SANDBOX

read 不经过确认

确认门挡的是写。它可以把读到的内容放进模型请求;工作区里的密钥、令牌、私人笔记,它都读得到。工作区是目录边界,不是沙箱。

LOCAL & PLAIN

本地明文

config.json 里存着 API Key;会话目录里有完整对话、路径和写入内容。这些文件不加密,靠的是这台机器的账户权限。

SINGLE USER

单机单用户

没有多用户、没有远程鉴权。能读到这些目录的人,就能读到完整账本——这既是可审计性,也是它的暴露面。

两个刻意的取舍

TRADEOFF · 01

宁可多问,也不猜

看不明白的命令语法直接归到 write;只读判定要求 bash 与 PowerShell 两套规则同时同意。代价是偶尔多一次确认,换来的是不会把一次写操作当成读放过去。

TRADEOFF · 02

机械压缩,不做「智能记忆」

省略与压缩的规则是确定的,结果能在 context.jsonl 里逐字对照。代价是它不会像记忆系统那样「理解并保留重点」;换来的是每一次请求都能解释。