diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 48790b8..05eda47 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -101,11 +101,13 @@ func run(ctx context.Context, cfg agentConfig, state state) error { // Stream the assistant's response, retrying on rate limits, transient // backend errors (5xx, network), and mid-stream drops. var content strings.Builder + var reasoning strings.Builder var toolCalls []backend.ToolCall var stopReason string for attempt := range maxTransientRetries + 1 { content.Reset() + reasoning.Reset() toolCalls = nil ch, err := cfg.Backend.ChatStream(ctx, history, cfg.Tools, cfg.GenerationParams) @@ -144,6 +146,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error { emit(cfg, Event{Role: "reasoning", Content: "\n"}) hadReasoning = true } + reasoning.WriteString(ev.Reasoning) emit(cfg, Event{Role: "reasoning", Content: ev.Reasoning}) } if ev.Content != "" { @@ -194,7 +197,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error { if hadReasoning { emit(cfg, Event{Role: "reasoning", Content: "\n\n"}) } - msg := backend.Message{Role: "assistant", Content: content.String(), ToolCalls: toolCalls} + msg := backend.Message{Role: "assistant", Content: content.String(), Reasoning: reasoning.String(), ToolCalls: toolCalls} var results []toolResult for _, tc := range toolCalls { results = append(results, toolResult{ @@ -241,7 +244,7 @@ func run(ctx context.Context, cfg agentConfig, state state) error { } // Execute tool calls, running consecutive parallel-read-safe tools concurrently. - msg := backend.Message{Role: "assistant", Content: content.String(), ToolCalls: toolCalls} + msg := backend.Message{Role: "assistant", Content: content.String(), Reasoning: reasoning.String(), ToolCalls: toolCalls} results := make([]toolResult, 0, len(toolCalls)) interrupted := false diff --git a/pkg/backend/backend.go b/pkg/backend/backend.go index 54e06cf..4f97028 100644 --- a/pkg/backend/backend.go +++ b/pkg/backend/backend.go @@ -32,6 +32,7 @@ type ImageSource struct { type Message struct { Role string `json:"role"` // "system" | "user" | "assistant" | "tool" Content string `json:"content"` + Reasoning string `json:"reasoning,omitempty"` // thinking/reasoning content (DeepSeek, OpenAI o-series) ContentBlocks []ContentBlock `json:"content_blocks,omitempty"` // when set, overrides Content ToolCalls []ToolCall `json:"tool_calls,omitempty"` // set by assistant when calling tools ToolCallID string `json:"tool_call_id,omitempty"` // set on role=tool replies (required by OpenAI) diff --git a/pkg/backend/openai.go b/pkg/backend/openai.go index 79690ff..37aebc9 100644 --- a/pkg/backend/openai.go +++ b/pkg/backend/openai.go @@ -132,11 +132,12 @@ type openAIContentBlock struct { } type openAIMessage struct { - Role string `json:"role"` - Content *string `json:"content"` - ContentBlocks []openAIContentBlock `json:"-"` // non-nil overrides Content during marshaling - ToolCalls []openAIToolCall `json:"tool_calls,omitempty"` - ToolCallID string `json:"tool_call_id,omitempty"` + Role string `json:"role"` + Content *string `json:"content"` + ReasoningContent *string `json:"reasoning_content,omitempty"` + ContentBlocks []openAIContentBlock `json:"-"` // non-nil overrides Content during marshaling + ToolCalls []openAIToolCall `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` } // MarshalJSON encodes content as an array of blocks when ContentBlocks is set @@ -145,11 +146,12 @@ type openAIMessage struct { func (m openAIMessage) MarshalJSON() ([]byte, error) { if len(m.ContentBlocks) > 0 { return json.Marshal(struct { - Role string `json:"role"` - Content []openAIContentBlock `json:"content"` - ToolCalls []openAIToolCall `json:"tool_calls,omitempty"` - ToolCallID string `json:"tool_call_id,omitempty"` - }{m.Role, m.ContentBlocks, m.ToolCalls, m.ToolCallID}) + Role string `json:"role"` + ReasoningContent *string `json:"reasoning_content,omitempty"` + Content []openAIContentBlock `json:"content"` + ToolCalls []openAIToolCall `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` + }{m.Role, m.ReasoningContent, m.ContentBlocks, m.ToolCalls, m.ToolCallID}) } type plain openAIMessage return json.Marshal(plain(m)) @@ -243,6 +245,9 @@ func encodeOpenAIMessages(messages []Message) []openAIMessage { Role: m.Role, ToolCallID: m.ToolCallID, } + if m.Reasoning != "" { + wm.ReasoningContent = &m.Reasoning + } for _, tc := range m.ToolCalls { wm.ToolCalls = append(wm.ToolCalls, openAIToolCall{ ID: tc.ID,