Skip to main content

Embed the engine directly

Embed Mecatl when you want the agent loop to run inside your Go service. You own the binary, lifecycle, adapters, and transport, with no separate daemon or gRPC connection.

For the shortest working example, start with Build your first agent. This guide covers the additional choices an embedding application owns.

When to choose this

Choose in-process embedding when:

  • You want the agent loop as a library component in a service, worker, or IDE backend.
  • You want to select provider SDKs and other dependencies explicitly.
  • You already have a runtime (HTTP server, worker loop, queue consumer) and want the agent to live inside it.
  • You want to supply a custom port.LLMProvider, such as an internal model proxy, router, or test double.

Choose a prebuilt binary when you want Mecatl to assemble authentication, TLS, metrics, configuration flags, and transports.

Dependency footprint

The engine is the separate Go module github.com/stacklok/mecatl/engine. Its runtime dependencies cover concurrency, glob matching, YAML parsing, HTML parsing, shell parsing, and cron expressions. It does not pull in provider SDKs, gRPC, the terminal UI, Kubernetes clients, or Mecatl's root module.

Adding the dependency

go get github.com/stacklok/mecatl/engine@latest

The engine module is self-contained. Add an opt-in provider module separately if you want one of Mecatl's provider implementations.

Minimum wiring

Configure the engine through agent.Deps. A useful run needs an LLM provider, a tool catalog, a permission policy, and a model identifier. Hooks, persistence, timing, and prompt helpers are optional.

The following example uses in-memory reference adapters:

package main

import (
"context"
"fmt"
"time"

"github.com/stacklok/mecatl/engine/adapter/memfs"
"github.com/stacklok/mecatl/engine/adapter/memledger"
"github.com/stacklok/mecatl/engine/adapter/memstore"
"github.com/stacklok/mecatl/engine/adapter/mockllm"
"github.com/stacklok/mecatl/engine/adapter/permpolicy"
"github.com/stacklok/mecatl/engine/adapter/permstore"
"github.com/stacklok/mecatl/engine/agent"
"github.com/stacklok/mecatl/engine/governance"
"github.com/stacklok/mecatl/engine/prompt"
"github.com/stacklok/mecatl/engine/session"
"github.com/stacklok/mecatl/engine/tool"
)

func main() {
ctx := context.Background()

// Register only the tools the model needs.
cat := tool.NewCatalog()
// cat.MustRegister(myTool)

// Decide whether each tool call runs, asks for approval, or is denied.
policy := permpolicy.NewPolicy([]governance.Rule{
{Scope: governance.ScopeManaged, Tool: "Read", Effect: governance.Allow},
{Scope: governance.ScopeManaged, Tool: "Write", Effect: governance.Ask},
}, permstore.New())

// Assemble the engine.
eng := agent.NewEngine(agent.Deps{
LLM: mockllm.New(mockllm.TextTurn("Hello, world.")), // replace with your provider
Catalog: cat,
Policy: policy,
// Nil hooks use the no-op behavior.
Store: memstore.New(),
PromptConfig: prompt.Config{
Env: prompt.Env{
Cwd: "/workspace",
OS: "linux",
Model: "my-model",
Date: time.Now().Format("2006-01-02"),
Mode: string(session.ModeDefault),
},
},
Model: "my-model",
})

// Use the same environment identity for the session and tool environment.
ref := session.EnvironmentRef{
Kind: session.EnvKindMem, ID: "/workspace", Revision: "example-v1",
}
sess := session.New(
"my-session-id",
session.ModeDefault,
ref,
session.Limits{MaxTurns: 20, MaxToolCalls: 50, MaxConsecutiveFailures: 3},
time.Now(),
)
ws := memfs.NewWorkspace("/workspace")

// Start the run and consume every event.
env := tool.MustEnvironment(ref, ws, memledger.New(), nil)
run := eng.Run(ctx, sess, env, agent.RunRequest{Text: "Summarize the project."})
for ev := range run.Events() {
fmt.Printf("%s %v\n", ev.Type, ev.Text)

// Surface permission asks to your UI in a production application.
if ev.Type == session.EvPermissionAsk && ev.Ask != nil {
run.Approve(ev.Ask.AskID, session.VerdictAllowOnce)
}
}
}

