diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index b3655b9..74e0567 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -345,6 +345,9 @@ func run(ctx context.Context, cfg agentConfig, state state) error { result = "error: no tool executor configured" isErr = true } + // Accumulate suffix text (PostTool context, user-interruptions, + // truncation hints) that must be emitted after streaming completes. + var suffix string if cfg.PostTool != nil { hr := cfg.PostTool(ctx, tc.Name, tc.Arguments, result) if hr.Failed > 0 { @@ -353,31 +356,37 @@ func run(ctx context.Context, cfg agentConfig, state state) error { if hr.Blocked { result = hr.Context } else if hr.Context != "" { - result += "\n" + hr.Context + suffix += "\n" + hr.Context } } if cfg.PopInject != nil { if injected := cfg.PopInject(); injected != "" { - result += "\n\n\n" + injected + "\n" + suffix += "\n\n\n" + injected + "\n" } } + result += suffix // Safety ceiling: unconditionally cap all tool results at 128KB. if len(result) > defaultToolResultMaxBytes { orig := len(result) result = strings.ToValidUTF8(result[:defaultToolResultMaxBytes], "") - result += fmt.Sprintf("\n\n[HARD LIMIT: %s output truncated — %d of %d bytes shown. This is a safety ceiling, not a semantic boundary.]", + suffix = fmt.Sprintf("\n\n[HARD LIMIT: %s output truncated — %d of %d bytes shown. This is a safety ceiling, not a semantic boundary.]", tc.Name, defaultToolResultMaxBytes, orig) + result += suffix + } + if readSafe && !isErr { + resultCache.Store(tc.Name+"\x00"+string(tc.Arguments), result) + } + // Invalidate cache on write operations that may change file contents. + if !readSafe && (tc.Name == "file_write" || tc.Name == "file_edit") { + *resultCache = sync.Map{} + } + if !streamed { + emit(cfg, Event{Role: "tool", Name: tc.Name, Content: result}) + } else if suffix != "" { + // Emit suffixes that were appended after streaming completed + // so they appear at the end of the chat output. + emit(cfg, Event{Role: "tool", Name: tc.Name, Content: suffix}) } - if readSafe && !isErr { - resultCache.Store(tc.Name+"\x00"+string(tc.Arguments), result) - } - // Invalidate cache on write operations that may change file contents. - if !readSafe && (tc.Name == "file_write" || tc.Name == "file_edit") { - *resultCache = sync.Map{} - } - if !streamed { - emit(cfg, Event{Role: "tool", Name: tc.Name, Content: result}) - } tier := TierHot if !isErr && cfg.ClassifyTier != nil { tier = cfg.ClassifyTier(tc.Name, tc.Arguments)