Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions schema/agentic_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,9 @@ type Reasoning struct {

// OpenAIExtension is the extension for OpenAI.
OpenAIExtension *openai.ReasoningExtension `json:"openai_extension,omitempty"`

// ClaudeExtension is the extension for Claude.
ClaudeExtension *claude.ReasoningExtension `json:"claude_extension,omitempty"`
}

type FunctionToolCall struct {
Expand Down Expand Up @@ -1315,6 +1318,7 @@ func concatReasoning(reasons []*Reasoning) (ret *Reasoning, err error) {
ret = &Reasoning{}

openaiExtensions := make([]*openai.ReasoningExtension, 0, len(reasons))
claudeExtensions := make([]*claude.ReasoningExtension, 0, len(reasons))

for _, r := range reasons {
if r == nil {
Expand All @@ -1329,6 +1333,9 @@ func concatReasoning(reasons []*Reasoning) (ret *Reasoning, err error) {
if r.OpenAIExtension != nil {
openaiExtensions = append(openaiExtensions, r.OpenAIExtension)
}
if r.ClaudeExtension != nil {
claudeExtensions = append(claudeExtensions, r.ClaudeExtension)
}
}

if len(openaiExtensions) > 0 {
Expand All @@ -1337,6 +1344,12 @@ func concatReasoning(reasons []*Reasoning) (ret *Reasoning, err error) {
return nil, fmt.Errorf("failed to concat openai reasoning extensions: %w", err)
}
}
if len(claudeExtensions) > 0 {
ret.ClaudeExtension, err = claude.ConcatReasoningExtensions(claudeExtensions)
if err != nil {
return nil, fmt.Errorf("failed to concat claude reasoning extensions: %w", err)
}
}

return ret, nil
}
Expand Down
56 changes: 56 additions & 0 deletions schema/agentic_message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,35 @@ package schema

import (
"context"
"encoding/json"
"reflect"
"testing"

"github.com/cloudwego/eino/schema/claude"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestClaudeRedactedThinkingJSONRoundTrip(t *testing.T) {
want := &AgenticMessage{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
NewContentBlock(&Reasoning{
ClaudeExtension: &claude.ReasoningExtension{
RedactedThinking: &claude.RedactedThinking{Data: "opaque-data"},
},
}),
},
}

data, err := json.Marshal(want)
require.NoError(t, err)

got := &AgenticMessage{}
require.NoError(t, json.Unmarshal(data, got))
assert.Equal(t, want, got)
}

func TestConcatAgenticMessages(t *testing.T) {
t.Run("single message", func(t *testing.T) {
msg := &AgenticMessage{
Expand Down Expand Up @@ -170,6 +192,40 @@ func TestConcatAgenticMessages(t *testing.T) {
assert.Equal(t, "Part1-Part3", result.ContentBlocks[0].Reasoning.Text)
})

t.Run("concat claude redacted thinking extension", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeReasoning,
Reasoning: &Reasoning{ClaudeExtension: &claude.ReasoningExtension{
RedactedThinking: &claude.RedactedThinking{Data: "opaque-data"},
}},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
{
Role: AgenticRoleTypeAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeReasoning,
Reasoning: &Reasoning{},
StreamingMeta: &StreamingMeta{Index: 0},
},
},
},
}

result, err := ConcatAgenticMessages(msgs)
require.NoError(t, err)
require.Len(t, result.ContentBlocks, 1)
require.NotNil(t, result.ContentBlocks[0].Reasoning.ClaudeExtension)
require.NotNil(t, result.ContentBlocks[0].Reasoning.ClaudeExtension.RedactedThinking)
assert.Equal(t, "opaque-data", result.ContentBlocks[0].Reasoning.ClaudeExtension.RedactedThinking.Data)
})

t.Run("concat user input text", func(t *testing.T) {
msgs := []*AgenticMessage{
{
Expand Down
31 changes: 31 additions & 0 deletions schema/claude/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ type AssistantGenTextExtension struct {
Citations []*TextCitation `json:"citations,omitempty"`
}

// ReasoningExtension contains Claude-specific reasoning data.
type ReasoningExtension struct {
// RedactedThinking contains an opaque encrypted thinking block returned by Claude.
RedactedThinking *RedactedThinking `json:"redacted_thinking,omitempty"`
}

// RedactedThinking is an opaque encrypted thinking block that must be passed back
// to Claude unchanged when continuing the conversation.
type RedactedThinking struct {
Data string `json:"data"`
}

type TextCitation struct {
Type TextCitationType `json:"type,omitempty"`

Expand Down Expand Up @@ -104,6 +116,25 @@ func ConcatAssistantGenTextExtensions(chunks []*AssistantGenTextExtension) (*Ass
return ret, nil
}

// ConcatReasoningExtensions merges multiple ReasoningExtension chunks into one.
func ConcatReasoningExtensions(chunks []*ReasoningExtension) (*ReasoningExtension, error) {
if len(chunks) == 0 {
return nil, fmt.Errorf("no reasoning extension found")
}
if len(chunks) == 1 {
return chunks[0], nil
}

ret := &ReasoningExtension{}
for _, ext := range chunks {
if ext != nil && ext.RedactedThinking != nil {
ret.RedactedThinking = ext.RedactedThinking
}
}

return ret, nil
}

// ConcatResponseMetaExtensions merges multiple ResponseMetaExtension chunks into one.
func ConcatResponseMetaExtensions(chunks []*ResponseMetaExtension) (*ResponseMetaExtension, error) {
if len(chunks) == 0 {
Expand Down
12 changes: 12 additions & 0 deletions schema/claude/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@ func TestConcatAssistantGenTextExtensions(t *testing.T) {
})
}

func TestConcatReasoningExtensions(t *testing.T) {
redacted := &RedactedThinking{Data: "opaque-data"}

result, err := ConcatReasoningExtensions([]*ReasoningExtension{
{},
{RedactedThinking: redacted},
{},
})
assert.NoError(t, err)
assert.Same(t, redacted, result.RedactedThinking)
}

func TestConcatResponseMetaExtensions(t *testing.T) {
t.Run("multiple extensions - takes last non-empty values", func(t *testing.T) {
exts := []*ResponseMetaExtension{
Expand Down