emit post-stream suffixes to chat output

When tool results are streamed, suffixes appended after execution
(truncation hints, user-interruptions, PostTool context) were only
written to the stored message history but never emitted to the chat
stream. This meant they never appeared in D-Bus signals or the chat
file.

Accumulate suffixes explicitly and emit them as a final event when
streaming was active, guaranteeing they appear at the end of the
tool output in all output paths.
This commit is contained in:
Levi Neely 2026-07-27 13:07:41 +02:00
parent 75f11a7dc9
commit 273a31c5f1
1 changed files with 22 additions and 13 deletions

View File

@ -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<system-user-interruption>\n" + injected + "\n</system-user-interruption>"
suffix += "\n\n<system-user-interruption>\n" + injected + "\n</system-user-interruption>"
}
}
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)