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:
+96
-17
@@ -7,18 +7,22 @@ import (
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"agentu/pkg/llm"
|
||||
"agentu/internal/tools"
|
||||
"agentu/pkg/llm"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMaxToolRounds = 50
|
||||
doomLoopThreshold = 3
|
||||
compactRecentMessages = 6
|
||||
summaryMessageLimit = 8000
|
||||
contextSummaryPrefix = "[context summary]"
|
||||
defaultMaxToolRounds = 50
|
||||
doomLoopThreshold = 3
|
||||
compactRecentMessages = 6
|
||||
summaryMessageLimit = 8000
|
||||
contextSummaryPrefix = "[context summary]"
|
||||
toolLogArgumentLimit = 400
|
||||
toolLogOutputPreviewBytes = 4096
|
||||
toolLogOutputMarker = "[output]"
|
||||
|
||||
summarizerSystemPrompt = "You are a conversation summarizer. Summarize the following conversation history into a concise but detailed summary. Preserve:\n" +
|
||||
"- Key facts, decisions, and conclusions\n" +
|
||||
@@ -29,6 +33,11 @@ const (
|
||||
"Output only the summary, no preamble."
|
||||
)
|
||||
|
||||
type toolExecutionResult struct {
|
||||
call llm.ToolCall
|
||||
output string
|
||||
}
|
||||
|
||||
type Agent struct {
|
||||
provider llm.Provider
|
||||
providerName string
|
||||
@@ -364,14 +373,15 @@ func (a *Agent) RunTurn(ctx context.Context, input string, out io.Writer, logs i
|
||||
}
|
||||
}
|
||||
|
||||
for _, call := range calls {
|
||||
result := a.executeTool(ctx, call, logs)
|
||||
results := a.executeTools(ctx, calls, logs)
|
||||
for _, result := range results {
|
||||
a.messages = append(a.messages, llm.Message{
|
||||
Role: llm.RoleTool,
|
||||
ToolCallID: call.ID,
|
||||
Content: result,
|
||||
ToolCallID: result.call.ID,
|
||||
Content: result.output,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return fmt.Errorf("tool round limit reached after %d rounds; the task may be too complex for a single turn", a.maxToolRounds)
|
||||
@@ -433,11 +443,8 @@ func (a *Agent) toolDefinitions() []llm.Tool {
|
||||
return a.toolRegistry.Definitions()
|
||||
}
|
||||
|
||||
func (a *Agent) executeTool(ctx context.Context, call llm.ToolCall, logs io.Writer) string {
|
||||
func (a *Agent) executeTool(ctx context.Context, call llm.ToolCall) string {
|
||||
name := call.Function.Name
|
||||
if logs != nil {
|
||||
fmt.Fprintf(logs, "\n[tool] %s %s\n", name, compact(call.Function.Arguments, 400))
|
||||
}
|
||||
|
||||
if a.toolRegistry == nil {
|
||||
return tools.FormatError(fmt.Errorf("tool registry is disabled"))
|
||||
@@ -461,12 +468,84 @@ func (a *Agent) executeTool(ctx context.Context, call llm.ToolCall, logs io.Writ
|
||||
}
|
||||
return tools.FormatError(err)
|
||||
}
|
||||
if logs != nil {
|
||||
fmt.Fprintf(logs, "[tool] %s done\n", name)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func (a *Agent) hasMutatingCall(calls []llm.ToolCall) bool {
|
||||
if a.toolRegistry == nil {
|
||||
return false
|
||||
}
|
||||
for _, call := range calls {
|
||||
if tool, ok := a.toolRegistry.Get(call.Function.Name); ok {
|
||||
if tools.IsMutating(tool) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *Agent) executeTools(ctx context.Context, calls []llm.ToolCall, logs io.Writer) []toolExecutionResult {
|
||||
results := make([]toolExecutionResult, len(calls))
|
||||
|
||||
// Log all tool starts sequentially so TUI gets ordered placeholders.
|
||||
if logs != nil {
|
||||
for _, call := range calls {
|
||||
logToolStart(logs, call)
|
||||
}
|
||||
}
|
||||
|
||||
if a.hasMutatingCall(calls) {
|
||||
// Sequential: avoid racing mutating tools.
|
||||
for i, call := range calls {
|
||||
output := a.executeTool(ctx, call)
|
||||
results[i] = toolExecutionResult{call: call, output: output}
|
||||
if logs != nil {
|
||||
logToolOutput(logs, call, output)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Concurrent: independent read-only tools.
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
for i, call := range calls {
|
||||
wg.Add(1)
|
||||
go func(idx int, c llm.ToolCall) {
|
||||
defer wg.Done()
|
||||
output := a.executeTool(ctx, c)
|
||||
results[idx] = toolExecutionResult{call: c, output: output}
|
||||
if logs != nil {
|
||||
mu.Lock()
|
||||
logToolOutput(logs, c, output)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(i, call)
|
||||
}
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
func logToolStart(logs io.Writer, call llm.ToolCall) {
|
||||
fmt.Fprintf(logs, "\n[tool] %s %s\n", call.Function.Name, compact(call.Function.Arguments, toolLogArgumentLimit))
|
||||
}
|
||||
|
||||
func logToolOutput(logs io.Writer, call llm.ToolCall, output string) {
|
||||
fmt.Fprintf(logs, "\n[tool] %s %s\n%s\n%s\n", call.Function.Name, compact(call.Function.Arguments, toolLogArgumentLimit), toolLogOutputMarker, toolOutputPreview(output))
|
||||
}
|
||||
|
||||
func toolOutputPreview(output string) string {
|
||||
output = strings.TrimSpace(output)
|
||||
if output == "" {
|
||||
return "(no output)"
|
||||
}
|
||||
if len(output) <= toolLogOutputPreviewBytes {
|
||||
return output
|
||||
}
|
||||
return output[:toolLogOutputPreviewBytes] + fmt.Sprintf("\n[truncated: showing first %d bytes; omitted %d bytes]", toolLogOutputPreviewBytes, len(output)-toolLogOutputPreviewBytes)
|
||||
}
|
||||
|
||||
type toolCallBuilder struct {
|
||||
index int
|
||||
id string
|
||||
|
||||
Reference in New Issue
Block a user