起源:从 Codex 开源到 Rust + TUI
先交代这个项目的来路。resolve-tui(仓库:github.com/erishen/resolve-tui)是一个纯 Rust + TUI 的终端编码 agent,它的起点是我看了 OpenAI Codex 的开源实现——原来"终端 + 自然语言"就能驱动一个会写代码的 agent。当时打动我的不是它调用了多强的模型,而是它把交互做成了工作台而不是聊天框:agent 直接读文件、跑命令、改代码,你看到的是它在你项目里动手的过程,而不只是一串回答。
这个形态让我很着迷。我决定用 Rust + TUI 自己写一个。我此前写过一个 Python 版的 harness(resolve-harness),把"能算的绝不调模型"那套机制验证了一遍;但 Rust 版不是它的移植,而是独立重写——参考 openai/codex 的「agent loop + tools + model client」三层架构,把确定性快路径、codegen 缓存、agent 主循环、沙箱隔离在 Rust 里各实现了一遍(lib.rs 里就是这么分的:model / llm / tools / sandbox / sessions / agent),并在此基础上新增了 skills 技能系统与 MCP 集成,最后用 ratatui 把它包成终端界面。
resolve-harness 那篇文章讲的是 Python 版的"能算的绝不调模型"——fastpath 直出、codegen 让模型自写检测器、PSE 多智能体编排。这篇文章讲的是 Rust 版 resolve-tui 里这套机制之外新增的部分:交互层如何从"聊天"升级成"工作台"——事件循环、内置工具与安全边界、skills 技能系统、MCP 集成,以及命令行的交互模式。
流行做法:Chat UI 是默认答案
如果你问大多数 AI 编码助手怎么和用户交互,答案几乎都是:一个聊天界面(Chat UI)。对话框里输入问题,回复流式渲染,你可以复制、回滚,图片和表格都能展示。这也是现在最主流的形态——ChatGPT、Claude、Cursor 都是这么做的。
这个选择有充分理由。聊天是人类最熟悉的交互范式,零学习成本:你提问,它回答,你把有用的部分拿走。对于"怎么实现一个 LRU 缓存""这段代码哪里不对"这类咨询型问题,Chat UI 是完美的——答案就是产品,模型把知识变成文字给你。
但当你服务的对象是程序员、并且这个助手要真正动手改代码时,Chat UI 暴露出一个结构性的错位:它把 agent 定位成了"顾问",而不是"执行者"。
在 Chat UI 里,一次"帮我重构这个函数"的对话是这样的:
- 你把代码复制进对话框;
- 模型给出一段重构建议;
- 你把建议复制回编辑器,自己动手改。
问题不在于聊天框本身,而在于agent 和你的工作是分离的:agent 在对话里,你的代码在编辑器里,两者之间靠复制粘贴搬运。于是 agent 永远只提供"建议",从不真正"动手"。多轮迭代时,每一轮都重复这个搬运循环——上下文在两个窗口之间来回倒,但改变代码的那只手始终是你自己的。
这其实是两种交互模型的区别,而不是两种前端技术的区别:
- Chat UI:agent 在对话里回答问题,和你的文件系统隔离。它是问答。
- Agent Workbench:agent 在你的项目里动手,读文件、跑命令、改代码、验证结果。它是执行。
resolve-tui 要做的,是把后者——Agent Workbench——落到终端里。终端天然是程序员的工作台:文件系统、shell、编辑器都在里面,agent 可以直接读写、执行、验证,它和你要干的事处在同一个上下文里。这不是"用终端对抗 Web",而是"把 agent 从顾问升级成执行者"。整篇文章,就是在讲我在 Rust + TUI 里怎么把这个升级做到可用、可扩展、可控制。
质疑
等等,先别急着鼓掌。
"把 agent 从顾问升级成执行者"听起来很美,但这条升级路有一条刺眼的反对意见:当 agent 真的开始动手时,它犯的错也从"建议错了"升级成了"把项目搞坏了"。 我必须诚实地把几个反例摆出来。
第一,agent 动手 = 风险变大。 Chat UI 里 agent 说错了,你复制粘贴前还有机会自己判断;Agent Workbench 里 agent 直接 write_file、跑 shell,一旦判断失误,文件被覆盖、命令产生副作用——出错的代价从"一段没用的回答"变成了"一个被改坏的仓库"。这是升级必须回答的问题:凭什么让 agent 动手?答案只能是安全边界,而不是"模型很聪明"。
第二,工具越多,可预测性越差。 Workbench 的卖点是可扩展——内置工具、skills、MCP server 都能挂进来。但每多一个工具,agent 的决策空间就大一圈,行为就越难预测。一个能读文件、写文件、跑命令、还能调外部服务的 agent,和聊天框里"只说话"的 agent,是完全不同量级的失控可能。
第三,交互复杂度上升。 Chat UI 只有一个输入框;Workbench 有快捷键、斜杠命令、审批流程、会话管理、沙箱工作区。这些都是要学、要维护、要出 bug 的东西。如果这些复杂度没有换来实际的执行能力,那它就是纯负担。
如果这些反例成立,那"Agent Workbench"是不是一种过度工程、把简单的事做复杂?
我的回答是:这些风险是真的,但它们是可控的——而"agent 只能当顾问、永远不能真正动手"这个 Chat UI 的天花板是结构性的。问题的关键不是"Workbench 一定比 Chat 好",而是"当 agent 需要真正动手改代码时,你愿不愿意用一套安全边界,去换一个能执行的工作台"。我在替代方案一节里,会展示我是怎么用事件循环、命令面板、工具策略、skills/MCP 系统,把 Workbench 的风险压到可控、把它的执行能力放大到 Chat UI 无法提供。
替代方案
如果回到起点,我其实考虑过三条路。
第一条路是继续做 Chat UI:一个网页聊天框,输入问题、流式回答。这条路最顺,因为聊天范式成熟,我也确实做过。但前面说过,它绕不开那个结构性错位——agent 和你的工作分离,只能当顾问,不能真正动手。而且它往往要多引入一层"进程间通信":agent 引擎和 UI 之间要序列化、传输、反序列化,每一步都是复杂度。
第二条路是纯 CLI:resolve-tui "任务" 一条命令跑完就退出。这对单次任务很合适,但没有交互:你不能追问、不能翻历史、不能审批工具调用、不能多轮迭代。CLI 是"一次性"的,而编码是一个"连续"的过程——你要盯着它、改它、验收它,这些都需要一个常驻的界面。
第三条路是我最终采用的:Agent Workbench——一个常驻的、交互式的终端界面。它在"terminal 原生"和"交互能力"之间取了交点:还是终端(没有浏览器、没有进程间通信),但有一个事件循环在跑(可以多轮对话、可以审批、可以翻历史),agent 直接在你的项目里动手。这就是 resolve-tui 的形态,也是"工作台"这个升级的落点。
TUI 的核心是一个事件循环,它把三件事接在一起:键盘输入、agent 事件流、定时器刷新。
let mut reader = EventStream::new();
let mut tick = interval(Duration::from_millis(80));
loop {
tokio::select! {
maybe = reader.next() => {
if let Some(Ok(ev)) = maybe {
handle_key(ev, &mut app, &cmd_tx, &approval_tx);
}
}
Some(ev) = rx.recv() => {
app.on_event(ev);
}
_ = tick.tick() => {
app.ticks = app.ticks.wrapping_add(1);
}
}
terminal.draw(|f| ui(f, &mut app))?;
if app.should_quit { break; }
}
这段代码里藏着 TUI 的几个关键决策:
EventStream是异步的键盘源:把 crossterm 的原始事件流变成 tokio 可以select的 stream,这样界面不会被一个卡住的读取阻塞。rx是 agent 事件通道:agent 任务通过mpsc::unbounded_channel把AgentEvent(Token / ToolCall / ToolResult / System / Error…)推给 UI,UI 侧on_event增量更新屏幕。UI 和 agent 是同进程的两个 task,靠通道通信——避免网络 IPC 和协议级序列化开销。tick是 80ms 的刷新定时器:让界面可以处理"光标闪烁"这类时间驱动的动画,同时保证tokio::select!有定期唤醒,不会在无事件时彻底睡着。
这个循环的产物是一个可以连续对话的终端界面:输入框在底部,历史在上方,agent 流式输出 token,工具调用和结果在历史里以不同颜色区分。
命令行交互模式:快捷键与命令面板
终端没有鼠标,所以交互全部收敛到两个入口:快捷键(作用于当前焦点)和斜杠命令(作用于会话)。命令面就是 /help 列出的那批:
命令:
/list 列出已归档会话(git-stash 风格)
/create [名称] 归档当前对话为会话并清空,开始新对话
/apply <索引|名称> 载入某个会话继续聊(同 /load)
/save [名称|路径] 给当前对话拍快照(不打断)
/clear 清空当前对话
/rm <索引|名称> 删除某个会话
/model [名称] 切换模型(无参查看当前)
/pse [on|off] 切换多 Agent 三角色模式
/sandbox [clean] 查看沙箱工作区 / 清理任务目录
/reasoning 切换推理过程展示(或 Ctrl-R)
/export [路径] 导出当前会话为 Markdown
/tools [on|off 名] 查看 / 启停工具(内置 + MCP)
/skills [reload] 查看技能;reload 热加载技能目录
/remember [事实] 长期记忆:无参查看;带参追加
/examples 输出工具使用示例(Markdown)
/mcp [add|remove|reload] MCP 状态 / 动态挂载并存盘
/quit | /exit | /q 退出(可裸写 q / exit / quit)
快捷键:
Enter 提交 · PageUp/PageDown 翻历史 · Ctrl-R 推理 · Ctrl-Y 复制回答
运行中 Esc 中止生成 · 空闲 Esc / Ctrl-C 退出
这些命令不是装饰,它们都是"终端里才有的交互":
/model:不用重启、不用改配置文件,运行时切换模型。因为model是 UI 与 agent 共享的Arc<Mutex<String>>,改一个值,下一轮对话就走新模型。/pse:运行时切换单 agent / 多 agent 三角色模式。pse是共享的Arc<AtomicBool>,UI 侧翻一下,agent 侧下一轮就换submit_roles走 PSE 编排。架构上没有任何重连、重启的成本——这就是同进程两个 task 共享状态的好处。/sandbox clean:清理所有任务工作区。沙箱目录在项目下,prune_task_workspaces一行调用即可回收磁盘。/export:把当前会话导出成 Markdown,方便归档或贴进文档。
而 Enter 提交、Esc 取消、PageUp/PageDown 翻历史、Ctrl-Y 复制,这些快捷键把"单次提交"升级成了"可以控制、可以回顾、可以中断"的完整会话。尤其 Esc 取消:agent 任务和 UI 共享同一个 cancel: Arc<AtomicBool>,运行中按 Esc 置位,drive 循环每一轮都会检查它并立即中止——在一个纯聊天界面里,你往往只能等它把当前回复吐完,或者干脆关掉会话。
内置工具:agent 从"顾问"变成"执行者"
如果说 TUI 解决了"上下文割裂",那工具系统解决的是"权限模糊"。resolve-tui 内置了四个工具,全部在沙箱里执行:
pub fn builtin_tools() -> Vec<ResponseTool> {
vec![
tool("shell", "在沙箱中执行 shell 命令,返回 stdout/stderr。", ...),
tool("read_file", "读取文本文件内容。", ...),
tool("write_file", "写入(覆盖)文本文件。路径基于当前沙箱工作区…", ...),
tool("list_dir", "列出目录下的条目。", ...),
]
}
这四件套的意义在于:agent 可以直接在你的项目里读文件、写文件、跑命令、看目录,而不是只在对话框里给你一段"请你自己去改"的代码。它和你要干的事在同一个文件系统里。
但"直接操作"也意味着"可能搞坏",所以每个工具都受 SandboxPolicy 约束:
write_file/shell只能落在当前任务的工作区(<sandbox_root>/task-<nanos>-<pid>/),工作区外不可写。read_file/list_dir只能读项目目录 + 工作区 + 系统临时目录,盘外文件拒绝——防止~/.ssh这类敏感文件被读进上下文外发。shell走 macOSsandbox-exec/ Linuxbwrap隔离,默认断网,写限白名单。
每个任务一个独立工作区,任务之间互不覆盖;系统提示里会注入"当前可写工作区 + 白名单 + 读取范围",模型不用瞎猜路径。
还有一个工程细节:输出截断。一条 cat 大文件 可能瞬间撑爆模型上下文,所以单次工具输出有 8KB 上限,超出后保留头尾、中间以标记省略:
pub(crate) fn truncate_output(s: String) -> String {
let total = s.chars().count();
if total <= MAX_TOOL_CHARS {
return s;
}
let keep = MAX_TOOL_CHARS / 2;
let head: String = s.chars().take(keep).collect();
let tail: String = s.chars().skip(total - keep).collect();
format!("{head}\n…[输出过长已截断:原文约 {total} 字符,中间内容省略]…\n{tail}")
}
按字符而非字节切,是为了不切断多字节的中文——一个会写中文的 agent,它的工具也得先学会尊重 UTF-8。
技能系统:用纯文本扩展 agent 能力
内置工具是固定的,但 agent 的"领域能力"应该是可插拔的。这就是 resolve-skills 技能包——一组 <skill>/SKILL.md 提示词包,对齐 Agent Skills 开放标准:
---
name: rust-review
description: Rust 代码评审
triggers: review, 代码审查
---
1. 检查所有权
2. 检查错误处理
…
一个技能就是"front matter 元数据 + 指令正文",可能还带 scripts/、references/、assets/ 目录。加载策略是自适应的:
- 声明了
triggers的技能,用户输入命中关键词才把正文注入当轮 system prompt(省 token); - 没声明
triggers的技能是"模型自选",正文常驻、由模型决定何时采用。
pub fn prompt_appendix(skills: &[Skill], user_text: &str) -> Option<String> {
let mut parts = Vec::new();
if let Some(idx) = index_prompt(skills) {
parts.push(idx);
}
parts.extend(active_bodies(skills, user_text));
...
}
s/ 一栏常驻的只是索引(名称 + 描述 + 触发词,省 token),命中后才注入正文。这样几十个技能也不会撑爆每次请求的 system prompt。
技能目录的查找顺序也很有意思——$HARNESS_SKILLS_DIR → 当前目录 .resolve-tui-skills/ → crate 捆绑的 resolve-skills/skills/(git submodule)→ 安装目录兜底。也就是说,技能是你的:你可以在项目里放一个 .resolve-tui-skills/,agent 就会自动发现并使用它,不需要改代码。TUI 里 /skills reload 可以热加载,改完技能立即生效。
更关键的是契约设计:parse_skill 会忽略所有未知的 front matter 键(when_to_use、allowed-tools、agents 这些 Claude Code / Codex 的字段)。这意味着 Claude Code 或 Codex 产出的技能包,可以零改动被 resolve-tui 加载——技能生态是互通的。
MCP:把 agent 的工具箱接到外部世界
内置工具覆盖文件与 shell,但一个真正的编码 agent 还需要更多:GitHub、数据库、浏览器……每接一个都要重写一次协议。这就是 MCP(Model Context Protocol)解决的问题——工具即服务,让 agent 通过标准 JSON-RPC 挂载任意 server 的能力。
resolve-tui 内置了一个极简 MCP stdio 客户端:
pub struct McpManager {
clients: Vec<McpClient>,
tools: Vec<ResponseTool>, // 远端合并进 LLM 的工具定义
routing: HashMap<String, (String, String)>, // 暴露名 → (server, 原始工具名)
status: Vec<String>,
}
启动时,connect_all 按配置拉起每个 server 子进程,走换行分隔的 JSON-RPC 2.0:initialize 握手 → notifications/initialized → tools/list 拉取工具,之后把远端工具以 mcp_<server>_<tool> 的暴露名合并进 LLM 工具列表;模型调用时按暴露名路由回对应 server 的 tools/call:
pub(crate) fn exposed_name(server: &str, tool: &str) -> String {
format!("mcp_{}_{}", sanitize_name(server), sanitize_name(tool))
}
三个设计点值得一提:
- 失败隔离:单个 server 连不上只记录状态、跳过,不阻断整体启动——
/mcp能看到每个 server 的连接状态。 - 动态挂载:
/mcp add fs npx -y @modelcontextprotocol/server-filesystem /path在运行时挂载一个新 server,并把配置持久化进config.toml(保留原文件排版,仅追加[mcp_servers.<name>]段)。/mcp remove摘除并同步删配置。不需要重启、不需要手改文件。 - 暴露名去重:不同 server 的同名工具,先到先得,冲突的丢弃并告警。
MCP 的意义在于:agent 的工具箱不再是固定的四个,而是可扩展的。你挂一个 GitHub server,agent 就能开 issue;挂一个数据库 server,agent 就能查表。这跟技能系统的思路完全一致——能力是可插拔的,由用户决定 agent 能碰什么。
会话与记忆:终端是"常驻工作台"
TUI 是常驻的,所以它天然承担了会话管理。resolve-tui 有 git-stash 风格的会话系统:
- 退出自动存档:agent 任务退出前,把当前对话
save到last.json。 - 启动自动续接:没显式指定
--resume时,若存在last.json自动续接——"退出即保存、启动即恢复"。 /list/create/apply/save/load/rm:git-stash 风格的会话管理,CLI 侧还有--resume list列出、--resume <索引|名称|路径>载入。
加上 /remember 长期记忆(写入系统配置目录的 MEMORY.md,权限 0600),agent 能在跨会话记住你的约定,比如"部署前先跑 cargo fmt"。
证据
TUI 交互层不是一个"能跑的 Demo",而是一套有明确边界、可测、可扩展的工程。 证据来自四个方向:工具的安全边界、技能的可插拔契约、MCP 的失败隔离、以及命令行的完整交互闭环。
证据一:工具的安全边界是"可证明"的,不是"口头"的
write_file 之所以敢让 agent 直接写盘,是因为每个工具调用都先过 SandboxPolicy 校验:
"write_file" => {
let raw = str_arg(&args, "path")?;
let path = policy.resolve(std::path::Path::new(raw));
let content = str_arg(&args, "content")?;
if !policy.is_writable(&path) {
return Err(HarnessError::tool(format!("路径不在可写白名单: {path:?}")));
}
...
}
路径不在白名单,直接返回错误,不写盘。read_file 同理校验 is_readable。这个边界不是靠"模型自觉",而是靠策略层强制——模型想读 ~/.ssh/id_rsa,is_readable 会拒绝它。
证据二:技能的"零改动兼容"是可测的
parse_skill 对未知 front matter 键的处理被测试钉死了:
#[test]
fn ignores_unknown_frontmatter_keys() {
let content = "---\nname: foo\ndescription: 演示\nwhen_to_use: 评审时\nallowed-tools: [Read, Grep]\nagents: openai.yaml\n---\n正文X\n";
let s = parse_skill(content, "fallback").expect("未知键不应导致失败");
assert_eq!(s.name, "foo");
assert!(s.body.contains("正文X"));
...
}
when_to_use、allowed-tools、agents 这些外来字段被忽略,技能仍被成功解析。这个测试证明:resolve-skills 与 Claude Code / Codex 的技能是互通的,不是宣称,是回归测试保证的。
证据三:MCP 的失败隔离是端到端验证过的
#[test]
fn bad_server_does_not_block_good_ones() {
let status = mgr.status_lines();
assert_eq!(status.len(), 2);
assert!(status[0].contains("连接失败"), "坏 server 应被跳过: {status:?}");
assert!(status[1].contains("已连接"), "好 server 不受影响: {status:?}");
}
一个坏 server 不阻断整体启动——这是测试里明确断言的行为,不是一个巧合。
证据四:命令行的交互闭环,从启动到退出都串起来了
main.rs 把入口分得很清楚:
// 多 Agent 开关:覆盖配置,开启三角色编排
let config = if multi_agent_flag { ... };
// `--resume list` 列出会话后退出(git stash 风格)
if resume.as_deref() == Some("list") { print_session_list(); return; }
// 指定 --resume 时强制进入 TUI
let tui = tui_flag || resume.is_some();
- CLI 模式:
resolve-tui "任务"一条命令跑完,适合脚本和单次任务。 - TUI 模式:
resolve-tui --tui(或带--resume)进入交互界面,多轮对话、审批、翻历史。 --multi-agent:一行启动 PSE 三角色模式。codegen list/delete/clear:管理已缓存的检测器插件(这是 resolve_harness 那篇文章的机制在 Rust 里的实现,这里只带一句)。
加上 panic 钩子会在崩溃时恢复终端(离开备用屏 + 关 raw mode),否则崩溃后整个终端会停留在残破状态——这是 TUI 项目才会有的"出厂细节"。
这些代码片段合在一起回答了一个问题:Agent Workbench 凭什么能让 agent 真正动手? 因为它的每一项能力都有代码落地:事件循环接住交互、工具策略守住安全、技能契约保证互通、MCP 让能力可扩展、会话系统让工作台常驻。它不是一个"做不出来的东西"或"勉强能用的东西",而是一套边界清晰的工程。
边界
结论先说:这套"Agent Workbench"的交互设计并非放之四海而皆准。它成立的前提是——你的用户是程序员,agent 要真正动手改代码,且交互以键盘为主。一旦前提变了,回到 Chat UI 反而更合理。
让我把这条边界摊开。
第一,输出以"回答"为主的地方,Chat UI 仍然更好。 如果 agent 的角色就是给你一段解释、一个方案、一段代码片段(咨询型问题),那聊天界面是恰到好处的形态——渲染轻量、零学习成本、Agent Workbench 的复杂度反而是负担。Workbench 的价值在于"agent 要动手",不在于"agent 要说话"。
第二,用户不是程序员/不熟悉终端的地方,Chat UI 是必须的。 快捷键、斜杠命令、raw mode、备用屏——这些都是终端用户习以为常、普通用户一脸懵的东西。面向非技术用户的 AI 助手,聊天界面是对的。
第三,纯自动化场景,Workbench 让位给 CLI。 如果 agent 只是流水线里的一环(CI 里跑个 lint 修复、批量处理文件),那交互本身就是多余的,一条 resolve-tui "任务" 直接跑完更合适。Workbench 的常驻交互只服务"人在回路"的场景。
而该用 Workbench 的场景有清晰的画像:
- 用户是程序员,工作台本来就是终端;
- agent 需要读文件、写文件、跑命令——和用户在同一上下文;
- 需要多轮迭代、中途取消(Esc)、审批工具调用;
- 需要把外部能力(MCP server)和领域知识(skills)动态挂进来。
我的判断是:"AI 编码助手"这个用法通常指向程序员和改代码,所以它大概率落在 Agent Workbench 的适用区——但这不是绝对的。如果你做的其实是"AI 问答助手""AI 分析报表助手",那请不要抄我的选择——你的用户和你的输出形态决定了你该留在 Chat UI。
让我再说几条 TUI 项目特有的硬边界,它们是"选 TUI 就必须接受"的约束。
第一条是终端崩溃恢复。raw mode 打开后如果程序崩溃不恢复,整个终端会残废。所以 main.rs 里装了 panic 钩子:
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
crossterm::terminal::disable_raw_mode().ok();
crossterm::execute!(
std::io::stdout(),
crossterm::terminal::LeaveAlternateScreen,
crossterm::event::DisableBracketedPaste
).ok();
default_hook(info);
}));
这不是可选的健壮性,是必须的底线——否则一次崩溃就毁掉用户的整个终端会话。
第二条是看门狗。agent 任务一旦 panic 崩溃,UI 侧必须收到通知并复位"运行中"状态,否则输入框会永远卡在运行态(Esc 被 running 拦住),用户只能 Ctrl-C 退出:
tokio::spawn(async move {
if let Err(e) = agent.await {
let _ = watchdog_tx.send(AgentEvent::Error(format!("agent 任务异常退出:{e}")));
let _ = watchdog_tx.send(AgentEvent::Finished);
}
});
第三条是备用屏外的输出污染。TUI 进入备用屏后,任何直接打到 stderr 的东西都会在退出时"闪"出来。所以启动诊断(配置告警、技能加载警告、.env 权限提示)全部收集成 startup_notes,在 TUI 内作为系统消息呈现,而不是打 stderr。
最后,让我把这条边界落到一个具体判断上:什么时候该从 Agent Workbench 退回 Chat UI?
答案很简单——当你要的不是"agent 动手",而是"agent 回答"时。如果你的使用模式回到"复制粘贴上下文、把建议拿走自己改",那 Workbench 的执行能力就没有被用上,Chat UI 的轻量反而更合适。但对我这个场景(本地、单用户、程序员、高频、要动代码),Workbench 不是情怀,是算过账的选择。
源码导航
为什么从 Chat UI 升级到 Agent Workbench?
因为编码助手的用户是程序员、工作台在终端、且 agent 要真正动手改代码。Chat UI 里 agent 和你的工作分离,只能当顾问(复制粘贴、自己动手);Workbench 让 agent 和你在同一个文件系统上下文里,直接读文件、跑命令、改代码,并通过 Arc<AtomicBool>/Arc<Mutex> 共享取消信号和模型名,交互控制零进程间通信开销。纯回答型、面向非程序员、纯自动化的场景,则留在 Chat UI 或 CLI。
resolve-tui 和 resolve-harness 是什么关系?
它们是两个独立的项目,不是”引擎 + 界面”的从属关系。resolve-harness 是 Python(LangGraph/LiteLLM)实现,验证了”能算的绝不调模型”这套机制;resolve-tui 是 Rust 实现,参考 openai/codex 架构,把 fastpath/codegen/agent 主循环/沙箱在 Rust 里各自重写了一遍,并新增了 skills 技能系统与 MCP 集成。两者共享设计理念,但代码是独立的。
TUI 的交互模式是什么样的?
入口两个:快捷键(Enter 提交、Esc 中止/退出、PageUp/PageDown 翻历史、Ctrl-R 推理、Ctrl-Y 复制)和斜杠命令(/model 切模型、/pse 切多 agent、/sandbox clean 清工作区、/skills reload 热加载技能、/mcp add/remove 动态挂载 MCP、/export 导出会话等)。运行中 Esc 通过共享取消信号立即中止 agent。
内置工具如何保证安全?
四个内置工具(shell/read_file/write_file/list_dir)全部受 SandboxPolicy 约束:写操作只能落在当前任务工作区,读操作限于项目+工作区+临时目录,shell 走 sandbox-exec/bwrap 隔离默认断网。路径校验由策略层强制,而非依赖模型自觉。单次输出超 8KB 截断保留头尾。
技能(skills)是怎么工作的?
技能是 <skill>/SKILL.md 提示词包(front matter + 正文),对齐 Agent Skills 标准。自适应激活:有 triggers 命中关键词才注入正文(省 token),无 triggers 则正文常驻。未知 front matter 键被忽略,所以 Claude Code/Codex 的技能包可零改动加载。目录查找按 $HARNESS_SKILLS_DIR → 项目 .resolve-tui-skills/ → 捆绑 submodule → 安装目录兜底。
MCP 集成怎么用?
resolve-tui 内置极简 MCP stdio 客户端,启动时按配置拉起 server(initialize → tools/list → 暴露为 mcp_
会话与记忆是怎么管理的?
git-stash 风格会话系统:退出自动存档到 last.json、启动自动续接;/list /create /apply /save /load /rm 管理;–resume list 列出、–resume <索引|名称|路径> 载入。/remember 写长期记忆到系统配置目录 MEMORY.md(0600 权限),跨会话生效。
发表回复