golem’s core is a library: github.com/ishi-o/golem/core depends on nothing
but eino, and every heavyweight driver
(model SDKs, databases, container clients) lives in its own module outside
core. You assemble the runtime yourself — this page shows the whole path
from an empty main to a firing agent.
go get github.com/ishi-o/golem/core
go get github.com/ishi-o/golem/store/sqlx # or mongodb / redis
core/config is plain structs — populate them from a file, flags, or code,
then call Normalize to fill defaults and validate:
cfg := config.Config{
Locale: "en",
Storage: config.Storage{Location: "data"},
}
if err := cfg.Normalize(); err != nil {
log.Fatal(err)
}
SystemPrompt and ScheduledTaskPrompt have defaults you can override;
cfg.AI.GuideThreshold sets the tool-result size above which oversized
results are diverted to a file in the user’s workspace. (The GOLEM_*
environment variables belong to golem-cli,
not to the library.)
Pick one persistence adapter; each implements both store.Backend (the
record stores) and chatmemory.Repository (conversation memory):
db, _ := sqlx.Open("sqlite3", "golem.db")
backend, err := sqlxstore.New(db, sqlxstore.WithDialect(sqlxstore.DialectSQLite))
if err != nil { log.Fatal(err) }
if err := backend.Migrate(ctx); err != nil { log.Fatal(err) }
The mongodb and redis adapters have the same shape — see Stores.
The agent takes any Eino model.ToolCallingChatModel. Core deliberately
ships no provider-specific model. Use Eino-ext directly for OpenAI-compatible
models:
import einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
chatModel, err := einoopenai.NewChatModel(ctx, &einoopenai.ChatModelConfig{
APIKey: apiKey,
Model: modelName,
BaseURL: baseURL, // any OpenAI-compatible endpoint
})
tools.NewProvider assembles each run’s tool set: the per-user families
(file, memory, skill, todo, ask, publish, clock — built per run against the
user’s workspace), any registered tools, and per-user MCP servers.
workspaces := storage.NewWorkspaceFactory(cfg.Storage.Location)
provider := tools.NewProvider(cfg, workspaces, backend, nil, // nil MCPBuilder
tools.WithLogger(logger))
Each run is backed by Eino’s compose.ToolsNode. The provider passes the
original Eino tools to that node and installs golem’s cross-cutting policies
as Eino tool middleware. Calls stay sequential because built-in tools can
write shared state; the agent’s queueing, cancellation, and turn-ending
rules remain outside the node.
The process-wide families are registered in one explicit call. Sandbox tools need a backend from sandbox/; scheduled tasks need an injected Scheduler:
// Shell tools (optional): one docker or kubernetes sandbox.
sandbox, err := docker.New(docker.Config{
Image: "your-shell-runner-image",
Workspaces: workspaces,
Credentials: tools.CredentialsFromRepository(backend.ShellCredentials()),
})
if err != nil { log.Fatal(err) }
if err := tools.RegisterBuiltins(provider, tools.Builtins{
Sandbox: sandbox,
SandboxConfig: docker.DefaultToolsConfig(),
}); err != nil { log.Fatal(err) }
// Scheduled tasks (optional): inject a scheduler you implement.
runner, err := schedule.New(backend.ScheduledTasks(), agent, schedule.Config{
Prompt: cfg.AI.ScheduledTaskPrompt,
Scheduler: yourScheduler,
}, logger)
if err != nil { log.Fatal(err) }
if err := runner.Start(ctx); err != nil { log.Fatal(err) }
schedule.RegisterBuiltins(provider, schedule.NewTools(runner, backend.ScheduledTasks()))
// Subagents (optional): the agent as a tool of its own. See Subagents.
subagent.Register(provider, agent, cfg, nil, logger)
Knowledge is a facade with no bundled implementation. core/knowledge
contains no vector database, in-memory backend, or Milvus adapter. Eino
supplies the retriever.Retriever and indexer.Indexer contracts, while
eino-ext supplies Milvus implementations such as
components/retriever/milvus2 and components/indexer/milvus2, plus the
embedder selected by the application.
Install and construct those Eino-ext components in the downstream
application. If the application only needs retrieval in its own Eino graph,
use the Eino-ext retriever directly. If it wants golem’s scoped knowledge
tools, it must provide its own knowledge.KnowledgeBase implementation over
the chosen indexer/retriever; that adapter is intentionally not part of this
repository.
The agent retrieves scoped knowledge before ordinary model turns and frames
the passages as untrusted reference data. Explicit KnowledgeRetrieval
requests can pin a different scope and filter, which is how unattended event
triage uses a source owner’s fixed playbook rather than event text as a query.
External events are an optional intake/sweeper pair. Intake only normalizes, deduplicates, correlates, and stores bounded evidence; the sweeper performs the model call later:
// base is an application-owned knowledge.KnowledgeBase, or omit the
// knowledge/playbook tools when the application does not expose them.
base := yourKnowledgeBase
eventCfg := events.Config{Enabled: true, Sources: map[string]events.SourceConfig{
"monitor": {
Owner: events.Owner{UserID: "triage-owner"},
Playbook: events.Playbook{Query: "database incident", DocIDs: []string{"runbook"}},
},
}}
intake, _ := events.NewIntake(eventCfg, backend.Situations(), backend.ObservedEvents(), backend.ProcessedMessages())
sweeper, _ := events.NewSweeper(eventCfg, a, backend.Situations(), backend.ObservedEvents(), backend.ProcessedMessages())
sweeper.Start(ctx)
for _, t := range events.NewTools(backend.Situations(), backend.ObservedEvents()).List() {
provider.Register(t, nil)
}
for _, t := range events.NewPlaybookTools(base, eventCfg, isAdmin).List() {
provider.Register(t, nil)
}
Connectors call intake.Observe after authenticating the source actor. The
playbook tools are administrator-only and write documents to the configured
owner’s personal knowledge scope. events.TriageScenario is memoryless and
does not receive scheduling, subagent, administrator-knowledge, or playbook
mutation tools.
The optional webhook and email modules are connector facades, not part of
core:
go get github.com/ishi-o/golem/connector/grafana
go get github.com/ishi-o/golem/connector/github
go get github.com/ishi-o/golem/connector/gitlab
go get github.com/ishi-o/golem/connector/email
Mount each handler at the route configured in the vendor and pass the same
events.Intake to it:
http.Handle("/webhooks/grafana", grafana.NewHandler(intake, grafanaSecret))
http.Handle("/webhooks/github", github.NewHandler(intake, githubSecret))
http.Handle("/webhooks/gitlab", gitlab.NewHandler(intake, gitlabToken))
GitHub uses X-Hub-Signature-256; GitLab uses X-Gitlab-Token; Grafana uses
the bearer or basic credential configured on its contact point. GitLab and
Grafana do not sign the body, so terminate HTTPS before these handlers. The
handlers bound raw request bodies, acknowledge ignored/test deliveries with
204, and return 500 when the intake fails so a vendor can retry. Email is
also represented as a transport-neutral facade:
import "github.com/ishi-o/golem/connector/email"
source := email.NewSource()
receiver.Receive(ctx, func(ctx context.Context, message email.Message) error {
observation, err := source.Observe(message)
if err != nil {
return err
}
return intake.Observe(observation)
})
The downstream receiver supplies a stable DeliveryID (for example an IMAP
UID plus UIDVALIDITY), parses MIME, and computes ThreadID from mail thread
headers. It sets AuthenticatedFrom only after a trusted gateway or verifier
has authenticated the sender. This repository provides no IMAP/POP3/SMTP
implementation. Slack likewise remains a downstream transport integration.
Your own tools register individually — see Extending. The full family inventory is in Built-in tools.
a := agent.New(chatModel, backend, provider, cfg,
agent.WithBackend(backend),
agent.WithLogger(logger),
agent.WithModelName(modelName),
agent.WithMemoryWindow(50), // messages kept per conversation
agent.WithDefaultListener(defaultListener), // observes every run
)
Fire is non-blocking (it returns once the run is accepted); Cancel
aborts a run by request id; Shutdown stops accepting and waits out
in-flight runs.
FireOrQueue offers a message to the run already in flight over the same
conversation by the same user: a correction, an addition, an answer to
“should I go on?” arriving while the agent works. True means the run took it
— it is read into the turn at the next tool boundary, framed so the model
knows it arrived mid-turn, and no run of its own is started. False means
nothing matching is live and the caller should Fire the request itself.
The run’s listeners hear OnMessageQueued when a message joins and
OnQueuedMessageRead when it is read in; a message the run never got round
to reading is re-fired as its own run after the first one finishes.
request := agent.NewRequest(
agent.ChatScenario,
text,
agent.WithRequestID(runID),
agent.WithIdentity(userID, chatID, "p2p"),
agent.WithConversation(conversationID, rootMessageID, replyMessageID),
agent.WithListener(agent.ListenerFuncs{
OnContentFunc: func(soFar string) { fmt.Print(soFar) },
OnErrorFunc: func(err error) { log.Print(err) },
OnFinishedFunc: func(outcome agent.Outcome) {
// agent.OutcomeCompleted / OutcomeFailed / OutcomeCancelled
},
}),
)
if err := a.Fire(request); err != nil { log.Fatal(err) }
// later: a.Cancel(runID)
Scenarios shape the run: ChatScenario keeps conversation memory and offers
every tool; ScheduledTaskScenario (used by the schedule runner) resumes
the creating conversation but excludes the schedule tools so a firing
cannot schedule more work; SubagentScenario (used by the subagent tools)
joins no conversation and withholds the subagent and schedule tools, which
is the depth cap — see Subagents. Event triage is a separate
memoryless scenario with the same unattended-work restrictions.
Listeners also carry OnReasoning (the model’s thinking so far, one block
per model call — a reasoning-capable model’s stream carries it) and
OnSubagent (what a run this one started is doing; only the parent’s
listeners receive it).
The ask tool is offered when the run carries a question handler and
cfg.AI.Tools.AskUserQuestion.Enabled is true (the default). Add the
handler when the run starts:
agent.WithListener(agent.ListenerFuncs{
OnStartFunc: func(run *agent.RunContext) {
run.AddQuestionHandler(myHandler) // implements tools.QuestionHandler
},
})
A handler that returns the answers inside the call (tools.InlineAnswers)
lets the model continue from them in the same run; any other handler ends
the turn, and the answers arrive in a later run. See the CLI’s terminal
handler (cmd/questions.go in
golem-cli) for a working example.
package main
import (
"context"
"log"
"log/slog"
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
"github.com/ishi-o/golem/core/agent"
"github.com/ishi-o/golem/core/config"
"github.com/ishi-o/golem/core/storage"
"github.com/ishi-o/golem/core/tools"
sqlxstore "github.com/ishi-o/golem/store/sqlx"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
func main() {
ctx := context.Background()
logger := slog.Default()
cfg := config.Config{}
if err := cfg.Normalize(); err != nil { log.Fatal(err) }
db, err := sqlx.Open("sqlite3", "golem.db")
if err != nil { log.Fatal(err) }
backend, err := sqlxstore.New(db, sqlxstore.WithDialect(sqlxstore.DialectSQLite))
if err != nil { log.Fatal(err) }
if err := backend.Migrate(ctx); err != nil { log.Fatal(err) }
chatModel, err := einoopenai.NewChatModel(ctx, &einoopenai.ChatModelConfig{
APIKey: "sk-...", Model: "gpt-4o",
})
if err != nil { log.Fatal(err) }
workspaces := storage.NewWorkspaceFactory(cfg.Storage.Location)
provider := tools.NewProvider(cfg, workspaces, backend, nil)
a := agent.New(chatModel, backend, provider, cfg,
agent.WithBackend(backend), agent.WithLogger(logger))
err = a.Fire(agent.NewRequest(agent.ChatScenario, "hello!",
agent.WithIdentity("user-1", "chat-1", "p2p"),
agent.WithListener(agent.ListenerFuncs{
OnContentFunc: func(soFar string) { log.Print(soFar) },
OnFinishedFunc: func(outcome agent.Outcome) { log.Print("done: ", outcome) },
}),
))
if err != nil { log.Fatal(err) }
_ = a.Shutdown(ctx)
_ = db.Close()
}
Next: Built-in tools · Scheduled tasks · Extending