golem

Extending

Custom tools

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.

Eino and Eino-ext adapters

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:

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.

MCP servers

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.

Todo handlers

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.

Question handlers

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.

Connectors

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.

Sandboxes and stores

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.