Register a downstream tool during application setup. The request identity is available through the typed tool context:
type profileInput struct{}
func profileTool() tool.InvokableTool {
return golemtools.MustTool(utils.InferTool(
"MyProfile",
"Return the profile of the person in this conversation.",
func(ctx context.Context, _ profileInput) (string, error) {
userID, err := golemtools.UserID.Require(ctx)
if err != nil {
return "", err
}
return "profile for " + userID, nil
},
))
}
provider.Register(profileTool(), nil)
utils.InferTool (from eino) derives the schema from the input struct;
MustTool panics on an uninferable schema — a programming error, caught at
first use. Identity keys: UserID, ChatID, ChatType, RootMessageID,
ReplyMessageID. The second Register argument optionally gates the tool
by scenario (see Built-in tools).
For a quick before/after hook, use the convenience interceptor facade:
provider := tools.NewProvider(cfg, workspaces, backend, nil,
tools.WithInterceptor(tools.InterceptorFuncs{
Before: func(ctx context.Context, name, arguments string) (string, error) {
return rewrite(name, arguments), nil
},
After: func(ctx context.Context, name, arguments, result string) (string, bool, error) {
return annotate(name, result), false, nil
},
}))
WithInterceptor is a thin adapter to Eino middleware. For the full native
Eino hook surface, register compose.ToolMiddleware with
tools.WithToolMiddleware; it receives the same compose.ToolInput and
endpoint that Eino uses, so it can rewrite arguments before calling next and
transform the returned ToolOutput:
provider := tools.NewProvider(cfg, workspaces, backend, nil,
tools.WithToolMiddleware(compose.ToolMiddleware{
Invokable: func(next compose.InvokableToolEndpoint) compose.InvokableToolEndpoint {
return func(ctx context.Context, input *compose.ToolInput) (*compose.ToolOutput, error) {
output, err := next(ctx, input)
if err != nil {
return nil, err
}
output.Result = "annotated: " + output.Result
return output, nil
}
},
}))
Use tools.EndTurnResult(result) when middleware or a tool must end the run
after returning its result. The provider’s built-in middleware handles large
results and converts ordinary tool errors into model-visible tool messages.
core intentionally depends on Eino’s stable interfaces, not on a model
vendor or a network client. Use Eino-ext
in the application or an optional integration module for implementations such
as:
components/model/openai for OpenAI-compatible chat models;components/tool/mcp for MCP clients;Pass the resulting Eino model.ToolCallingChatModel to agent.New, and return
Eino tool.InvokableTool values from the provider’s MCPBuilder. This keeps
the facade in core small and lets an integration choose its own credentials,
transport, and lifecycle.
Eino already provides the native component seams used by core: chat-model,
retriever, tool, compose.ToolsNode, compose.ToolMiddleware, and callback
handler interfaces. Eino-ext provides concrete model implementations such as
components/model/openai, MCP tools, embeddings, retrievers/vector stores,
and callback handlers. Do not add those providers to core. In particular,
model/openai is an eino-ext package, not a golem package.
The knowledge.KnowledgeBase facade intentionally remains slightly wider than
Eino’s retriever.Retriever: it also needs scoped index/list/delete/move. No
knowledge implementation or Milvus adapter is shipped here. If an
application wants golem’s knowledge tools, it owns the adapter around its
eino-ext indexer/retriever and exposes the search half through
knowledge.NewRetriever.
Core does not include a tool-search index or a model-provider registry. A
large tool catalog can be selected in the integration layer and exposed
through MCPBuilder or Provider.Register, using the Eino/Eino-ext component
that matches the deployment.
The provider accepts an MCPBuilder, so a downstream connector chooses its
own MCP client and returns the tools it connected for the current user:
type mcpBuilder struct {
connect func(context.Context, string, string) ([]tool.InvokableTool, io.Closer, error)
}
func (b mcpBuilder) Build(ctx context.Context, userID, chatID string) (golemtools.MCPTools, error) {
serverTools, closer, err := b.connect(ctx, userID, chatID)
if err != nil {
return golemtools.MCPTools{}, err
}
return golemtools.MCPTools{Tools: serverTools, Closer: closer}, nil
}
provider := golemtools.NewProvider(cfg, workspaces, backend, mcpBuilder{
connect: connectMCP,
})
Connections are built per run (MCP handshakes are blocking network work) and closed when the run ends; a failing server costs its tools, never the run.
The model records plans through TodoWrite; every event is delivered to
the run’s todo handlers. Add one when the run starts:
agent.WithListener(agent.ListenerFuncs{
OnStartFunc: func(run *agent.RunContext) {
run.AddTodoHandler(func(ctx context.Context, event tools.TodoEvent) {
renderTodoCard(event.Todos) // your surface
})
},
})
tools.TodoFanOut combines handlers; a panicking handler costs only its
own view.
The ask tool needs a tools.QuestionHandler on the run — see
User questions. Handlers that return answers
inline implement tools.InlineAnswers; others end the turn (tools.ErrNotAnswered
marks “no answer inside this run”), and the answers arrive in a later run.
A connector translates between a chat surface and the agent API: receive a
message → agent.Fire with identity and conversation ids → stream
OnContent to the surface → deliver answers to outstanding asks. The
repository’s connector/feishu (Feishu/Lark: message events, cards,
replies, duplicate-message protection) is the reference implementation.
For external events, connector/webhook supplies the HTTP facade and
connector/github, connector/gitlab, and connector/grafana supply the
vendor authentication and payload adapters. They produce
observing.Observation values and never call a model or choose persistence.
Mount them on the application’s HTTP server and pass events.Intake to their
NewHandler function. Slack and email need a Slack client/socket transport or
an IMAP/MIME client; those are not supplied by Eino/eino-ext. The
connector/email facade defines the transport-neutral Message, Receiver,
and Source seams; a downstream receiver supplies the protocol client and
passes messages through Source.Observe before calling the event intake.
New persistence adapters implement store.Backend +
chatmemory.Repository; new sandbox backends implement tools.Sandbox
(Ensure/Remove/Close) and register through
tools.RegisterBuiltins. Both are plain interfaces — no registration
machinery beyond the provider.