Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
87df1f8
feat: define AgenticModel component interface
mrh997 Oct 16, 2025
0728ce9
feat: change the index in StreamMeta to a non-pointer (#573)
mrh997 Nov 25, 2025
918c9d6
feat: improve AgenticResponseMeta definition (#575)
mrh997 Nov 25, 2025
3fc7ff0
feat: improve AssistantGenText definition (#577)
mrh997 Nov 26, 2025
368d25d
feat: improve extension type name (#578)
mrh997 Nov 26, 2025
d49e463
feat: modify package name (#579)
mrh997 Nov 26, 2025
baee8c5
feat: remove TokenUsage definition in CallbackOutput (#580)
mrh997 Nov 26, 2025
7dda937
feat: add helper functions for AgenticMessage (#582)
mrh997 Dec 1, 2025
2203090
feat: improve MCPToolCallError definition (#592)
mrh997 Dec 1, 2025
f0366a8
feat: improve Options definition (#593)
mrh997 Dec 1, 2025
4cf22d5
feat: add CallbackInput definition for CallbackInput (#594)
mrh997 Dec 1, 2025
eafa0d5
feat: define 'omitempty' flag in json tag (#595)
mrh997 Dec 1, 2025
082d950
fix: MCPToolApprovalRequest definition (#600)
mrh997 Dec 2, 2025
a21e1f5
feat: define StreamResponseError for openai (#601)
mrh997 Dec 3, 2025
7d68eeb
feat: support agentic message concat (#576)
meguminnnnnnnnn Dec 3, 2025
5af6475
fix: concat agentic messages (#604)
mrh997 Jan 6, 2026
bf2c536
fix: concat agentic messages (#604)
mrh997 Jan 6, 2026
9d84fdd
fix(schema): agentic concat support extra (#670)
meguminnnnnnnnn Jan 8, 2026
1fd8814
feat(schema): optimize agent message format (#671)
meguminnnnnnnnn Jan 8, 2026
86a7b6e
fix: openai ConcatResponseMetaExtensions (#678)
mrh997 Jan 12, 2026
28ed2b9
feat: improve comment (#679)
mrh997 Jan 12, 2026
906136e
feat: add agentic callbacks template (#681)
mrh997 Jan 13, 2026
75a2655
feat: improve AgenticToolChoice (#684)
mrh997 Jan 15, 2026
3d88d32
feat: define AgenticCallbackInput/Output (#689)
mrh997 Jan 15, 2026
d5aa8a0
feat: improve callback definition (#692)
mrh997 Jan 15, 2026
a7c6486
feat: improve callback definition (#702)
mrh997 Jan 19, 2026
3b481db
feat: agentic model support MaxTokens (#703)
mrh997 Jan 19, 2026
5e29f6f
feat: agentic model support stop option
mrh997 Jan 20, 2026
1840123
feat: add json tag for agentic message (#880)
mrh997 Mar 13, 2026
927da7a
feat(adk): add agentmd middleware for auto-injecting Agents.md into m…
fanlv Mar 13, 2026
7d3f27e
feat(adk): add TurnLoop and Cancellable interfaces
luohq-bytedance Feb 12, 2026
1e7f8a4
refactor(adk): improve TurnLoop API signatures
luohq-bytedance Feb 12, 2026
72d65cf
fix(adk): TurnLoop.Run
luohq-bytedance Feb 13, 2026
b982edd
chore
meguminnnnnnnnn Feb 14, 2026
729759a
chore
meguminnnnnnnnn Feb 14, 2026
dce1a20
chore
meguminnnnnnnnn Feb 14, 2026
ff5148c
chore
meguminnnnnnnnn Feb 14, 2026
a4acca9
feat(adk): modify on agent events (#795)
meguminnnnnnnnn Feb 19, 2026
680c726
feat(adk): turn loop support front and exit loop (#796)
meguminnnnnnnnn Feb 21, 2026
2279e23
feat(adk): implement cancel mechanism for ChatModelAgent (#797)
hi-pender Feb 24, 2026
aaa035f
feat(adk): modify cancel func (#800)
meguminnnnnnnnn Feb 24, 2026
bb3f4cb
fix(adk): select cancel after front without preemptive (#802)
meguminnnnnnnnn Feb 24, 2026
2cd1c25
fix: implement IsCallbacksEnabled and GetType for cancelableChatModel…
hi-pender Mar 2, 2026
14044c2
fix: rebase error
shentongmartin Mar 23, 2026
1230732
refactor(adk): replace TurnLoop with push-based API (#835)
shentongmartin Mar 26, 2026
8fd6123
fix(adk): skip saving checkpoint when TurnLoop is idle (#916)
shentongmartin Mar 27, 2026
d832c4f
feat(adk): export NewEventSenderToolWrapper for customizable tool eve…
shentongmartin Apr 1, 2026
a64d35e
fix(adk): prevent panic when orphaned tool goroutine sends event afte…
shentongmartin Apr 2, 2026
d29b95e
feat(adk): improve TurnLoop stop cleanup and add StopOption controls …
shentongmartin Apr 8, 2026
f25c591
feat(compose): support tool name and argument aliases in ToolsNode (#…
JonXSnow Apr 8, 2026
87cea07
feat(adk): add failover support for ChatModel (#885)
fanlv Apr 9, 2026
8c5a10e
feat: tool search (#884)
meguminnnnnnnnn Apr 9, 2026
99572ac
fix(adk): propagate missing ToolsNodeConfig fields in ChatModelAgent …
JonXSnow Apr 10, 2026
b710dd8
refactor(adk): improve cancel propagation, encapsulate TurnLoop stop …
shentongmartin Apr 14, 2026
592c470
feat(adk): add ShouldRetry callback with EOF-gated verdict signal for…
shentongmartin Apr 15, 2026
bef5008
docs(adk): add NOT RECOMMENDED advisory to agent transfer and workflo…
shentongmartin Apr 16, 2026
67aadb1
fix(adk): preserve nil agentCancelOpts in stopSignal.check to prevent…
shentongmartin Apr 17, 2026
a4460f3
refactor(adk): improve cancel API naming, enforce recursive teardown,…
shentongmartin Apr 20, 2026
d720849
feat(adk): integrate AgenticMessage into ADK (#920)
shentongmartin Apr 21, 2026
28a9142
feat(adk): add EnhancedRead with custom FileContentPart types (#973)
JonXSnow Apr 21, 2026
8683f14
feat(adk): add BeforeFinalAnswer hook in ChatModelAgentMiddleware
shentongmartin Mar 30, 2026
e8a6e4d
refactor(adk): replace bool with FinalAnswerDecision, fix nil state p…
shentongmartin Apr 16, 2026
c0c1a80
refactor(adk): short-circuit BeforeFinalAnswer on first reject, wire …
shentongmartin Apr 21, 2026
5843ac1
fix(adk): resolve rebase conflicts with alpha/09 AgenticMessage integ…
shentongmartin Apr 21, 2026
2570259
refactor(adk): make BeforeFinalAnswer generic and wire into agentic g…
shentongmartin Apr 21, 2026
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: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ output/*

# Reports (generated analysis files)
reports/
/todos

.DS_Store
*.log
*.log*
.claude
CLAUDE.md
*.jsonl
*.txt

# Specs directories
*/specs
Expand Down
117 changes: 85 additions & 32 deletions adk/agent_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,34 @@ func NewAgentTool(_ context.Context, agent Agent, options ...AgentToolOption) to
}
}

type agentTool struct {
agent Agent
// NewTypedAgentTool creates a new agent tool that wraps a TypedAgent as a tool.BaseTool.
func NewTypedAgentTool[M messageType](_ context.Context, agent TypedAgent[M], options ...AgentToolOption) tool.BaseTool {
opts := &AgentToolOptions{}
for _, opt := range options {
opt(opts)
}

return &typedAgentTool[M]{
agent: agent,
fullChatHistoryAsInput: opts.fullChatHistoryAsInput,
inputSchema: opts.agentInputSchema,
}
}

type typedAgentTool[M messageType] struct {
agent TypedAgent[M]

fullChatHistoryAsInput bool
inputSchema *schema.ParamsOneOf
}

func (at *agentTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
type agentTool = typedAgentTool[*schema.Message]

type agentToolRequest struct {
Request string `json:"request"`
}

func (at *typedAgentTool[M]) Info(ctx context.Context) (*schema.ToolInfo, error) {
name := at.agent.Name(ctx)
if name == "" {
return nil, errors.New("agent tool requires a non-empty Name")
Expand All @@ -119,7 +139,6 @@ func (at *agentTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
if desc == "" {
return nil, errors.New("agent tool requires a non-empty Description")
}

param := at.inputSchema
if param == nil {
param = defaultAgentToolParam
Expand All @@ -132,57 +151,60 @@ func (at *agentTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
}, nil
}

func (at *agentTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
func (at *typedAgentTool[M]) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
gen, enableStreaming := getEmitGeneratorAndEnableStreaming(opts)
var ms *bridgeStore
var iter *AsyncIterator[*AgentEvent]
var iter *AsyncIterator[*TypedAgentEvent[M]]
var err error

wasInterrupted, hasState, state := tool.GetInterruptState[[]byte](ctx)
if !wasInterrupted {
ms = newBridgeStore()
var input []Message

var input []M
if at.fullChatHistoryAsInput {
input, err = getReactChatHistory(ctx, at.agent.Name(ctx))
if err != nil {
return "", err
var zero M
if _, ok := any(zero).(*schema.Message); !ok {
return "", fmt.Errorf("fullChatHistoryAsInput is only supported for *schema.Message agents")
}
msgInput, histErr := getReactChatHistory(ctx, at.agent.Name(ctx))
if histErr != nil {
return "", histErr
}
input = any(msgInput).([]M)
} else {
if at.inputSchema == nil {
// default input schema
type request struct {
Request string `json:"request"`
}

req := &request{}
req := &agentToolRequest{}
err = sonic.UnmarshalString(argumentsInJSON, req)
if err != nil {
return "", err
}
argumentsInJSON = req.Request
}
input = []Message{
schema.UserMessage(argumentsInJSON),
}
input = newTypedUserMessages[M](argumentsInJSON)
}

iter = newInvokableAgentToolRunner(at.agent, ms, enableStreaming).Run(ctx, input,
append(getOptionsByAgentName(at.agent.Name(ctx), opts), WithCheckPointID(bridgeCheckpointID), withSharedParentSession())...)
runner := newTypedInvokableAgentToolRunner[M](at.agent, ms, enableStreaming)
iter = runner.Run(ctx, input,
append(extractAndDeriveCancelCtx(ctx, at.agent.Name(ctx), opts), WithCheckPointID(bridgeCheckpointID), withSharedParentSession())...)
} else {
if !hasState {
return "", fmt.Errorf("agent tool '%s' interrupt has happened, but cannot find interrupt state", at.agent.Name(ctx))
}

ms = newResumeBridgeStore(state)
ms = newResumeBridgeStore(bridgeCheckpointID, state)

iter, err = newInvokableAgentToolRunner(at.agent, ms, enableStreaming).
Resume(ctx, bridgeCheckpointID, append(getOptionsByAgentName(at.agent.Name(ctx), opts), withSharedParentSession())...)
agentOpts := extractAndDeriveCancelCtx(ctx, at.agent.Name(ctx), opts)
agentOpts = append(agentOpts, withSharedParentSession())

runner := newTypedInvokableAgentToolRunner[M](at.agent, ms, enableStreaming)
iter, err = runner.Resume(ctx, bridgeCheckpointID, agentOpts...)
if err != nil {
return "", err
}
}

var lastEvent *AgentEvent
var lastEvent *TypedAgentEvent[M]
for {
event, ok := iter.Next()
if !ok {
Expand All @@ -208,9 +230,13 @@ func (at *agentTool) InvokableRun(ctx context.Context, argumentsInJSON string, o
rp = append(rp, event.RunPath...)
event.RunPath = rp
}
tmp := copyAgentEvent(event)
gen.Send(event)
event = tmp
if msgEvent, ok := any(event).(*AgentEvent); ok {
tmp := copyTypedAgentEvent(msgEvent)
gen.Send(msgEvent)
event = any(tmp).(*TypedAgentEvent[M])
} else {
return "", fmt.Errorf("cross-message-type agent tools are not supported: cannot use an AgenticMessage agent as a tool of a Message agent")
}
}
}

Expand Down Expand Up @@ -241,7 +267,7 @@ func (at *agentTool) InvokableRun(ctx context.Context, argumentsInJSON string, o
if err != nil {
return "", err
}
ret = msg.Content
ret = extractTextContent(msg)
}
}

Expand Down Expand Up @@ -281,6 +307,18 @@ func getOptionsByAgentName(agentName string, opts []tool.Option) []AgentRunOptio
return ret
}

func extractAndDeriveCancelCtx(ctx context.Context, agentName string, opts []tool.Option) []AgentRunOption {
agentOpts := getOptionsByAgentName(agentName, opts)
baseOpts := getCommonOptions(nil, agentOpts...)
if baseOpts.cancelCtx != nil {
childCtx := baseOpts.cancelCtx.deriveChild(ctx)
agentOpts = append(agentOpts, WrapImplSpecificOptFn(func(o *options) {
o.cancelCtx = childCtx
}))
}
return agentOpts
}

func getEmitGeneratorAndEnableStreaming(opts []tool.Option) (*AsyncGenerator[*AgentEvent], bool) {
o := tool.GetImplSpecificOptions[agentToolOptions](nil, opts...)
if o == nil {
Expand All @@ -293,8 +331,11 @@ func getEmitGeneratorAndEnableStreaming(opts []tool.Option) (*AsyncGenerator[*Ag
func getReactChatHistory(ctx context.Context, destAgentName string) ([]Message, error) {
var messages []Message
err := compose.ProcessState(ctx, func(ctx context.Context, st *State) error {
if len(st.Messages) == 0 {
return nil
}
messages = make([]Message, len(st.Messages)-1)
copy(messages, st.Messages[:len(st.Messages)-1]) // remove the last assistant message, which is the tool call message
copy(messages, st.Messages[:len(st.Messages)-1])
return nil
})
if err != nil {
Expand Down Expand Up @@ -324,8 +365,20 @@ func getReactChatHistory(ctx context.Context, destAgentName string) ([]Message,
return history, nil
}

func newInvokableAgentToolRunner(agent Agent, store compose.CheckPointStore, enableStreaming bool) *Runner {
return &Runner{
func newTypedUserMessages[M messageType](text string) []M {
var zero M
switch any(zero).(type) {
case *schema.Message:
return any([]Message{schema.UserMessage(text)}).([]M)
case *schema.AgenticMessage:
return any([]*schema.AgenticMessage{schema.UserAgenticMessage(text)}).([]M)
default:
return nil
}
}

func newTypedInvokableAgentToolRunner[M messageType](agent TypedAgent[M], store compose.CheckPointStore, enableStreaming bool) *TypedRunner[M] {
return &TypedRunner[M]{
a: agent,
enableStreaming: enableStreaming,
store: store,
Expand Down
93 changes: 93 additions & 0 deletions adk/agent_tool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,36 @@ import (
"fmt"
"strings"
"sync"
"sync/atomic"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
)

type mockChatModelForAttack struct {
generateFn func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error)
}

func (m *mockChatModelForAttack) Generate(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
return m.generateFn(ctx, input, opts...)
}

func (m *mockChatModelForAttack) Stream(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
result, err := m.generateFn(ctx, input, opts...)
if err != nil {
return nil, err
}
r, w := schema.Pipe[*schema.Message](1)
go func() { defer w.Close(); w.Send(result, nil) }()
return r, nil
}

// mockAgent implements the Agent interface for testing
type mockAgentForTool struct {
name string
Expand Down Expand Up @@ -1146,3 +1166,76 @@ func TestInvokableAgentTool_ErrorCases(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "", out2)
}

func TestCrossTypeAgentToolGracefulError(t *testing.T) {
ctx := context.Background()

innerModel := &mockAgenticModel{
generateFn: func(ctx context.Context, input []*schema.AgenticMessage, opts ...model.Option) (*schema.AgenticMessage, error) {
return agenticMsg("inner result"), nil
},
}

innerAgent, err := NewTypedChatModelAgent[*schema.AgenticMessage](ctx, &TypedChatModelAgentConfig[*schema.AgenticMessage]{
Name: "AgenticInner",
Description: "An agentic agent used as a tool",
Model: innerModel,
})
require.NoError(t, err)

agenticAgentTool := NewTypedAgentTool(ctx, TypedAgent[*schema.AgenticMessage](innerAgent))

var outerCallCount int32
outerModel := &mockChatModelForAttack{
generateFn: func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
count := atomic.AddInt32(&outerCallCount, 1)
if count == 1 {
return &schema.Message{
Role: schema.Assistant,
ToolCalls: []schema.ToolCall{
{ID: "c1", Function: schema.FunctionCall{Name: "AgenticInner", Arguments: `{"request":"test"}`}},
},
}, nil
}
return schema.AssistantMessage("done", nil), nil
},
}

outerAgent, err := NewChatModelAgent(ctx, &ChatModelAgentConfig{
Name: "OuterMessageAgent",
Description: "A Message agent using an AgenticMessage sub-agent tool",
Model: outerModel,
ToolsConfig: ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{agenticAgentTool},
},
},
})
require.NoError(t, err)

runner := NewRunner(ctx, RunnerConfig{Agent: outerAgent, EnableStreaming: true})
iter := runner.Query(ctx, "test cross-type")

var capturedErr error
for {
event, ok := iter.Next()
if !ok {
break
}
if event.Err != nil {
capturedErr = event.Err
t.Logf("Cross-type error message: %v", event.Err)
}
}

if capturedErr == nil {
t.Log("DESIGN CONCERN: Cross-type agent tool (AgenticMessage sub-agent in Message agent) " +
"only errors at event forwarding time when streaming is enabled. " +
"The error check happens in the gen.Send path, which is only exercised " +
"when the outer agent actually calls the tool AND streaming is enabled. " +
"Without streaming, the tool result is returned as a string, so no type mismatch occurs.")
} else {
assert.Contains(t, capturedErr.Error(), "cross-message-type",
"Error should mention cross-message-type incompatibility")
}
}
Loading
Loading