Eino: ToolsNode & Tool Guide
Introduction
Tool in Eino is an external capability that a ChatModel may choose to call, including local functions, MCP server tools, etc.
ToolsNode is the designated tool executor in Eino. Whether inside a Graph or in an Agent, tool execution is performed via ToolsNode:
// compose/tool_node.go
// run tools using Invoke
func (tn *ToolsNode) Invoke(ctx context.Context, input *schema.Message,
opts ...ToolsNodeOption) ([]*schema.Message, error)
// run tools using Stream
func (tn *ToolsNode) Stream(ctx context.Context, input *schema.Message,
opts ...ToolsNodeOption) (*schema.StreamReader[[]*schema.Message], error)
Configure ToolsNode with a list of tools and supporting policies:
// compose/tool_node.go
type ToolsNodeConfig struct {
Tools []tool.BaseTool
UnknownToolsHandler func(ctx context.Context, name, input string) (string, error)
ExecuteSequentially bool
ToolArgumentsHandler func(ctx context.Context, name, arguments string) (string, error)
ToolCallMiddlewares []ToolMiddleware
}
With this, ToolsNode can execute configured tools and gain extensibility such as execution ordering, error handling, argument processing, and middleware.
How does ToolsNode decide which tool to run? It does not decide; it executes according to the incoming *schema.Message:
// schema/message.go
type Message struct {
// role should be 'assistant' for tool call message
Role RoleType `json:"role"`
// each ToolCall is generated by ChatModel and to be executed by ToolsNode
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
// other fields...
}
// ToolCall in a message (used in Assistant when tool calls should be made)
type ToolCall struct {
// Index helps identify the chunk when multiple calls exist (for merging in stream mode)
Index *int `json:"index,omitempty"`
// ID identifies the specific tool call
ID string `json:"id"`
// Type of the tool call, default "function"
Type string `json:"type"`
// Function payload
Function FunctionCall `json:"function"`
// Extra information
Extra map[string]any `json:"extra,omitempty"`
}
type FunctionCall struct {
Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"`
}
ChatModel generates a list of ToolCalls (name, arguments, etc.) and places them in a *schema.Message for ToolsNode. ToolsNode executes each ToolCall in turn. If ExecuteSequentially is set, tools are executed in the order of the ToolCalls. Each result is wrapped into a *schema.Message and returned as part of ToolsNode output.
Interfaces
Tool interfaces have three levels:
Code:
eino/compose/tool/interface.go
// Basic tool interface, provides tool information
type BaseTool interface {
Info(ctx context.Context) (*schema.ToolInfo, error)
}
// Invokable tool, supports synchronous calls
type InvokableTool interface {
BaseTool
InvokableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (string, error)
}
// Streamable tool, supports streaming output
type StreamableTool interface {
BaseTool
StreamableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (*schema.StreamReader[string], error)
}
Info
- Purpose: return tool description for the model
- Params:
ctx: context
- Returns:
*schema.ToolInfo: tool metadata (name/desc/params)error
InvokableRun
- Purpose: synchronous tool execution
- Params:
ctx: context (request-scoped; carries callback manager)argumentsInJSON: JSON string of argumentsopts: tool options
- Returns:
string: execution resulterror
StreamableRun
- Purpose: streaming tool execution
- Params:
ctx: context (request-scoped; carries callback manager)argumentsInJSON: JSON string of argumentsopts: tool options
- Returns:
*schema.StreamReader[string]: streaming resulterror
ToolInfo
Code:
eino/schema/tool.go
type ToolInfo struct {
// Unique tool name, clearly expressing its purpose
Name string
// Guidance for the model on how/when/why to use the tool
// You can include brief examples in the description
Desc string
// Definition of accepted parameters
// Two ways to describe:
// 1. ParameterInfo: schema.NewParamsOneOfByParams(params)
// 2. OpenAPI v3: schema.NewParamsOneOfByOpenAPIV3(openAPIV3)
*ParamsOneOf
}
- Name: unique tool name
- Desc: guidance for when/how/why to use the tool (can include brief examples)
- ParamsOneOf: define accepted parameters in one of two ways:
- ParameterInfo:
schema.NewParamsOneOfByParams(params) - OpenAPI v3:
schema.NewParamsOneOfByOpenAPIV3(openAPIV3)
- ParameterInfo:
Options
ToolOption configures tool behavior. ToolsNode has no global options; implementations define specific options via WrapToolImplSpecificOptFn.
Usage
import (
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
)
toolsNode := compose.NewToolsNode([]tool.Tool{
searchTool, // Search tool
weatherTool, // Weather query tool
calculatorTool, // Calculator tool
})
input := &schema.Message{
Role: schema.Assistant,
ToolCalls: []schema.ToolCall{
{
Function: schema.FunctionCall{
Name: "weather",
Arguments: `{"city": "深圳", "date": "tomorrow"}`,
},
},
},
}
toolMessages, err := toolsNode.Invoke(ctx, input)
ToolsNode is typically used after a ChatModel inside orchestration.
In Orchestration
import (
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
)
// Chain
chain := compose.NewChain[*schema.Message, []*schema.Message]()
chain.AppendToolsNode(toolsNode)
// Graph
graph := compose.NewGraph[*schema.Message, []*schema.Message]()
graph.AddToolsNode(toolsNode)
Option Mechanism
import "github.com/cloudwego/eino/components/tool"
type MyToolOptions struct {
Timeout time.Duration
MaxRetries int
RetryInterval time.Duration
}
func WithTimeout(timeout time.Duration) tool.Option {
return tool.WrapImplSpecificOptFn(func(o *MyToolOptions) {
o.Timeout = timeout
})
}
Callbacks
import (
"context"
callbackHelper "github.com/cloudwego/eino/utils/callbacks"
"github.com/cloudwego/eino/callbacks"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/components/tool"
)
// 创建 callback handler
handler := &callbackHelper.ToolCallbackHandler{
OnStart: func(ctx context.Context, info *callbacks.RunInfo, input *tool.CallbackInput) context.Context {
fmt.Printf("开始执行工具,参数: %s\n", input.ArgumentsInJSON)
return ctx
},
OnEnd: func(ctx context.Context, info *callbacks.RunInfo, output *tool.CallbackOutput) context.Context {
fmt.Printf("工具执行完成,结果: %s\n", output.Response)
return ctx
},
OnEndWithStreamOutput: func(ctx context.Context, info *callbacks.RunInfo, output *schema.StreamReader[*tool.CallbackOutput]) context.Context {
fmt.Println("工具开始流式输出")
go func() {
defer output.Close()
for {
chunk, err := output.Recv()
if errors.Is(err, io.EOF) {
return
}
if err != nil {
return
}
fmt.Printf("收到流式输出: %s\n", chunk.Response)
}
}()
return ctx
},
}
// 使用 callback handler
helper := callbackHelper.NewHandlerHelper().
Tool(handler).
Handler()
/*** compose a chain
* chain := NewChain
* chain.appendxxx().
* appendxxx().
* ...
*/
// 在运行时使用
runnable, err := chain.Compile()
if err != nil {
return err
}
result, err := runnable.Invoke(ctx, input, compose.WithCallbacks(helper))
Implementations
- Google Search: Tool — GoogleSearch
- DuckDuckGo: Tool — DuckDuckGoSearch
- MCP server as tool: Tool — MCP
v0.5.x → v0.6.x Migration
Eino removed all OpenAPI 3.0 related definitions and methods and switched to JSONSchema 2020-12 because:
- Major model vendors and MCP tool protocols specify input/output schemas via JSONSchema
getkin/kin-openapi@v0.118.0has security issues, and later secure versions introduced breaking changes
See details: https://github.com/cloudwego/eino/discussions/397
After upgrading, some eino-ext modules may error with undefined: schema.NewParamsOneOfByOpenAPIV3; upgrade those modules to the latest versions.
If schema migration is complex, use the helper tooling linked in the discussion above.