-
Notifications
You must be signed in to change notification settings - Fork 7
refactor(runtime): 第一阶段剥离 context 模块 #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
minorcell
merged 3 commits into
1024XEngineer:main
from
wynxing:codex/issue-101-context-builder
Apr 1, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package context | ||
|
|
||
| import "context" | ||
|
|
||
| // DefaultBuilder preserves the current runtime context-building behavior. | ||
| type DefaultBuilder struct{} | ||
|
|
||
| // NewBuilder returns the default context builder implementation. | ||
| func NewBuilder() Builder { | ||
| return &DefaultBuilder{} | ||
| } | ||
|
|
||
| // Build assembles the provider-facing context for the current round. | ||
| func (b *DefaultBuilder) Build(ctx context.Context, input BuildInput) (BuildResult, error) { | ||
| if err := ctx.Err(); err != nil { | ||
| return BuildResult{}, err | ||
| } | ||
|
|
||
| return BuildResult{ | ||
| SystemPrompt: defaultSystemPrompt(), | ||
| Messages: trimMessages(input.Messages), | ||
| }, nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| package context | ||
|
|
||
| import ( | ||
| stdcontext "context" | ||
| "fmt" | ||
| "testing" | ||
|
|
||
| "neo-code/internal/provider" | ||
| ) | ||
|
|
||
| func TestDefaultBuilderBuild(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| builder := NewBuilder() | ||
| input := BuildInput{ | ||
| Messages: []provider.Message{ | ||
| {Role: "user", Content: "hello"}, | ||
| }, | ||
| Workdir: t.TempDir(), | ||
| } | ||
|
|
||
| got, err := builder.Build(stdcontext.Background(), input) | ||
| if err != nil { | ||
| t.Fatalf("Build() error = %v", err) | ||
| } | ||
| if got.SystemPrompt == "" { | ||
| t.Fatalf("expected non-empty system prompt") | ||
| } | ||
| if got.SystemPrompt != defaultSystemPrompt() { | ||
| t.Fatalf("expected default prompt to remain unchanged") | ||
| } | ||
| if len(got.Messages) != 1 { | ||
| t.Fatalf("expected 1 message, got %d", len(got.Messages)) | ||
| } | ||
| if &got.Messages[0] == &input.Messages[0] { | ||
| t.Fatalf("expected messages slice to be cloned") | ||
| } | ||
| } | ||
|
|
||
| func TestDefaultBuilderBuildHonorsCancellation(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| builder := NewBuilder() | ||
| ctx, cancel := stdcontext.WithCancel(stdcontext.Background()) | ||
| cancel() | ||
|
|
||
| _, err := builder.Build(ctx, BuildInput{}) | ||
| if err != stdcontext.Canceled { | ||
| t.Fatalf("expected context.Canceled, got %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestTrimMessagesPreservesToolPairs(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| messages := make([]provider.Message, 0, maxContextTurns+4) | ||
| for i := 0; i < 8; i++ { | ||
| messages = append(messages, provider.Message{Role: "user", Content: fmt.Sprintf("u-%d", i)}) | ||
| } | ||
| messages = append(messages, | ||
| provider.Message{ | ||
| Role: "assistant", | ||
| ToolCalls: []provider.ToolCall{ | ||
| {ID: "call-1", Name: "filesystem_edit", Arguments: "{}"}, | ||
| }, | ||
| }, | ||
| provider.Message{Role: "tool", ToolCallID: "call-1", Content: "tool-result"}, | ||
| provider.Message{Role: "assistant", Content: "after-tool"}, | ||
| provider.Message{Role: "user", Content: "latest"}, | ||
| ) | ||
|
|
||
| trimmed := trimMessages(messages) | ||
| if len(trimmed) > len(messages) { | ||
| t.Fatalf("trimmed messages should not grow") | ||
| } | ||
|
|
||
| foundAssistantToolCall := false | ||
| foundToolResult := false | ||
| for _, message := range trimmed { | ||
| if message.Role == "assistant" && len(message.ToolCalls) > 0 { | ||
| foundAssistantToolCall = true | ||
| } | ||
| if message.Role == "tool" && message.ToolCallID == "call-1" { | ||
| foundToolResult = true | ||
| } | ||
| } | ||
| if foundAssistantToolCall != foundToolResult { | ||
| t.Fatalf("expected tool call and tool result to be preserved together, got %+v", trimmed) | ||
| } | ||
| } | ||
|
|
||
| func TestTrimMessagesBoundaries(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| input []provider.Message | ||
| wantLen int | ||
| assert func(t *testing.T, original []provider.Message, trimmed []provider.Message) | ||
| }{ | ||
| { | ||
| name: "within max turns returns full cloned slice", | ||
| input: []provider.Message{ | ||
| {Role: "user", Content: "one"}, | ||
| {Role: "assistant", Content: "two"}, | ||
| }, | ||
| wantLen: 2, | ||
| assert: func(t *testing.T, original []provider.Message, trimmed []provider.Message) { | ||
| t.Helper() | ||
| if &trimmed[0] == &original[0] { | ||
| t.Fatalf("expected trimmed slice to be cloned") | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "long message list with limited spans keeps full history", | ||
| input: func() []provider.Message { | ||
| messages := make([]provider.Message, 0, maxContextTurns+3) | ||
| for i := 0; i < maxContextTurns-1; i++ { | ||
| messages = append(messages, provider.Message{Role: "user", Content: fmt.Sprintf("u-%d", i)}) | ||
| } | ||
| messages = append(messages, | ||
| provider.Message{ | ||
| Role: "assistant", | ||
| ToolCalls: []provider.ToolCall{ | ||
| {ID: "call-1", Name: "filesystem_edit", Arguments: "{}"}, | ||
| }, | ||
| }, | ||
| provider.Message{Role: "tool", ToolCallID: "call-1", Content: "tool-1"}, | ||
| provider.Message{Role: "tool", ToolCallID: "call-1", Content: "tool-2"}, | ||
| ) | ||
| return messages | ||
| }(), | ||
| wantLen: maxContextTurns + 2, | ||
| assert: func(t *testing.T, original []provider.Message, trimmed []provider.Message) { | ||
| t.Helper() | ||
| if len(trimmed) != len(original) { | ||
| t.Fatalf("expected full history to remain, got %d want %d", len(trimmed), len(original)) | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "message count beyond limit trims by span count", | ||
| input: func() []provider.Message { | ||
| messages := make([]provider.Message, 0, maxContextTurns+5) | ||
| for i := 0; i < maxContextTurns+1; i++ { | ||
| messages = append(messages, provider.Message{Role: "user", Content: fmt.Sprintf("u-%d", i)}) | ||
| } | ||
| messages = append(messages, | ||
| provider.Message{ | ||
| Role: "assistant", | ||
| ToolCalls: []provider.ToolCall{ | ||
| {ID: "call-2", Name: "filesystem_edit", Arguments: "{}"}, | ||
| }, | ||
| }, | ||
| provider.Message{Role: "tool", ToolCallID: "call-2", Content: "tool-result"}, | ||
| ) | ||
| return messages | ||
| }(), | ||
| wantLen: maxContextTurns + 1, | ||
| assert: func(t *testing.T, original []provider.Message, trimmed []provider.Message) { | ||
| t.Helper() | ||
| if trimmed[0].Content != "u-2" { | ||
| t.Fatalf("expected oldest spans to be removed, got first message %+v", trimmed[0]) | ||
| } | ||
| if trimmed[len(trimmed)-1].Role != "tool" { | ||
| t.Fatalf("expected trailing tool result to remain, got %+v", trimmed[len(trimmed)-1]) | ||
| } | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| tt := tt | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| trimmed := trimMessages(tt.input) | ||
| if len(trimmed) != tt.wantLen { | ||
| t.Fatalf("expected len %d, got %d", tt.wantLen, len(trimmed)) | ||
| } | ||
| if tt.assert != nil { | ||
| tt.assert(t, tt.input, trimmed) | ||
| } | ||
| }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package context | ||
|
|
||
| func defaultSystemPrompt() string { | ||
| return `You are NeoCode, a local coding agent. | ||
|
|
||
| Be concise and accurate. | ||
| Use tools when necessary. | ||
| When a tool fails, inspect the error and continue safely. | ||
| Stay within the workspace and avoid destructive behavior unless clearly requested.` | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package context | ||
|
|
||
| import "neo-code/internal/provider" | ||
|
|
||
| const maxContextTurns = 10 | ||
|
|
||
| func trimMessages(messages []provider.Message) []provider.Message { | ||
| if len(messages) <= maxContextTurns { | ||
| return append([]provider.Message(nil), messages...) | ||
| } | ||
|
|
||
| type span struct { | ||
| start int | ||
| end int | ||
| } | ||
|
|
||
| spans := make([]span, 0, len(messages)) | ||
| for i := 0; i < len(messages); { | ||
| start := i | ||
| i++ | ||
|
|
||
| if messages[start].Role == provider.RoleAssistant && len(messages[start].ToolCalls) > 0 { | ||
| for i < len(messages) && messages[i].Role == provider.RoleTool { | ||
| i++ | ||
| } | ||
| } | ||
|
|
||
| spans = append(spans, span{start: start, end: i}) | ||
| } | ||
|
|
||
| if len(spans) <= maxContextTurns { | ||
| return append([]provider.Message(nil), messages...) | ||
| } | ||
|
|
||
| start := spans[len(spans)-maxContextTurns].start | ||
| return append([]provider.Message(nil), messages[start:]...) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package context | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "neo-code/internal/provider" | ||
| ) | ||
|
|
||
| // Builder builds the provider-facing context for a single model round. | ||
| type Builder interface { | ||
| Build(ctx context.Context, input BuildInput) (BuildResult, error) | ||
| } | ||
|
|
||
| // BuildInput contains the runtime state needed to assemble model context. | ||
| type BuildInput struct { | ||
| Messages []provider.Message | ||
| Workdir string | ||
| } | ||
|
|
||
| // BuildResult is the provider-facing context produced for a single round. | ||
| type BuildResult struct { | ||
| SystemPrompt string | ||
| Messages []provider.Message | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.