Skip to content

Commit 4b440e3

Browse files
authored
Merge pull request #112 from Yumiue/codex/issue-111-context-sources
feat(context): 注入显示项目规制和状态
2 parents e6caa9e + e3042d3 commit 4b440e3

15 files changed

Lines changed: 734 additions & 15 deletions

.gitattributes

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
1+
# 统一仓库内常见文本文件为 LF,避免 Windows 下出现无意义的换行警告
2+
*.go text eol=lf
3+
*.md text eol=lf
4+
*.yml text eol=lf
5+
*.yaml text eol=lf
6+
go.mod text eol=lf
7+
go.sum text eol=lf
8+
.gitignore text eol=lf
9+
.gitattributes text eol=lf
10+
111
# 强制 scripts 目录下的 Shell 脚本始终使用 LF 换行符
2-
scripts/*.sh text eol=lf
12+
scripts/*.sh text eol=lf

docs/neocode-coding-agent-mvp-architecture.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66

77
`用户输入 -> Agent 推理 -> 调用工具 -> 获取结果 -> 继续推理 -> UI 展示`
88

9-
MVP 聚焦五个模块
9+
MVP 聚焦六个模块
1010

1111
1. provider:统一不同模型/API 的调用方式
1212
2. TUI:用户交互入口,承载输入、对话、侧边栏、会话
1313
3. tools:统一工具定义、参数校验、执行与结果封装
1414
4. config:管理本地配置、provider 切换、模型选择
15-
5. agent runtime:驱动整个 agent loop,是系统核心
15+
5. context:负责 system prompt、显式上下文源与历史消息裁剪
16+
6. agent runtime:驱动整个 agent loop,是系统核心
1617

1718
---
1819

@@ -46,6 +47,7 @@ flowchart LR
4647
- TUI:负责交互和渲染
4748
- Application:负责启动和依赖注入
4849
- Runtime:负责 Agent Loop 和状态编排
50+
- Context:负责模型请求前的上下文构建
4951
- Provider:负责模型调用抽象
5052
- Tool Manager:负责工具注册、校验、执行
5153
- Config:负责配置加载与选择
@@ -295,7 +297,7 @@ type UserInput struct {
295297
Runtime 内部建议拆分:
296298

297299
- `SessionStore`:管理会话和消息历史
298-
- `PromptBuilder`:组装 prompt/messages/tools
300+
- `context.Builder`:组装核心 prompt、显式上下文源与裁剪后的消息
299301
- `Executor`:执行 loop
300302
- `EventBus`:向 TUI 推送运行事件
301303

@@ -379,6 +381,13 @@ sequenceDiagram
379381
│ │ ├── loader.go
380382
│ │ ├── model.go
381383
│ │ └── validate.go
384+
│ ├── context/
385+
│ │ ├── builder.go
386+
│ │ ├── metadata.go
387+
│ │ ├── prompt.go
388+
│ │ ├── source_rules.go
389+
│ │ ├── source_system.go
390+
│ │ └── trim.go
382391
│ ├── provider/
383392
│ │ ├── provider.go
384393
│ │ ├── openai/

docs/runtime-provider-event-flow.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,36 @@
2222
8. 执行返回的工具调用,并保存每一个工具结果。
2323
9. 如果仍需继续推理,则进入下一轮;否则结束。
2424

25+
### Context Builder 输入与职责
26+
27+
- `runtime` 只向 `context.Builder` 传递本轮所需元数据:
28+
- 历史消息
29+
- `workdir`
30+
- `shell`
31+
- 当前 `provider`
32+
- 当前 `model`
33+
- `context.Builder` 负责统一组装:
34+
- 固定核心 system prompt
35+
-`workdir` 向上发现的 `AGENTS.md`
36+
- 系统状态摘要(`workdir` / `shell` / `provider` / `model` / git branch / git dirty)
37+
- 裁剪后的历史消息
38+
- `runtime` 不直接读取规则文件,也不直接查询 git 状态。
39+
- `provider` 只消费最终生成的 `SystemPrompt`、消息列表和工具 schema,不感知上下文来源。
40+
41+
### System Prompt 注入顺序
42+
43+
当前 `system prompt` 按以下顺序拼装:
44+
45+
1. 固定核心指令
46+
2. `Project Rules` section
47+
3. `System State` section
48+
49+
其中:
50+
51+
- 规则文件只支持大写文件名 `AGENTS.md`
52+
- 多份命中结果按“从全局到局部”的顺序注入
53+
- git 只注入摘要,不注入完整 `git status`
54+
2555
## 流式桥接
2656

2757
- Provider 发出 `StreamEvent`

internal/context/builder.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ package context
33
import "context"
44

55
// DefaultBuilder preserves the current runtime context-building behavior.
6-
type DefaultBuilder struct{}
6+
type DefaultBuilder struct {
7+
gitRunner gitCommandRunner
8+
}
79

810
// NewBuilder returns the default context builder implementation.
911
func NewBuilder() Builder {
10-
return &DefaultBuilder{}
12+
return &DefaultBuilder{
13+
gitRunner: runGitCommand,
14+
}
1115
}
1216

1317
// Build assembles the provider-facing context for the current round.
@@ -16,8 +20,22 @@ func (b *DefaultBuilder) Build(ctx context.Context, input BuildInput) (BuildResu
1620
return BuildResult{}, err
1721
}
1822

23+
rules, err := loadProjectRules(ctx, input.Metadata.Workdir)
24+
if err != nil {
25+
return BuildResult{}, err
26+
}
27+
28+
systemState, err := collectSystemState(ctx, input.Metadata, b.gitRunner)
29+
if err != nil {
30+
return BuildResult{}, err
31+
}
32+
1933
return BuildResult{
20-
SystemPrompt: defaultSystemPrompt(),
21-
Messages: trimMessages(input.Messages),
34+
SystemPrompt: composeSystemPrompt(
35+
defaultSystemPrompt(),
36+
renderProjectRulesSection(rules),
37+
renderSystemStateSection(systemState),
38+
),
39+
Messages: trimMessages(input.Messages),
2240
}, nil
2341
}

internal/context/builder_test.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package context
33
import (
44
stdcontext "context"
55
"fmt"
6+
"strings"
67
"testing"
78

89
"neo-code/internal/provider"
@@ -16,7 +17,7 @@ func TestDefaultBuilderBuild(t *testing.T) {
1617
Messages: []provider.Message{
1718
{Role: "user", Content: "hello"},
1819
},
19-
Workdir: t.TempDir(),
20+
Metadata: testMetadata(t.TempDir()),
2021
}
2122

2223
got, err := builder.Build(stdcontext.Background(), input)
@@ -26,8 +27,17 @@ func TestDefaultBuilderBuild(t *testing.T) {
2627
if got.SystemPrompt == "" {
2728
t.Fatalf("expected non-empty system prompt")
2829
}
29-
if got.SystemPrompt != defaultSystemPrompt() {
30-
t.Fatalf("expected default prompt to remain unchanged")
30+
if !strings.Contains(got.SystemPrompt, defaultSystemPrompt()) {
31+
t.Fatalf("expected default prompt to remain in composed prompt")
32+
}
33+
if !strings.Contains(got.SystemPrompt, "## System State") {
34+
t.Fatalf("expected system state section in composed prompt")
35+
}
36+
if strings.Contains(got.SystemPrompt, "## Project Rules") {
37+
t.Fatalf("did not expect project rules section without AGENTS.md")
38+
}
39+
if !strings.Contains(got.SystemPrompt, input.Metadata.Workdir) {
40+
t.Fatalf("expected workdir in system state section")
3141
}
3242
if len(got.Messages) != 1 {
3343
t.Fatalf("expected 1 message, got %d", len(got.Messages))

internal/context/metadata.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package context
2+
3+
// Metadata contains the non-message runtime state needed by context sources.
4+
type Metadata struct {
5+
Workdir string
6+
Shell string
7+
Provider string
8+
Model string
9+
}
10+
11+
// GitState is the summarized git metadata exposed to the prompt builder.
12+
type GitState struct {
13+
Available bool
14+
Branch string
15+
Dirty bool
16+
}
17+
18+
// SystemState is the summarized runtime metadata exposed to the prompt builder.
19+
type SystemState struct {
20+
Workdir string
21+
Shell string
22+
Provider string
23+
Model string
24+
Git GitState
25+
}

internal/context/prompt.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package context
22

3+
import "strings"
4+
35
func defaultSystemPrompt() string {
46
return `You are NeoCode, a local coding agent.
57
@@ -8,3 +10,15 @@ func defaultSystemPrompt() string {
810
When a tool fails, inspect the error and continue safely.
911
Stay within the workspace and avoid destructive behavior unless clearly requested.`
1012
}
13+
14+
func composeSystemPrompt(parts ...string) string {
15+
sections := make([]string, 0, len(parts))
16+
for _, part := range parts {
17+
part = strings.TrimSpace(part)
18+
if part == "" {
19+
continue
20+
}
21+
sections = append(sections, part)
22+
}
23+
return strings.Join(sections, "\n\n")
24+
}

0 commit comments

Comments
 (0)