终端 AI Agent 经常同时面对两种输出场景:一种是持续写入标准输出的流式响应,另一种是带状态栏、消息列表和交互控件的 TUI。covo-agent v0.1.0 的关键变化,是让这两种界面使用同一套渲染引擎。这个调整看似属于界面层,实际上会直接影响 Markdown 展示、增量更新、工具调用状态和后续功能演进的一致性。
covo-agent 基于 covonaut 框架开发,以 Go 编写,要求 Go 1.25.0 起,并采用 AGPL-3.0 协议开源。它提供 general 与 code 两种工作模式,覆盖日常知识工作、软件开发、自动化、持久上下文管理以及与外部系统协作等场景。
为什么统一渲染链路值得单独关注
流式终端与 TUI 的外观不同,但它们消费的上游事件通常高度相似:模型生成文本片段、开始或结束工具调用、报告错误,以及完成当前回合。如果两套界面各自解释这些事件,很容易产生行为分叉。
例如,同一段包含代码块的 Markdown,可能在流式模式中边接收边打印,在 TUI 中却要先写入消息模型再重新布局。随着功能增加,两边还要分别处理 ANSI 样式、宽字符、窗口宽度变化、工具执行状态和取消信号。结果往往是一个修复需要实现两遍,而且表现仍不完全一致。
统一渲染引擎后,可以把链路拆成三个稳定部分:
- Agent 和工具层只产生结构化事件,不直接操作终端。
- 渲染器把事件归并为统一的可展示状态。
- 流式输出与 TUI 仅负责把该状态投射到各自的终端表面。
这意味着两种前端可以共享 Markdown 解析、样式规则和事件语义。TUI 仍然能够执行局部刷新,普通流式模式也仍然可以顺序写入标准输出,但二者不必各自维护一套内容解释逻辑。
general 与 code 模式共享的底层能力
general 和 code 面向不同任务,但不应该因此变成两套互不相干的程序。
general 模式更适合资料整理、知识问答、任务分解和跨系统协作;code 模式则更关注代码阅读、文件修改、命令执行与开发流程。两者真正需要区分的是提示词、工具权限和工作流策略,而不是终端渲染、上下文存储或事件传输方式。
统一渲染链路尤其适合显示工具调用。无论 Agent 正在检索外部系统,还是在代码仓库中执行测试,界面都可以采用相同的状态模型:
queued -> running -> succeeded
-> failed
-> cancelled
如果这些状态由结构化事件驱动,普通终端可以追加一行执行结果,TUI 则可以原地更新对应任务。展示方式不同,状态含义保持一致。
持久上下文也能从这种边界划分中受益。上下文层保存消息与工具结果,渲染层决定哪些内容需要展开、折叠或着色。避免把 ANSI 控制字符和界面布局写入历史记录,后续才能可靠地恢复会话,或把同一段上下文交给其他客户端使用。
可以这样实践:用 Go 搭一个双前端渲染原型
下面是一个独立、可运行的最小示例,用来演示“同一组事件、同一个渲染器、两种终端输出适配器”。它不是 covo-agent 的真实内部 API,而是根据这次更新体现的架构方向构造的实践示例。
创建 main.go:
package main
import (
"fmt"
"os"
"strings"
"time"
)
type EventKind string
const (
TextDelta EventKind = "text_delta"
ToolStart EventKind = "tool_start"
ToolEnd EventKind = "tool_end"
Done EventKind = "done"
)
type Event struct {
Kind EventKind
Text string
}
type View struct {
Answer string
Status string
Done bool
}
type Renderer struct {
view View
}
func (r *Renderer) Apply(event Event) View {
switch event.Kind {
case TextDelta:
r.view.Answer += event.Text
case ToolStart:
r.view.Status = "running: " + event.Text
case ToolEnd:
r.view.Status = "finished: " + event.Text
case Done:
r.view.Done = true
}
return r.view
}
type Surface interface {
Draw(View)
}
type StreamSurface struct {
printed int
}
func (s *StreamSurface) Draw(view View) {
if len(view.Answer) > s.printed {
fmt.Print(view.Answer[s.printed:])
s.printed = len(view.Answer)
}
if view.Done {
fmt.Printf("\n[%s]\n", view.Status)
}
}
type TUISurface struct{}
func (TUISurface) Draw(view View) {
fmt.Print("\033[2J\033[H")
fmt.Println("covo-style TUI demo")
fmt.Println(strings.Repeat("-", 32))
fmt.Println(view.Answer)
fmt.Println(strings.Repeat("-", 32))
fmt.Println("status:", view.Status)
}
func main() {
mode := "stream"
if len(os.Args) > 1 {
mode = os.Args[1]
}
var surface Surface = &StreamSurface{}
if mode == "tui" {
surface = TUISurface{}
}
events := []Event{
{Kind: ToolStart, Text: "inspect repository"},
{Kind: TextDelta, Text: "Unified "},
{Kind: TextDelta, Text: "rendering "},
{Kind: TextDelta, Text: "keeps both interfaces consistent."},
{Kind: ToolEnd, Text: "inspect repository"},
{Kind: Done},
}
renderer := &Renderer{}
for _, event := range events {
surface.Draw(renderer.Apply(event))
time.Sleep(250 * time.Millisecond)
}
}
使用 Go 1.25 或更高版本运行:
go run main.go stream
go run main.go tui
两个命令处理完全相同的事件序列。stream 只输出新增文本,tui 则清屏并绘制完整状态;内容归并规则都位于 Renderer.Apply 中。实际项目可以继续把 Event 扩展为 Markdown 块、工具参数、执行结果、确认请求和错误信息,但应避免让渲染器直接调用模型或执行工具。
接入真实 Agent 时要守住的边界
统一引擎不等于把所有逻辑塞进一个大型渲染函数。较稳妥的实现应明确区分事件、状态和终端能力。
事件需要携带稳定标识。工具调用可能并发执行,仅凭工具名称无法可靠更新对应状态;生产实现通常还需要调用 ID、消息 ID和时间戳。文本增量也应说明它属于哪条消息,避免多个并行响应互相拼接。
终端能力必须显式处理。非交互式管道、CI 日志和重定向文件通常不适合 ANSI 清屏或光标移动,因此应该根据输出目标选择流式表面。例如可以这样检查输出是否连接终端:
if test -t 1; then
echo "interactive terminal"
else
echo "plain output or redirected stream"
fi
同时要考虑 Unicode 宽度、窗口缩放、超长代码行和未闭合 Markdown 代码块。流式内容可能在任意字节片段处到达,渲染层不能假定每次增量都是完整句子,甚至不能草率地假定它一定构成完整 UTF-8 字符。
工具输出还涉及安全边界。code 模式能够触及文件和命令时,应把权限确认、执行状态和最终结果建模为事件,但不能让渲染层替代权限控制。界面显示“等待确认”并不代表后端已经阻止执行,两者必须分别落实。
升级与采用建议
评估 covo-agent v0.1.0 时,可以重点验证以下事项:
- 同一回答在流式输出与 TUI 中是否具有一致的 Markdown 和代码块表现。
- 工具调用开始、完成、失败和取消时,两种界面是否呈现相同语义。
- 输出重定向到文件或 CI 后,是否不会混入清屏和光标控制序列。
- 持久化上下文中是否只保存结构化内容,而非终端样式。
general与code模式的工具权限是否彼此隔离并可审计。- 部署环境是否满足 Go 1.25.0 起的工具链要求,并接受 AGPL-3.0 带来的分发与网络服务合规义务。
v0.1.0 仍是早期版本,接口和行为可能继续变化,不宜仅凭版本号判断生产成熟度。更合理的采用方式是先在可回滚的内部任务中验证终端兼容性、上下文恢复和工具权限。统一渲染链路为后续演进建立了清晰基础,但真正决定 Agent 是否可靠的,仍是事件模型、执行隔离和错误恢复能否经受实际工作流的压力。