- 流式输出: SSE 逐 token 接收, \\n\n\ 段落缓冲后 mdprint 彩色渲染 - 日志系统: charmbracelet/log v2 双写(stderr + log.yml), yunshu log 命令 - 会议室架构: dialog(main) + weather/profile/note(sub) 多 Agent 编排 - 泛型工具注册: NewTool[T] 反射推导 JSON Schema, 类型安全 - 安全加固: safeMemoryPath 三段校验(EvalSymlinks+Rel), maxToolCalls=2 - 性能优化: sync.Once 延迟加载, note 一步完成, obs/summary 合并 - Prompt 适配: 流式输出原则(先调工具不说话), 单 Agent 查询跳过 obs+summary - 文档: AGENTS.md + architecture.md + changelog.md 全部同步至 v2.3.0
20 KiB
云枢·Agent 版本变更日志
坐看云卷云舒,静听花开花落
[2.3.0] - 2026-05-16
日志系统 + 性能优化 + 安全加固
日志系统
引入 charmbracelet/log v2 作为结构化日志库,取代零散的 log.Printf / fmt.Fprintln(os.Stderr, ...):
| 组件 | 说明 |
|---|---|
logger.go |
charmbracelet/log 全局实例,输出到 stderr |
log.go |
log.yml 持久化(YAML 序列追加)、双写 wrapper、yunshu log 命令 |
日志流向:
warnLog/errorLog/infoLog
├─ logToStderr ? → charmbracelet/log → stderr(交互模式默认关闭,/log on 开启)
└─ 始终写入 → appendLog → log.yml(YAML 序列,位于 ~/.config/yunshu/log.yml)
新增日志点:
[INFO] task(weather) 开始
[INFO] 子 Agent 开始 agent=weather
[INFO] LLM 调用完成 tokens=1420 duration=3.2s
[INFO] task(weather) 完成
[WARN] 解析缓存失败 agent=weather err=...
yunshu log 子命令:
yunshu log → 全量显示(时间倒序)
yunshu log --top N → 只看最后 N 条
yunshu log --level warn → 过滤级别
yunshu log --clear → 清空
yunshu log --watch → 监听模式(2s 轮询)
流式输出
LLM 响应改为 SSE 流式输出,按 \n\n 段落边界缓冲后经 mdprint 渲染到 stdout:
| 组件 | 文件 | 说明 |
|---|---|---|
| SSE 类型 | llm.go |
sseChunk/sseDelta/sseToolCallDelta 等 |
CallLLMStream |
llm.go |
请求加 stream: true,bufio.Reader 逐行解析 |
| 段落缓冲 | llm.go |
blockBuf + tryFlushBlocks:检测最后一个 \n\n,完成块过 mdprint,残段续传 |
| 流结束刷残段 | llm.go |
末尾 mdprint.Print(blockBuf) 兜底 |
| 重建响应 | llm.go |
tool_call 按 index 累积 + SSE 碎片合并 |
关键设计:
LLM SSE → blockBuf += content
tryFlushBlocks:
├─ 有 \n\n → 之前的部分 mdprint.Print(complete)
│ → 剩余部分留在 blockBuf
└─ 无 \n\n → stay
流结束 → mdprint.Print(blockBuf)
Prompt 适配(agents/dialog-agent.md):
- 新增"流式输出原则":先调工具,不要先说话
- 调度表
回应 → task(profile)改为静默调 task,拿到结果后再回应
性能优化
| 优化 | 改动 | 效果 |
|---|---|---|
| note-sub 一步完成 | prompt 收紧:read→write→立即返回 | note 保存从 ~50s 降至 ~10s |
| dialog 合并 obs+summary | 单 Agent 查询跳过观察和摘要;综合查询合并为一轮 | 节省 ~30s |
| maxToolCalls=2 | RunSubAgent 超限兜底 | 防止死循环 |
代码加固
| 改动 | 文件 | 说明 |
|---|---|---|
| LLM 延迟加载 | llm.go |
init() → sync.Once,--help 不再读 config |
| 路径安全检查 | tool.go |
safeMemoryPath():Clean + EvalSymlinks + Rel 三段校验 |
| 静默错误 → warnLog | 8 处 across runtime/tool/registry | 所有 json.Unmarshal / yaml.Unmarshal 吞掉的错误改为结构化日志 |
| 对话记忆修剪 | runtime.go |
LoadSession 只返回最近 40 条消息 |
| Onboard 补齐 | onboard.go |
ensureUserConfig() 创建 user.md/soul.md/notes/ |
| 热加载 | main.go |
交互模式每轮 ScanAgents(),新增 agent 即时生效 |
UX 改进
- 交互模式默认不显示日志(
/log on开启) --help移除环境变量章节,增加log命令说明- 单次查询模式保留日志显示
[2.2.0] - 2026-05-16
存储重组:session 目录 + .yml 统一后缀
目录结构调整
| 旧路径 | 新路径 | 原因 |
|---|---|---|
session.json |
session/session.json |
对话会话文件归入 session 目录 |
context/dialog.yaml |
session/dialog.yml |
context 语义模糊,与 session 合并;.yml 后缀 |
config.yaml |
config.yml |
统一后缀 |
log.yaml |
log.yml |
统一后缀 |
context/ |
已删除 | 文件移至 session/ |
所有 yaml 文件统一使用 .yml 后缀。配置文件、对话摘要、日志文件全部一致性调整。
代码改动
| 文件 | 改动 |
|---|---|
runtime.go |
sessionPath() 改为 session/session.json |
config.go |
LoadConfig/SaveConfig 读写 config.yml |
onboard.go |
提示信息改为 config.yml |
main.go |
新增 migrateFilePaths() 处理所有旧路径迁移 |
tool.go |
描述字符串更新 context/→session/,.yaml→.yml |
agents/dialog-agent.md |
所有 context/dialog.yaml→session/dialog.yml |
docs/ |
存储树、写入策略、编码规范全部同步更新 |
迁移逻辑
migrateFilePaths() 在启动时自动处理:
config.yaml→ 复制到config.yml,删除旧文件session.json→ 复制到session/session.json,删除旧文件context/dialog.yaml→ 复制到session/dialog.yml,删除旧文件及空目录log.yaml→ 复制到log.yml,删除旧文件
所有迁移仅在目标文件不存在时执行,支持幂等。
[2.1.0] - 2026-05-16
用户画像 + 备忘录系统
拆分 memory.json
旧的 memory.json 一锅烩,现在拆成按用途分文件:
| 旧文件 | 新文件 | 格式 | 维护者 |
|---|---|---|---|
memory.json["personality"] |
config/soul.md |
Markdown | 用户手动编辑 |
memory.json["dialog_context"] |
context/dialog.yaml |
YAML | dialog 每轮写入 |
memory.json["agent_errors"] |
log.yaml |
YAML | 系统追加 |
| (不存在) | config/user.md |
Markdown | profile-sub 维护 |
| (不存在) | notes.md + notes/*.md |
Markdown | note-sub 维护 |
迁移逻辑在 main.go:migrateMemoryJSON(),启动时自动检测并迁移,确认完成后再删除 memory.json。
新增子 Agent
| Agent | 文件 | 用途 |
|---|---|---|
| profile-sub | agents/profile-sub.md |
从对话中提取用户画像,增量合并到 config/user.md |
| note-sub | agents/note-sub.md |
笔记管理。默认 notes.md 列表,复杂内容可存为 notes/{name}.md |
memory.read/write 工具改造
从旧的 flat JSON key-value 改为路径路由:
.md文件 → 全文覆写(memory.write("config/user.md", markdown_str)).yaml文件 → 合并写入(memory.write("context/dialog.yaml", {topic, last_agent}))- 目录 → 返回文件列表
- 安全检查:拦截
..遍历和绝对路径 - 目录自动创建
dialog-agent.md 更新
- 新增调度规则:检测用户透露个人信息 → 调
task("profile", ...)提取画像 - 新增调度规则:检测"帮我记住" → 调
task("note", ...)存备忘录 - 对话摘要改为写入
context/dialog.yaml,而非旧memory.json["dialog_context"] - 用户画像改为读取
config/user.md - 新增观察规则:每轮回复后向
## AI观察到段写入语气/情绪/性格观察
heading-aware merge
memory.write 对 .md 文件由全文覆写改为按 ## 标题合并:
memory.write("config/user.md", "## 画像\n...")只替换## 画像段,## AI观察到段不受影响- 同一 writer 可多次写入同一标题,每次覆盖该段内容
- 不同 writer 写不同标题,互不干扰
- 底层函数
mdMerge()按##拆分 → 标题匹配 → 重组
profile-sub.md 更新
- 改为写
## 画像段(不再写整篇 user.md) - 文档规范化:给出
## 画像格式示例 - 强调不破坏其他段(
## AI观察到等)
发布会:会议室架构核心引擎就绪
架构规划:会议室模式(阶段一全部完成,超计划交付)
核心引擎
| 组件 | 文件 | 说明 |
|---|---|---|
| Agent 注册中心 | registry.go |
ScanAgents() 按 type(main/sub) 分类,用户目录覆盖 |
| 子 Agent 隔离运行 | runtime.go: RunSubAgent |
隔离 LLM 循环,返回 ---RESULT---/---TEXT--- |
| 主 Agent 循环 | runtime.go: RunAgent |
session 持久化 + mdprint 渲染 |
| 缓存系统 | runtime.go: cache 辅助 |
SHA256[:6] 拼 key,惰性过期 |
| 工具目录生成 | catalog.go |
GenerateToolsYAML、BuildSubAgentPrompt |
新增工具
| 工具 | 注册方式 | 说明 |
|---|---|---|
task |
NewTool[TaskInput] |
调度子 Agent + 缓存管理 |
memory.read |
NewTool[MemoryReadInput] |
读 ~/.config/yunshu/memory.json |
memory.write |
NewTool[MemoryWriteInput] |
写长期记忆,value 支持任意 JSON 类型 |
实际子 Agent
| Agent | 文件 | 状态 |
|---|---|---|
| dialog-agent.md | agents/dialog-agent.md |
主持者(type:main)含多步骤编排指令 |
| weather-sub.md | agents/weather-sub.md |
天气子 Agent(type:sub),Markdown 排版 + 生活建议 |
✨ 计划外新增
泛型 + 反射工具注册(toolschema.go)
受 Charmbracelet/Fantasy 启发,引入 NewTool[T any]() 泛型构造函数:
- 输入结构体
json/description/enumtags → 自动反射生成 JSON Schema - 消除 ~120 行手写 Schema 模板代码(旧的
ToolParameter/ToolProperty类型已删除) - handler 内参数为类型安全的结构体字段,无需
args["x"].(string)类型断言 - 支持嵌套结构体、slice、map、基础类型、interface{} 类型
多步骤编排(runtime 改造)
移除 capturedOutput 覆写机制,子 Agent 结果作为普通工具响应留在对话上下文中:
- 主 Agent 可以连续多次调
task()(weather → train → hotel) - 每次返回后 LLM 继续推理,决定下一步
- 信息收集完毕再综合回答,不再被
capturedOutput截断 - 单步骤查询(如纯天气)行为不变,LLM 按 prompt 指令直接输出子 Agent 结果
其他变更
http-get.headers从 JSON 字符串改为map[string]string,LLM 直接传对象memory.write.value从string改为interface{},直接存储任意 JSON 值types.go:删除旧的ToolParameter/ToolProperty结构体,新增Schema(map[string]any)类型catalog.go:buildToolList适配新版 Schemaagents/dialog-agent.md:加入多步骤编排指令 + 数据传递说明
文档更新
docs/architecture.md:更新工具列表、核心流程、当前状态docs/AGENTS.md:工具注册规范更新为NewTool[T]方式docs/会议室架构计划书.md:添加实现状态标记、多步骤编排章节、泛型注册章节
架构规划:会议室模式
完成从单 Agent 到"会议室架构"的完整设计,核心变更:
新增角色体系:
type: main— 主持者(对话 Agent),唯一用户入口type: sub— 发言人(领域子 Agent),被task调才说话
新增工具(待实现):
task— 调度子 Agent + 缓存管理memory.read/memory.write— 长期记忆读写
新增 Cache 机制:
- 子 Agent Frontmatter 声明
cache.ttl+cache.keys task工具机械化拼 key、查/写缓存- 一个 Agent 一个缓存 JSON 文件,子 Agent 无感知
设计文档:
docs/会议室架构计划书.md— 完整架构方案docs/architecture.md— 更新后续演进章节docs/AGENTS.md— 更新 Agent 定义规范(type, cache 字段)docs/taolun.md— 追加 2026-05-11 讨论历史
MSN 天气接口更新:
- 新增
hourlyforecast端点文档 - 标记
weathertrends为已失效 - 更新
skills/msn-weather-api/SKILL.md和agents/weather-agent.md
技术细节
- Frontmatter 新增
type字段(main/sub) - Frontmatter 新增
cache字段({ttl: int, keys: [string]}) - 用户配置目录
~/.config/yunshu/下可选覆盖 agents/ - 详见
docs/会议室架构计划书.md
[1.1.0] - 2026-05-09
发布摘要
第二版发布。核心变化:Markdown 渲染器从"一行流"重构为 AST 架构,新增终端视觉系统(标题符号 + Monet 配色),Go 版本升级至 1.25,项目结构从 src/ 扁平目录重组为根目录 + pkg/ 子包架构。
新增:pkg/mdprint — Markdown → ANSI 渲染引擎
从头编写的 AST 架构渲染引擎,替代原来的"一行流"字符串匹配逻辑:
块级解析(parse.go):有限状态机逐行扫描,识别 7 种块级元素
| 类型 | 语法 | 解析策略 |
|---|---|---|
| Heading | # ~ ###### |
前缀匹配,记录级别 |
| CodeBlock | ``` fence |
状态切换,支持语言标识 |
| Blockquote | > 前缀 |
前缀剥离,递归解析 |
| List | - / * / 1. |
前缀匹配,自动编号检测 |
| Table | ` | ` 分隔 |
| ThematicBreak | --- 独占一行 |
精确匹配 |
| Paragraph | 默认兜底 | 连续非空文本块合并 |
行内解析(inline.go):递归下降扫描,支持 4 种行内元素嵌套
| 类型 | 语法 | 特性 |
|---|---|---|
| Bold | **text** |
可嵌套 Italic / Code / Link |
| Italic | *text* |
可嵌套 Bold / Code / Link |
| Code | `text` |
原始文本(内部不解析) |
| Link | [text](url) |
text 可嵌套 Bold / Italic |
ANSI 渲染(render.go):type switch 按节点类型分发,标题按级别分配颜色和符号
测试覆盖:19 个单元测试,覆盖所有块级/行内类型的正常、边界和嵌套场景
新增:标题视觉系统
用符号 ▪ / ▫ 替代 Markdown 原生 # 前缀,视觉效果更接近"标题"而非"标记":
| 级别 | 符号 | 字体 | 色彩(Monet 睡莲) | 色值 |
|---|---|---|---|---|
| H1 | ▪ |
加粗 | 雾蓝灰 | #6B8E9B |
| H2 | ▪ |
加粗 | 鼠尾草绿 | #89A894 |
| H3 | ▪ |
加粗 | 薄荷青 | #A6C0B5 |
| H4 | ▫ |
加粗 | 淡紫粉 | #C3B1BD |
| H5 | ▫ |
加粗 | 暖灰绿 | #7B8E8A |
| H6 | 无 | 加粗(Dim) | 浅灰 | 继承 |
排版规则:所有标题前插空行,H1 前后各插空行,--- 横线前后空行,响应与输入之间空行,输出末尾空行。
新增:pkg/style — 终端颜色样式库
在原有 8 色 ANSI 基础上新增 24-bit 真彩色支持:
Fg(color)/Bg(color):保留原有 8 色 APIFgHex("#RRGGBB")/BgHex("#RRGGBB"):新增真彩色,输出\033[38;2;R;G;Bm格式- 所有
.Render(text)使用统一占位板.codes []string,颜色和样式可组合 NO_COLOR/TERM=dumb环境变量自动禁用颜色
新增:pkg/termui — 终端输入组件
提供三个交互式输入函数,统一使用 bufio.NewReader(os.Stdin) + ensureLineMode() 解决 Windows 控制台输入问题:
| 函数 | 用途 | 特性 |
|---|---|---|
ReadLine() |
基础行输入 | 去除 \r\n |
TextInput() |
文本输入 | 支持默认值、必填校验、自定义验证器 |
PasswordInput() |
密码输入 | 输入后隐藏回显,用 * 遮盖 |
Confirm() |
确认提示 | 支持 Y/n / y/N 默认值 |
修复
- 行内解析器未闭合分隔符死循环:扫描
*/`分隔符时未处理文件末尾无闭合标记的情况,改为找到匹配闭合或到达末尾时终止 - 代码 fence 不识语言标识:
```go、```json等带语言的 fence 被当作文本行,改为行首```后允许非空白后缀 - Windows 控制台输入模式冲突:
bufio.Scanner在 Windows 控制台因ENABLE_WINDOW_INPUT标志导致Scan()永久阻塞。改为每轮输入前调用GetConsoleMode+SetConsoleMode确保模式为ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT,并使用bufio.NewReader.ReadString('\n')替代 Scanner - 尝试自实现 Tab 补全未成功(后放弃):raw mode +
ReadConsoleInputW方案遇到光标位置计算不一致问题,最终回退到简单ReadLine()。后续如需补全功能引入第三方库
变更
- 项目结构重组:
src/目录删除,全部.go文件移至根目录;新增pkg/子包,按职责分离为mdprint/、style/、termui/ - 模块名修改:
yunshu→hub.gaomia.site/titor/YunShu - Go 版本升级:
1.21→1.25.0 Completer类型和ReadLineWithCompletion函数移除:completer.go清空,相关测试删除.gitignore修复:yunshu.exe→yunshu.exe*(避免调试版本遗漏)
技术栈
- 语言:Go 1.25.0
- 依赖:仅
gopkg.in/yaml.v3 - 默认 LLM:豆包(火山引擎)
doubao-seed-2-0-pro-260215 - 数据源:MSN 天气非公开 API(
assets.msn.cn) - 运行平台:Windows 10+(基于 kernel32 控制台 API,ANSI VT 处理)
- 输入编码:UTF-8(通过
SetConsoleOutputCP(65001)设置控制台代码页)
[1.0.0] - 2026-05-08
发布说明
第一个稳定发布版本。云枢·Agent 作为可独立运行的 Agent 系统,支持通过 LLM + 工具注册表 + 外挂 Agent 定义实现自然语言驱动的天气查询。
功能
- 三层分离架构:Agent Skill(行为)↔ 普通 Skill(知识)↔ Tool(确定性执行)
- 外挂 Agent 定义:
.md文件即 Agent,YAML frontmatter + Markdown body - 4 个内置工具:
http-get、skill、read-file、geocode - Session 会话管理:
~/.config/yunshu/session.json持久化对话历史 - 交互模式 + 单次查询双模式运行
onboard交互式初始化向导- 双路径搜索:项目目录优先,
~/.config/yunshu/后备 - 旧配置自动迁移:
~/.config/weather-cli/→~/.config/yunshu/ - LLM 配置:支持配置文件 + 环境变量双重配置,兼容 OpenAI Chat Completion API
技术栈
- 语言:Go 1.21
- 依赖:仅
gopkg.in/yaml.v3 - 默认 LLM:豆包(火山引擎)
doubao-seed-2-0-pro-260215 - 数据源:MSN 天气非公开 API(
assets.msn.cn)
[1.0.0-rc.1] - 2026-05-07
重大变更
- 项目更名:weather-cia → 云枢·Agent(英文名 YunShu / yunshu)
- 配置目录迁移:
~/.config/weather-cli/→~/.config/yunshu/(自动迁移) - 二进制名称改为
yunshu
[0.3.0] - 2026-05-07
新增
geocode工具:通过 wttr.in 查询城市坐标,支持中文和英文城市名skills/geocoding/SKILL.md:地理编码验证规则(同名城市检测、国家核对)- 架构分离:agent skill 只放行为,普通 skill 只放知识,tool 负责确定性执行
变更
agents/weather-agent.md精简为纯行为定义(去掉所有 MSN API 内联细节,改为按需加载 skill)- 城市定位方式:从静态 cities.json 查表 → 调用
geocode工具实时查询 agents/weather-agent.mdtools 新增geocode- session 文件从项目目录移至
~/.config/weather-cli/session.json
[0.2.0] - 2026-05-07
新增
onboard子命令:交互式初始化向导,引导用户配置 LLM 连接信息- 全局配置文件
~/.config/weather-cli/config.yaml,存储 LLM host/model/key - 双路径搜索机制:项目目录优先,
~/.config/weather-cli/后备 - 首次运行检测:未配置时提示用户运行
weather-cia onboard
变更
- 项目重命名为
weather-cia - 配置加载改为:配置文件 → 环境变量(环境变量优先级更高)
- Agent/skill 搜索路径扩展:项目目录 → 全局配置目录
onboard自动复制默认 agents/skills/data 到全局配置目录
[0.1.0] - 2026-05-07
新增
- 项目初始化,基于 Go 实现的轻量级 agent 框架
- 核心架构:.md 文件定义 agent 行为,代码只负责加载和执行
- 工具系统:声明式注册(http-get, skill, read-file)
- Session 会话管理:session.json 记录对话历史,支持上下文追问
- 天气情报官 agent(weather-agent.md):通过 MSN 天气 API 查询实时天气和预报
- MSN 天气 API Skill(msn-weather-api/SKILL.md):API 知识按需加载
- 内置 42 个中国城市经纬度数据库(data/cities.json)
- 支持单次查询和交互模式两种运行方式
- 默认集成豆包(火山引擎)LLM,通过环境变量可切换
技术细节
- 语言:Go 1.21
- 依赖:仅 gopkg.in/yaml.v3(用于解析 frontmatter)
- API 兼容 OpenAI Chat Completion 格式
- 环境变量:
OPENAI_API_KEY(必填)、LLM_ENDPOINT、LLM_MODEL