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
6 changes: 6 additions & 0 deletions adk/agentic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,12 @@ func TestAgenticChatModelAgentRun_WithMiddleware(t *testing.T) {

mw := &testAgenticMiddleware{
beforeFn: func(ctx context.Context, state *TypedChatModelAgentState[*schema.AgenticMessage], mc *TypedModelContext[*schema.AgenticMessage]) (context.Context, *TypedChatModelAgentState[*schema.AgenticMessage], error) {
currentMessages, err := TypedGetMessages[*schema.AgenticMessage](ctx)
require.NoError(t, err)
require.Equal(t, state.Messages, currentMessages)
currentMessages[0] = nil
require.NotNil(t, state.Messages[0], "returned slice must not alias state slice")

state.Messages = append(state.Messages, schema.UserAgenticMessage("extra"))
return ctx, state, nil
},
Expand Down
26 changes: 26 additions & 0 deletions adk/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,32 @@ func processTypedState(ctx context.Context, fn func(extra map[string]any) map[st
})
}

// TypedGetMessages returns the current ChatModelAgent message list.
//
// It can be called from middleware and tool hooks while a ChatModelAgent is
// running or resuming, including hooks that do not receive
// TypedChatModelAgentState directly. The returned slice is a snapshot: callers
// may append to or replace its entries without changing agent state. The
// messages themselves are not deep-copied and must be treated as read-only.
func TypedGetMessages[M MessageType](ctx context.Context) ([]M, error) {
var messages []M
err := compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error {
messages = append([]M(nil), st.Messages...)
return nil
})
if err != nil {
return nil, fmt.Errorf("TypedGetMessages failed: must be called within a ChatModelAgent Run() or Resume() execution context: %w", err)
}
return messages, nil
}

// GetMessages returns the current ChatModelAgent message list.
//
// It is the *schema.Message specialization of TypedGetMessages.
func GetMessages(ctx context.Context) ([]Message, error) {
return TypedGetMessages[*schema.Message](ctx)
}

// SetRunLocalValue sets a key-value pair that persists for the duration of the current agent Run() invocation.
// The value is scoped to this specific execution and is not shared across different Run() calls or agent instances.
//
Expand Down
50 changes: 50 additions & 0 deletions adk/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"

"github.com/cloudwego/eino/components/model"
Expand Down Expand Up @@ -1421,6 +1422,55 @@ func TestModelWrapper_InputModification(t *testing.T) {
})
}

func TestGetMessages(t *testing.T) {
t.Run("returns current messages from middleware", func(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t)
cm := mockModel.NewMockToolCallingChatModel(ctrl)

cm.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).
Return(schema.AssistantMessage("response", nil), nil).Times(1)

var captured []Message
agent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
Name: "TestAgent",
Description: "Test agent",
Instruction: "system instruction",
Model: cm,
Handlers: []ChatModelAgentMiddleware{
&testBeforeModelRewriteStateHandler{fn: func(ctx context.Context, state *ChatModelAgentState, _ *ModelContext) (context.Context, *ChatModelAgentState, error) {
var getErr error
captured, getErr = GetMessages(ctx)
require.NoError(t, getErr)
require.Equal(t, state.Messages, captured)

captured[0] = nil
require.NotNil(t, state.Messages[0], "returned slice must not alias state slice")
return ctx, state, nil
}},
},
})
require.NoError(t, err)

iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}})
for {
event, ok := iter.Next()
if !ok {
break
}
require.NoError(t, event.Err)
}

require.Len(t, captured, 2)
})

t.Run("fails outside agent execution", func(t *testing.T) {
messages, err := GetMessages(context.Background())
assert.Nil(t, messages)
assert.ErrorContains(t, err, "GetMessages failed")
})
}

func TestRunLocalValueFunctions(t *testing.T) {
t.Run("SetAndGetRunLocalValue", func(t *testing.T) {
ctx := context.Background()
Expand Down
23 changes: 3 additions & 20 deletions adk/middlewares/skill/skill.go
Original file line number Diff line number Diff line change
Expand Up @@ -671,26 +671,9 @@ func isNilMessage[M adk.MessageType](msg M) bool {
}

func (s *typedSkillTool[M]) getMessagesFromState(ctx context.Context) ([]M, error) {
var messages []M
var zero M
switch any(zero).(type) {
case *schema.Message:
err := compose.ProcessState(ctx, func(_ context.Context, st *adk.State) error {
messages = make([]M, len(st.Messages))
for i, m := range st.Messages {
messages[i] = any(m).(M)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to process state: %w", err)
}
case *schema.AgenticMessage:
// Fork mode is not supported for AgenticMessage because the internal
// agent state type (agenticState) is unexported from the adk package,
// making it inaccessible via compose.ProcessState from middleware packages.
// Agent mode (the default) works normally for AgenticMessage.
return nil, fmt.Errorf("fork mode is not supported for AgenticMessage; use agent mode instead")
messages, err := adk.TypedGetMessages[M](ctx)
if err != nil {
return nil, fmt.Errorf("failed to get agent messages: %w", err)
}
return messages, nil
}
Expand Down