eng.Run returns immediately while the loop runs in the background. Consume run.Events() until the channel closes. See The agent loop for the full event taxonomy and permission flow.

Ports and configuration

The following fields are the main integration points in agent.Deps:

FieldTypeRequired?Reference adapterNotes
LLMport.LLMProviderYesengine/adapter/mockllm for tests; bring your own for productionImplement Stream and Capabilities. See engine/port/llm.go.
Catalog*tool.CatalogYestool.NewCatalog() and cat.MustRegister(...)Register only the tools your agent should use.
Policyport.PermissionPolicyYesengine/adapter/permpolicy and engine/adapter/permstorepermpolicy.NewPolicy(rules, permstore.New()) is the standard wiring.
Hooksport.HookRunnerNoNoneNil hooks use the no-op behavior.
Storeport.SessionStoreNoengine/adapter/memstoreNil disables persistence. Use memstore.New() for in-process persistence.
Clockport.ClockNoengine/adapter/wallclockNil disables tool-call timing.
ModelstringYesNoneSent on every LLMRequest. Must match your provider's model identifier.
PromptConfigprompt.ConfigNoNoneSeeds the stable system prompt prefix. Most embeddings set Env.Cwd, Env.Model, Env.Date, and Env.Mode.

Optional fields with non-trivial defaults:

FieldDefault behaviour
CompactorHeuristicCompactor, which trims the conversation at the context-window threshold.
TokenCounterHeuristicTokenCounter, a character-based estimate.
ContextWindowNil, which disables compaction. Return the model's token window to enable it.
MaxNoProgressNudges2. The loop sends up to two continuation nudges before StopNoProgress.
MaxRunTokens0, which disables the per-engine token limit.
Instructionsprompt.RootAssembler, which looks for AGENTS.md or CLAUDE.md at the workspace root.

Token budgets with delegation

Deps.MaxRunTokens applies independently to each engine. The main engine, subagents, parallel branches, team members, and lead synthesis inherit the configured value, but each counts only its own session usage. A delegation tree can therefore exceed the configured value in aggregate.

For teams, agent.WithTeamTokenBudget sets a separate aggregate budget. Mecatl checks it between rounds. Crossing it prevents another round but allows the current round and lead synthesis to finish. It does not count work outside that team.

Optional evidence reflection

The optional learning package selects bounded evidence from a run and can produce evidence-backed reflection proposals. learning.MaterializeEvidence returns canonical evidence and an immutable manifest, or a content-free reason for abstaining. Persist the manifest with a staged proposal so later review uses the same evidence rather than rerunning selection.

The package also defines learned-skill lifecycle interfaces for validation, content-addressed versions, evaluation, review, and activation. Reference adapters cover in-memory storage and validation. You can replace each interface with your own implementation.

An embedding application owns proposal persistence, review authorization, scheduling, and transport. See the architecture guide for the evidence and output-validation contract.

What you do not get

An embedding application is responsible for the capabilities outside the engine:

CapabilityStatus
HTTP / gRPC serverNot included. Wire your own transport and relay events.
AuthenticationNot included. Add authentication in your application.
TLSNot included.
Prometheus metricsNot included. Wire port.ToolCallRecorder and port.Diagnostics to your own observability stack.
Kubernetes manifestsNot included.
CLI flagsNot included. Set the corresponding Deps fields in code.
Provider adaptersImport github.com/stacklok/mecatl/provider/openai, provider/openaichat, or provider/anthropic as separate modules. Each adds only its provider SDK and the engine.
Session store backends (JSONL, Redis)Not included in the engine module. memstore is. For durable or Redis-backed storage, import the root module's adapters.

If you need several of these capabilities, use mecated, which assembles them for you. See Run mecated standalone.

go.work for monorepo development

The engine is a separate Go module inside the Mecatl monorepo, connected via go.work. If you develop against a local checkout of Mecatl rather than the published module, set up a go.work in your own repo's parent:

# Run from the directory that contains your go.mod file.
go work init .
go work use /path/to/mecatl/engine

The resulting go.work file:

go 1.27

use .
use /path/to/mecatl/engine

go build and go test now resolve the engine from the local checkout. If your team develops against both repositories, commit go.work.sum with go.work.

go.work is local-only

Use go.work for local development. A published module should depend on a tagged release or use a replace directive in go.mod during development.

Next steps