Skip to content

Commit a69dc32

Browse files
xgopilotpionxe
andcommitted
merge: resolve conflicts with origin/main and keep tui runtime contract boundaries
Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: pionxe <148670367+pionxe@users.noreply.github.com>
2 parents 4473628 + 1c4a2cb commit a69dc32

137 files changed

Lines changed: 9589 additions & 3422 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ Gateway 转发与自动拉起说明:
116116
- `/memo`:查看记忆索引
117117
- `/remember <text>`:保存记忆
118118
- `/forget <keyword>`:按关键词删除记忆
119+
- `/skills`:查看当前可用 skills(含当前会话激活标记)
120+
- `/skill use <id>`:在当前会话启用 skill
121+
- `/skill off <id>`:在当前会话停用 skill
122+
- `/skill active`:查看当前会话已激活 skills
119123
- `& <command>`:在当前工作区执行本地命令
120124

121125
示例输入:
@@ -152,6 +156,7 @@ Gateway 转发与自动拉起说明:
152156
- [Session 持久化设计](docs/session-persistence-design.md)
153157
- [Context Compact 说明](docs/context-compact.md)
154158
- [Tools 与 TUI 集成](docs/tools-and-tui-integration.md)
159+
- [Skills 设计与使用](docs/skills-system-design.md)
155160
- [MCP 配置指南](docs/guides/mcp-configuration.md)
156161
- [更新与升级](docs/guides/update.md)
157162

docs/context-compact.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ context:
2525
compact:
2626
manual_strategy: keep_recent
2727
manual_keep_recent_messages: 10
28+
micro_compact_retained_tool_spans: 6
2829
read_time_max_message_spans: 24
2930
max_summary_chars: 1200
3031
micro_compact_disabled: false
@@ -41,6 +42,8 @@ context:
4142
在 `keep_recent` 模式下保留最近消息数量,并按 tool call 与 tool result 的原子块整体保留。
4243
- `read_time_max_message_spans`
4344
控制 `context.Builder` 读时 trim 可保留的 message span 上限;该值越大,普通“继续”续跑时越不容易在未触发 compact 前丢掉较早的文件读取结果。
45+
- `micro_compact_retained_tool_spans`
46+
控制 read-time micro compact 默认保留原始内容的最近可压缩工具块数量;默认值为 `6`,显式配置为更小值时可更积极地回收旧工具结果。
4447
- `max_summary_chars`
4548
控制 compact summary 的最大字符数。
4649
- `micro_compact_disabled`
@@ -61,6 +64,7 @@ context:
6164

6265
新增工具时,micro compact 策略不再由 `context` 层静态白名单维护,而是由 `internal/tools` 中的工具实现声明。
6366
默认情况下,已注册工具都会参与 micro compact;只有显式声明保留历史结果的工具才会跳过旧结果清理。
67+
默认 pin 仅对 `filesystem_write_file` 与 `filesystem_edit` 这类文件内容修改工具生效,用于保留 `README`、spec/schema、`go.mod`、`package.json` 等关键产物的最近结果;`.env*` 不参与默认 pin,避免敏感内容在上下文中滞留更久。
6468
但 micro compact 只有在当前会话已经建立非空 `TaskState` 时才会生效;没有 durable task state 时,context 仅做 trim,不清理旧 tool result。
6569

6670
## 执行链路

