From 549f717263b3fc4520ec1fede5009ddc951db057 Mon Sep 17 00:00:00 2001 From: iceymoss <114280774+iceymoss@users.noreply.github.com> Date: Fri, 24 Apr 2026 20:40:08 +0800 Subject: [PATCH] fix(agent): implement OnRetry logging with structured retry fields (#2700) --- internal/agent/agent.go | 19 ++++++++++++++++++- internal/agent/agent_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index d62ef16bc1b4c7380e40ba05d7718d5004e360bd..5e32751219c213472ee6841432822e1f2120d87c 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -378,7 +378,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy return a.messages.Update(ctx, *currentAssistant) }, OnRetry: func(err *fantasy.ProviderError, delay time.Duration) { - // TODO: implement + slog.Warn("Provider request failed, retrying", providerRetryLogFields(err, delay)...) }, OnToolCall: func(tc fantasy.ToolCallContent) error { toolCall := message.ToolCall{ @@ -1318,3 +1318,20 @@ func buildSummaryPrompt(todos []session.Todo) string { } return sb.String() } + +func providerRetryLogFields(err *fantasy.ProviderError, delay time.Duration) []any { + fields := []any{ + "retry_delay", delay.String(), + } + if err == nil { + return fields + } + fields = append(fields, "status_code", err.StatusCode) + if err.Title != "" { + fields = append(fields, "title", err.Title) + } + if err.Message != "" { + fields = append(fields, "message", err.Message) + } + return fields +} diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index e49e2900da8d51b99eaeaf946877b33eab5f09ab..263a0691ada91d5c44fa419eaee8f6ad02891cf9 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -8,6 +8,7 @@ import ( "runtime" "strings" "testing" + "time" "charm.land/fantasy" "charm.land/x/vcr" @@ -793,3 +794,34 @@ func TestPreparePrompt_OrphanedToolUseMixed(t *testing.T) { } require.Equal(t, 1, syntheticCount, "expected exactly one synthetic result for the orphaned call") } + +func TestProviderRetryLogFields(t *testing.T) { + t.Run("nil provider error", func(t *testing.T) { + fields := providerRetryLogFields(nil, 2*time.Second) + require.Equal(t, []any{"retry_delay", "2s"}, fields) + }) + + t.Run("provider error with title and message", func(t *testing.T) { + fields := providerRetryLogFields(&fantasy.ProviderError{ + StatusCode: 429, + Title: "rate limit", + Message: "too many requests", + }, 1500*time.Millisecond) + require.Equal(t, []any{ + "retry_delay", "1.5s", + "status_code", 429, + "title", "rate limit", + "message", "too many requests", + }, fields) + }) + + t.Run("provider error without optional strings", func(t *testing.T) { + fields := providerRetryLogFields(&fantasy.ProviderError{ + StatusCode: 503, + }, time.Second) + require.Equal(t, []any{ + "retry_delay", "1s", + "status_code", 503, + }, fields) + }) +}