agent: reduce repetition and blind writes with dedup caches and range tracking
- Revised system prompt with concrete prohibitions against verbosity and premature stopping - Added GenerationParams (max_tokens, temperature, frequency/presence penalty) threading from agent config through backend ChatStream calls - Added stall detection: emits "stalled" role on max-steps hit or zero tool calls with content, surfaces as "stalled" in status bar - Added per-session file read range tracking: warns on overlapping re-reads, blocks file_write unless the target range was previously read - Added general tool-call dedup: warns on exact (name, args) repeats for non-file tools - Both caches invalidated on /compact and /clear; file read cache invalidated per-path on file_write - Updated all agent configs with documenting defaults for new generation params Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ea07f6d3a0
commit
1c4c3049ac
|
|
@ -32,6 +32,7 @@ type Config struct {
|
|||
MaxSteps int
|
||||
Output OutputFn
|
||||
SystemPrompt string
|
||||
GenerationParams backend.GenerationParams
|
||||
}
|
||||
|
||||
// ConfirmFn requests user confirmation for an action. Returns true if approved.
|
||||
|
|
@ -43,6 +44,10 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
maxSteps = 1
|
||||
}
|
||||
|
||||
var totalToolCalls int
|
||||
var hadContent bool
|
||||
hitLimit := false
|
||||
|
||||
for step := range maxSteps {
|
||||
history := state.History()
|
||||
if cfg.SystemPrompt != "" {
|
||||
|
|
@ -53,7 +58,7 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
var ch <-chan backend.StreamEvent
|
||||
for attempt := range maxRateLimitRetries + 1 {
|
||||
var err error
|
||||
ch, err = cfg.Backend.ChatStream(ctx, cfg.Model, history, cfg.Tools)
|
||||
ch, err = cfg.Backend.ChatStream(ctx, cfg.Model, history, cfg.Tools, cfg.GenerationParams)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
|
@ -92,6 +97,10 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
if !done {
|
||||
return fmt.Errorf("step %d: stream ended without done event", step)
|
||||
}
|
||||
if content.Len() > 0 {
|
||||
hadContent = true
|
||||
}
|
||||
totalToolCalls += len(toolCalls)
|
||||
|
||||
// Announce and execute tool calls.
|
||||
msg := backend.Message{Role: "assistant", Content: content.String(), ToolCalls: toolCalls}
|
||||
|
|
@ -149,10 +158,18 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
break
|
||||
}
|
||||
if step >= maxSteps-1 {
|
||||
hitLimit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Surface stall conditions so the UI can indicate them.
|
||||
if hitLimit {
|
||||
emit(cfg, OutputMsg{Role: "stalled", Content: "max steps"})
|
||||
} else if totalToolCalls == 0 && hadContent {
|
||||
emit(cfg, OutputMsg{Role: "stalled", Content: "no tools"})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ func (s *Session) Compact(ctx context.Context, b backend.Backend, model string)
|
|||
|
||||
ch, err := b.ChatStream(ctx, model, []backend.Message{
|
||||
{Role: "user", Content: prompt},
|
||||
}, nil)
|
||||
}, nil, backend.GenerationParams{})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("compact: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,9 +64,18 @@ func (e *RateLimitError) Error() string {
|
|||
return fmt.Sprintf("rate limited: %s", e.Message)
|
||||
}
|
||||
|
||||
// GenerationParams controls sampling behaviour for a single ChatStream call.
|
||||
// Zero values mean "use the API default".
|
||||
type GenerationParams struct {
|
||||
MaxTokens int // 0 = no limit
|
||||
Temperature *float64 // nil = API default
|
||||
FrequencyPenalty *float64 // nil = API default
|
||||
PresencePenalty *float64 // nil = API default
|
||||
}
|
||||
|
||||
// Backend is the interface all LLM providers must implement.
|
||||
// Streaming is the only supported mode; backends that wrap blocking APIs
|
||||
// should implement ChatStream as a single-event stream.
|
||||
type Backend interface {
|
||||
ChatStream(ctx context.Context, model string, messages []Message, tools []Tool) (<-chan StreamEvent, error)
|
||||
ChatStream(ctx context.Context, model string, messages []Message, tools []Tool, params GenerationParams) (<-chan StreamEvent, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ type ollamaChatResponse struct {
|
|||
|
||||
// -- implementation --
|
||||
|
||||
func (b *OllamaBackend) ChatStream(ctx context.Context, model string, messages []Message, tools []Tool) (<-chan StreamEvent, error) {
|
||||
func (b *OllamaBackend) ChatStream(ctx context.Context, model string, messages []Message, tools []Tool, _ GenerationParams) (<-chan StreamEvent, error) {
|
||||
wireMessages := make([]ollamaMessage, len(messages))
|
||||
for i, m := range messages {
|
||||
wireMessages[i] = ollamaMessage{Role: m.Role, Content: m.Content, ToolCallID: m.ToolCallID}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ type openAIChatRequest struct {
|
|||
Tools []openAITool `json:"tools,omitempty"`
|
||||
Stream bool `json:"stream"`
|
||||
StreamOptions *openAIStreamOptions `json:"stream_options,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"`
|
||||
PresencePenalty *float64 `json:"presence_penalty,omitempty"`
|
||||
}
|
||||
|
||||
type openAIUsage struct {
|
||||
|
|
@ -96,7 +100,7 @@ type openAIStreamResponse struct {
|
|||
|
||||
// -- implementation --
|
||||
|
||||
func (b *OpenAIBackend) ChatStream(ctx context.Context, model string, messages []Message, tools []Tool) (<-chan StreamEvent, error) {
|
||||
func (b *OpenAIBackend) ChatStream(ctx context.Context, model string, messages []Message, tools []Tool, params GenerationParams) (<-chan StreamEvent, error) {
|
||||
wireMessages := make([]openAIMessage, len(messages))
|
||||
for i, m := range messages {
|
||||
wm := openAIMessage{
|
||||
|
|
@ -138,6 +142,10 @@ func (b *OpenAIBackend) ChatStream(ctx context.Context, model string, messages [
|
|||
Tools: wireTools,
|
||||
Stream: true,
|
||||
StreamOptions: &openAIStreamOptions{IncludeUsage: true},
|
||||
MaxTokens: params.MaxTokens,
|
||||
Temperature: params.Temperature,
|
||||
FrequencyPenalty: params.FrequencyPenalty,
|
||||
PresencePenalty: params.PresencePenalty,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ type Config struct {
|
|||
Hooks map[string]string `json:"hooks,omitempty"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
TrustedTools []string `json:"trustedTools,omitempty"`
|
||||
MaxTokens int `json:"maxTokens,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
FrequencyPenalty *float64 `json:"frequencyPenalty,omitempty"`
|
||||
PresencePenalty *float64 `json:"presencePenalty,omitempty"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
|
|
|
|||
197
main.go
197
main.go
|
|
@ -8,6 +8,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -26,12 +27,11 @@ import (
|
|||
"ollie/tools"
|
||||
)
|
||||
|
||||
const systemPromptBase = `NEVER describe what you are about to do. NEVER list planned steps. Call tools immediately and directly.
|
||||
Be terse. No preamble, narration, or filler ("Let me...", "I'll now...", "Great!").
|
||||
Output only errors, ambiguities requiring clarification, and deliverables.
|
||||
const systemPromptBase = `Use the fewest words possible. No preamble, filler, or narration ("Let me...", "I'll now...", "Great!"). No explanations of actions taken. No summaries of completed work. No reasoning unless asked. If the answer is one word, write one word.
|
||||
Call tools immediately and directly. Never describe what you are about to do — act.
|
||||
Complete tasks fully before stopping. Do not pause mid-task to narrate progress or request confirmation.
|
||||
Do not ask clarifying questions unless the task is genuinely ambiguous. Attempt the task; correct based on feedback.
|
||||
Do not restate tasks, hedge, or self-congratulate.
|
||||
Always use tools to perform actions; never simulate or guess outputs.
|
||||
Do not describe what you are about to do. Just do it — emit the tool call immediately.
|
||||
Do not attempt tasks outside your tools.
|
||||
Do not use hedging language ("it looks like", "it appears", "it seems", "likely", "probably"). If you are uncertain, use tools to find out. Give definite answers based on evidence.
|
||||
Do not re-read or re-fetch any file or resource that already has a result in the conversation history. Use the existing result.
|
||||
|
|
@ -149,6 +149,7 @@ const (
|
|||
agentRunningTool
|
||||
agentRetrying
|
||||
agentConfirming
|
||||
agentStalled
|
||||
)
|
||||
|
||||
// resolveBackendName returns a short human-readable backend label derived
|
||||
|
|
@ -208,6 +209,7 @@ type model struct {
|
|||
builtinExec *execpkg.Executor
|
||||
confirmPtr *agent.ConfirmFn // indirection so startAgent can set it per-run
|
||||
ctxOverhead int // fixed per-request char overhead (system prompt + tool schemas)
|
||||
invalidateCaches func() // clears tool-call dedup caches; set from agentEnv
|
||||
|
||||
quitPending bool // whether a second Ctrl+C should quit
|
||||
lastCtrlC time.Time // timestamp of last Ctrl+C press
|
||||
|
|
@ -250,8 +252,10 @@ type agentEnv struct {
|
|||
confirm *agent.ConfirmFn // pointer filled in by startAgent per-run
|
||||
hooks map[string]string
|
||||
systemPrompt string
|
||||
genParams backend.GenerationParams
|
||||
ctxOverhead int
|
||||
messages []string // startup / status messages to display
|
||||
invalidateCaches func() // clears per-session tool-call dedup caches on compact/clear
|
||||
}
|
||||
|
||||
// buildAgentEnv constructs the runtime environment for a given agent config.
|
||||
|
|
@ -294,6 +298,7 @@ func buildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) agentEnv {
|
|||
hooks := map[string]string{}
|
||||
agentPrompt := ""
|
||||
trustedTools := map[string]struct{}{}
|
||||
var genParams backend.GenerationParams
|
||||
if cfg != nil {
|
||||
if cfg.Hooks != nil {
|
||||
hooks = cfg.Hooks
|
||||
|
|
@ -302,6 +307,12 @@ func buildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) agentEnv {
|
|||
for _, t := range cfg.TrustedTools {
|
||||
trustedTools[t] = struct{}{}
|
||||
}
|
||||
genParams = backend.GenerationParams{
|
||||
MaxTokens: cfg.MaxTokens,
|
||||
Temperature: cfg.Temperature,
|
||||
FrequencyPenalty: cfg.FrequencyPenalty,
|
||||
PresencePenalty: cfg.PresencePenalty,
|
||||
}
|
||||
}
|
||||
|
||||
sp := systemPrompt(allTools)
|
||||
|
|
@ -322,7 +333,19 @@ func buildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) agentEnv {
|
|||
var confirmFn agent.ConfirmFn // filled in by startAgent
|
||||
confirmPtr := &confirmFn
|
||||
|
||||
execFn := func(ctx context.Context, name string, args json.RawMessage) (string, error) {
|
||||
// fileRanges tracks which line ranges of each file have been read this session.
|
||||
// Used to warn on overlapping re-reads and to guard file_write against blind overwrites.
|
||||
// Cleared on /compact and /clear; individual entries deleted when a file is written.
|
||||
type fileReadState struct {
|
||||
ranges []lineRange
|
||||
totalLines int // total lines when last read; used for whole-file write coverage check
|
||||
}
|
||||
fileRanges := make(map[string]*fileReadState)
|
||||
|
||||
// toolCallSeen tracks all non-file tool calls by exact (name, args) to warn on repeats.
|
||||
toolCallSeen := make(map[string]bool)
|
||||
|
||||
rawExec := func(ctx context.Context, name string, args json.RawMessage) (string, error) {
|
||||
if server, ok := serverOf[name]; ok {
|
||||
raw, err := mcpExec.Execute(server, name, args)
|
||||
if err != nil {
|
||||
|
|
@ -335,11 +358,84 @@ func buildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) agentEnv {
|
|||
if _, trusted := trustedTools[name]; !trusted {
|
||||
cfn = *confirmPtr
|
||||
}
|
||||
|
||||
if name == "file_read" {
|
||||
var a struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
json.Unmarshal(args, &a) //nolint:errcheck
|
||||
meta, err := dispatchFileRead(cfn, args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content := meta.content
|
||||
if a.Path != "" {
|
||||
st := fileRanges[a.Path]
|
||||
if st != nil && rangesOverlap(st.ranges, meta.start, meta.end) {
|
||||
content = fmt.Sprintf("[WARNING: Lines %d-%d of this file were already read this session. Do not re-read ranges already in your context.]\n", meta.start, meta.end) + content
|
||||
}
|
||||
if st == nil {
|
||||
st = &fileReadState{}
|
||||
fileRanges[a.Path] = st
|
||||
}
|
||||
st.ranges = append(st.ranges, lineRange{meta.start, meta.end})
|
||||
st.totalLines = meta.totalLines
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
if name == "file_write" {
|
||||
var a struct {
|
||||
Path string `json:"path"`
|
||||
StartLine int `json:"start_line"`
|
||||
EndLine int `json:"end_line"`
|
||||
}
|
||||
json.Unmarshal(args, &a) //nolint:errcheck
|
||||
if a.Path != "" {
|
||||
st := fileRanges[a.Path]
|
||||
if st == nil {
|
||||
return "", fmt.Errorf("file_write: %s has not been read this session; read it first to avoid overwriting unknown changes", a.Path)
|
||||
}
|
||||
ws, we := a.StartLine, a.EndLine
|
||||
if ws == 0 && we == 0 {
|
||||
// Whole-file write: verify full coverage.
|
||||
totalLines := st.totalLines
|
||||
if totalLines == 0 {
|
||||
data, err := os.ReadFile(a.Path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("file_write: cannot verify read coverage: %w", err)
|
||||
}
|
||||
totalLines = len(strings.Split(string(data), "\n"))
|
||||
}
|
||||
ws, we = 1, totalLines
|
||||
}
|
||||
if !rangesCover(st.ranges, ws, we) {
|
||||
return "", fmt.Errorf("file_write: lines %d-%d of %s have not been read this session; read them first to avoid overwriting unknown changes", ws, we, a.Path)
|
||||
}
|
||||
// Invalidate: file content has changed, any cached ranges are stale.
|
||||
delete(fileRanges, a.Path)
|
||||
}
|
||||
}
|
||||
|
||||
return dispatchBuiltinExec(ctx, name, builtinExec, cfn, args)
|
||||
}
|
||||
return "", fmt.Errorf("unknown tool: %s", name)
|
||||
}
|
||||
|
||||
execFn := func(ctx context.Context, name string, args json.RawMessage) (string, error) {
|
||||
result, err := rawExec(ctx, name, args)
|
||||
// Warn on repeated identical tool calls (excluding file_read/file_write which
|
||||
// are handled separately above).
|
||||
if err == nil && name != "file_read" && name != "file_write" {
|
||||
key := name + "\x00" + string(args)
|
||||
if toolCallSeen[key] {
|
||||
result = "[WARNING: This exact tool call was already made this session. Result may be unchanged. Do not repeat unless something has changed.]\n" + result
|
||||
}
|
||||
toolCallSeen[key] = true
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
return agentEnv{
|
||||
mcpExec: mcpExec,
|
||||
tools: allTools,
|
||||
|
|
@ -347,8 +443,13 @@ func buildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) agentEnv {
|
|||
confirm: confirmPtr,
|
||||
hooks: hooks,
|
||||
systemPrompt: sp,
|
||||
genParams: genParams,
|
||||
ctxOverhead: overhead,
|
||||
messages: messages,
|
||||
invalidateCaches: func() {
|
||||
clear(fileRanges)
|
||||
clear(toolCallSeen)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -417,6 +518,7 @@ func main() {
|
|||
Tools: env.tools,
|
||||
Exec: env.exec,
|
||||
MaxSteps: 20,
|
||||
GenerationParams: env.genParams,
|
||||
}
|
||||
|
||||
ta := textarea.New()
|
||||
|
|
@ -452,6 +554,7 @@ func main() {
|
|||
builtinExec: builtinExec,
|
||||
confirmPtr: env.confirm,
|
||||
ctxOverhead: env.ctxOverhead,
|
||||
invalidateCaches: env.invalidateCaches,
|
||||
quitPending: false,
|
||||
state: agentIdle,
|
||||
})
|
||||
|
|
@ -507,6 +610,8 @@ func (m model) renderStatusBar() string {
|
|||
stateStr = fmt.Sprintf("retry %ds", m.retrySecsLeft)
|
||||
case agentConfirming:
|
||||
stateStr = "confirm [y/n]"
|
||||
case agentStalled:
|
||||
stateStr = "stalled"
|
||||
}
|
||||
|
||||
// Token usage segment.
|
||||
|
|
@ -600,6 +705,9 @@ func (m *model) apply(am agentMsg) {
|
|||
m.confirmCh = am.confirmCh
|
||||
m.appendDisplay("Confirm: " + am.content + " [y/n]")
|
||||
|
||||
case "stalled":
|
||||
m.state = agentStalled
|
||||
|
||||
case "usage":
|
||||
// Update status bar only; no display line appended.
|
||||
m.lastUsage = am.usage
|
||||
|
|
@ -668,7 +776,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.apply(msg)
|
||||
m.refreshView()
|
||||
if msg.done {
|
||||
if m.state != agentStalled {
|
||||
m.state = agentIdle
|
||||
}
|
||||
m.currentTool = ""
|
||||
m.agentCh = nil
|
||||
m.cancel = nil
|
||||
|
|
@ -869,8 +979,10 @@ func (m *model) handleCommand(input string) bool {
|
|||
m.loopcfg.SystemPrompt = env.systemPrompt
|
||||
m.loopcfg.Tools = env.tools
|
||||
m.loopcfg.Exec = env.exec
|
||||
m.loopcfg.GenerationParams = env.genParams
|
||||
m.ctxOverhead = env.ctxOverhead
|
||||
m.confirmPtr = env.confirm
|
||||
m.invalidateCaches = env.invalidateCaches
|
||||
m.agentName = name
|
||||
m.session = nil // new agent, new session
|
||||
for _, msg := range env.messages {
|
||||
|
|
@ -891,6 +1003,9 @@ func (m *model) handleCommand(input string) bool {
|
|||
m.display = append(m.display, "nothing to compact")
|
||||
} else {
|
||||
m.display = append(m.display, fmt.Sprintf("compacted %d messages", n))
|
||||
if m.invalidateCaches != nil {
|
||||
m.invalidateCaches()
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
|
|
@ -922,6 +1037,9 @@ func (m *model) handleCommand(input string) bool {
|
|||
m.session = nil
|
||||
m.display = nil
|
||||
m.buf = ""
|
||||
if m.invalidateCaches != nil {
|
||||
m.invalidateCaches()
|
||||
}
|
||||
return true
|
||||
|
||||
case "/help":
|
||||
|
|
@ -1098,7 +1216,8 @@ func extractMCPText(raw json.RawMessage) string {
|
|||
func dispatchBuiltinExec(ctx context.Context, name string, e *execpkg.Executor, confirm agent.ConfirmFn, args json.RawMessage) (string, error) {
|
||||
switch name {
|
||||
case "file_read":
|
||||
return dispatchFileRead(confirm, args)
|
||||
r, err := dispatchFileRead(confirm, args)
|
||||
return r.content, err
|
||||
case "file_write":
|
||||
return dispatchFileWrite(confirm, args)
|
||||
case "execute_pipe":
|
||||
|
|
@ -1226,28 +1345,68 @@ func dispatchExecutePipe(ctx context.Context, e *execpkg.Executor, confirm agent
|
|||
|
||||
const fileReadMaxLines = 500
|
||||
|
||||
func dispatchFileRead(confirm agent.ConfirmFn, args json.RawMessage) (string, error) {
|
||||
// lineRange is an inclusive [start, end] line range (1-based).
|
||||
type lineRange struct{ start, end int }
|
||||
|
||||
// fileReadResult carries the output of dispatchFileRead plus the range metadata
|
||||
// needed for per-session coverage tracking.
|
||||
type fileReadResult struct {
|
||||
content string
|
||||
start int // actual first line returned (1-based)
|
||||
end int // actual last line returned (1-based)
|
||||
totalLines int // total lines in the file before any truncation
|
||||
}
|
||||
|
||||
// rangesOverlap reports whether [ws, we] overlaps any interval in ranges.
|
||||
func rangesOverlap(ranges []lineRange, ws, we int) bool {
|
||||
for _, r := range ranges {
|
||||
if r.start <= we && r.end >= ws {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// rangesCover reports whether the union of ranges fully contains [ws, we].
|
||||
func rangesCover(ranges []lineRange, ws, we int) bool {
|
||||
sorted := make([]lineRange, len(ranges))
|
||||
copy(sorted, ranges)
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i].start < sorted[j].start })
|
||||
cur := 0
|
||||
for _, r := range sorted {
|
||||
if r.start > cur+1 {
|
||||
break
|
||||
}
|
||||
if r.end > cur {
|
||||
cur = r.end
|
||||
}
|
||||
}
|
||||
return cur >= we
|
||||
}
|
||||
|
||||
func dispatchFileRead(confirm agent.ConfirmFn, args json.RawMessage) (fileReadResult, error) {
|
||||
var a struct {
|
||||
Path string `json:"path"`
|
||||
StartLine int `json:"start_line"`
|
||||
EndLine int `json:"end_line"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return "", fmt.Errorf("file_read: bad args: %w", err)
|
||||
return fileReadResult{}, fmt.Errorf("file_read: bad args: %w", err)
|
||||
}
|
||||
if a.Path == "" {
|
||||
return "", fmt.Errorf("file_read: 'path' is required")
|
||||
return fileReadResult{}, fmt.Errorf("file_read: 'path' is required")
|
||||
}
|
||||
if confirm != nil && !confirm(fmt.Sprintf("read %s", a.Path)) {
|
||||
return "", fmt.Errorf("file_read: denied by user")
|
||||
return fileReadResult{}, fmt.Errorf("file_read: denied by user")
|
||||
}
|
||||
data, err := os.ReadFile(a.Path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("file_read: %w", err)
|
||||
return fileReadResult{}, fmt.Errorf("file_read: %w", err)
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
totalLines := len(lines)
|
||||
start := 1
|
||||
end := len(lines)
|
||||
end := totalLines
|
||||
if a.StartLine > 0 {
|
||||
start = a.StartLine
|
||||
}
|
||||
|
|
@ -1257,11 +1416,11 @@ func dispatchFileRead(confirm agent.ConfirmFn, args json.RawMessage) (string, er
|
|||
if start < 1 {
|
||||
start = 1
|
||||
}
|
||||
if end > len(lines) {
|
||||
end = len(lines)
|
||||
if end > totalLines {
|
||||
end = totalLines
|
||||
}
|
||||
if start > end {
|
||||
return "", fmt.Errorf("file_read: start_line %d > end_line %d", start, end)
|
||||
return fileReadResult{}, fmt.Errorf("file_read: start_line %d > end_line %d", start, end)
|
||||
}
|
||||
truncated := false
|
||||
if end-start+1 > fileReadMaxLines {
|
||||
|
|
@ -1272,11 +1431,11 @@ func dispatchFileRead(confirm agent.ConfirmFn, args json.RawMessage) (string, er
|
|||
for i, line := range lines[start-1 : end] {
|
||||
fmt.Fprintf(&out, "%d\t%s\n", start+i, line)
|
||||
}
|
||||
result := strings.TrimRight(out.String(), "\n")
|
||||
content := strings.TrimRight(out.String(), "\n")
|
||||
if truncated {
|
||||
result += fmt.Sprintf("\n[truncated: showing lines %d-%d of %d; use start_line/end_line or grep -n to narrow range]", start, end, len(lines))
|
||||
content += fmt.Sprintf("\n[truncated: showing lines %d-%d of %d; use start_line/end_line or grep -n to narrow range]", start, end, totalLines)
|
||||
}
|
||||
return result, nil
|
||||
return fileReadResult{content: content, start: start, end: end, totalLines: totalLines}, nil
|
||||
}
|
||||
|
||||
func dispatchFileWrite(confirm agent.ConfirmFn, args json.RawMessage) (string, error) {
|
||||
|
|
|
|||
Reference in New Issue