docs/guides/configuration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ context:
5656
compact:
5757
manual_strategy: keep_recent
5858
manual_keep_recent_messages: 10
59+
micro_compact_retained_tool_spans: 6
5960
read_time_max_message_spans: 24
6061
max_summary_chars: 1200
6162
micro_compact_disabled: false
@@ -81,6 +82,7 @@ context:
8182
|------|------|
8283
| `context.compact.manual_strategy` | `/compact` 手动压缩策略,支持 `keep_recent` / `full_replace` |
8384
| `context.compact.manual_keep_recent_messages` | `keep_recent` 策略下保留的最近消息数 |
85+
| `context.compact.micro_compact_retained_tool_spans` | read-time micro compact 默认保留原始内容的最近可压缩工具块数量,默认 `6` |
8486
| `context.compact.read_time_max_message_spans` | context 读时保留的 message span 上限,用于降低“继续”时较早文件读取结果被过早裁掉的风险 |
8587
| `context.compact.max_summary_chars` | compact summary 最大字符数 |
8688
| `context.compact.micro_compact_disabled` | 是否关闭默认启用的 micro compact |
@@ -89,6 +91,8 @@ context:
8991
| `context.auto_compact.reserve_tokens` | 自动阈值推导时预留 token 缓冲(`resolved_threshold = context_window - reserve_tokens`) |
9092
| `context.auto_compact.fallback_input_token_threshold` | 自动推导失败时使用的保底阈值 |
9193

94+
默认 pin 仅对 `filesystem_write_file` 与 `filesystem_edit` 这类文件修改工具生效,用于保留关键产物文件的最近结果;`.env*` 不参与默认 pin,避免敏感内容在上下文中保留更久。
95+
9296
### `runtime` 字段
9397

9498
| 字段 | 说明 |

docs/runtime-provider-event-flow.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
- `permission_requested`
1919
- `permission_resolved`
2020
- `token_usage`
21+
- `skill_activated`
22+
- `skill_deactivated`
23+
- `skill_missing`
2124
- `compact_start`
2225
- `compact_applied`
2326
- `compact_error`

docs/skills-system-design.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Skills 设计与使用说明
2+
3+
## 1. 目标与定位
4+
Skills 是 NeoCode 的“能力提示层”,用于给模型提供任务约束、参考资料和工具偏好,不是新的执行层。
5+
6+
主链路保持不变:
7+
8+
`TUI -> Runtime -> Provider / Tool Manager -> Security -> Executor`
9+
10+
Skills 只影响:
11+
- Context 注入内容
12+
- 工具暴露顺序(提示优先级)
13+
14+
Skills 不影响:
15+
- 工具是否真正可执行
16+
- 权限 ask/deny/allow 决策
17+
- MCP 注册与权限链路
18+
19+
## 2. 发现机制(Discovery)
20+
当前本地发现路径:
21+
- `~/.neocode/skills/`
22+
23+
加载规则:
24+
- 扫描 root 下的子目录(忽略隐藏目录)
25+
- 每个 skill 目录要求存在 `SKILL.md`
26+
- 也支持 root 目录直接放置一个 `SKILL.md`
27+
- 缺失文件、无效 metadata、空内容会记录为 `LoadIssue`,不阻塞其它 skill 加载
28+
29+
## 3. 加载机制(Loader + Registry)
30+
核心模块:
31+
- `internal/skills/loader.go`:本地扫描与解析
32+
- `internal/skills/registry.go`:内存索引、查询与刷新
33+
- `internal/skills/filter.go`:按 source/scope/workspace 过滤
34+
35+
关键约束:
36+
- `SKILL.md` 单文件读取有大小上限(默认 1 MiB)
37+
- 前置 metadata 和正文解析后统一归一化
38+
- skill id 去重冲突时 fail-closed(冲突项不进入可用列表)
39+
40+
## 4. skill 文件结构(建议)
41+
`SKILL.md` 支持 frontmatter + 正文 section:
42+
43+
```md
44+
---
45+
id: go-review
46+
name: Go Review
47+
description: Go 代码审查助手
48+
version: v1
49+
scope: session
50+
source: local
51+
tool_hints:
52+
- filesystem_read_file
53+
- filesystem_grep
54+
---
55+
56+
## Instruction
57+
优先做静态阅读,再给出可执行修改建议。
58+
59+
## References
60+
- [代码规范](./guides/go-style.md)
61+
62+
## Examples
63+
- 先总结问题,再给补丁
64+
65+
## ToolHints
66+
- filesystem_read_file
67+
- filesystem_grep
68+
```
69+
70+
## 5. 激活与会话模型
71+
Runtime 提供会话级接口:
72+
- `ActivateSessionSkill(session_id, skill_id)`
73+
- `DeactivateSessionSkill(session_id, skill_id)`
74+
- `ListSessionSkills(session_id)`
75+
- `ListAvailableSkills(session_id)`
76+
77+
TUI 入口:
78+
- `/skills`
79+
- `/skill use <id>`
80+
- `/skill off <id>`
81+
- `/skill active`
82+
83+
说明:
84+
- `use/off/active` 需要当前有 active session
85+
- session 重载后会恢复 `activated_skills` 状态
86+
- skill 在 registry 中缺失时,会标记为 missing 并发出事件
87+
88+
## 6. 模型如何使用 skill
89+
Runtime 在每轮 context 构建时把激活 skills 注入 `Skills` section,内容包含:
90+
- instruction
91+
- tool_hints(裁剪)
92+
- references(裁剪)
93+
- examples(裁剪)
94+
95+
模型预期行为:
96+
- 把 skill 当成策略与工作流提示
97+
- 只调用当前真实暴露的工具 schema
98+
- 通过正常工具调用链路执行,不跳过权限层
99+
100+
## 7. Tools / Security / MCP 边界
101+
Skills 与安全边界的约束:
102+
- skill 不能注入未注册工具
103+
- skill 不能变成权限 allowlist
104+
- skill 不能绕过 `PermissionEngine` 的 ask/deny/allow
105+
- MCP 工具仍经过统一 registry + exposure filter + permission 检查
106+
107+
当前实现中,`tool_hints` 仅用于对已暴露工具做排序优先级调整,不会新增工具,也不会改变权限决策。
108+
109+
## 8. 可观测事件
110+
Runtime 会发出以下 skills 事件(供 TUI/日志调试):
111+
- `skill_activated`
112+
- `skill_deactivated`
113+
- `skill_missing`
114+
115+
## 9. 兼容与扩展
116+
当前 focus 是本地 skills;后续如需引入 remote source / marketplace,可在 `Loader``Registry` 层扩展,不需要改动 runtime 主执行链路。

