feat: Phase 1 code-agent capabilities — file_edit, git tools, parallel execution, tool output preview
New tools: - file_edit: surgical line-range or pattern edits (yolo only) - git_status: show changed files (read-only) - git_diff: show unstaged/staged diff (read-only) - git_log: show recent commits (read-only) Agent improvements: - parallel execution for independent read-only tool calls - mutating tools (file_write, file_edit, shell_run) run sequentially - tool output preview in logs with [output] marker TUI improvements: - tool cards now show truncated output preview (up to 8 lines) - mergeToolLog updates placeholder cards with results - toolStatusText keeps activity bar single-line Documentation: - README updated with git tools and file_edit descriptions
This commit is contained in:
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"agentu/pkg/llm"
|
||||
"agentu/internal/tools"
|
||||
"agentu/pkg/llm"
|
||||
)
|
||||
|
||||
type fakeProvider struct {
|
||||
@@ -340,3 +340,174 @@ func TestAgentHardLimitStopsVaryingCalls(t *testing.T) {
|
||||
t.Fatalf("calls = %d, want %d", provider.calls, limit)
|
||||
}
|
||||
}
|
||||
|
||||
// slowTool blocks on a channel before returning.
|
||||
type slowTool struct {
|
||||
name string
|
||||
started chan<- string
|
||||
release <-chan struct{}
|
||||
}
|
||||
|
||||
func (t *slowTool) Definition() llm.Tool {
|
||||
return llm.Tool{
|
||||
Type: "function",
|
||||
Function: llm.ToolFunction{
|
||||
Name: t.name,
|
||||
Description: "slow test tool",
|
||||
Parameters: json.RawMessage(`{"type":"object","properties":{"text":{"type":"string"}}}`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *slowTool) Execute(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
var args struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &args); err != nil {
|
||||
return "", err
|
||||
}
|
||||
t.started <- t.name
|
||||
<-t.release
|
||||
return args.Text, nil
|
||||
}
|
||||
|
||||
// mutatingSlowTool is like slowTool but implements MutatingTool.
|
||||
type mutatingSlowTool struct {
|
||||
slowTool
|
||||
}
|
||||
|
||||
func (*mutatingSlowTool) Mutates() bool { return true }
|
||||
|
||||
func TestAgentExecutesIndependentToolCallsInParallel(t *testing.T) {
|
||||
started := make(chan string, 2)
|
||||
release := make(chan struct{})
|
||||
|
||||
tool1 := &slowTool{name: "slow_one", started: started, release: release}
|
||||
tool2 := &slowTool{name: "slow_two", started: started, release: release}
|
||||
|
||||
provider := ¶llelProvider{
|
||||
responses: [][]llm.ToolCallDelta{
|
||||
{
|
||||
{Index: 0, ID: "call_1", Type: "function", Name: "slow_one", Arguments: `{"text":"one"}`},
|
||||
{Index: 1, ID: "call_2", Type: "function", Name: "slow_two", Arguments: `{"text":"two"}`},
|
||||
},
|
||||
},
|
||||
finalContent: "done",
|
||||
}
|
||||
|
||||
a := New(Options{
|
||||
Provider: provider,
|
||||
Model: "test",
|
||||
SystemPrompt: "system",
|
||||
ToolRegistry: tools.NewRegistry(tool1, tool2),
|
||||
})
|
||||
|
||||
var out strings.Builder
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- a.RunTurn(context.Background(), "run both", &out, nil)
|
||||
}()
|
||||
|
||||
// Both tools must start before we release them.
|
||||
names := make(map[string]bool)
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case name := <-started:
|
||||
names[name] = true
|
||||
case <-make(chan struct{}):
|
||||
// Use a timer in real code, but for test simplicity we rely on deadlock detection.
|
||||
t.Fatal("timed out waiting for tool to start")
|
||||
}
|
||||
}
|
||||
if !names["slow_one"] || !names["slow_two"] {
|
||||
t.Fatalf("expected both tools to start, got %v", names)
|
||||
}
|
||||
close(release)
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out.String() != "done" {
|
||||
t.Fatalf("out = %q", out.String())
|
||||
}
|
||||
// Verify tool results were in order.
|
||||
msgs := a.Messages()
|
||||
var toolMsgs []string
|
||||
for _, m := range msgs {
|
||||
if m.Role == llm.RoleTool {
|
||||
toolMsgs = append(toolMsgs, m.Content)
|
||||
}
|
||||
}
|
||||
if len(toolMsgs) != 2 || toolMsgs[0] != "one" || toolMsgs[1] != "two" {
|
||||
t.Fatalf("tool messages = %v, want [one two]", toolMsgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentRunsMutatingToolCallsSequentially(t *testing.T) {
|
||||
started := make(chan string, 2)
|
||||
release := make(chan struct{})
|
||||
|
||||
tool1 := &mutatingSlowTool{slowTool{name: "mut_one", started: started, release: release}}
|
||||
tool2 := &mutatingSlowTool{slowTool{name: "mut_two", started: started, release: release}}
|
||||
|
||||
provider := ¶llelProvider{
|
||||
responses: [][]llm.ToolCallDelta{
|
||||
{
|
||||
{Index: 0, ID: "call_1", Type: "function", Name: "mut_one", Arguments: `{"text":"one"}`},
|
||||
{Index: 1, ID: "call_2", Type: "function", Name: "mut_two", Arguments: `{"text":"two"}`},
|
||||
},
|
||||
},
|
||||
finalContent: "done",
|
||||
}
|
||||
|
||||
a := New(Options{
|
||||
Provider: provider,
|
||||
Model: "test",
|
||||
SystemPrompt: "system",
|
||||
ToolRegistry: tools.NewRegistry(tool1, tool2),
|
||||
})
|
||||
|
||||
var out strings.Builder
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- a.RunTurn(context.Background(), "run both", &out, nil)
|
||||
}()
|
||||
|
||||
// Only the first tool should start.
|
||||
select {
|
||||
case name := <-started:
|
||||
if name != "mut_one" {
|
||||
t.Fatalf("expected mut_one to start first, got %q", name)
|
||||
}
|
||||
case <-make(chan struct{}):
|
||||
t.Fatal("timed out waiting for first tool to start")
|
||||
}
|
||||
|
||||
// Second tool should NOT have started yet.
|
||||
select {
|
||||
case name := <-started:
|
||||
t.Fatalf("second tool started too early: %q", name)
|
||||
default:
|
||||
// Expected: second tool hasn't started.
|
||||
}
|
||||
|
||||
close(release)
|
||||
if err := <-done; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// parallelProvider emits a batch of tool calls on the first request, then returns finalContent.
|
||||
type parallelProvider struct {
|
||||
responses [][]llm.ToolCallDelta
|
||||
finalContent string
|
||||
calls int
|
||||
}
|
||||
|
||||
func (p *parallelProvider) ChatStream(ctx context.Context, req llm.ChatRequest, emit func(llm.StreamEvent) error) error {
|
||||
p.calls++
|
||||
if p.calls <= len(p.responses) {
|
||||
return emit(llm.StreamEvent{ToolCalls: p.responses[p.calls-1]})
|
||||
}
|
||||
return emit(llm.StreamEvent{Content: p.finalContent})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user