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)