docs/tools-and-tui-integration.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
- TUI 的 `/memo``/remember``/forget` 等 Slash Command 不再直接依赖 memo service,而是通过 `Runtime.ExecuteSystemTool` 统一入口触发系统工具执行,保证 UI 与 memo 逻辑解耦。
3131
- TUI 不会展示后台自动提取的中间状态。
3232

33+
## Skills 能力集成
34+
- Skills 由 `internal/skills` 统一发现、加载和注册;TUI 不直接读取 `SKILL.md` 文件。
35+
- TUI 通过 runtime 接口管理会话激活状态:`/skills``/skill use <id>``/skill off <id>``/skill active`
36+
- Skills 只影响提示注入与工具排序优先级,不改变工具执行入口;真实调用仍走 `Runtime -> Tool Manager -> Security -> Executor`
37+
- Skills 不提供权限豁免;命中 ask/deny 规则时行为与未启用 skill 保持一致。
38+
3339
## TUI 集成方式
3440
- 本地配置操作统一通过 Slash Command 完成,例如 Base URL、API Key 和模型选择
3541
- runtime 事件以内联形式渲染到 transcript 中,而不是单独拆出控制台面板

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ require (
1111
github.com/charmbracelet/glamour v1.0.0
1212
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
1313
github.com/creativeprojects/go-selfupdate v1.5.2
14-
github.com/openai/openai-go/v3 v3.30.0
14+
github.com/openai/openai-go/v3 v3.32.0
1515
github.com/prometheus/client_golang v1.23.2
1616
github.com/spf13/cobra v1.10.2
1717
github.com/spf13/viper v1.21.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
184184
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
185185
github.com/openai/openai-go/v3 v3.30.0 h1:T8VkhqAm6BuvxwpVG+Aw+H4TcYIsbj9nqytjpWcE/aU=
186186
github.com/openai/openai-go/v3 v3.30.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
187+
github.com/openai/openai-go/v3 v3.32.0 h1:aHp/3wkX1W6jB8zTtf9xV0aK0qPFSVDqS7AHmlJ4hXs=
188+
github.com/openai/openai-go/v3 v3.32.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
187189
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
188190
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
189191
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

internal/app/bootstrap.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"neo-code/internal/tools/filesystem"
2727
"neo-code/internal/tools/mcp"
2828
memotool "neo-code/internal/tools/memo"
29+
"neo-code/internal/tools/spawnsubagent"
2930
"neo-code/internal/tools/todo"
3031
"neo-code/internal/tools/webfetch"
3132
"neo-code/internal/tui"
@@ -349,6 +350,7 @@ func buildToolRegistry(cfg config.Config) (*tools.Registry, func() error, error)
349350
SupportedContentTypes: cfg.Tools.WebFetch.SupportedContentTypes,
350351
}))
351352
toolRegistry.Register(todo.New())
353+
toolRegistry.Register(spawnsubagent.New())
352354
mcpRegistry, err := buildMCPRegistry(cfg)
353355
if err != nil {
354356
return nil, nil, err

internal/app/bootstrap_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,43 @@ func TestBuildToolRegistryUsesWebFetchConfig(t *testing.T) {
181181
}
182182
}
183183

