diff --git a/pkg/model/provider/anthropic/beta_converter.go b/pkg/model/provider/anthropic/beta_converter.go index fa1c04dcf..1f3abd6b6 100644 --- a/pkg/model/provider/anthropic/beta_converter.go +++ b/pkg/model/provider/anthropic/beta_converter.go @@ -23,6 +23,7 @@ import ( // blocks from the same assistant message MUST be grouped into a single user message. func (c *Client) convertBetaMessages(ctx context.Context, messages []chat.Message) ([]anthropic.BetaMessageParam, error) { var betaMessages []anthropic.BetaMessageParam + pendingAssistantToolUse := false for i := 0; i < len(messages); i++ { msg := &messages[i] @@ -75,20 +76,19 @@ func (c *Client) convertBetaMessages(ctx context.Context, messages []chat.Messag } // Add tool calls - if len(msg.ToolCalls) > 0 { - for _, toolCall := range msg.ToolCalls { - var inpts map[string]any - if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &inpts); err != nil { - inpts = map[string]any{} - } - contentBlocks = append(contentBlocks, anthropic.BetaContentBlockParamUnion{ - OfToolUse: &anthropic.BetaToolUseBlockParam{ - ID: toolCall.ID, - Input: inpts, - Name: toolCall.Function.Name, - }, - }) + hasToolCalls := len(msg.ToolCalls) > 0 + for _, toolCall := range msg.ToolCalls { + var inpts map[string]any + if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &inpts); err != nil { + inpts = map[string]any{} } + contentBlocks = append(contentBlocks, anthropic.BetaContentBlockParamUnion{ + OfToolUse: &anthropic.BetaToolUseBlockParam{ + ID: toolCall.ID, + Input: inpts, + Name: toolCall.Function.Name, + }, + }) } if len(contentBlocks) > 0 { @@ -97,28 +97,29 @@ func (c *Client) convertBetaMessages(ctx context.Context, messages []chat.Messag Content: contentBlocks, }) } + pendingAssistantToolUse = hasToolCalls continue } if msg.Role == chat.MessageRoleTool { // Collect consecutive tool messages and merge them into a single user message // This is required by Anthropic API: all tool_result blocks for tool_use blocks // from the same assistant message must be in the same user message - toolResultBlocks := []anthropic.BetaContentBlockParamUnion{ - convertBetaToolResultBlock(msg), - } - - // Look ahead for consecutive tool messages and merge them - j := i + 1 + var toolResultBlocks []anthropic.BetaContentBlockParamUnion + j := i for j < len(messages) && messages[j].Role == chat.MessageRoleTool { toolResultBlocks = append(toolResultBlocks, convertBetaToolResultBlock(&messages[j])) j++ } - // Add the merged user message with all tool results - betaMessages = append(betaMessages, anthropic.BetaMessageParam{ - Role: anthropic.BetaMessageParamRoleUser, - Content: toolResultBlocks, - }) + // Only include tool results if they follow an assistant message with tool_use. + // Orphan tool_result blocks (e.g. from corrupted session history) are dropped. + if pendingAssistantToolUse && len(toolResultBlocks) > 0 { + betaMessages = append(betaMessages, anthropic.BetaMessageParam{ + Role: anthropic.BetaMessageParamRoleUser, + Content: toolResultBlocks, + }) + } + pendingAssistantToolUse = false // Skip the messages we've already processed i = j - 1 diff --git a/pkg/model/provider/anthropic/client.go b/pkg/model/provider/anthropic/client.go index 10e05b701..9a637b9f6 100644 --- a/pkg/model/provider/anthropic/client.go +++ b/pkg/model/provider/anthropic/client.go @@ -776,67 +776,103 @@ func contentArray(m map[string]any) []any { return nil } -// validateSequencing generically validates that every assistant message with tool_use blocks -// is immediately followed by a user message with corresponding tool_result blocks. -// It works on both standard (MessageParam) and Beta (BetaMessageParam) types by -// marshaling to map[string]any for inspection. +// validateSequencing generically validates that: +// 1. Every assistant message with tool_use blocks is immediately followed by a user +// message with corresponding tool_result blocks. +// 2. Every user message with tool_result blocks is immediately preceded by an assistant +// message that contains the corresponding tool_use blocks. func validateSequencing[T any](msgs []T) error { for i := range msgs { m, ok := marshalToMap(msgs[i]) - if !ok || m["role"] != "assistant" { + if !ok { continue } - toolUseIDs := collectToolUseIDs(contentArray(m)) - if len(toolUseIDs) == 0 { - continue - } + // Forward check: assistant tool_use → next user must have matching tool_results + if m["role"] == "assistant" { + toolUseIDs := collectToolUseIDs(contentArray(m)) + if len(toolUseIDs) == 0 { + continue + } - if i+1 >= len(msgs) { - slog.Warn("Anthropic sequencing invalid: assistant tool_use present but no next user tool_result message", "assistant_index", i) - return errors.New("assistant tool_use present but no subsequent user message with tool_result blocks") - } + if i+1 >= len(msgs) { + slog.Warn("Anthropic sequencing invalid: assistant tool_use present but no next user tool_result message", "assistant_index", i) + return errors.New("assistant tool_use present but no subsequent user message with tool_result blocks") + } + + next, ok := marshalToMap(msgs[i+1]) + if !ok || next["role"] != "user" { + slog.Warn("Anthropic sequencing invalid: next message after assistant tool_use is not user", "assistant_index", i, "next_role", next["role"]) + return errors.New("assistant tool_use must be followed by a user message containing corresponding tool_result blocks") + } - next, ok := marshalToMap(msgs[i+1]) - if !ok || next["role"] != "user" { - slog.Warn("Anthropic sequencing invalid: next message after assistant tool_use is not user", "assistant_index", i, "next_role", next["role"]) - return errors.New("assistant tool_use must be followed by a user message containing corresponding tool_result blocks") + toolResultIDs := collectToolResultIDs(contentArray(next)) + missing := differenceIDs(toolUseIDs, toolResultIDs) + if len(missing) > 0 { + slog.Warn("Anthropic sequencing invalid: missing tool_result for tool_use id in next user message", "assistant_index", i, "tool_use_id", missing[0], "missing_count", len(missing)) + return fmt.Errorf("missing tool_result for tool_use id %s in the next user message", missing[0]) + } } - toolResultIDs := collectToolResultIDs(contentArray(next)) - missing := differenceIDs(toolUseIDs, toolResultIDs) - if len(missing) > 0 { - slog.Warn("Anthropic sequencing invalid: missing tool_result for tool_use id in next user message", "assistant_index", i, "tool_use_id", missing[0], "missing_count", len(missing)) - return fmt.Errorf("missing tool_result for tool_use id %s in the next user message", missing[0]) + // Reverse check: user with tool_result → previous message must be assistant with matching tool_use + if m["role"] == "user" { + toolResultIDs := collectToolResultIDs(contentArray(m)) + if len(toolResultIDs) == 0 { + continue + } + + if i == 0 { + slog.Warn("Anthropic sequencing invalid: user tool_result with no preceding assistant message", "user_index", i) + return errors.New("user tool_result blocks with no preceding assistant message") + } + + prev, ok := marshalToMap(msgs[i-1]) + if !ok || prev["role"] != "assistant" { + slog.Warn("Anthropic sequencing invalid: user tool_result not preceded by assistant", "user_index", i) + return errors.New("user tool_result blocks must be preceded by an assistant message with corresponding tool_use blocks") + } + + toolUseIDs := collectToolUseIDs(contentArray(prev)) + orphan := differenceIDs(toolResultIDs, toolUseIDs) + if len(orphan) > 0 { + slog.Warn("Anthropic sequencing invalid: orphan tool_result referencing non-existent tool_use", "user_index", i, "tool_use_id", orphan[0], "orphan_count", len(orphan)) + return fmt.Errorf("orphan tool_result for tool_use id %s: no matching tool_use in preceding assistant message", orphan[0]) + } } } return nil } // repairSequencing generically inserts a synthetic user message after any assistant -// tool_use message that is missing corresponding tool_result blocks. The makeSynthetic +// tool_use message that is missing corresponding tool_result blocks. When the next +// message is already a user message with partial tool_results, the missing results +// are merged into that message to avoid splitting results across messages (which +// would cause the API to reject orphan tool_result references). The makeSynthetic // callback builds the appropriate user message type for the remaining tool_use IDs. func repairSequencing[T any](msgs []T, makeSynthetic func(toolUseIDs map[string]struct{}) T) []T { if len(msgs) == 0 { return msgs } repaired := make([]T, 0, len(msgs)+2) - for i := range msgs { - repaired = append(repaired, msgs[i]) - + for i := 0; i < len(msgs); i++ { m, ok := marshalToMap(msgs[i]) if !ok || m["role"] != "assistant" { + repaired = append(repaired, msgs[i]) continue } + repaired = append(repaired, msgs[i]) + toolUseIDs := collectToolUseIDs(contentArray(m)) if len(toolUseIDs) == 0 { continue } - // Remove any IDs that already have results in the next user message + // Check if the next message is a user message with some tool_results + hasNextUser := false if i+1 < len(msgs) { if next, ok := marshalToMap(msgs[i+1]); ok && next["role"] == "user" { + hasNextUser = true toolResultIDs := collectToolResultIDs(contentArray(next)) for id := range toolResultIDs { delete(toolUseIDs, id) @@ -844,7 +880,46 @@ func repairSequencing[T any](msgs []T, makeSynthetic func(toolUseIDs map[string] } } - if len(toolUseIDs) > 0 { + if len(toolUseIDs) == 0 { + continue + } + + if hasNextUser { + // Merge the synthetic results into the existing next user message + // by replacing it with a combined message (synthetic first, then original). + // This avoids splitting tool_results across separate user messages. + synthetic := makeSynthetic(toolUseIDs) + synthMap, _ := marshalToMap(synthetic) + nextMap, _ := marshalToMap(msgs[i+1]) + + synthContent := contentArray(synthMap) + nextContent := contentArray(nextMap) + + mergedContent := append(synthContent, nextContent...) + nextMap["content"] = mergedContent + + // Re-marshal the merged message back into the typed parameter + mergedBytes, err := json.Marshal(nextMap) + if err != nil { + slog.Warn("Failed to marshal merged tool_result message, inserting synthetic instead", "error", err) + repaired = append(repaired, synthetic) + i++ // skip msgs[i+1] — it would split tool_results across user messages + continue + } + var merged T + if err := json.Unmarshal(mergedBytes, &merged); err != nil { + slog.Warn("Failed to unmarshal merged tool_result message, inserting synthetic instead", "error", err) + repaired = append(repaired, synthetic) + i++ // skip msgs[i+1] — it would split tool_results across user messages + continue + } + + slog.Debug("Merged synthetic tool_results into existing user message", + "assistant_index", i, + "missing_count", len(toolUseIDs)) + // Replace the next message so we emit the merged version + msgs[i+1] = merged + } else { slog.Debug("Inserting synthetic user message for missing tool_results", "assistant_index", i, "missing_count", len(toolUseIDs)) diff --git a/pkg/model/provider/anthropic/client_test.go b/pkg/model/provider/anthropic/client_test.go index 3e53a15a4..6335a27b4 100644 --- a/pkg/model/provider/anthropic/client_test.go +++ b/pkg/model/provider/anthropic/client_test.go @@ -600,3 +600,208 @@ func TestExtractSystemBlocksCacheControl(t *testing.T) { assert.Equal(t, "ephemeral", string(blocks[3].CacheControl.Type)) assert.Empty(t, string(blocks[3].CacheControl.TTL)) } + +func TestValidateSequencing_ReverseOrphanToolResult(t *testing.T) { + // A user message with tool_result that references a tool_use_id not present + // in the preceding assistant message should be caught by reverse validation. + msgs := []anthropic.MessageParam{ + anthropic.NewUserMessage(anthropic.NewTextBlock("start")), + anthropic.NewAssistantMessage( + anthropic.ContentBlockParamUnion{ + OfToolUse: &anthropic.ToolUseBlockParam{ + ID: "tool-A", + Input: map[string]any{}, + Name: "read_file", + }, + }, + ), + // User message with tool_results for A (matching) and B (orphan) + anthropic.NewUserMessage( + anthropic.NewToolResultBlock("tool-A", "result-A", false), + anthropic.NewToolResultBlock("tool-B", "result-B", false), + ), + } + + err := validateAnthropicSequencing(msgs) + require.Error(t, err) + assert.Contains(t, err.Error(), "orphan tool_result") + assert.Contains(t, err.Error(), "tool-B") +} + +func TestValidateSequencing_ReverseNoAssistantBeforeToolResult(t *testing.T) { + // A user message with tool_result as the first message should fail. + msgs := []anthropic.MessageParam{ + anthropic.NewUserMessage( + anthropic.NewToolResultBlock("tool-A", "result-A", false), + ), + } + + err := validateAnthropicSequencing(msgs) + require.Error(t, err) + assert.Contains(t, err.Error(), "no preceding assistant message") +} + +func TestRepairSequencing_MergesIntoExistingUserMessage(t *testing.T) { + // When an assistant has tool_use A and B, but the next user message only has + // tool_result for A, repair should merge a synthetic tool_result for B into + // the same user message rather than inserting a separate synthetic message. + msgs := []anthropic.MessageParam{ + anthropic.NewUserMessage(anthropic.NewTextBlock("start")), + anthropic.NewAssistantMessage( + anthropic.ContentBlockParamUnion{ + OfToolUse: &anthropic.ToolUseBlockParam{ + ID: "tool-A", + Input: map[string]any{}, + Name: "t1", + }, + }, + anthropic.ContentBlockParamUnion{ + OfToolUse: &anthropic.ToolUseBlockParam{ + ID: "tool-B", + Input: map[string]any{}, + Name: "t2", + }, + }, + ), + // Next user message only has tool_result for A, missing B + anthropic.NewUserMessage( + anthropic.NewToolResultBlock("tool-A", "result-A", false), + ), + anthropic.NewUserMessage(anthropic.NewTextBlock("continue")), + } + + // Should fail validation + require.Error(t, validateAnthropicSequencing(msgs)) + + // Repair + repaired := repairAnthropicSequencing(msgs) + + // Should pass validation after repair + require.NoError(t, validateAnthropicSequencing(repaired)) + + // The total message count should NOT increase (merged, not inserted) + assert.Len(t, repaired, 4, "repair should merge into existing user message, not insert a new one") + + // Verify the user message at index 2 now has both tool_results + b, err := json.Marshal(repaired[2]) + require.NoError(t, err) + var m map[string]any + require.NoError(t, json.Unmarshal(b, &m)) + + content, ok := m["content"].([]any) + require.True(t, ok) + + toolResultIDs := make(map[string]struct{}) + for _, c := range content { + if cb, ok := c.(map[string]any); ok { + if cb["type"] == "tool_result" { + if id, _ := cb["tool_use_id"].(string); id != "" { + toolResultIDs[id] = struct{}{} + } + } + } + } + assert.Contains(t, toolResultIDs, "tool-A") + assert.Contains(t, toolResultIDs, "tool-B") +} + +func TestRepairSequencing_InsertsWhenNoNextUserMessage(t *testing.T) { + // When an assistant has tool_use but there's no following user message at all, + // repair should insert a synthetic user message. + msgs := []anthropic.MessageParam{ + anthropic.NewUserMessage(anthropic.NewTextBlock("start")), + anthropic.NewAssistantMessage( + anthropic.ContentBlockParamUnion{ + OfToolUse: &anthropic.ToolUseBlockParam{ + ID: "tool-X", + Input: map[string]any{}, + Name: "do_thing", + }, + }, + ), + } + + require.Error(t, validateAnthropicSequencing(msgs)) + + repaired := repairAnthropicSequencing(msgs) + + require.NoError(t, validateAnthropicSequencing(repaired)) + assert.Len(t, repaired, 3, "should insert a synthetic user message") +} + +func TestConvertBetaMessages_DropsOrphanToolResults(t *testing.T) { + // When a tool result message appears without a preceding assistant message + // with tool_use, the beta converter should drop it. + msgs := []chat.Message{ + {Role: chat.MessageRoleUser, Content: "start"}, + {Role: chat.MessageRoleAssistant, Content: "sure, let me help"}, + // Orphan tool result — previous assistant has no tool_use + {Role: chat.MessageRoleTool, ToolCallID: "tool-orphan", Content: "orphan result"}, + {Role: chat.MessageRoleUser, Content: "continue"}, + } + + converted, err := testClient().convertBetaMessages(t.Context(), msgs) + require.NoError(t, err) + + // Should have: user(start), assistant(text), user(continue) + // The orphan tool result should be dropped + require.Len(t, converted, 3) + + for _, msg := range converted { + b, err := json.Marshal(msg) + require.NoError(t, err) + var m map[string]any + require.NoError(t, json.Unmarshal(b, &m)) + content, _ := m["content"].([]any) + for _, c := range content { + if cb, ok := c.(map[string]any); ok { + assert.NotEqual(t, "tool_result", cb["type"], + "orphan tool_result should not be included in beta messages") + } + } + } +} + +func TestConvertBetaMessages_IncludesToolResultsAfterToolUse(t *testing.T) { + // Normal case: assistant with tool_use followed by tool results should work. + msgs := []chat.Message{ + {Role: chat.MessageRoleUser, Content: "start"}, + { + Role: chat.MessageRoleAssistant, + ToolCalls: []tools.ToolCall{ + {ID: "tool-1", Function: tools.FunctionCall{Name: "read_file", Arguments: "{}"}}, + {ID: "tool-2", Function: tools.FunctionCall{Name: "write_file", Arguments: "{}"}}, + }, + }, + {Role: chat.MessageRoleTool, ToolCallID: "tool-1", Content: "file content"}, + {Role: chat.MessageRoleTool, ToolCallID: "tool-2", Content: "ok"}, + {Role: chat.MessageRoleUser, Content: "done"}, + } + + converted, err := testClient().convertBetaMessages(t.Context(), msgs) + require.NoError(t, err) + + // Should have: user(start), assistant(tool_use x2), user(tool_result x2), user(done) + require.Len(t, converted, 4) + + // Verify the tool results are in the third message + b, err := json.Marshal(converted[2]) + require.NoError(t, err) + var m map[string]any + require.NoError(t, json.Unmarshal(b, &m)) + assert.Equal(t, "user", m["role"]) + content, ok := m["content"].([]any) + require.True(t, ok) + assert.Len(t, content, 2) + + ids := make(map[string]struct{}) + for _, c := range content { + if cb, ok := c.(map[string]any); ok && cb["type"] == "tool_result" { + if id, _ := cb["tool_use_id"].(string); id != "" { + ids[id] = struct{}{} + } + } + } + assert.Contains(t, ids, "tool-1") + assert.Contains(t, ids, "tool-2") +} diff --git a/pkg/runtime/event.go b/pkg/runtime/event.go index 5a1160bb0..81cef421f 100644 --- a/pkg/runtime/event.go +++ b/pkg/runtime/event.go @@ -138,29 +138,33 @@ func StreamStarted(sessionID, agentName string) Event { } type AgentChoiceEvent struct { - Type string `json:"type"` - Content string `json:"content"` + Type string `json:"type"` + Content string `json:"content"` + SessionID string `json:"session_id,omitempty"` AgentContext } -func AgentChoice(agentName, content string) Event { +func AgentChoice(agentName, sessionID, content string) Event { return &AgentChoiceEvent{ Type: "agent_choice", Content: content, + SessionID: sessionID, AgentContext: newAgentContext(agentName), } } type AgentChoiceReasoningEvent struct { - Type string `json:"type"` - Content string `json:"content"` + Type string `json:"type"` + Content string `json:"content"` + SessionID string `json:"session_id,omitempty"` AgentContext } -func AgentChoiceReasoning(agentName, content string) Event { +func AgentChoiceReasoning(agentName, sessionID, content string) Event { return &AgentChoiceReasoningEvent{ Type: "agent_choice_reasoning", Content: content, + SessionID: sessionID, AgentContext: newAgentContext(agentName), } } diff --git a/pkg/runtime/persistent_runtime.go b/pkg/runtime/persistent_runtime.go index fea609fa2..f47ffce6d 100644 --- a/pkg/runtime/persistent_runtime.go +++ b/pkg/runtime/persistent_runtime.go @@ -73,6 +73,9 @@ func (r *PersistentRuntime) handleEvent(ctx context.Context, sess *session.Sessi switch e := event.(type) { case *AgentChoiceEvent: + if e.SessionID != sess.ID { + return + } // Accumulate streaming content streaming.content.WriteString(e.Content) streaming.agentName = e.AgentName @@ -80,6 +83,9 @@ func (r *PersistentRuntime) handleEvent(ctx context.Context, sess *session.Sessi r.persistStreamingContent(ctx, sess.ID, streaming) case *AgentChoiceReasoningEvent: + if e.SessionID != sess.ID { + return + } // Accumulate streaming reasoning content streaming.reasoningContent.WriteString(e.Content) streaming.agentName = e.AgentName @@ -98,6 +104,9 @@ func (r *PersistentRuntime) handleEvent(ctx context.Context, sess *session.Sessi } case *MessageAddedEvent: + if e.SessionID != sess.ID { + return + } // Finalize the streaming message with complete metadata if streaming.messageID != 0 { // Update the existing streaming message with final content diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index a1e651ecf..88e5c9824 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -282,7 +282,7 @@ func TestSimple(t *testing.T) { UserMessage("Hi", sess.ID, nil, 0), StreamStarted(sess.ID, "root"), ToolsetInfo(0, false, "root"), - AgentChoice("root", "Hello"), + AgentChoice("root", sess.ID, "Hello"), MessageAdded(sess.ID, msgAdded.Message, "root"), NewTokenUsageEvent(sess.ID, "root", &Usage{InputTokens: 3, OutputTokens: 2, ContextLength: 5, LastMessage: &MessageUsage{ Usage: chat.Usage{InputTokens: 3, OutputTokens: 2}, @@ -321,11 +321,11 @@ func TestMultipleContentChunks(t *testing.T) { UserMessage("Please greet me", sess.ID, nil, 0), StreamStarted(sess.ID, "root"), ToolsetInfo(0, false, "root"), - AgentChoice("root", "Hello "), - AgentChoice("root", "there, "), - AgentChoice("root", "how "), - AgentChoice("root", "are "), - AgentChoice("root", "you?"), + AgentChoice("root", sess.ID, "Hello "), + AgentChoice("root", sess.ID, "there, "), + AgentChoice("root", sess.ID, "how "), + AgentChoice("root", sess.ID, "are "), + AgentChoice("root", sess.ID, "you?"), MessageAdded(sess.ID, msgAdded.Message, "root"), NewTokenUsageEvent(sess.ID, "root", &Usage{InputTokens: 8, OutputTokens: 12, ContextLength: 20, LastMessage: &MessageUsage{ Usage: chat.Usage{InputTokens: 8, OutputTokens: 12}, @@ -362,9 +362,9 @@ func TestWithReasoning(t *testing.T) { UserMessage("Hi", sess.ID, nil, 0), StreamStarted(sess.ID, "root"), ToolsetInfo(0, false, "root"), - AgentChoiceReasoning("root", "Let me think about this..."), - AgentChoiceReasoning("root", " I should respond politely."), - AgentChoice("root", "Hello, how can I help you?"), + AgentChoiceReasoning("root", sess.ID, "Let me think about this..."), + AgentChoiceReasoning("root", sess.ID, " I should respond politely."), + AgentChoice("root", sess.ID, "Hello, how can I help you?"), MessageAdded(sess.ID, msgAdded.Message, "root"), NewTokenUsageEvent(sess.ID, "root", &Usage{InputTokens: 10, OutputTokens: 15, ContextLength: 25, LastMessage: &MessageUsage{ Usage: chat.Usage{InputTokens: 10, OutputTokens: 15}, @@ -402,10 +402,10 @@ func TestMixedContentAndReasoning(t *testing.T) { UserMessage("Hi there", sess.ID, nil, 0), StreamStarted(sess.ID, "root"), ToolsetInfo(0, false, "root"), - AgentChoiceReasoning("root", "The user wants a greeting"), - AgentChoice("root", "Hello!"), - AgentChoiceReasoning("root", " I should be friendly"), - AgentChoice("root", " How can I help you today?"), + AgentChoiceReasoning("root", sess.ID, "The user wants a greeting"), + AgentChoice("root", sess.ID, "Hello!"), + AgentChoiceReasoning("root", sess.ID, " I should be friendly"), + AgentChoice("root", sess.ID, " How can I help you today?"), MessageAdded(sess.ID, msgAdded.Message, "root"), NewTokenUsageEvent(sess.ID, "root", &Usage{InputTokens: 15, OutputTokens: 20, ContextLength: 35, LastMessage: &MessageUsage{ Usage: chat.Usage{InputTokens: 15, OutputTokens: 20}, diff --git a/pkg/runtime/streaming.go b/pkg/runtime/streaming.go index 8f7d44960..3d06a8916 100644 --- a/pkg/runtime/streaming.go +++ b/pkg/runtime/streaming.go @@ -164,7 +164,7 @@ func (r *LocalRuntime) handleStream(ctx context.Context, stream chat.MessageStre } if choice.Delta.ReasoningContent != "" { - events <- AgentChoiceReasoning(a.Name(), choice.Delta.ReasoningContent) + events <- AgentChoiceReasoning(a.Name(), sess.ID, choice.Delta.ReasoningContent) fullReasoningContent.WriteString(choice.Delta.ReasoningContent) } @@ -174,7 +174,7 @@ func (r *LocalRuntime) handleStream(ctx context.Context, stream chat.MessageStre } if choice.Delta.Content != "" { - events <- AgentChoice(a.Name(), choice.Delta.Content) + events <- AgentChoice(a.Name(), sess.ID, choice.Delta.Content) fullContent.WriteString(choice.Delta.Content) } }