Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
f62f3e3
feat: define AgenticModel component interface
mrh997 Oct 16, 2025
a861903
feat: change the index in StreamMeta to a non-pointer (#573)
mrh997 Nov 25, 2025
1800d0b
feat: improve AgenticResponseMeta definition (#575)
mrh997 Nov 25, 2025
89aec45
feat: improve AssistantGenText definition (#577)
mrh997 Nov 26, 2025
f702d4d
feat: improve extension type name (#578)
mrh997 Nov 26, 2025
e23ca5e
feat: modify package name (#579)
mrh997 Nov 26, 2025
b732df2
feat: remove TokenUsage definition in CallbackOutput (#580)
mrh997 Nov 26, 2025
32dd004
feat: add helper functions for AgenticMessage (#582)
mrh997 Dec 1, 2025
a806310
feat: improve MCPToolCallError definition (#592)
mrh997 Dec 1, 2025
2ffaac5
feat: improve Options definition (#593)
mrh997 Dec 1, 2025
8e84321
feat: add CallbackInput definition for CallbackInput (#594)
mrh997 Dec 1, 2025
a0e0cc5
feat: define 'omitempty' flag in json tag (#595)
mrh997 Dec 1, 2025
da62be6
fix: MCPToolApprovalRequest definition (#600)
mrh997 Dec 2, 2025
9be3d3c
feat: define StreamResponseError for openai (#601)
mrh997 Dec 3, 2025
05f2a48
feat: support agentic message concat (#576)
meguminnnnnnnnn Dec 3, 2025
c12ae61
fix: concat agentic messages (#604)
mrh997 Jan 6, 2026
b6b9a15
fix: concat agentic messages (#604)
mrh997 Jan 6, 2026
ab8e7db
fix(schema): agentic concat support extra (#670)
meguminnnnnnnnn Jan 8, 2026
e0b037b
feat(schema): optimize agent message format (#671)
meguminnnnnnnnn Jan 8, 2026
f853a8c
fix: openai ConcatResponseMetaExtensions (#678)
mrh997 Jan 12, 2026
0d23c8d
feat: improve comment (#679)
mrh997 Jan 12, 2026
eecb5e2
feat: add agentic callbacks template (#681)
mrh997 Jan 13, 2026
09ab149
feat: improve AgenticToolChoice (#684)
mrh997 Jan 15, 2026
c5f97e2
feat: define AgenticCallbackInput/Output (#689)
mrh997 Jan 15, 2026
bd766b0
feat: improve callback definition (#692)
mrh997 Jan 15, 2026
7a5ec42
feat: improve callback definition (#702)
mrh997 Jan 19, 2026
1d4dc1f
feat: agentic model support MaxTokens (#703)
mrh997 Jan 19, 2026
5521bb4
feat: agentic model support stop option
mrh997 Jan 20, 2026
ff19c86
feat: add json tag for agentic message (#880)
mrh997 Mar 13, 2026
4b0e7e6
feat(adk): add agentmd middleware for auto-injecting Agents.md into m…
fanlv Mar 13, 2026
0e2f191
feat(adk): add TurnLoop and Cancellable interfaces
luohq-bytedance Feb 12, 2026
75c2a0e
refactor(adk): improve TurnLoop API signatures
luohq-bytedance Feb 12, 2026
77b03a8
fix(adk): TurnLoop.Run
luohq-bytedance Feb 13, 2026
0d1b71d
chore
meguminnnnnnnnn Feb 14, 2026
7baf059
chore
meguminnnnnnnnn Feb 14, 2026
7fe6b25
chore
meguminnnnnnnnn Feb 14, 2026
c46caf4
chore
meguminnnnnnnnn Feb 14, 2026
db6eb48
feat(adk): modify on agent events (#795)
meguminnnnnnnnn Feb 19, 2026
70c4017
feat(adk): turn loop support front and exit loop (#796)
meguminnnnnnnnn Feb 21, 2026
c8b489a
feat(adk): implement cancel mechanism for ChatModelAgent (#797)
hi-pender Feb 24, 2026
1b8b21c
feat(adk): modify cancel func (#800)
meguminnnnnnnnn Feb 24, 2026
6e21ab6
fix(adk): select cancel after front without preemptive (#802)
meguminnnnnnnnn Feb 24, 2026
ff6b8f8
fix: implement IsCallbacksEnabled and GetType for cancelableChatModel…
hi-pender Mar 2, 2026
7421e86
fix: rebase error
shentongmartin Mar 23, 2026
23da416
refactor(adk): replace TurnLoop with push-based API (#835)
shentongmartin Mar 26, 2026
332bd51
fix(adk): skip saving checkpoint when TurnLoop is idle (#916)
shentongmartin Mar 27, 2026
1b775ea
feat(adk): export NewEventSenderToolWrapper for customizable tool eve…
shentongmartin Apr 1, 2026
8e5a7ab
fix(adk): prevent panic when orphaned tool goroutine sends event afte…
shentongmartin Apr 2, 2026
66277f1
feat(adk): improve TurnLoop stop cleanup and add StopOption controls …
shentongmartin Apr 8, 2026
6f01907
feat(compose): support tool name and argument aliases in ToolsNode (#…
JonXSnow Apr 8, 2026
61e9bea
feat(adk): add failover support for ChatModel (#885)
fanlv Apr 9, 2026
e927af1
feat: tool search (#884)
meguminnnnnnnnn Apr 9, 2026
b74d319
fix(adk): propagate missing ToolsNodeConfig fields in ChatModelAgent …
JonXSnow Apr 10, 2026
965ff48
refactor(adk): improve cancel propagation, encapsulate TurnLoop stop …
shentongmartin Apr 14, 2026
b0c380d
feat(adk): add ShouldRetry callback with EOF-gated verdict signal for…
shentongmartin Apr 15, 2026
8b2c43c
docs(adk): add NOT RECOMMENDED advisory to agent transfer and workflo…
shentongmartin Apr 16, 2026
52f41b8
fix(adk): preserve nil agentCancelOpts in stopSignal.check to prevent…
shentongmartin Apr 17, 2026
62b7701
refactor(adk): improve cancel API naming, enforce recursive teardown,…
shentongmartin Apr 20, 2026
e5bd5eb
feat(adk): integrate AgenticMessage into ADK (#920)
shentongmartin Apr 21, 2026
b51e3cf
feat(adk): add EnhancedRead with custom FileContentPart types (#973)
JonXSnow Apr 21, 2026
1f3e04d
refactor(adk): move tool lists into agent state and migrate middlewar…
shentongmartin Apr 23, 2026
15c0613
feat(adk): validate pages parameter in MultiModalReadFileTool (#993)
JonXSnow Apr 24, 2026
f7e4e9d
refactor(adk): replace AfterToolCallsRewriteState with per-run WithAf…
shentongmartin Apr 24, 2026
9b7a790
feat(compose): unify FunctionToolResult with Blocks (#968)
JonXSnow Apr 27, 2026
da46b77
feat(adk): generify ModelRetryConfig, ModelFailoverConfig, ModelConte…
shentongmartin Apr 28, 2026
dfc121a
feat(adk): build ToolsNodeConfig via shallow copy + field override (#…
JonXSnow Apr 29, 2026
c8748f3
feat(adk): add eino-internal message ID via Message.Extra (#986)
shentongmartin Apr 29, 2026
d1089ef
feat(adk): add AfterAgent hook to TypedChatModelAgentMiddleware (#1002)
shentongmartin May 6, 2026
1df3092
test(adk): fix handler order in EventSenderToolWrapper tests after re…
shentongmartin May 6, 2026
20891cd
fix(serialization): fix ToolInfo.ParamsOneOf loss during checkpoint d…
shentongmartin May 6, 2026
664af32
feat(adk): deprecate SummarizeMessages (#1009)
mrh997 May 6, 2026
e5e2b18
chore: reduction close copied tool output stream (#1008)
N3kox May 7, 2026
7f1a569
fix(adk): prevent TurnLoop stall after between-turns preempt drain (#…
shentongmartin May 8, 2026
181e1af
fix: ShouldRetry not called when no error during summarization (#1014)
mrh997 May 9, 2026
39d5285
feat(schema): add Type field to FunctionToolResultBlock (#1012)
JonXSnow May 9, 2026
eb1de22
fix: summarization failover count is off by one (#1015)
mrh997 May 9, 2026
722a4ff
feat(adk): generify all remaining middlewares for AgenticMessage supp…
shentongmartin May 12, 2026
9e877d3
feat(adk): persist TurnLoop checkpoint on business interrupt (#1013)
shentongmartin May 12, 2026
ca6754a
feat(adk): optimize default token estimation in summarization middlew…
mrh997 May 13, 2026
b3675fb
feat(adk): auto memory middleware
N3kox Mar 25, 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
126 changes: 94 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,65 @@ 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 {
// fullChatHistoryAsInput is only supported for *schema.Message agents and will not
// be extended to *schema.AgenticMessage. The chat history format and role semantics
// differ fundamentally between Message and AgenticMessage, and the history rewriting
// logic (role attribution, system message filtering, transfer messages) is specific
// to the Message model.
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 +235,17 @@ 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 {
// Cross-message-type agent tools are not supported and will not be supported.
// An AgenticMessage agent cannot be used as a tool within a Message agent's
// event stream. The agent tool still executes correctly and returns its text
// result; only real-time event streaming to the parent is blocked.
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 +276,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 +316,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 +340,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 +374,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