184+
func TestBuildToolRegistryRegistersSpawnSubAgent(t *testing.T) {
185+
t.Parallel()
186+
187+
cfg := config.StaticDefaults().Clone()
188+
cfg.Workdir = t.TempDir()
189+
190+
registry, cleanup, err := buildToolRegistry(cfg)
191+
if err != nil {
192+
t.Fatalf("buildToolRegistry() error = %v", err)
193+
}
194+
if cleanup != nil {
195+
defer cleanup()
196+
}
197+
198+
tool, err := registry.Get(tools.ToolNameSpawnSubAgent)
199+
if err != nil {
200+
t.Fatalf("registry.Get(spawn_subagent) error = %v", err)
201+
}
202+
if tool.Name() != tools.ToolNameSpawnSubAgent {
203+
t.Fatalf("tool.Name() = %q, want %q", tool.Name(), tools.ToolNameSpawnSubAgent)
204+
}
205+
specs, err := registry.ListAvailableSpecs(context.Background(), tools.SpecListInput{})
206+
if err != nil {
207+
t.Fatalf("ListAvailableSpecs() error = %v", err)
208+
}
209+
found := false
210+
for _, spec := range specs {
211+
if spec.Name == tools.ToolNameSpawnSubAgent {
212+
found = true
213+
break
214+
}
215+
}
216+
if !found {
217+
t.Fatalf("expected %q in available specs, got %+v", tools.ToolNameSpawnSubAgent, specs)
218+
}
219+
}
220+
184221
func TestBuildMCPRegistryFromConfig(t *testing.T) {
185222
stubClient := &stubMCPServerClient{
186223
tools: []mcp.ToolDescriptor{
@@ -1672,6 +1709,13 @@ func (s *stubRemoteRuntimeForBootstrap) ListSessionSkills(context.Context, strin
16721709
return nil, nil
16731710
}
16741711

1712+
func (s *stubRemoteRuntimeForBootstrap) ListAvailableSkills(
1713+
context.Context,
1714+
string,
1715+
) ([]services.AvailableSkillState, error) {
1716+
return nil, nil
1717+
}
1718+
16751719
func (s *stubRemoteRuntimeForBootstrap) Close() error {
16761720
s.closed = true
16771721
return nil

0 commit comments

Comments
 (0)