fix: batch preamble injection and stripCold into single LLM calls
- Remove per-step preamble prepending in loop.go; inject once at turn start in turn.go instead of re-sending thousands of tokens every tool step. - Re-inject preamble after overflow retry compaction where it was lost. - Replace stripCold() N sequential LLM calls with one batched call that summarizes all cold-zone tool results via a single JSON response.
This commit is contained in:
parent
cca3d9a3f2
commit
e0f846bd31
|
|
@ -141,28 +141,6 @@ func (s *History) update(assistant backend.Message, results []toolResult) {
|
|||
}
|
||||
}
|
||||
|
||||
// llmSummarizeToolResult uses the LLM to produce a concise summary of a tool result.
|
||||
func llmSummarizeToolResult(ctx context.Context, b backend.Backend, name, content string) string {
|
||||
prompt := fmt.Sprintf("Summarize this tool result from %q in 1-3 sentences, preserving key facts, file paths, values, and findings:\n\n%s", name, content)
|
||||
ch, err := b.ChatStream(ctx, []backend.Message{{Role: "user", Content: prompt}}, nil, backend.GenerationParams{})
|
||||
if err != nil {
|
||||
return content
|
||||
}
|
||||
var sb strings.Builder
|
||||
for ev := range ch {
|
||||
if ev.Content != "" {
|
||||
sb.WriteString(ev.Content)
|
||||
}
|
||||
if ev.Done {
|
||||
break
|
||||
}
|
||||
}
|
||||
if s := strings.TrimSpace(sb.String()); s != "" {
|
||||
return fmt.Sprintf("[summary of %s result]: %s", name, s)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
// removeCancelledToolResults filters out tool results that were cancelled due
|
||||
// to interrupt, keeping completed work. Also removes the corresponding tool
|
||||
// calls from assistant messages to maintain a valid message sequence.
|
||||
|
|
@ -371,6 +349,21 @@ func flattenToolMessages(messages []backend.Message) []backend.Message {
|
|||
return out
|
||||
}
|
||||
|
||||
// injectPreamble ensures the preamble system message is at history position 0.
|
||||
// If a system message already exists there, its Content is replaced; otherwise
|
||||
// one is prepended.
|
||||
func (s *History) injectPreamble(preamble string) {
|
||||
if preamble == "" {
|
||||
return
|
||||
}
|
||||
msg := backend.Message{Role: "system", Content: preamble}
|
||||
if len(s.messages) > 0 && s.messages[0].Role == "system" {
|
||||
s.messages[0] = msg
|
||||
} else {
|
||||
s.messages = append([]backend.Message{msg}, s.messages...)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *History) appendUserMessage(content string) {
|
||||
s.messages = append(s.messages, backend.Message{Role: "user", Content: content})
|
||||
}
|
||||
|
|
@ -417,16 +410,62 @@ func (s *History) estimateTokens() int {
|
|||
}
|
||||
|
||||
// stripCold summarizes large tool-result messages outside the hot tail
|
||||
// using an LLM call. Messages in the last hotTailSize slots are left verbatim.
|
||||
// using a single batched LLM call. Messages in the last hotTailSize slots
|
||||
// are left verbatim.
|
||||
func (s *History) stripCold(ctx context.Context, b backend.Backend) {
|
||||
hot := len(s.messages) - hotTailSize
|
||||
if hot < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Collect cold-zone tool results exceeding 200 chars.
|
||||
type item struct {
|
||||
id int
|
||||
idx int // index into s.messages
|
||||
orig string
|
||||
}
|
||||
var items []item
|
||||
for i := range s.messages {
|
||||
if i >= hot {
|
||||
break
|
||||
}
|
||||
m := &s.messages[i]
|
||||
if m.Role == "tool" && len(m.Content) > 200 {
|
||||
m.Content = llmSummarizeToolResult(ctx, b, m.ToolCallID, m.Content)
|
||||
items = append(items, item{id: len(items), idx: i, orig: m.Content})
|
||||
}
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Build a single prompt listing each item.
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Compress each of the following tool results to 1-3 sentences preserving key facts, file paths, values, and findings.\n\nReturn a JSON object where each key is the id and the value is the summary, for example:\n{\"0\": \"summarized result 0\", \"1\": \"summarized result 1\"}\n\nOnly include keys that were in the input.\n\n")
|
||||
for _, it := range items {
|
||||
fmt.Fprintf(&sb, "---\nid=%d:\n%s\n", it.id, it.orig)
|
||||
}
|
||||
|
||||
ch, err := b.ChatStream(ctx, []backend.Message{{Role: "user", Content: sb.String()}}, nil, backend.GenerationParams{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var resp strings.Builder
|
||||
for ev := range ch {
|
||||
if ev.Content != "" {
|
||||
resp.WriteString(ev.Content)
|
||||
}
|
||||
if ev.Done {
|
||||
break
|
||||
}
|
||||
}
|
||||
summaries := make(map[string]string)
|
||||
json.Unmarshal([]byte(resp.String()), &summaries)
|
||||
|
||||
// Apply summaries back into history.
|
||||
for _, it := range items {
|
||||
key := fmt.Sprintf("%d", it.id)
|
||||
if summary, ok := summaries[key]; ok && summary != "" {
|
||||
s.messages[it.idx].Content = summary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,9 +126,6 @@ func (ag *Agent) run(ctx context.Context) error {
|
|||
}
|
||||
|
||||
history := h.history()
|
||||
if preamble := rt.PreambleString(); preamble != "" {
|
||||
history = append([]backend.Message{{Role: "system", Content: preamble}}, history...)
|
||||
}
|
||||
|
||||
responseID := paths.NewUUID()
|
||||
content, reasoning, toolCalls, stopReason, err := ag.streamResponse(ctx, history, responseID, step)
|
||||
|
|
|
|||
|
|
@ -197,6 +197,11 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
|
|||
ag.history.resetTurnAccumulators()
|
||||
}
|
||||
|
||||
// Inject the preamble system message into history before running.
|
||||
if ag.history != nil {
|
||||
ag.history.injectPreamble(ag.runtime.PreambleString())
|
||||
}
|
||||
|
||||
// Run the turn, retrying once after compaction on context overflow.
|
||||
var (
|
||||
overflowRetried bool
|
||||
|
|
@ -229,6 +234,8 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
|
|||
actCtx, actCancel = context.WithCancelCause(ctx)
|
||||
handle = &actionHandle{cancel: actCancel}
|
||||
ag.currentAction.Store(handle)
|
||||
// Re-inject preamble — compaction strips it from history.
|
||||
ag.history.injectPreamble(ag.runtime.PreambleString())
|
||||
continue
|
||||
}
|
||||
break
|
||||
|
|
|
|||
Loading…
Reference in New Issue