From d20d971e94f193e418d1c9f88cb15f45105c33bc Mon Sep 17 00:00:00 2001 From: Greg Slepak Date: Sat, 13 Jun 2026 20:49:22 -0700 Subject: [PATCH 1/6] fix(agent): preserve sub-agent output when parent cost update fails --- internal/agent/coordinator.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 86ca09e3bf..5d1d114681 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -1268,9 +1268,14 @@ func (c *coordinator) runSubAgent(ctx context.Context, params subAgentParams) (f return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to generate response: %s", err)), nil } - // Update parent session cost + // Update parent session cost on a best-effort basis. A failure here must + // not discard the sub-agent output that was already produced. if err := c.updateParentSessionCost(ctx, session.ID, params.SessionID); err != nil { - return fantasy.ToolResponse{}, err + slog.Warn("Failed to update parent session cost", + "child_session", session.ID, + "parent_session", params.SessionID, + "error", err, + ) } return fantasy.NewTextResponse(result.Response.Content.Text()), nil From 24c4d14a9d2fd70f1c45ca93b7f74294b685520d Mon Sep 17 00:00:00 2001 From: Greg Slepak Date: Sat, 13 Jun 2026 20:53:51 -0700 Subject: [PATCH 2/6] fix(agent): recover sub-agent output from fallback sources --- internal/agent/coordinator.go | 49 ++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 5d1d114681..4a13fd6183 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -1278,7 +1278,54 @@ func (c *coordinator) runSubAgent(ctx context.Context, params subAgentParams) (f ) } - return fantasy.NewTextResponse(result.Response.Content.Text()), nil + return fantasy.NewTextResponse(c.subAgentOutput(ctx, session.ID, result)), nil +} + +func (c *coordinator) subAgentOutput(ctx context.Context, sessionID string, result *fantasy.AgentResult) string { + if result != nil { + if text := result.Response.Content.Text(); strings.TrimSpace(text) != "" { + return text + } + + var texts []string + for _, step := range result.Steps { + texts = append(texts, responseContentText(step.Response.Content)...) + } + if len(texts) > 0 { + return strings.Join(texts, "\n\n") + } + } + + if c.messages != nil { + messages, err := c.messages.List(ctx, sessionID) + if err != nil { + slog.Warn("Failed to list sub-agent messages", "session", sessionID, "error", err) + } else { + for i := len(messages) - 1; i >= 0; i-- { + msg := messages[i] + if msg.Role != message.Assistant { + continue + } + if text := msg.Content().String(); strings.TrimSpace(text) != "" { + return text + } + } + } + } + + return "Sub-agent completed but produced no text output." +} + +func responseContentText(content fantasy.ResponseContent) []string { + texts := make([]string, 0) + for _, part := range content { + textContent, ok := fantasy.AsContentType[fantasy.TextContent](part) + if !ok || strings.TrimSpace(textContent.Text) == "" { + continue + } + texts = append(texts, textContent.Text) + } + return texts } // updateParentSessionCost accumulates the cost from a child session to its parent session. From e73ac97f3644e475ca85eb837b24ff37eea64ff8 Mon Sep 17 00:00:00 2001 From: Greg Slepak Date: Sat, 13 Jun 2026 21:00:12 -0700 Subject: [PATCH 3/6] test(agent): cover sub-agent output recovery --- internal/agent/coordinator.go | 2 +- internal/agent/coordinator_test.go | 128 +++++++++++++++++++++++++++++ internal/server/recover_test.go | 8 +- 3 files changed, 133 insertions(+), 5 deletions(-) diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 4a13fd6183..8326803359 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -1289,7 +1289,7 @@ func (c *coordinator) subAgentOutput(ctx context.Context, sessionID string, resu var texts []string for _, step := range result.Steps { - texts = append(texts, responseContentText(step.Response.Content)...) + texts = append(texts, responseContentText(step.Content)...) } if len(texts) > 0 { return strings.Join(texts, "\n\n") diff --git a/internal/agent/coordinator_test.go b/internal/agent/coordinator_test.go index c522ef5de1..8be1d1ca05 100644 --- a/internal/agent/coordinator_test.go +++ b/internal/agent/coordinator_test.go @@ -10,6 +10,7 @@ import ( "charm.land/fantasy/providers/anthropic" "charm.land/fantasy/providers/bedrock" "github.com/charmbracelet/crush/internal/config" + "github.com/charmbracelet/crush/internal/message" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -54,6 +55,7 @@ func newTestCoordinator(t *testing.T, env fakeEnv, providerID string, providerCf return &coordinator{ cfg: cfg, sessions: env.sessions, + messages: env.messages, } } @@ -83,6 +85,22 @@ func agentResultWithText(text string) *fantasy.AgentResult { } } +func agentResultWithStepTexts(texts ...string) *fantasy.AgentResult { + result := &fantasy.AgentResult{ + Steps: make([]fantasy.StepResult, 0, len(texts)), + } + for _, text := range texts { + result.Steps = append(result.Steps, fantasy.StepResult{ + Response: fantasy.Response{ + Content: fantasy.ResponseContent{ + fantasy.TextContent{Text: text}, + }, + }, + }) + } + return result +} + func TestRunSubAgent(t *testing.T) { const providerID = "test-provider" providerCfg := config.ProviderConfig{ID: providerID} @@ -113,6 +131,116 @@ func TestRunSubAgent(t *testing.T) { assert.False(t, resp.IsError) }) + t.Run("cost update failure preserves output", func(t *testing.T) { + env := testEnv(t) + coord := newTestCoordinator(t, env, providerID, providerCfg) + + agent := newMockAgent(providerID, 4096, func(_ context.Context, _ SessionAgentCall) (*fantasy.AgentResult, error) { + return agentResultWithText("output before cost failure"), nil + }) + + resp, err := coord.runSubAgent(t.Context(), subAgentParams{ + Agent: agent, + SessionID: "missing-parent-session", + AgentMessageID: "msg-1", + ToolCallID: "call-1", + Prompt: "test", + SessionTitle: "Test", + }) + require.NoError(t, err) + assert.False(t, resp.IsError) + assert.Equal(t, "output before cost failure", resp.Content) + }) + + t.Run("persisted assistant message fallback", func(t *testing.T) { + env := testEnv(t) + coord := newTestCoordinator(t, env, providerID, providerCfg) + + parentSession, err := env.sessions.Create(t.Context(), "Parent") + require.NoError(t, err) + + agent := newMockAgent(providerID, 4096, func(ctx context.Context, call SessionAgentCall) (*fantasy.AgentResult, error) { + _, err := env.messages.Create(ctx, call.SessionID, message.CreateMessageParams{ + Role: message.Assistant, + Parts: []message.ContentPart{ + message.TextContent{Text: "persisted assistant output"}, + }, + }) + require.NoError(t, err) + return &fantasy.AgentResult{}, nil + }) + + resp, err := coord.runSubAgent(t.Context(), subAgentParams{ + Agent: agent, + SessionID: parentSession.ID, + AgentMessageID: "msg-1", + ToolCallID: "call-1", + Prompt: "test", + SessionTitle: "Test", + }) + require.NoError(t, err) + assert.False(t, resp.IsError) + assert.Equal(t, "persisted assistant output", resp.Content) + }) + + t.Run("aggregates earlier step output", func(t *testing.T) { + env := testEnv(t) + coord := newTestCoordinator(t, env, providerID, providerCfg) + + parentSession, err := env.sessions.Create(t.Context(), "Parent") + require.NoError(t, err) + + agent := newMockAgent(providerID, 4096, func(_ context.Context, _ SessionAgentCall) (*fantasy.AgentResult, error) { + return agentResultWithStepTexts("first step", "second step"), nil + }) + + resp, err := coord.runSubAgent(t.Context(), subAgentParams{ + Agent: agent, + SessionID: parentSession.ID, + AgentMessageID: "msg-1", + ToolCallID: "call-1", + Prompt: "test", + SessionTitle: "Test", + }) + require.NoError(t, err) + assert.False(t, resp.IsError) + assert.Equal(t, "first step\n\nsecond step", resp.Content) + }) + + t.Run("final response text wins over fallback sources", func(t *testing.T) { + env := testEnv(t) + coord := newTestCoordinator(t, env, providerID, providerCfg) + + parentSession, err := env.sessions.Create(t.Context(), "Parent") + require.NoError(t, err) + + agent := newMockAgent(providerID, 4096, func(ctx context.Context, call SessionAgentCall) (*fantasy.AgentResult, error) { + _, err := env.messages.Create(ctx, call.SessionID, message.CreateMessageParams{ + Role: message.Assistant, + Parts: []message.ContentPart{ + message.TextContent{Text: "persisted fallback"}, + }, + }) + require.NoError(t, err) + + result := agentResultWithText("final response") + result.Steps = agentResultWithStepTexts("earlier step").Steps + return result, nil + }) + + resp, err := coord.runSubAgent(t.Context(), subAgentParams{ + Agent: agent, + SessionID: parentSession.ID, + AgentMessageID: "msg-1", + ToolCallID: "call-1", + Prompt: "test", + SessionTitle: "Test", + }) + require.NoError(t, err) + assert.False(t, resp.IsError) + assert.Equal(t, "final response", resp.Content) + }) + t.Run("ModelCfg.MaxTokens overrides default", func(t *testing.T) { env := testEnv(t) coord := newTestCoordinator(t, env, providerID, providerCfg) diff --git a/internal/server/recover_test.go b/internal/server/recover_test.go index 2bbd61efd5..45b616677b 100644 --- a/internal/server/recover_test.go +++ b/internal/server/recover_test.go @@ -23,7 +23,7 @@ func TestRecoverHandler_PanicReturns500(t *testing.T) { })) rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/test", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test", nil) h.ServeHTTP(rec, req) require.Equal(t, http.StatusInternalServerError, rec.Code) @@ -48,7 +48,7 @@ func TestRecoverHandler_NoPanicPassthrough(t *testing.T) { })) rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/test", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test", nil) h.ServeHTTP(rec, req) require.Equal(t, http.StatusTeapot, rec.Code) @@ -71,7 +71,7 @@ func TestRecoverHandler_PanicAfterWriteHeader(t *testing.T) { })) rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/test", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test", nil) require.NotPanics(t, func() { h.ServeHTTP(rec, req) }) require.Equal(t, http.StatusOK, rec.Code) require.Equal(t, "partial", rec.Body.String()) @@ -89,6 +89,6 @@ func TestRecoverHandler_AbortHandlerPropagates(t *testing.T) { })) rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/test", nil) + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test", nil) require.PanicsWithValue(t, http.ErrAbortHandler, func() { h.ServeHTTP(rec, req) }) } From 25eb07323762bb94363554efed3cf516678e8124 Mon Sep 17 00:00:00 2001 From: Greg Slepak Date: Sun, 14 Jun 2026 12:38:37 -0700 Subject: [PATCH 4/6] fix(agent): preserve multi-block sub-agent final responses --- internal/agent/coordinator.go | 4 ++-- internal/agent/coordinator_test.go | 38 +++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 8326803359..443505ac5c 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -1283,8 +1283,8 @@ func (c *coordinator) runSubAgent(ctx context.Context, params subAgentParams) (f func (c *coordinator) subAgentOutput(ctx context.Context, sessionID string, result *fantasy.AgentResult) string { if result != nil { - if text := result.Response.Content.Text(); strings.TrimSpace(text) != "" { - return text + if texts := responseContentText(result.Response.Content); len(texts) > 0 { + return strings.Join(texts, "\n\n") } var texts []string diff --git a/internal/agent/coordinator_test.go b/internal/agent/coordinator_test.go index 8be1d1ca05..0d0130a426 100644 --- a/internal/agent/coordinator_test.go +++ b/internal/agent/coordinator_test.go @@ -76,11 +76,17 @@ func newMockAgent(providerID string, maxTokens int64, runFunc func(context.Conte // agentResultWithText creates a minimal AgentResult with the given text response. func agentResultWithText(text string) *fantasy.AgentResult { + return agentResultWithTextBlocks(text) +} + +func agentResultWithTextBlocks(texts ...string) *fantasy.AgentResult { + content := make(fantasy.ResponseContent, 0, len(texts)) + for _, text := range texts { + content = append(content, fantasy.TextContent{Text: text}) + } return &fantasy.AgentResult{ Response: fantasy.Response{ - Content: fantasy.ResponseContent{ - fantasy.TextContent{Text: text}, - }, + Content: content, }, } } @@ -241,6 +247,32 @@ func TestRunSubAgent(t *testing.T) { assert.Equal(t, "final response", resp.Content) }) + t.Run("aggregates multi-block final response", func(t *testing.T) { + env := testEnv(t) + coord := newTestCoordinator(t, env, providerID, providerCfg) + + parentSession, err := env.sessions.Create(t.Context(), "Parent") + require.NoError(t, err) + + agent := newMockAgent(providerID, 4096, func(_ context.Context, _ SessionAgentCall) (*fantasy.AgentResult, error) { + result := agentResultWithTextBlocks("first final block", "second final block") + result.Steps = agentResultWithStepTexts("earlier step").Steps + return result, nil + }) + + resp, err := coord.runSubAgent(t.Context(), subAgentParams{ + Agent: agent, + SessionID: parentSession.ID, + AgentMessageID: "msg-1", + ToolCallID: "call-1", + Prompt: "test", + SessionTitle: "Test", + }) + require.NoError(t, err) + assert.False(t, resp.IsError) + assert.Equal(t, "first final block\n\nsecond final block", resp.Content) + }) + t.Run("ModelCfg.MaxTokens overrides default", func(t *testing.T) { env := testEnv(t) coord := newTestCoordinator(t, env, providerID, providerCfg) From a43561d869a441b467ee699ad0b57b1a05277d84 Mon Sep 17 00:00:00 2001 From: Greg Slepak Date: Sun, 14 Jun 2026 13:37:53 -0700 Subject: [PATCH 5/6] fix(agent): flag sub-agent no-output as error; deflake jq pre-cancel test Return the sub-agent diagnostic fallback as an error response (IsError=true, nil Go error) so the parent LLM can distinguish "no usable output" from a real answer; subAgentOutput now returns (text, ok) with ok=false only for the last-resort diagnostic. Also replace the brittle 100ms wall-clock assertion in TestJQ_CtxCancel_PreCancel with an invariant-based reader that fails if read, fixing flaky Windows CI under -race. --- internal/agent/coordinator.go | 24 ++++++++++++++++++------ internal/agent/coordinator_test.go | 28 ++++++++++++++++++++++++++++ internal/shell/jq_test.go | 24 ++++++++++++++++++------ 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 443505ac5c..f5c890ad77 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -1278,13 +1278,25 @@ func (c *coordinator) runSubAgent(ctx context.Context, params subAgentParams) (f ) } - return fantasy.NewTextResponse(c.subAgentOutput(ctx, session.ID, result)), nil + // subAgentOutput signals via ok whether it recovered real sub-agent text. + // When ok is false it only produced the diagnostic fallback, so report it + // as a soft tool error (IsError=true, nil Go error) to let the parent LLM + // distinguish "no output" from a real answer without aborting the turn. + output, ok := c.subAgentOutput(ctx, session.ID, result) + if !ok { + return fantasy.NewTextErrorResponse(output), nil + } + return fantasy.NewTextResponse(output), nil } -func (c *coordinator) subAgentOutput(ctx context.Context, sessionID string, result *fantasy.AgentResult) string { +// subAgentOutput extracts the sub-agent's textual output, returning ok=true +// when real text was recovered from any layer (final response → aggregated +// steps → persisted assistant message). It returns ok=false only for the +// last-resort diagnostic fallback, signalling that no usable output exists. +func (c *coordinator) subAgentOutput(ctx context.Context, sessionID string, result *fantasy.AgentResult) (string, bool) { if result != nil { if texts := responseContentText(result.Response.Content); len(texts) > 0 { - return strings.Join(texts, "\n\n") + return strings.Join(texts, "\n\n"), true } var texts []string @@ -1292,7 +1304,7 @@ func (c *coordinator) subAgentOutput(ctx context.Context, sessionID string, resu texts = append(texts, responseContentText(step.Content)...) } if len(texts) > 0 { - return strings.Join(texts, "\n\n") + return strings.Join(texts, "\n\n"), true } } @@ -1307,13 +1319,13 @@ func (c *coordinator) subAgentOutput(ctx context.Context, sessionID string, resu continue } if text := msg.Content().String(); strings.TrimSpace(text) != "" { - return text + return text, true } } } } - return "Sub-agent completed but produced no text output." + return "Sub-agent completed but produced no text output.", false } func responseContentText(content fantasy.ResponseContent) []string { diff --git a/internal/agent/coordinator_test.go b/internal/agent/coordinator_test.go index 0d0130a426..8fc1a6a901 100644 --- a/internal/agent/coordinator_test.go +++ b/internal/agent/coordinator_test.go @@ -273,6 +273,34 @@ func TestRunSubAgent(t *testing.T) { assert.Equal(t, "first final block\n\nsecond final block", resp.Content) }) + t.Run("no recoverable output returns error response", func(t *testing.T) { + env := testEnv(t) + coord := newTestCoordinator(t, env, providerID, providerCfg) + + parentSession, err := env.sessions.Create(t.Context(), "Parent") + require.NoError(t, err) + + // Empty result, no persisted assistant message: no layer can recover + // real text, so the diagnostic fallback must surface as a soft tool + // error (IsError=true) with a nil Go error. + agent := newMockAgent(providerID, 4096, func(_ context.Context, _ SessionAgentCall) (*fantasy.AgentResult, error) { + return &fantasy.AgentResult{}, nil + }) + + resp, err := coord.runSubAgent(t.Context(), subAgentParams{ + Agent: agent, + SessionID: parentSession.ID, + AgentMessageID: "msg-1", + ToolCallID: "call-1", + Prompt: "test", + SessionTitle: "Test", + }) + require.NoError(t, err) + assert.True(t, resp.IsError) + assert.NotEmpty(t, resp.Content) + assert.Equal(t, "Sub-agent completed but produced no text output.", resp.Content) + }) + t.Run("ModelCfg.MaxTokens overrides default", func(t *testing.T) { env := testEnv(t) coord := newTestCoordinator(t, env, providerID, providerCfg) diff --git a/internal/shell/jq_test.go b/internal/shell/jq_test.go index 9b1ecb3302..73a33f58c3 100644 --- a/internal/shell/jq_test.go +++ b/internal/shell/jq_test.go @@ -155,28 +155,40 @@ func TestJQ_CtxCancel_MidReadAll(t *testing.T) { } } +// failOnReadReader fails the test if Read is ever called. It proves that a +// code path short-circuited before touching the input, without relying on +// wall-clock timing. +type failOnReadReader struct { + t *testing.T +} + +func (r failOnReadReader) Read(p []byte) (int, error) { + r.t.Error("Read called; outer guard did not short-circuit before io.ReadAll") + return 0, io.EOF +} + // TestJQ_CtxCancel_PreCancel verifies the fast-fail path: a ctx already // cancelled before handleJQ is called returns context.Canceled // immediately via the outer-loop guard, never entering io.ReadAll. // Complements TestJQ_CtxCancel_MidReadAll. +// +// The invariant — not wall-clock timing — is what proves the guard fired: +// the input reader fails the test if it is ever read from. A previous +// version asserted a 100ms ceiling, which measured scheduler latency rather +// than the guard and flaked on contended Windows CI runners under -race. func TestJQ_CtxCancel_PreCancel(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(t.Context()) cancel() - start := time.Now() err := handleJQ(ctx, []string{"jq", "-R", "."}, - bytes.NewReader(bytes.Repeat([]byte("a"), 1024)), + failOnReadReader{t: t}, io.Discard, io.Discard) - elapsed := time.Since(start) if !errors.Is(err, context.Canceled) { t.Fatalf("expected context.Canceled, got %v", err) } - if elapsed > 100*time.Millisecond { - t.Fatalf("pre-cancel fast-fail took %v; outer guard is not firing", elapsed) - } } // TestJQ_Success confirms the ctx-aware refactor did not regress the From aed4c193a361a5d68e96a19b282d8264bafd2cd1 Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Mon, 15 Jun 2026 15:59:42 -0400 Subject: [PATCH 6/6] refactor(agent): simplify sub-agent output extraction and test helpers Collapse agentResultWithTextBlocks into agentResultWithText and fix subAgentOutput doc to match actual fantasy behavior. --- go.mod | 62 +++++++------ go.sum | 134 ++++++++++++++--------------- internal/agent/coordinator.go | 62 ++----------- internal/agent/coordinator_test.go | 116 +++---------------------- 4 files changed, 112 insertions(+), 262 deletions(-) diff --git a/go.mod b/go.mod index ae40d2fea1..31c12e7e9b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( charm.land/bubbletea/v2 v2.0.7 charm.land/catwalk v0.44.14 charm.land/fang/v2 v2.0.1 - charm.land/fantasy v0.31.0 + charm.land/fantasy v0.31.1 charm.land/glamour/v2 v2.0.1 charm.land/lipgloss/v2 v2.0.4 charm.land/log/v2 v2.0.0 @@ -67,10 +67,10 @@ require ( github.com/tidwall/sjson v1.2.5 github.com/zeebo/xxh3 v1.1.0 go.uber.org/goleak v1.3.0 - golang.org/x/net v0.55.0 - golang.org/x/sync v0.20.0 - golang.org/x/sys v0.45.0 - golang.org/x/text v0.37.0 + golang.org/x/net v0.56.0 + golang.org/x/sync v0.21.0 + golang.org/x/sys v0.46.0 + golang.org/x/text v0.38.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.52.0 @@ -88,21 +88,21 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect - github.com/aws/aws-sdk-go-v2 v1.41.9 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 // indirect - github.com/aws/aws-sdk-go-v2/config v1.32.20 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.19 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 // indirect - github.com/aws/smithy-go v1.26.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.25 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.24 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect + github.com/aws/smithy-go v1.27.2 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.2 // indirect @@ -116,11 +116,11 @@ require ( github.com/dlclark/regexp2/v2 v2.1.1 // indirect github.com/ebitengine/purego v0.10.1 // indirect github.com/esiqveland/notify v0.13.3 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.9.0 // indirect - github.com/go-json-experiment/json v0.0.0-20260520185125-572e7c383686 // indirect + github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -147,10 +147,8 @@ require ( github.com/jackmordaunt/icns/v3 v3.0.1 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/kaptinlin/go-i18n v0.4.9 // indirect - github.com/kaptinlin/jsonpointer v0.4.25 // indirect - github.com/kaptinlin/jsonschema v0.7.15 // indirect - github.com/kaptinlin/messageformat-go v0.6.4 // indirect + github.com/kaptinlin/jsonpointer v0.4.26 // indirect + github.com/kaptinlin/jsonschema v0.8.0 // indirect github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect @@ -197,17 +195,17 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect - golang.org/x/crypto v0.52.0 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect - golang.org/x/image v0.38.0 // indirect + golang.org/x/image v0.42.0 // indirect golang.org/x/mod v0.36.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/term v0.43.0 // indirect + golang.org/x/term v0.44.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.45.0 // indirect - google.golang.org/api v0.282.0 // indirect - google.golang.org/genai v1.58.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/api v0.284.0 // indirect + google.golang.org/genai v1.60.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect google.golang.org/grpc v1.81.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect diff --git a/go.sum b/go.sum index 047f67c650..aaf0b5d8ee 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ charm.land/catwalk v0.44.14 h1:VMTLK9L2qVHHv8cBLiBq4Wd6lfAGo8rTp8hxNHwYKHs= charm.land/catwalk v0.44.14/go.mod h1:bw6/oiChsa9Fkr8LgYn84KrbbJY2z1+m7Jmlmmv3B2A= charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY= charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII= -charm.land/fantasy v0.31.0 h1:ioLVRi7A8lZXR8mrCIeseuCcq0KqAak46revmGumnpc= -charm.land/fantasy v0.31.0/go.mod h1:lAE2gO68SrB1S5TrW5g0TRoxz9V+qJcg0Elx/uPWsDI= +charm.land/fantasy v0.31.1 h1:QypVgCd1zlBW1hqhss4sjTB4qk0cdBtEF8EvA+vnc3g= +charm.land/fantasy v0.31.1/go.mod h1:2Eqwxsoh5rIoAcfd52dEHBnkhMo53X9tNyonZscuP6k= charm.land/glamour/v2 v2.0.1 h1:xl+r00A4aJWU0z8fgwKd9fQQ4rsphqGUzuEiXZP5n+c= charm.land/glamour/v2 v2.0.1/go.mod h1:jo9z8XqVKPeEFMVdvCRLGk++RyJ3CdUwgNr7EvXLw3k= charm.land/lipgloss/v2 v2.0.4 h1:lcPeVtcp23SNra7lHy8iYE4UC2aIipVQ47sbGyyxR5Q= @@ -56,36 +56,36 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go-v2 v1.41.9 h1:/rYeyO2+HrMztAmxAq9++XJtFMqSIpSsNA0yDGALYq4= -github.com/aws/aws-sdk-go-v2 v1.41.9/go.mod h1:+HsoOEX80qAVUitj1A2DhCNTjmb3edVyuDypb6LNEeo= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 h1:h5+3VT69KUBK24grGuuA5saDJTj2IIjLb9au668Fo5I= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11/go.mod h1:dnakxebH6UwFvcvujL0LVggYQ8nEvBGjU4G/V79Nv94= -github.com/aws/aws-sdk-go-v2/config v1.32.20 h1:8VMDnWc/kEzxsI/1ngGM9mG81a8IGmIHD8KLcYGwagc= -github.com/aws/aws-sdk-go-v2/config v1.32.20/go.mod h1:PuwEpciweIXGULWeOeSTXtSbH4CW9mWdWrhdCKQI1sM= -github.com/aws/aws-sdk-go-v2/credentials v1.19.19 h1:yuFzSV1U0aRNYCQGVaTY2zW2M/L93pYHnXnrJUphYhU= -github.com/aws/aws-sdk-go-v2/credentials v1.19.19/go.mod h1:7y63L1kGzeoDlJaQ3Z578KrnmfBut96JjvJUzGwR+YE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 h1:0w6dCiO8iez+YKwRhRBlL1CH/E3GTfdkuzrwj1by8vo= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25/go.mod h1:9FDWUothyr5RCRAHc45XOiVCzUR8n/IhCYX+uVqw6vk= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 h1:Uii3frf9ztec/ABM2/FSH9/z7PLzxfpG8h4RpkUFflQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25/go.mod h1:G6kntsA2GorAxDPbap6xgB2F+amSLUF8GJTi7PUoX44= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 h1:r1+/l6m+WaUJF9HISEsNOLHSNj5EXYQxK8VX6Cz9NlA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25/go.mod h1:cKf+D+NMDK1LndD7BowHbBZPgR9V0/5HubH0PFWvA+c= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 h1:A1PmWU2zfkIm9EyFlJncFXL4W4phML+h8KjltUsCvNQ= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26/go.mod h1:dY4MRzXEizrD4hqtpKvWVGPX7QleSGGVY+EBolo1RmM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 h1:d5/908OJ4bXg8lyjeMPvXetEKqoDoLi5Owy1zNue3yg= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10/go.mod h1:a57l7Hwh+FWI+we50g5NPJHYUKeJKfXbc4w8SyXu8Ig= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 h1:dD3dhHNglpd98gs72my22Ndqi1hqQGllFFg1F+twfxg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25/go.mod h1:0yAbjPfd64gG7mj85RW+fMEYdfBgCRZw8g/oWcL1pjc= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 h1:1VwbP3qMNfxUDEXWki4rCE5iA+44VA1lokTz9HasGzw= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.1/go.mod h1:vUtyoSj0OPji3kjIVSc/GlKuWEiL33f/WFxl6dmpy/A= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 h1:N6pIsdFOW1Kd9S4KyFKXdGRBojPPxkP32+uHFWLv4Hc= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.19/go.mod h1:3gt5WJArFooNmyLONS+h/R4J+o86II8du38IgCwj9dE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 h1:hc+lBYiiTr8Zk4MTzIsQ92MeDWCIDvWGmzKUWOaBcOg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2/go.mod h1:hU6fqB3OJA6/ePheD47LQnxvjYk6br6PtQxs+Q9ojvk= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3SJXQNEgksORW3Js= -github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8= -github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= -github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= +github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM= +github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= +github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= +github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk= +github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-nativeclipboard v0.1.3 h1:FmAWHPTwneAixu7uGDn3cL42xPlUCdNp2J8egMn3P1k= github.com/aymanbagabas/go-nativeclipboard v0.1.3/go.mod h1:2o7MyZwwi4pmXXpOpvOS5FwaHyoCIUks0ktjUvB0EoE= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= @@ -171,8 +171,8 @@ github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMD github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o= github.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= @@ -184,8 +184,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= -github.com/go-json-experiment/json v0.0.0-20260520185125-572e7c383686 h1:NZBJxCpbHS1gzS6xAmyxbJznosZIIPk9IB42v62UvKA= -github.com/go-json-experiment/json v0.0.0-20260520185125-572e7c383686/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= +github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6 h1:nxP4pPoyqOAgX8lYDFCfl3DyKeXErCvSvhcyzwGV9CE= +github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -264,14 +264,10 @@ github.com/jordanella/go-ansi-paintbrush v0.0.0-20240728195301-b7ad996ecf3d h1:o github.com/jordanella/go-ansi-paintbrush v0.0.0-20240728195301-b7ad996ecf3d/go.mod h1:SV0W0APWP9MZ1/gfDQ/NzzTlWdIgYZ/ZbpN4d/UXRYw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/kaptinlin/go-i18n v0.4.9 h1:kxYNfExb1GG241BIPnQaD3YOvWkP7poHwfj8ctwIpkw= -github.com/kaptinlin/go-i18n v0.4.9/go.mod h1:15vHHYLhwo1stIdztJatujSttIY6sRFt+7v8K1ik1AA= -github.com/kaptinlin/jsonpointer v0.4.25 h1:iJ197e8n+WwqaqBsa53FqG3rPJCg5oijyFXEXNWWC3E= -github.com/kaptinlin/jsonpointer v0.4.25/go.mod h1:wVOBaXGGnP42YsMb6zev/3W5POTvspdNfh8DXzf8XS8= -github.com/kaptinlin/jsonschema v0.7.15 h1:5v9TBnGOm/DV4EEsC13dfR0UXid/3OcR7yAnpRlruE0= -github.com/kaptinlin/jsonschema v0.7.15/go.mod h1:5njr9isKoP9FnqZrFV1Iygx7QqjN3SBZ/AkgBl4Elug= -github.com/kaptinlin/messageformat-go v0.6.4 h1:6nC70fsqEn2xxg/Xoby2+Dk2r77kvxa3QNnYL/hsNcM= -github.com/kaptinlin/messageformat-go v0.6.4/go.mod h1:553UGZ1x5jmGtyH4pQKYwLGMyPm71deCoZICjq1DtR8= +github.com/kaptinlin/jsonpointer v0.4.26 h1:tw616yszHek+B3/GtDSia+uzBa3sLXGpmo4tYeMhBZw= +github.com/kaptinlin/jsonpointer v0.4.26/go.mod h1:wVOBaXGGnP42YsMb6zev/3W5POTvspdNfh8DXzf8XS8= +github.com/kaptinlin/jsonschema v0.8.0 h1:GhY966O2q3ZQsg1zkQj988KF2MADJ6EA7pKBMpGmb9A= +github.com/kaptinlin/jsonschema v0.8.0/go.mod h1:dxt7s98W5NEuWEwCnAwGrhYGQdaRLqXZImR28DuxcMU= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -333,8 +329,6 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= -github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= -github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= @@ -463,13 +457,13 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= -golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= +golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY= +golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -488,8 +482,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -499,8 +493,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -516,8 +510,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -529,8 +523,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -540,8 +534,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -555,16 +549,16 @@ golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.282.0 h1:WmJiSVqUnKqJCpJOx7YADbXaC+9DDsnGSfllFSj7R2I= -google.golang.org/api v0.282.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM= -google.golang.org/genai v1.58.0 h1:MNA3ZkRyr7MnRwZ9RNZ60p4+UMKV3yYRw6pyHq4pp0U= -google.golang.org/genai v1.58.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= -google.golang.org/genproto v0.0.0-20260526163538-3dc84a4a5aaa h1:mfj8IS4EA4VAR9a6QDVxTQkLY64iBybb5QI1B4pXrpE= -google.golang.org/genproto v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:fuT7yonGw1Iq2oa+YC0fyqPPQJkgo/54gPNC6VitOkI= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/api v0.284.0 h1:i+cKTgeQRcRySkP7QTl5PDO7/pAm8EcMFIUMlNbk4Vc= +google.golang.org/api v0.284.0/go.mod h1:AU44fU+XVZOCcd8uLaBIa/ZgzgPf/0qqY3+m7lQaado= +google.golang.org/genai v1.60.0 h1:uAkea4tYhCz1LlUmxdiOFAmlrLFaLs8PbXucgZHqHVo= +google.golang.org/genai v1.60.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU= +google.golang.org/genproto v0.0.0-20260610212136-7ab31c22f7ad h1:cYL1DPJAQr4JMvhfGao0PDXoaf03ifMljAuDyrbMBd0= +google.golang.org/genproto v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:cVHIikDNAdx8ISZeW+2rYkEMf3xn0GSaBYmVnWXQBUo= +google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad h1:3iLyITS/sySRwbUKoC7ogfj2Yr1Cjs0pfaRKj5U5HEw= +google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:KdNqO+rCIWgFumrNBSEDlDNrkrQnpkax7Tv1WxNY8V4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index f5c890ad77..8858134099 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -1278,66 +1278,18 @@ func (c *coordinator) runSubAgent(ctx context.Context, params subAgentParams) (f ) } - // subAgentOutput signals via ok whether it recovered real sub-agent text. - // When ok is false it only produced the diagnostic fallback, so report it - // as a soft tool error (IsError=true, nil Go error) to let the parent LLM - // distinguish "no output" from a real answer without aborting the turn. - output, ok := c.subAgentOutput(ctx, session.ID, result) - if !ok { - return fantasy.NewTextErrorResponse(output), nil + output := subAgentOutput(result) + if output == "" { + return fantasy.NewTextErrorResponse("Sub-agent completed but produced no text output."), nil } return fantasy.NewTextResponse(output), nil } -// subAgentOutput extracts the sub-agent's textual output, returning ok=true -// when real text was recovered from any layer (final response → aggregated -// steps → persisted assistant message). It returns ok=false only for the -// last-resort diagnostic fallback, signalling that no usable output exists. -func (c *coordinator) subAgentOutput(ctx context.Context, sessionID string, result *fantasy.AgentResult) (string, bool) { - if result != nil { - if texts := responseContentText(result.Response.Content); len(texts) > 0 { - return strings.Join(texts, "\n\n"), true - } - - var texts []string - for _, step := range result.Steps { - texts = append(texts, responseContentText(step.Content)...) - } - if len(texts) > 0 { - return strings.Join(texts, "\n\n"), true - } - } - - if c.messages != nil { - messages, err := c.messages.List(ctx, sessionID) - if err != nil { - slog.Warn("Failed to list sub-agent messages", "session", sessionID, "error", err) - } else { - for i := len(messages) - 1; i >= 0; i-- { - msg := messages[i] - if msg.Role != message.Assistant { - continue - } - if text := msg.Content().String(); strings.TrimSpace(text) != "" { - return text, true - } - } - } - } - - return "Sub-agent completed but produced no text output.", false -} - -func responseContentText(content fantasy.ResponseContent) []string { - texts := make([]string, 0) - for _, part := range content { - textContent, ok := fantasy.AsContentType[fantasy.TextContent](part) - if !ok || strings.TrimSpace(textContent.Text) == "" { - continue - } - texts = append(texts, textContent.Text) +func subAgentOutput(result *fantasy.AgentResult) string { + if result == nil { + return "" } - return texts + return result.Response.Content.Text() } // updateParentSessionCost accumulates the cost from a child session to its parent session. diff --git a/internal/agent/coordinator_test.go b/internal/agent/coordinator_test.go index 8fc1a6a901..e32536f90c 100644 --- a/internal/agent/coordinator_test.go +++ b/internal/agent/coordinator_test.go @@ -10,7 +10,6 @@ import ( "charm.land/fantasy/providers/anthropic" "charm.land/fantasy/providers/bedrock" "github.com/charmbracelet/crush/internal/config" - "github.com/charmbracelet/crush/internal/message" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -76,35 +75,13 @@ func newMockAgent(providerID string, maxTokens int64, runFunc func(context.Conte // agentResultWithText creates a minimal AgentResult with the given text response. func agentResultWithText(text string) *fantasy.AgentResult { - return agentResultWithTextBlocks(text) -} - -func agentResultWithTextBlocks(texts ...string) *fantasy.AgentResult { - content := make(fantasy.ResponseContent, 0, len(texts)) - for _, text := range texts { - content = append(content, fantasy.TextContent{Text: text}) - } return &fantasy.AgentResult{ Response: fantasy.Response{ - Content: content, - }, - } -} - -func agentResultWithStepTexts(texts ...string) *fantasy.AgentResult { - result := &fantasy.AgentResult{ - Steps: make([]fantasy.StepResult, 0, len(texts)), - } - for _, text := range texts { - result.Steps = append(result.Steps, fantasy.StepResult{ - Response: fantasy.Response{ - Content: fantasy.ResponseContent{ - fantasy.TextContent{Text: text}, - }, + Content: fantasy.ResponseContent{ + fantasy.TextContent{Text: text}, }, - }) + }, } - return result } func TestRunSubAgent(t *testing.T) { @@ -158,38 +135,7 @@ func TestRunSubAgent(t *testing.T) { assert.Equal(t, "output before cost failure", resp.Content) }) - t.Run("persisted assistant message fallback", func(t *testing.T) { - env := testEnv(t) - coord := newTestCoordinator(t, env, providerID, providerCfg) - - parentSession, err := env.sessions.Create(t.Context(), "Parent") - require.NoError(t, err) - - agent := newMockAgent(providerID, 4096, func(ctx context.Context, call SessionAgentCall) (*fantasy.AgentResult, error) { - _, err := env.messages.Create(ctx, call.SessionID, message.CreateMessageParams{ - Role: message.Assistant, - Parts: []message.ContentPart{ - message.TextContent{Text: "persisted assistant output"}, - }, - }) - require.NoError(t, err) - return &fantasy.AgentResult{}, nil - }) - - resp, err := coord.runSubAgent(t.Context(), subAgentParams{ - Agent: agent, - SessionID: parentSession.ID, - AgentMessageID: "msg-1", - ToolCallID: "call-1", - Prompt: "test", - SessionTitle: "Test", - }) - require.NoError(t, err) - assert.False(t, resp.IsError) - assert.Equal(t, "persisted assistant output", resp.Content) - }) - - t.Run("aggregates earlier step output", func(t *testing.T) { + t.Run("response with text returns it", func(t *testing.T) { env := testEnv(t) coord := newTestCoordinator(t, env, providerID, providerCfg) @@ -197,41 +143,7 @@ func TestRunSubAgent(t *testing.T) { require.NoError(t, err) agent := newMockAgent(providerID, 4096, func(_ context.Context, _ SessionAgentCall) (*fantasy.AgentResult, error) { - return agentResultWithStepTexts("first step", "second step"), nil - }) - - resp, err := coord.runSubAgent(t.Context(), subAgentParams{ - Agent: agent, - SessionID: parentSession.ID, - AgentMessageID: "msg-1", - ToolCallID: "call-1", - Prompt: "test", - SessionTitle: "Test", - }) - require.NoError(t, err) - assert.False(t, resp.IsError) - assert.Equal(t, "first step\n\nsecond step", resp.Content) - }) - - t.Run("final response text wins over fallback sources", func(t *testing.T) { - env := testEnv(t) - coord := newTestCoordinator(t, env, providerID, providerCfg) - - parentSession, err := env.sessions.Create(t.Context(), "Parent") - require.NoError(t, err) - - agent := newMockAgent(providerID, 4096, func(ctx context.Context, call SessionAgentCall) (*fantasy.AgentResult, error) { - _, err := env.messages.Create(ctx, call.SessionID, message.CreateMessageParams{ - Role: message.Assistant, - Parts: []message.ContentPart{ - message.TextContent{Text: "persisted fallback"}, - }, - }) - require.NoError(t, err) - - result := agentResultWithText("final response") - result.Steps = agentResultWithStepTexts("earlier step").Steps - return result, nil + return agentResultWithText("the answer"), nil }) resp, err := coord.runSubAgent(t.Context(), subAgentParams{ @@ -244,10 +156,10 @@ func TestRunSubAgent(t *testing.T) { }) require.NoError(t, err) assert.False(t, resp.IsError) - assert.Equal(t, "final response", resp.Content) + assert.Equal(t, "the answer", resp.Content) }) - t.Run("aggregates multi-block final response", func(t *testing.T) { + t.Run("nil result returns error response", func(t *testing.T) { env := testEnv(t) coord := newTestCoordinator(t, env, providerID, providerCfg) @@ -255,9 +167,7 @@ func TestRunSubAgent(t *testing.T) { require.NoError(t, err) agent := newMockAgent(providerID, 4096, func(_ context.Context, _ SessionAgentCall) (*fantasy.AgentResult, error) { - result := agentResultWithTextBlocks("first final block", "second final block") - result.Steps = agentResultWithStepTexts("earlier step").Steps - return result, nil + return nil, nil }) resp, err := coord.runSubAgent(t.Context(), subAgentParams{ @@ -269,20 +179,17 @@ func TestRunSubAgent(t *testing.T) { SessionTitle: "Test", }) require.NoError(t, err) - assert.False(t, resp.IsError) - assert.Equal(t, "first final block\n\nsecond final block", resp.Content) + assert.True(t, resp.IsError) + assert.Equal(t, "Sub-agent completed but produced no text output.", resp.Content) }) - t.Run("no recoverable output returns error response", func(t *testing.T) { + t.Run("empty result returns error response", func(t *testing.T) { env := testEnv(t) coord := newTestCoordinator(t, env, providerID, providerCfg) parentSession, err := env.sessions.Create(t.Context(), "Parent") require.NoError(t, err) - // Empty result, no persisted assistant message: no layer can recover - // real text, so the diagnostic fallback must surface as a soft tool - // error (IsError=true) with a nil Go error. agent := newMockAgent(providerID, 4096, func(_ context.Context, _ SessionAgentCall) (*fantasy.AgentResult, error) { return &fantasy.AgentResult{}, nil }) @@ -297,7 +204,6 @@ func TestRunSubAgent(t *testing.T) { }) require.NoError(t, err) assert.True(t, resp.IsError) - assert.NotEmpty(t, resp.Content) assert.Equal(t, "Sub-agent completed but produced no text output.", resp.Content) })