Skip to content

Commit 568544c

Browse files
authored
Merge pull request #74 from zhongweili/fix/cron-firejob-and-llm-retry
fix(cron): include AccountID in fireJob InboundMessage + LLM retry
2 parents ce8a9ba + b892d3f commit 568544c

1 file changed

Lines changed: 53 additions & 4 deletions

File tree

internal/agent/loop.go

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto/sha256"
66
"encoding/json"
7+
"errors"
78
"fmt"
89
"log/slog"
910
"os"
@@ -1776,6 +1777,50 @@ func (a *Agent) flushLeftoverSteer(sess *session.Session) {
17761777
}
17771778
}
17781779

1780+
// llmRetry wraps an LLM call with retry logic for transient errors (network
1781+
// glitches, server 5xx, EOF). Context cancellation / deadline exceeded are
1782+
// treated as terminal — there's no point retrying when the caller has gone
1783+
// away or the deadline has passed. Uses exponential backoff (1s, 4s, 9s)
1784+
// across up to wechatLLMRetryAttempts calls.
1785+
//
1786+
// The label argument is used for structured logging (typically a.name).
1787+
const llmRetryAttempts = 3
1788+
1789+
func llmRetry(ctx context.Context, label string, fn func(context.Context) (*provider.Response, error)) (*provider.Response, error) {
1790+
var lastErr error
1791+
for attempt := 1; attempt <= llmRetryAttempts; attempt++ {
1792+
resp, err := fn(ctx)
1793+
if err == nil {
1794+
if attempt > 1 {
1795+
slog.Info("LLM call succeeded after retries",
1796+
"agent", label, "attempts", attempt)
1797+
}
1798+
return resp, nil
1799+
}
1800+
lastErr = err
1801+
1802+
// Context errors are terminal — don't retry.
1803+
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
1804+
return nil, err
1805+
}
1806+
1807+
if attempt < llmRetryAttempts {
1808+
backoff := time.Duration(attempt*attempt) * time.Second // 1s, 4s, 9s
1809+
slog.Warn("LLM call failed, retrying",
1810+
"agent", label, "attempt", attempt,
1811+
"max", llmRetryAttempts, "backoff", backoff, "error", err)
1812+
select {
1813+
case <-time.After(backoff):
1814+
case <-ctx.Done():
1815+
return nil, errors.Join(lastErr, ctx.Err())
1816+
}
1817+
}
1818+
}
1819+
slog.Error("LLM call failed after all retries",
1820+
"agent", label, "attempts", llmRetryAttempts, "error", lastErr)
1821+
return nil, lastErr
1822+
}
1823+
17791824
// HandleMessage processes an inbound message through the ReAct loop.
17801825
func (a *Agent) HandleMessage(ctx context.Context, msg bus.InboundMessage) string {
17811826
// Check for slash commands first. Empty reply means "handled but
@@ -2039,14 +2084,16 @@ func (a *Agent) HandleMessage(ctx context.Context, msg bus.InboundMessage) strin
20392084
})
20402085
}
20412086
dumpLLMRequest(a.name, a.model, llmMessages, callTools)
2042-
resp, err := a.streamChatToResponse(ctx, llmMessages, callTools)
2087+
resp, err := llmRetry(ctx, a.name, func(ctx context.Context) (*provider.Response, error) {
2088+
return a.streamChatToResponse(ctx, llmMessages, callTools)
2089+
})
20432090

20442091
// Hook: AfterModelCall
20452092
hcAfter := &HookContext{AgentName: a.name, Point: AfterModelCall, Messages: messages, Response: resp, Error: err, StartTime: hcBefore.StartTime, Channel: msg.Channel, AccountID: msg.AccountID, ChatID: msg.ChatID, UserID: a.ownerUserID, GoalSessionKey: a.registry.GoalSessionKey()}
20462093
a.hooks.Run(ctx, hcAfter)
20472094

20482095
if err != nil {
2049-
slog.Error("LLM chat failed", "agent", a.name, "error", err)
2096+
slog.Error("LLM chat failed after retries", "agent", a.name, "error", err)
20502097
emitEvent(ctx, ChatEvent{Type: "error", Data: map[string]any{"message": err.Error()}})
20512098
emitEvent(ctx, ChatEvent{Type: "done"})
20522099
return "Sorry, I encountered an error processing your request."
@@ -2693,13 +2740,15 @@ func (a *Agent) HandleMessageStream(ctx context.Context, msg bus.InboundMessage)
26932740
a.hooks.Run(ctx, hcBefore)
26942741

26952742
dumpLLMRequest(a.name, a.model, messages, toolDefs)
2696-
resp, err := a.provider.Chat(ctx, messages, toolDefs, a.model, a.maxTokens, a.temperature)
2743+
resp, err := llmRetry(ctx, a.name, func(ctx context.Context) (*provider.Response, error) {
2744+
return a.provider.Chat(ctx, messages, toolDefs, a.model, a.maxTokens, a.temperature)
2745+
})
26972746

26982747
hcAfter := &HookContext{AgentName: a.name, Point: AfterModelCall, Messages: messages, Response: resp, Error: err, StartTime: hcBefore.StartTime, Channel: msg.Channel, AccountID: msg.AccountID, ChatID: msg.ChatID, UserID: a.ownerUserID, GoalSessionKey: a.registry.GoalSessionKey()}
26992748
a.hooks.Run(ctx, hcAfter)
27002749

27012750
if err != nil {
2702-
slog.Error("LLM chat failed", "agent", a.name, "error", err)
2751+
slog.Error("LLM chat failed after retries", "agent", a.name, "error", err)
27032752
return a.stringStream("Sorry, I encountered an error processing your request.")
27042753
}
27052754
a.meterTokens(ctx, sess.Key(), resp.Usage, 0)

0 commit comments

Comments
 (0)