Tui chat cn
TUI Chat 是 Jianmu 的终端交互应用,基于 Textual 构建,提供面向 Agent 的聊天、审批和运行观察界面。它采用“薄 UI、厚适配层”的组织方式:界面层主要负责渲染与输入收集,运行时编排、挂起恢复、审批协调和事件投影则集中在 apps/tui_chat/ 的适配层中。本文重点说明它的结构、运行路径和与 Jianmu 内核的衔接方式。
架构分层总览¶
TUI Chat 的架构可划分为四个正交层次:入口路由层 负责分发 CLI 命令与 TUI 启动;UI 组合层(Textual App + Widgets)定义布局绑定与事件路由;适配编排层(Runner + Factory + Projector)将 Jianmu 运行时事件转化为 UI 可消费的快照;能力支撑层(配置、会话、技能、审批、文件、转录等模块)提供独立的功能单元。
graph TD
subgraph 入口路由
MAIN[__main__.py] --> CLI[cli.py / 非UI命令]
MAIN --> APP[app.py / Textual TUI]
end
subgraph UI组合层
APP --> WIDGETS[15个 Textual Widgets]
APP --> SCREENS[4个 Modal Screens]
end
subgraph 适配编排层
APP --> RUNNER[TUIAgentRunner]
RUNNER --> FACTORY[TUIAgentFactory]
RUNNER --> PROJECTOR[TUIRuntimeProjector]
FACTORY --> JIANMU_AGENT[Agent / SkillNode]
PROJECTOR --> SNAPSHOT[RunSnapshot]
end
subgraph 能力支撑层
SESSIONS[SessionStore] --> APP
CONFIG[TUIConfig] --> APP
APPROVALS[ApprovalCoordinator] --> APP
SKILLS[SkillsCatalog] --> APP
TOOLS[ToolAssembly] --> FACTORY
TRACES[TraceSink] --> RUNNER
end
JIANMU_AGENT --> REACTIVE[ReactiveRunner]
JIANMU_AGENT --> EVENTBUS[RuntimeEventBus]
EVENTBUS -->|subscribe| PROJECTOR
入口路由:CLI 双通道与依赖检查¶
__main__.py 实现了双通道入口逻辑。当用户传入 -h/--help 或模型管理类子命令(add-model、list-models、set-default-model、smoke-test、probe-openai)时,直接路由到 cli.py 的无 UI 命令行处理器;否则进入 Textual TUI 通道。
在进入 TUI 通道前,ensure_textual_installed() 会检查 textual 和 rich 两个可选依赖是否已安装,若缺失则抛出清晰的 RuntimeError 提示用户安装。这一设计使得 tui_chat 可以作为可选应用独立于框架核心存在——用户只有在需要终端界面时才需要安装额外依赖。
CLI 子命令系统通过 argparse 构建,涵盖以下无 UI 操作:
| 命令 | 功能 | 是否需要 Textual |
|---|---|---|
add-model |
添加或替换模型配置(--name/--provider/--model/--base-url/--api-key-env) | 否 |
list-models |
列出已配置模型及默认标记 | 否 |
set-default-model |
设置默认模型 | 否 |
smoke-test |
运行一次非 UI 运行时冒烟测试 | 否 |
probe-openai |
直接探测 OpenAI 兼容端点连通性 | 否 |
export-session |
将会话导出为 Markdown | 否 |
trace-list |
列出最近 trace 文件摘要 | 否 |
trace-view |
从 trace 文件或 run id 渲染紧凑 waterfall | 否 |
inspect |
启动本地 loopback request inspector 查看器 | 否 |
其中 smoke-test 很实用:它在无 UI 环境下走通 TUIAgentRunner 的初始化、提交和关闭流程,输出 outcome、status、attempted_model、事件数量、挂起状态和 token 用量,适合用于 CI 校验和问题排查。
UI 组合层:Textual App 的布局、绑定与事件路由¶
JianmuTUIApp 继承自 textual.app.App,是整个终端界面的根容器。它通过 compose() 方法声明式定义布局层级,通过 BINDINGS 注册全局键盘快捷键,通过 @on 装饰器和 @work 装饰器实现事件驱动的异步任务调度。
双栏布局与组件编排¶
界面采用经典的左右分栏结构:
┌─────────────────────────────────────────────────┐
│ HeaderBar (标题栏) │
├──────────────────────┬──────────────────────────┤
│ MessageHistory │ TodoPanel │
│ (对话历史/3fr) │ (任务进度) │
│ │ │
│ StreamingBar │ FilePanel │
│ (流式输出指示) │ (文件变更) │
│ │ │
│ CommandList │ CodePanel │
│ (命令帮助) │ (代码编辑/1fr) │
├──────────────────────┴──────────────────────────┤
│ ApprovalBar / AskUserBar (挂起交互栏) │
│ StatusBar (状态栏) │
│ InputBox (输入框 / dock: bottom) │
└─────────────────────────────────────────────────┘
左侧占用 3fr(3 份弹性空间),作为主要的对话交互区;右侧占用 1fr,展示辅助信息面板。ApprovalBar 和 AskUserBar 默认隐藏(display: none),仅在运行时挂起时通过 -visible CSS class 切换显隐。
键盘绑定与动作系统¶
全局 BINDINGS 定义了 6 个核心键盘操作:
| 绑定 | 动作方法 | 功能 |
|---|---|---|
ctrl+c |
action_copy_or_quit |
优先复制对话到剪贴板,若无内容则退出 |
ctrl+q |
action_quit |
直接退出应用 |
ctrl+s |
action_save_code_panel |
保存代码面板中当前文件 |
escape |
action_escape |
关闭弹窗 / 取消等待输入 / 取消活跃 worker |
a |
action_approve |
批准当前待审批请求 |
r |
action_reject |
拒绝当前待审批请求 |
action_escape 采用三段式处理:先尝试关闭 Modal Screen,再尝试取消 awaiting_user_input 状态,最后取消所有活跃 worker,也就是中断当前 agent 运行。action_copy_or_quit 则利用 last_visible_transcript 缓存来避免意外退出。
事件驱动的消息流¶
Textual 的消息机制被充分利用来实现组件间解耦通信。以下是核心消息类型及其流向:
sequenceDiagram
participant Input as InputBox
participant App as JianmuTUIApp
participant Runner as TUIAgentRunner
participant Bus as RuntimeEventBus
participant Widgets as UI Widgets
Input->>App: CommandSubmitted(text)
alt 斜杠命令
App->>App: parse_command → _handle_command
else 普通对话
App->>Runner: submit(text)
Runner->>Bus: subscribe(projector.apply_event)
Bus-->>App: text.delta → StreamingBar
Bus-->>App: reply.completed → final_answer
Bus-->>App: tool.call.* → transcript
Bus-->>App: approval.requested → ApprovalBar
Runner-->>App: RunSnapshot
App->>Widgets: _apply_snapshot
end
CommandSubmitted 消息由 InputBox 在用户按下回车后发出,携带着原始文本。App.on_command_submitted 首先尝试 parse_command 解析斜杠命令,失败则视为普通 prompt 进入 handle_prompt 异步工作流。
@work(exclusive=True) 的并发控制¶
所有可能触发 agent 运行的 handler 均标记为 @work(exclusive=True)——这确保同一时刻只有一个异步 worker 在执行。当用户在 agent 运行期间输入新内容时,新 worker 会排队等待而非并发执行,避免了状态竞争。action_escape 中的 self.workers.cancel_all() 则提供紧急中断能力。
适配编排层:从运行时事件到 UI 快照¶
适配编排层是 TUI Chat 的架构核心,由三个类协同完成 Jianmu 运行时到终端 UI 的映射:TUIAgentFactory 负责组装运行时对象图,TUIAgentRunner 负责编排运行生命周期,TUIRuntimeProjector 负责将运行时事件流投影为 UI 可消费的 RunSnapshot。
TUIAgentFactory:运行时对象图组装¶
TUIAgentFactory.create() 是运行时对象图的工厂方法,它按依赖顺序组装以下组件:
graph LR
CONFIG[TUIConfig + ProjectConfig] --> MC[ModelClient.resolve]
CONFIG --> TOOLS[build_tool_profile + MCPToolProvider]
TOOLS --> TOOLSET[ToolSet]
MC --> NODE[SkillNode]
TOOLSET --> NODE
CONFIG --> CHK[FileCheckpointer]
BUS[RuntimeEventBus] --> CTX[RunContext]
APPROVALS[ApprovalCoordinator] --> CTX
CONSTRAINTS[Constraints] --> CTX
MC --> CTX
NODE --> AGENT[Agent]
STATEMGR[StateManager/ChatState] --> AGENT
CTX --> AGENT
关键设计决策:
-
模型解析优先级:
TUIAgentFactory._resolve_model_client()优先使用 TUI 配置的ModelEntry(含 provider、base_url、api_key_env),回退到ModelClient.resolve(preference=["openai", "litellm"])的自动检测逻辑。当ModelEntry.api_key_env存在时,工厂会将对应环境变量的值注入API_KEY/OPENAI_API_KEY,实现模型凭据的按名隔离。 -
工具装配:TUI 现在使用统一的 workspace 工具集。除基础四件套(Calculator、FileRead、FileWrite、Bash)外,ListDir、GlobSearch、GrepSearch、FileInfo、StrReplace、ApplyPatch 等编码辅助工具也会始终可用。MCP 工具通过
MCPToolProvider动态接入并与内建工具集合并。旧 session 中的tool_profile元数据会保留用于兼容,但不再改变运行行为。
TUI 里的 read_file 还是一个 app-local 版本:当 grep_search 命中候选位置后,会优先引导模型使用 around_line 加上较小的 before / after 窗口读取局部片段,而不是直接整文件读取。投影层也会把 grep 结果转换成 suggested_read_file_calls,并进一步收紧模型可见的 read_file 返回上限。
- 约束配置:TUI 启用 Permission V2,并向 core 提交纯
guard.permission配置。项目中的旧tool_policy/tool_rules会在 TUI host 层迁移成 V2 rules,再参与运行时构建。write_file、str_replace、apply_patch、mkdir、move_path统一声明为edit,bash声明为bash。所有内建文件系统写操作以及 shell 命令都需要审批;只读文件工具仍可直接执行。
TUIAgentRunner:生命周期编排与挂起恢复¶
TUIAgentRunner 是适配编排层的门面,封装了 agent 的完整生命周期。
初始化采用延迟模式——构造时仅记录 session 元数据和创建 TUIRuntimeProjector/TUIAgentFactory,实际的 agent 构建在首次 initialize() 时发生。initialize() 支持 force=True 参数以触发完全重建(先关闭旧 bundle 再创建新的),这在模型配置变更或 MCP 重载时使用。
核心运行方法 submit() 和 resume() 共享一致的执行模板:
ensure_ready()确保 agent 已初始化projector.begin_run()创建新的RunSnapshot- 应用挂起历史消息(
_apply_pending_history) - 调用
agent.run_until_suspend()并传入SuspensionMode.YIELD——这使运行时在遇到挂起条件时立即返回而非阻塞等待 _capture_result()将RunResult投影为RunSnapshot_persist_session_if_needed()持久化会话
max_ticks=50 限制了单次运行的 tick 上限,防止失控循环。reset_tree=True 确保每次运行从干净的行为树开始,reset_data=False 则保留状态数据的连续性。
普通 prompt 提交和 approval resume 现在还会把 event_driven_checkpoint=True 传给 run_until_suspend()。也就是说,底层 runner 只会在 checkpointable state 或 tree status 真正变化时保存 checkpoint,而不会在每个 RUNNING tick 上重复写入。TUI 仍然保留 file-backed checkpoint,但长工具阶段不会再刷出大量相同 checkpoint。
审批回复统一使用 Core 的 once / always / reject contract。always
会在 workspace scope 下保存精确 action/resource allow rule,并自动解决
其他已被新规则满足的 pending 请求。拒绝也会恢复原运行:工具不会执行,
拒绝结果作为 tool observation 返回模型,模型可以调整方案,而不是由 TUI
提前终止整个任务。
审批栏会展示工具提供的结构化 action、resources、精确持久化范围和限长
diff preview。持久授权需要二次明确确认,并可通过 /permissions 查看和撤销;configured deny
始终优先。
TUIRuntimeProjector:事件到快照的投影引擎¶
TUIRuntimeProjector 通过订阅 RuntimeEventBus,将 Jianmu 的运行时事件流转化为结构化的 RunSnapshot。它是整个适配层中事件语义最密集的组件:
| 事件类型 | 投影行为 |
|---|---|
text.delta |
累积追加到 snapshot.streaming_text |
reply.completed |
设置 snapshot.final_answer |
execution.suspended |
设置 snapshot.suspension 和 pending_interaction |
approval.requested |
插入 system 角色 transcript 消息 |
tool.call.started |
插入 tool_use 类型 transcript 消息 |
tool.call.completed |
插入 tool_result 类型 transcript 消息(ok=true) |
tool.call.failed |
插入 tool_result 类型 transcript 消息(ok=false) |
apply_result() 在运行结束后执行"终态快照"合并——从 RunResult、StateManager、工具诊断、MCP 状态等多个来源聚合最终状态。其中 _build_diagnostic() 方法在 final_answer 为空且运行失败时生成诊断信息,帮助用户理解失败原因(如 "Agent run failed before a reply was produced. model=xxx model_calls=1 last_event=model.call.failed")。
会话持久化:SessionStore 与 SessionRecord¶
会话系统是整个 TUI 应用的状态骨架。SessionStore 以 JSONL 文件格式将会话持久化到磁盘,SessionRecord 封装单个会话的元数据、消息列表和挂起状态。
存储格式¶
每个会话存储为一个 .jsonl 文件,第一行为 header(含 _type: "metadata" 标记、SessionMetadata 数据和 suspension 快照),后续每行为一条 normalize_message() 处理后的消息记录。这种格式兼具人类可读性和逐行追加的可扩展性:
{"_type": "metadata", "metadata": {"session_id": "s_abc123...", ...}, "suspension": null}
{"role": "user", "content": [{"type": "text", "text": "帮我分析..."}]}
{"role": "assistant", "content": [{"type": "text", "text": "好的,让我..."}]}
SessionStore.list() 仅解析每个文件的 header 行以构建会话列表,避免全量加载所有消息——这对于会话数量增长后的性能至关重要。resolve_id() 支持精确匹配和前缀匹配,允许用户通过简短前缀快速恢复会话。
会话生命周期¶
stateDiagram-v2
[*] --> Ephemeral: 新建空会话(session_id="")
Ephemeral --> Active: 首次消息触发 _ensure_session
Active --> Active: submit/resume 后 _persist_session_if_needed
Active --> Suspended: 运行时挂起(approval/ask_user)
Suspended --> Active: 用户响应后 resume
Active --> Saved: 落盘到 JSONL
Saved --> Active: /sessions 选择后 _swap_session
Active --> [*]: /exit 或 /quit
_ensure_session() 在首次交互时触发,分配 session_id 和 thread_id(两者使用相同的 s_ 前缀随机 ID),创建时间戳,并重建 runner。_swap_session() 是会话切换的核心——它先保存当前会话(隐含在 _persist_session_if_needed 中),再加载目标会话的全部消息到 UI 并恢复挂起状态。
终端交互能力全景¶
Slash 命令系统¶
TUI Chat 实现了一组内置命令和动态发现的技能命令。命令解析由 parse_command() 完成——以 / 开头,空格分隔命令名和参数。load_available_commands() 将内置命令与 SkillsCatalog 发现的技能合并排序。/compact 会立即总结较早的上下文并持久化 active model-history projection;它不等待下一条 prompt,也不会伪造一轮对话消息。
实时过滤是提升可用性的关键设计:当用户在输入框中输入 / 开头的内容时,on_input_changed 事件触发 show_filtered() 方法,在 CommandList 面板中实时展示匹配的命令。这使用户无需记忆完整命令名即可快速发现功能。
审批与人机交互挂起¶
审批流由 ApprovalCoordinator 作为桥接层实现。它构建一个绑定自身回调的 ApprovalManager 实例注入 RunContext——当运行时工具调用触发审批时,回调将审批请求存入 pending 字典并返回 PENDING 状态,导致 agent.run_until_suspend() 以 SuspensionMode.YIELD 立即返回。
UI 层检测到 snapshot.suspension.reason == "approval_pending" 后显示 ApprovalBar,用户通过键盘 a(批准)或 r(拒绝)触发 action_approve/action_reject,进而调用 runner.resolve_approval() 将决策写入 ApprovalCoordinator 并恢复运行。
ask_user 挂起遵循相同的模式,但额外支持结构化问卷:AskUserBar 解析 payload.questions 列表,为每个问题显示标签,收集用户回答后通过 ResumePayloadSubmitted 消息回传。
流式输出与工具调用可视化¶
流式输出通过 StreamingBar(一个简单的 Static widget)实现——每次 text.delta 事件触发时累积更新文本,reply.completed 后转为正式的 assistant 消息写入 MessageHistory。
工具调用通过 RichLog.write() 以 Markdown 格式渲染:
- Tool call: - Tool call: \tool_name` input: `...`- **Tool result**:- Tool result (ok/error): `tool_name` output: ...`
CodePanel 进一步将文件写入工具的效果可视化为可编辑的代码面板——collect_file_effects_from_event() 从 tool.call.* 事件的 args_preview.path 中提取文件路径,build_file_entries() 合并现有条目(保留 dirty 标记),最终在 CodePanel 中以 ListView + TextArea(带行号和语法高亮)展示。
辅助面板:Todo 与 File Effects¶
TodoPanel 从当前会话消息和运行时事件中推导进度摘要——最后一条用户消息作为 "Current task",工具调用计数作为 "Tools started/completed"。FilePanel 展示 RunSnapshot.file_effects 中的文件读写记录。两个面板在每次 _apply_snapshot() 时通过 _refresh_side_panels() 更新。
Modal Screen 系统¶
四个 Modal Screen 扩展了功能深度而不增加主界面复杂度:
| Screen | 触发方式 | 功能 |
|---|---|---|
SessionPickerScreen |
/sessions 或 /resume |
浏览、恢复、删除已保存会话 |
ModelManagerScreen |
/model |
添加/替换/设默认/删除模型配置 |
FirstRunWizardScreen |
自动(无模型配置时) | 首次引导配置模型 |
DebugScreen |
/debug 或 /trace |
查看 trace 事件摘要和诊断信息 |
所有 Modal Screen 均绑定 escape 键关闭,遵循 Textual 的 ModalScreen 约定。
模型配置管理¶
TUIConfig 是 TUI 应用的独立配置模型(区别于框架层的 jianmu.yaml),持久化在 ~/.jianmu_tui/config.yaml。它管理:
- 模型列表 (
models: list[ModelEntry]):每个条目包含 name/provider/model/base_url/api_key_env - 默认模型 (
default_model):当前会话默认使用的模型名称 - 技能目录 (
skills_dir):覆盖框架的 skills 搜索路径 - 目录路径 (
session_dir/checkpoint_dir/trace_dir):各自默认指向~/.jianmu_tui/下对应子目录 - MCP 路径 (
mcp_config_path):MCP 服务器配置文件路径 - 并行探索上限 (
max_parallel_tasks):TUI 专属的parallel_tasks工具上限,默认允许 2 个只读 explore 子任务并发 - 上下文压缩 (
context_compaction):默认启用,按估算 100,000 token 自动触发。recent tail 同时受 turn 数(默认 2)和 token 预算约束(默认取触发阈值的 25%,并限制在 2,000–8,000 token),keep_recent_messages仅作为消息数硬上限;摘要输入/输出上限和超时仍可配置。/compact会立即执行同一条 durable compaction 路径;如果保留 tail 后没有可摘要区间,则改为总结完整历史。
模型配置的运行时生效通过 _rebuild_runner() 实现——每次添加、删除或切换默认模型后,runner 被完全重建,新模型立即生效。模型凭据的隔离通过 ModelEntry.api_key_env 实现:不同模型可指定不同的环境变量名作为 API Key 来源,构建时由 TUIAgentFactory._resolve_model_client() 按名注入。
技能发现与显式调用¶
TUI Chat 通过 resolve_skills_catalog() 获取 SkillsCatalog 实例,优先使用 TUI 配置的 skills_dir,回退到 SkillsCatalog.resolve_runtime() 的全局发现。发现的技能被转化为 SkillCommand 并合并到 slash 命令列表中。
显式技能调用通过 /skill_name prompt 语法触发:_handle_command 在匹配到技能命令后调用 _run_skill_prompt(),该方法设置 session.metadata.selected_skill 并重建 runner(SkillNode 的 enabled_skills 参数被限定为该技能),然后按标准 prompt 流程运行。
MCP 生命周期管理¶
MCP(Model Context Protocol)工具通过 MCPToolProvider 动态接入。load_mcp_status() 封装了 provider 的初始化和工具集合并逻辑——成功加载的 MCP 工具以 mcp: 前缀注入 diagnostics,失败的以 mcp:error: 记录。
/mcp reload 命令触发 _reload_mcp(),调用 runner.reload_mcp() → runner.rebuild() 完成工具重载和 agent 重建。/mcp(无参数)显示当前 MCP 状态:配置文件路径、已连接服务器列表、已注册工具列表。
转录与导出¶
转录系统围绕"结构化内容部件"模型构建,定义了 5 种部件类型:
| 部件类型 | 结构 | 用途 |
|---|---|---|
text |
{"type": "text", "text": "..."} |
纯文本消息 |
thinking |
{"type": "thinking", "thinking": "..."} |
模型思考过程 |
tool_use |
{"type": "tool_use", "name": "...", "input": ...} |
工具调用请求 |
tool_result |
{"type": "tool_result", "name": "...", "content": "...", "ok": bool} |
工具调用结果 |
system |
{"type": "system", "text": "..."} |
系统通知/审批提示 |
normalize_parts() 负责将各种格式(字符串、列表、遗留格式)统一为部件列表;normalize_message() 确保每条消息的 role 和 content 字段符合存储规范。Markdown 导出由 render_message_markdown() 逐条渲染后通过 export_transcript_markdown() 写入文件。
/copy 命令利用 transcript_to_text() 生成纯文本并通过 Textual 的 copy_to_clipboard() 写入系统剪贴板,/export 命令生成 Markdown 文件到 ~/.jianmu_tui/exports/。
Trace 可观测性¶
TraceSink 将每次运行的 versioned event envelope 以 JSONL 格式写入 ~/.jianmu_tui/traces/{session_id}_{run_id}.jsonl。submit/resume 使用不同的 TUI run id,resume 还记录 parent_run_id;trace-view 因而可以重建 run → step → model/tool/child span,未闭合 span 标记为 aborted。写盘边界会先统一 sanitize 事件 payload;默认情况下 llm.request.messages 只保存结构、计数和长度元数据,不保存完整 prompt 内容。usage_from_events() 聚合所有 model.call.completed 事件中的 token 用量,生成 UsageSnapshot。
/debug 或 /trace 命令展示当前会话的 trace 摘要,包括 session/model/usage/trace_path/mcp_servers/mcp_tools/tool_diagnostics 等诊断信息。无 UI 的 trace-view 可以从同一 JSONL 文件渲染紧凑 waterfall。设置 JIANMU_TUI_INSPECTOR=1 会启用 normalized request capture,并在 TUI 进程内启动仅绑定 loopback 的实时 SSE viewer(默认端口 8765,可用 JIANMU_TUI_INSPECTOR_PORT 修改);jianmu-tui inspect 仍可脱离 Textual 查看已脱敏导出。inspector/broadcast 错误只做 best-effort 降级,不会终止 Agent 主路径。
上下文、审批与并行 Explore Task¶
TUIAgentFactory 会根据 session 的 workspace root 注入 PromptRuntimeContext,因此 AGENTS.md 等 workspace bootstrap 文件通过 Jianmu 既有 prompt builder 解析。TUI 自有 hooks 都以 HookFailurePolicy.IGNORE 注册,并在 callback 内提供本地 fallback,覆盖 tool observation projection、外部文件变更提示、非破坏性 working-view compaction 和可选 normalized request capture。手动与自动压缩都通过独立的无工具、非流式内部模型调用总结 middle,失败时降级为确定性摘要。active summary 和消息边界随 session 持久化,而 UI transcript、canonical runtime history、导出和 checkpoint 仍保留完整原消息。
审批仍由 Core ApprovalManager 管理,但 TUI callback 增加 request-scoped one-shot decision 和 workspace-scoped exact persistent grant,持久规则保存在 ~/.jianmu_tui/permissions.sqlite。审批栏展示完整格式化参数、命中 rule/risk 信息(若存在),并提供仅本次允许、对当前 workspace 永久允许同一精确动作、拒绝三种选择。持久规则可通过 /permissions 列出或撤销。
Core ToolExecutor 现在也支持原生 multi-tool batching:每次具体调用得到带原因的 ParallelDecision,相邻安全调用并发执行,不安全调用形成串行 batch;完成事件按真实 wall-clock 顺序发出,canonical tool observation 仍按原始 tool-call 顺序提交。shell、文件写入和持久 REPL 默认串行,参数感知工具可以显式放行只读调用。统一 TUI 工具集包含 parallel_tasks,用于显式 fan-out 多个只读 child exploration。
对于 parallel_tasks,Trace 和 Inspector 事件流会在子任务执行前通过 tui.child_task.scheduling 报告调度决策,其中包括请求与实际并发度、调度模式以及 Provider 并发限制。每个子任务随后依次发出 tui.child_task.started,再发出 tui.child_task.completed、tui.child_task.failed 或 tui.child_task.cancelled。失败子任务会作为该子任务的失败结果返回,不会被静默地显示为成功完成。
与框架层的关系¶
大多数产品行为仍留在 TUI 层。原生 multi-tool 并发是有意开放的窄 Core 例外:Core 只提供 ParallelDecision 和相邻 concurrent/sequential batch,审批、压缩、inspector 与 UI policy 仍在 apps/tui_chat/。与框架层的关键对接点:
| 框架层模块 | 对接方式 | TUI 适配层 |
|---|---|---|
jianmu.Agent |
构造 + run_until_suspend() |
TUIAgentFactory / TUIAgentRunner |
jianmu.engine.events.RuntimeEventBus |
subscribe(projector.apply_event) |
TUIRuntimeProjector |
jianmu.engine.interaction.RunResult |
apply_result() 投影 |
TUIRuntimeProjector |
jianmu.engine.state.StateManager |
共享 ChatState 实例 |
TUIAgentBundle |
jianmu.engine.deps.PromptRuntimeContext |
workspace bootstrap 与 skills context | apps.tui_chat.context |
jianmu.hooks.HookManager |
TUI 自有 best-effort transform hooks | apps.tui_chat.context / inspector capture |
jianmu.guard.approval.ApprovalManager |
自定义 callback | ApprovalCoordinator |
jianmu.node.SkillNode |
配置构造 | TUIAgentFactory |
jianmu.model.ModelClient |
resolve() 工厂方法 |
TUIAgentFactory._resolve_model_client() |
jianmu.tool.builtin.* |
直接实例化 | ToolAssembly / build_builtin_toolset() |
jianmu.mcp.provider.MCPToolProvider |
initialize() + ToolSet.from_providers() |
build_tool_profile() |
这种"薄壳 + 厚适配层"架构使得 TUI 应用可以独立演进而不阻塞框架层发展,同时框架层的任何 API 变更只影响明确的适配点。
阅读指引¶
本文档覆盖了 TUI Chat 的完整架构与实现细节。建议按以下路径继续深入: