tacklr

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

README

Tacklr

CI Coverage Go Reference Go Version License

A Go framework for running AI agents in real products — not a one-off chat demo.

go get github.com/ryanaldo34/tacklr

What problem does it solve?

Most agent demos look like this:

  1. Send the whole chat history to the model
  2. Call tools
  3. Append more messages
  4. Repeat until the context window is full, then “summarize everything”

That wastes tokens, confuses the model with old noise, and is hard to run in editors, APIs, or long-lived services.

Tacklr is opinionated about the agent loop:

Problem What Tacklr does
Context fills with junk Keeps a plan and todos; when a step finishes, it builds a handoff (clean context for the next step)
Tools and models mixed into one blob Clear layers: model I/O, agent loop, client protocol
Hard to cancel or ask the user a question Turns, cancel, and interrupts (pause for input, then resume)
State dies when the process exits Checkpoints you can save and reload
Wiring into IDEs / HTTP Same registry over ACP (e.g. Zed) or SSE
Naive RAG dumps stale chunks into the window Optional knowledge base (brain): temporal + graph retrieval, dual-store design, agent tools only when wired

It is a framework (it defines how agents should run), not a loose bag of helpers.


How it works (big picture)

Think of three layers:

  Your model API (OpenAI-compatible, Azure, …)
            │
            ▼  tokens / tool calls from the model
     ┌──────────────┐
     │   harness    │  the agent loop: tools, plan, handoff, cancel
     │   (tacklr)   │
     └──────┬───────┘
            ▼  StreamEvent (shared event types)
     ┌──────────────┐
     │    server    │  Registry + protocol (ACP, SSE, …)
     └──────────────┘
            │
            ▼  editor / HTTP client
  1. Inference — talks to the model only (parse the stream into chunks).
  2. Harness — owns the turn: tools, plan builtins, context, save/load.
  3. Server — maps that to a client protocol (stdio ACP, SSE, …).

You can use the harness alone in a Go program, or put a registry in front for multi-agent HTTP/ACP.

One turn

A turn is one user prompt (or one resume after an interrupt) until the agent finishes, errors, or waits for the user:

User prompt
    → model may call tools (search, create_plan, complete_todo, …)
    → results go back into context
    → model continues until done / interrupt / cancel

Built-in plan tools drive a simple lifecycle:

create_plan  →  do work with tools  →  complete_todo  →  handoff  →  next work

Quick start

Minimal agent
package main

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/ryanaldo34/tacklr"
	"github.com/ryanaldo34/tacklr/inference"
	"github.com/ryanaldo34/tacklr/stores"
)

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

	model := inference.NewOpenAIInferenceStrategy(&http.Client{Timeout: 2 * time.Minute})
	model.WithURL(os.Getenv("OPENAI_BASE_URL")). // e.g. https://api.openai.com/v1
		WithApiKey(os.Getenv("OPENAI_API_KEY")).
		WithModel(os.Getenv("OPENAI_MODEL"))

	agent := tacklr.NewAgent(ctx, tacklr.AgentOptions{
		Config: tacklr.Config{
			MaxWindowSize: 8192,
			SystemPrompt:  "You are a concise assistant.",
		},
		Model: model,
		Store: stores.NewInMemoryStore(),
	})
	defer agent.Close()

	events, err := agent.Run(ctx, "Say hello in one short sentence.")
	if err != nil {
		panic(err)
	}
	for ev := range events {
		switch ev.Type {
		case tacklr.StreamEventMessage:
			fmt.Print(ev.Content)
		case tacklr.StreamEventError:
			fmt.Println("error:", ev.Error, ev.Content)
		case tacklr.StreamEventComplete:
			fmt.Println()
		}
	}
}
Your own tool

Tools are normal Go functions. Optional HarnessRuntime is for your state, progress pings, and interrupts — not for changing the framework plan.

type SearchArgs struct {
	Query string `json:"query" desc:"The search query"`
	Limit int    `json:"limit,omitempty" desc:"Max results"`
}

tool := tacklr.NewTool(tacklr.ToolConfig{
	Name:        "search_web",
	Description: "Search the web for information.",
	Handler: func(ctx context.Context, args SearchArgs, rt tacklr.HarnessRuntime) (string, error) {
		// rt.StateGet / StateSet  — small DI bag for your tool
		// rt.EmitUpdate(...)      — progress to the client
		// rt.RaiseInterrupt(...)  — ask the user and wait
		return doSearch(ctx, args.Query, args.Limit)
	},
})

Handler shapes supported: with or without args, with or without HarnessRuntime. JSON schema comes from struct tags (json, desc, enum).

Serve over ACP or SSE

Two stores matter for ACP:

Store Role
stores.BaseStore on the Registry Agent harness checkpoints (conversation, plan, tools)
server.ProtocolWireStore on the ACP protocol Wire session envelope (session/new / session/load: cwd, mcp, config)

You can implement either interface against your own DB (Redis, SQLite, database/sql, …). Built-in Postgres helpers use *pgx.Conn.

Short-hand (recommended):

store := stores.NewInMemoryStore() // or stores.NewPostgresStore(conn)
reg := server.NewRegistry(store, "my-agent")
reg.Register("my-agent", server.AgentSpec{
	Name: "Demo",
	Config: tacklr.Config{
		MaxWindowSize: 8192,
		SystemPrompt:  "You are a helpful assistant.",
	},
	Model: model,
	Tools: []*tacklr.Tool{tool},
})

// In-process ACP (memory wire store) — one line
srv := server.NewACPServer(reg)

// Editor / stdio (Zed, etc.)
_ = srv.ServeStdio(ctx, os.Stdin, os.Stdout)

// HTTP: WebSocket + Streamable HTTP on /acp
// _ = srv.ServeHTTP(ctx, ":8080")
//   ws://localhost:8080/acp
//   POST/GET/DELETE https://localhost:8080/acp  (HTTP/2 recommended for Streamable)

Durable wire sessions (Postgres, same connection as harness is fine):

// harness + wire schemas are separate tables on the same *pgx.Conn
harness := stores.NewPostgresStore(conn)
reg := server.NewRegistry(harness, "my-agent")
// reg.Register(...)
srv := server.NewACPServerPostgres(reg, conn)

Custom wire store or multi-protocol:

// Your ProtocolWireStore (Redis, etc.)
srv := server.NewACPServerWithWire(reg, myWireStore)

// ACP + SSE on one server
srv = server.NewServer(reg, server.NewACPProtocolMemory(), server.SSE)

// Explicit Postgres protocol only
srv = server.NewServer(reg, server.NewACPProtocolPostgres(conn))
Helper Meaning
NewACPServer(reg) ACP + memory wire store
NewACPServerWithWire(reg, wire) ACP + your ProtocolWireStore
NewACPServerPostgres(reg, conn) ACP + Postgres wire store (*pgx.Conn)
NewACPProtocolMemory() Protocol only (compose with NewServer)
NewACPProtocolPostgres(conn) Protocol only, Postgres wire

Or native HTTP + SSE (non-ACP wire):

srv := server.NewServer(reg, server.SSE)
_ = srv.ServeHTTP(ctx, ":8080")
# SSE prompt (native SSE protocol, not ACP)
curl -N -X POST http://localhost:8080/ \
  -H "Accept: text/event-stream" \
  -d '{"agent_id":"my-agent","prompt":"Hello"}'
Try the test server

cmd/testserver is a harness showcase: no toy host tools. The agent only gets Tacklr builtins (create_plan, list_plan, edit_plan, complete_todo, ask_user_choice, and web_search when EXA_API_KEY is set), plus optional skills via SKILL_DIRECTORIES.

By default it exports OTLP traces/metrics/logs to localhost:4317 (gRPC) with service.name=tacklr-testserver when a collector is listening. Override with OTEL_* env vars, or set OTEL_SDK_DISABLED=true to turn exporters off.

# .env: OPENAI_BASE_URL, OPENAI_API_KEY, OPENAI_MODEL
# optional: EXA_API_KEY, SKILL_DIRECTORIES, MAX_WINDOW_SIZE, OTEL_*
go build -o bin/testserver ./cmd/testserver
./bin/testserver --stdio   # ACP stdio (Zed, etc.)
./bin/testserver           # HTTP ACP on PORT or :3000
#   WebSocket:        ws://localhost:3000/acp
#   Streamable HTTP:  POST/GET/DELETE http://localhost:3000/acp
#   Legacy unary:     POST http://localhost:3000/
# or: make testserver

Core ideas (a bit more detail)

Plans and handoffs

The agent is pushed to work from a plan document and a todo list (built-in tools: create_plan, list_plan, edit_plan, complete_todo).

  • After create_plan, context is tightened around the user goal + plan.
  • After complete_todo (or a real plan-text edit), Tacklr runs a handoff: a short, structured carry-over for the next step instead of dumping the entire chat again.

That is the main “better context” idea in the project.

Sessions and checkpoints
agent := tacklr.NewAgent(ctx, opts)                      // new
agent, err := tacklr.NewAgentFromSession(ctx, id, opts) // restore

On save, a Checkpointer packages conversation window, plan, tool/user state, and pending interrupts. A store (in-memory or Postgres) persists it.

Tools vs framework state
Your tools Built-in plan tools
API HarnessRuntime Internal session manager (not passed to you)
Can State, interrupts, progress, store Create/edit plan, complete todos
Cannot Rewrite the plan store directly

This keeps product tools from breaking the planning system by accident.

  • MCP — pass MCPConfigs on the agent (or via ACP session); tools are discovered and run for you.
  • Skills — set Config.SkillDirectories to folders of SKILL.md (default skills.DirectoryLoader). Inject a source-bound AgentOptions.SkillsLoader for object storage, including skills.S3Loader and skills.BlobLoader. A short catalog lands in the system prompt; full text loads via read_skill when needed.
  • Web search (Exa) — when EXA_API_KEY is set in the environment (or AgentOptions.ExaAPIKey), the harness injects a built-in web_search tool (read access, token-efficient highlights by default). Hosts that use .env should load it before NewAgent (the test server already does). No Exa Go SDK; the harness calls Exa’s REST API.
  • Knowledge base (brain) — optional; see Knowledge base (brain) below.
Public harness surface

AgentHarness fields are unexported. Hosts use:

  • NewAgent / NewAgentFromSession + AgentOptions (model, store, tools, MCP, skills, interceptors, hooks, optional Brain)
  • SessionID() / BindSessionID (registry thread binding)
  • ToolRuntime() for interrupt helpers that need *HarnessRuntime
  • Messages() / RestoreMessages for the conversation window
  • Run / ReturnFromInterrupt / Close

Plan builtins return typed BuiltinResult effects (install plan, handoff) instead of name-keyed hooks.

Knowledge base (brain)

Tacklr’s knowledge package is not “stuff the last N chunks into context.” It is a host-owned retrieval engine with:

  • Postgres as the source of truth for full objects, parts/chunks, BM25 + dense hybrid search, filters, soft-delete, and containment (parent_id)
  • Helix (optional graph backend) for first-class entity nodes and cross-object edges (not chunks)—text/vector indexes, topology, edge metadata
  • Dual-write on parent Put / SoftDelete / Link so graph nodes stay live with the store
  • Scope (namespace) on every hydrate so multi-tenant isolation is engine-enforced

Hosts build an Engine, then attach it on the agent. The harness registers knowledge tools only when the engine is set; capability-gated tools appear only when the graph backend supports them.

Boot sketch
import (
	"github.com/ryanaldo34/tacklr"
	"github.com/ryanaldo34/tacklr/brain"
	"github.com/ryanaldo34/tacklr/brain/helixgraph"
	"github.com/ryanaldo34/tacklr/telemetry"
)

// store: brain.NewPostgresStore(pool) in production, or brain.NewMemoryStore() in tests.
store, err := brain.NewPostgresStore(pool)
if err != nil { /* … */ }

g, err := helixgraph.New(helixURL) // optional graph backend
if err != nil { /* … */ }
// Required for find_objects on Helix. Prefer true when the image supports tenant indexes.
if err := g.Bootstrap(ctx, false); err != nil { /* … */ }
// Required per relation label before find_links can search edge notes on Helix.
for _, rel := range []string{"about", "has_buyer", "references"} {
	if err := g.EnsureEdgeTextIndex(ctx, rel); err != nil { /* … */ }
}

eng, err := brain.NewEngine(store,
	brain.WithEmbedder(emb),                    // optional dense channel
	brain.WithGraph(g),                         // MemoryGraph also implements searchers
	brain.WithObserver(telemetry.NewBrainObserver()), // optional OTEL
	// brain.WithExpandRecipes(...),            // optional named ExpandRequest templates
	// brain.WithReranker(...),                 // optional post-hydrate host scoring
)
if err != nil { /* … */ }
if err := eng.ApplyKinds(ctx, kindSpecs...); err != nil { /* … */ }

agent := tacklr.NewAgent(ctx, tacklr.AgentOptions{
	// … Model, Store, Config …
	Brain: eng,
	BrainWriteKinds: brain.WriteKinds{
		Discovery: "Discovery", // non-empty → save_discovery tool
		Fact:      "Fact",
		Memory:    "Memory",
	},
	SearchNamespace: &tenantNS, // optional isolation (checkpointed)
})

Offline / tests: brain.NewMemoryStore() + brain.NewMemoryGraph() need no Bootstrap; edge text search works in-process.

Agent tools (capability matrix)
Tool When registered
schema, read, search, find_exact, continue, expand AgentOptions.Brain != nil
find_objects graph implements object text/vector search and is ready (Bootstrap on Helix)
find_links graph implements edge text search (Helix after EnsureEdgeTextIndex for that label)
link graph implements GraphWriter
save_discovery / save_fact / save_memory corresponding BrainWriteKinds field is non-empty

expand supports multi-hop (max_hops), direction (out / in / both), and mixed containment + graph labels. Large result sets page via continue.

Host GraphRAG composition (not agent tools)

Hosts can orchestrate the same path product code uses:

find_objects / search → LandingIDs / LandingIDsFromPage
  → Expand / ExpandMany / ExpandByRecipe
  → optional FindLinks
  → search(scope_ids=…) for neighborhood corpus
  → optional Reranker / SortRichObjects

LandingIDs promotes part hits to first-class parent ids so expand/link always target dual-written entities. See package docs: brain.

Observability

With brain.WithObserver(telemetry.NewBrainObserver()), retrieval ops emit tacklr.brain spans/metrics: search, find_exact, find_objects, find_links, continue, expand, expand_many (closed enum; degrade modes include lexical-only and containment-only).


Observability (optional)

Tacklr can emit traces and metrics with OpenTelemetry. You bring the backend (Grafana Alloy/Collector, Tempo, Prometheus/Mimir, etc.). Logs are normal slog; use telemetry.NewLogger if you want trace_id / span_id on log lines for Grafana/Loki.

Simple process (one OTLP endpoint for traces + metrics):

shutdown, err := telemetry.Init(ctx, telemetry.Config{
	ServiceName:  "my-agent",
	OTLPEndpoint: "localhost:4317", // Alloy / collector
	Insecure:     true,
})
defer shutdown(ctx)
// then NewRegistry / NewAgent — globals are used by default

Library host (you already own OTEL):

reg := server.NewRegistry(store, "my-agent",
	server.WithTracerProvider(myTP),
	server.WithMeterProvider(myMP),
)

Prometheus scrape (you own /metrics):

promReg := prometheus.NewRegistry()
mp, _ := telemetry.MeterProviderFromPrometheusRegisterer(promReg, "my-agent", "")
reg := server.NewRegistry(store, "my-agent", server.WithMeterProvider(mp))
// http.Handle("/metrics", promhttp.HandlerFor(promReg, ...))

With no endpoint and no injection, traces and metrics are no-ops. Prompt/tool content is not attached by default.

OTLP is the export path for traces, metrics, and logs. Point any collector (or vendor backend) at OTEL_EXPORTER_OTLP_ENDPOINT. slog can dual-write to stderr and OTLP via telemetry.InstallDefaultWithOTLP.


Packages

Package Role
tacklr Agent harness, tools, plan loop, subagents
brain Knowledge engine: store, expand, find_objects, kinds, dual-write
brain/helixgraph HelixDB adapter (WithGraph); Bootstrap + edge text indexes
inference OpenAI-compatible model client
server Registry + ACP / SSE
stores Session checkpoints
interrupt Interrupt types and registry for tool pause/resume
streaming Shared message/event types
mcp MCP config types (public)
skills SKILL.md loading (SkillLoader injectable; includes S3Loader / BlobLoader)
telemetry OTEL init, metrics helpers, brain observer, log correlation
internal/session Session manager, plan store, checkpointer, tool runtime

Develop

make test
make vet
Agent harness benchmarks

Multi-turn scenarios (plan, memory/brain, multi-hop QA, domain end-state, optional web) live in internal/agentbench with seed data in Go. Runner:

# List cases (no model)
go run ./cmd/agent-bench -list
go run ./cmd/agent-bench -dry-run

# Live run (same env as testserver)
export OPENAI_BASE_URL OPENAI_API_KEY OPENAI_MODEL
# hybrid dense channel (default text-embedding-3-small; same base URL/key)
export OPENAI_EMBEDDING_MODEL=text-embedding-3-small
# optional: EXA_API_KEY for web_augmented
go run ./cmd/agent-bench -suite all -out /tmp/agent-bench.json
# lexical-only ablation: go run ./cmd/agent-bench -lexical-only ...

Brain is seeded and agent saves with hybrid search (BM25-style lexical + dense embeddings via OpenAI-compatible /embeddings). Not run in default CI (model cost). Cases are industry-aligned (LoCoMo-style memory, multi-hop QA, τ-bench-style domain), not official leaderboard ports.

Contribution rules and design ethos live in AGENTS.md.


License

See LICENSE.

Documentation

Index

Constants

View Source
const (
	RoleUser      MessageRole = "user"
	RoleAssistant MessageRole = "assistant"
	RoleReasoning MessageRole = "reasoning"
	RoleSystem    MessageRole = "system"
	RoleDeveloper MessageRole = "developer"
	RoleTool      MessageRole = "tool"

	StatusInProgress ItemStatus = "in_progress"
	StatusCompleted  ItemStatus = "completed"
	StatusIncomplete ItemStatus = "incomplete"

	ContentTypeOutputText = "output_text"
	ContentTypeInputText  = "input_text"
	ContentTypeInputImage = "input_image"
	ContentTypeInputFile  = "input_file"
	ContentTypeRefusal    = "refusal"

	StreamEventMessage      StreamEventType = "message"
	StreamEventReasoning    StreamEventType = "reasoning"
	StreamEventFunctionCall StreamEventType = "function_call"
	StreamEventToolResult   StreamEventType = "tool_result"
	StreamEventComplete     StreamEventType = "complete"
	StreamEventError        StreamEventType = "error"
	StreamEventInterrupt    StreamEventType = "yield"
)
View Source
const (
	PermissionAllowOnce    = interrupt.PermissionAllowOnce
	PermissionAllowAlways  = interrupt.PermissionAllowAlways
	PermissionRejectOnce   = interrupt.PermissionRejectOnce
	PermissionRejectAlways = interrupt.PermissionRejectAlways
)
View Source
const CancelledToolResultContent = "cancelled: user interrupted the agent"

CancelledToolResultContent is written into the context window for tool calls aborted by session cancel or mid-turn steer (user interrupt).

Variables

View Source
var (
	ErrWorkerNotFound    = errors.New("worker not found")
	ErrWorkerNoOutput    = errors.New("worker produced no output")
	ErrWorkerIncomplete  = errors.New("worker finished without completing")
	ErrWorkerNoModel     = errors.New("worker has no model")
	ErrEmptyWorkerTask   = errors.New("worker task is empty")
	ErrWorkerParkMissing = errors.New("parked worker state is missing")
)

Sentinel errors for the subagent orchestrator.

View Source
var (
	ErrModelRefused         = errors.New("model refused")
	ErrMaxTokens            = errors.New("max tokens reached")
	ErrMaxTurnRequests      = errors.New("max turn model requests exceeded")
	ErrApiKeyNotSet         = errors.New("api key not set")
	ErrModelNotSet          = errors.New("model not set")
	ErrUnknownModel         = errors.New("unknown model")
	ErrToolNotFound         = errors.New("tool not found")
	ErrToolTimeout          = errors.New("tool timed out")
	ErrToolPermissionDenied = errors.New("tool permission denied")
	// ErrModelAfterTools is a model failure after a successful tool batch.
	// Tools completed; the next model request failed.
	ErrModelAfterTools = errors.New("model request failed after tools completed")
)
View Source
var (
	ErrInterruptNotFound     = interrupt.ErrInterruptNotFound
	ErrInvalidPayload        = interrupt.ErrInvalidPayload
	DefaultPermissionOptions = interrupt.DefaultPermissionOptions
)

Functions

func RegisterInterrupt

func RegisterInterrupt(factory func() Interrupt)

RegisterInterrupt registers a custom interrupt factory for session rehydrate.

func ResolveToolTitle

func ResolveToolTitle(displayName, toolName, argsJSON string) string

ResolveToolTitle fills {param} in DisplayName from top-level string args. Empty displayName → toolName. Missing/non-string args → empty slot.

func ToolsAsJson

func ToolsAsJson(tools []*Tool) string

ToolsAsJson serializes tool definitions for model requests. An empty catalog is "[]". Namespace-qualified names use "namespace.name".

func TypeToJSONSchema

func TypeToJSONSchema(v any) (map[string]any, error)

TypeToJSONSchema builds a JSON Schema for v. Prefer NewTool typed handlers for tools; this is mainly for structured model output.

func UnsupportedMIMEs

func UnsupportedMIMEs(s InferenceStrategy, mimes []string) []string

UnsupportedMIMEs returns mimes for which s.SupportsMIME is false (first-seen order).

func WrapStopReason

func WrapStopReason(kind, cause error) error

WrapStopReason attaches cause under a stop-reason sentinel for errors.Is. Returns kind when cause is nil, or cause when kind is nil.

Types

type AbsorbResult

type AbsorbResult struct {
	// SummaryChunks are compress summaries to stream when StreamFitSummary is true.
	SummaryChunks []LLMResponseChunk
}

AbsorbResult is returned by Absorb after incorporating a message.

type AgentHarness

type AgentHarness struct {
	// contains filtered or unexported fields
}

AgentHarness is the product agent. Create with NewAgent or NewAgentFromSession. Fields are unexported.

func NewAgent

func NewAgent(ctx context.Context, opts AgentOptions) *AgentHarness

NewAgent builds a session-scoped harness. Turn-scoped Runtime is created in Run.

func NewAgentFromSession

func NewAgentFromSession(ctx context.Context, sessionId string, opts AgentOptions) (*AgentHarness, error)

NewAgentFromSession loads a harness from a session checkpoint. opts.Store is required. Uses the same AgentOptions shape as NewAgent.

func (*AgentHarness) AskUserQuestion

func (a *AgentHarness) AskUserQuestion(toolCallID string) string

AskUserQuestion returns the ask_user_choice question for toolCallID, or empty. Used by ACP elicitation. Reads session state (survives the turn).

func (*AgentHarness) ClearSearchNamespace

func (a *AgentHarness) ClearSearchNamespace()

ClearSearchNamespace clears retrieval isolation for knowledge tools.

func (*AgentHarness) Close

func (a *AgentHarness) Close()

Close releases harness resources (for example MCP clients). Call after the Run events channel is drained.

func (*AgentHarness) FinalizeCancelledWork

func (a *AgentHarness) FinalizeCancelledWork(ctx context.Context)

FinalizeCancelledWork pairs open tools as cancelled into the window only, clears interrupt park, checkpoints. Parked/steer path (no live turn stream).

func (*AgentHarness) HasOpenToolWork

func (a *AgentHarness) HasOpenToolWork() bool

HasOpenToolWork reports pending tool calls, session interrupts, or unpaired assistant tool_calls in the window (parked / mid-cancel state).

func (*AgentHarness) Messages

func (a *AgentHarness) Messages() []*Message

Messages returns a snapshot of the conversation window. Observation only; do not use this to rehydrate or rewrite the window.

func (*AgentHarness) ReturnFromInterrupt

func (a *AgentHarness) ReturnFromInterrupt(ctx context.Context, finishedInterrupts map[string][]byte) (<-chan StreamEvent, error)

func (*AgentHarness) Run

func (a *AgentHarness) Run(ctx context.Context, prompt string) (<-chan StreamEvent, error)

Run starts a turn with a plain-text user message (SSE and simple hosts).

func (*AgentHarness) RunMessage

func (a *AgentHarness) RunMessage(ctx context.Context, user *Message) (<-chan StreamEvent, error)

RunMessage starts a turn with a full user Message (Content and optional ContentParts).

func (*AgentHarness) SearchNamespace

func (a *AgentHarness) SearchNamespace() (uuid.UUID, bool)

SearchNamespace returns the host-set search namespace, if any.

func (*AgentHarness) SessionID

func (a *AgentHarness) SessionID() string

SessionID returns the durable session id, or empty if unbound. Set with AgentOptions.SessionID at construction.

func (*AgentHarness) SetSearchNamespace

func (a *AgentHarness) SetSearchNamespace(id uuid.UUID)

SetSearchNamespace sets retrieval isolation for knowledge tools.

type AgentOptions

type AgentOptions struct {
	Config Config
	// SessionID is the durable thread id. Set at construction; do not change mid-turn.
	SessionID  string
	Model      InferenceStrategy
	Store      stores.BaseStore
	WatchDog   AgentWatchDog
	Tools      []*Tool
	MCPConfigs []mcp.MCPConfig
	SubAgents  []*SubAgent
	// ContextManager is the conversation window. Nil uses NewModelContextManager.
	ContextManager ContextManager
	// ModelTasks runs Turn, Absorb, and Handoff. Nil uses DefaultModelTasks.
	ModelTasks ModelTasks
	// ContextPolicy sets pressure/compress ratios when non-zero fields are set.
	ContextPolicy ContextPolicy
	// ToolInterceptors wrap each tool call (outermost first).
	// Nil: built-in planning lock and permission gate.
	// Non-nil: replaces that chain (empty slice disables interceptors).
	ToolInterceptors []ToolInterceptor
	// ToolResultHooks map tool name → post-success window effects for host tools.
	// Plan builtins use BuiltinResult instead.
	ToolResultHooks map[string]ToolResultHook
	// SkillsLoader loads skills. Nil uses DirectoryLoader with Config.SkillDirectories.
	SkillsLoader skills.SkillLoader
	// ExaAPIKey enables web_search and web_fetch. Empty falls back to EXA_API_KEY.
	// When both are empty, those tools are not registered.
	ExaAPIKey string
	// Brain enables knowledge builtins when non-nil. Workers inherit the same engine.
	// Configure Store, optional QueryEmbedder, and optional GraphReader/GraphWriter on the Engine
	// before NewAgent (e.g. brain.WithGraph(helixgraph.New(...))). The harness does
	// not construct graph backends.
	Brain *brain.Engine
	// BrainWriteKinds maps save_discovery / save_fact / save_memory to host kind names.
	// Empty fields skip that tool. Kinds should be registered via brain.ApplyKinds / WithKinds.
	// Ignored when Brain is nil.
	BrainWriteKinds brain.WriteKinds
	// SearchNamespace isolates brain retrieval when set (session-owned, checkpointed).
	// Nil leaves a loaded session value unchanged. Workers get a copy at spawn.
	SearchNamespace *uuid.UUID
}

AgentOptions configures NewAgent and NewAgentFromSession.

Usual fields: Config, Model, Store, Tools, MCPConfigs, SubAgents, SessionID. ContextManager, ModelTasks, and ContextPolicy override the built-in ACM path; leave them nil unless you replace that path.

type AgentWatchDog

type AgentWatchDog interface {
	RecordThinking(*Message) error
	RecordOutput(*Message) error
	RecordError(error) error
	RecordTokens(int, int) error
	RecordToolCalls(*Message) error
	RecordToolResult(*Message) error
}

AgentWatchDog records optional turn telemetry (thinking, tools, tokens).

type Annotation

type Annotation = streaming.Annotation

type BuiltinResult

type BuiltinResult struct {
	Output string
	// Effect is merged for the batch and applied once at batch end.
	Effect ToolResultEffect
	// SuppressWindowMessage omits the tool Message from the window.
	// The client still receives StreamEventToolResult.
	SuppressWindowMessage bool
}

BuiltinResult is a tool success that can queue ACM window effects. Output is the model-visible tool string. Plan tools use this type.

type Config

type Config struct {
	MaxWindowSize    int
	SystemPrompt     string
	SkillDirectories []string
	// MaxTurnRequests limits Model.Invoke calls per Run. 0 = unlimited.
	// Exceeding the limit ends the turn with ErrMaxTurnRequests.
	MaxTurnRequests int
}

Config is harness limits and prompt settings.

type ContentPart

type ContentPart = streaming.ContentPart

type ContextManager

type ContextManager interface {
	// Messages returns a retainable snapshot of the live window.
	Messages() []*Message
	// Snapshot is for checkpointing (shallow copy of message pointers).
	Snapshot() []*Message
	// Restore copies window into storage (caller keeps its slice).
	Restore(window []*Message)
	// Replace takes ownership of window; do not reuse the slice after.
	Replace(window []*Message)
	// Add appends without pressure fitting (streamed assistant/reasoning).
	Add(msg *Message)
	// InstallPlanDocument sets the window to [user, plan document].
	InstallPlanDocument(planRaw string) error
}

ContextManager owns the conversation window structure only (no inference). ModelTasks does model work and applies results with Replace or InstallPlanDocument. Snapshot must be safe while another path Absorbs or Replaces after resume.

type ContextPolicy

type ContextPolicy struct {
	// PressureRatio is the max-size fraction that triggers compress (for example 0.85).
	PressureRatio float64
	// CompressFraction seeds how much of the window to summarize.
	CompressFraction float64
	// StreamFitSummary streams compress summary chunks to the client when true.
	StreamFitSummary bool
}

ContextPolicy controls window compress under pressure (used by ModelTasks.Absorb).

func DefaultContextPolicy

func DefaultContextPolicy() ContextPolicy

DefaultContextPolicy is the product default pressure and compress settings.

type DefaultModelTasks

type DefaultModelTasks struct {
	// contains filtered or unexported fields
}

DefaultModelTasks is the product ModelTasks implementation.

func NewDefaultModelTasks

func NewDefaultModelTasks(model InferenceStrategy, ctx ContextManager, policy ContextPolicy, maxSize int) *DefaultModelTasks

NewDefaultModelTasks builds DefaultModelTasks for model, context, and policy.

func (*DefaultModelTasks) Absorb

func (t *DefaultModelTasks) Absorb(ctx context.Context, msg *Message, tools []*Tool, systemPrompt string) (AbsorbResult, error)

func (*DefaultModelTasks) Handoff

func (t *DefaultModelTasks) Handoff(ctx context.Context, plan []Todo, planDoc string, tools []*Tool, systemPrompt string) error

func (*DefaultModelTasks) Turn

func (t *DefaultModelTasks) Turn(ctx context.Context, tools []*Tool, systemPrompt string) (<-chan LLMResponseChunk, error)

type FileData

type FileData = streaming.FileData

type HarnessRuntime

type HarnessRuntime = session.Runtime

HarnessRuntime is the tool-facing API for handlers and interceptors: EmitUpdate, StateGet, StateSet, StateDelete, RaiseInterrupt, Store, and CurrentToolCallID. Turn lifecycle helpers live in internal/session.

type ImageURL

type ImageURL = streaming.ImageURL

type InferenceStrategy

type InferenceStrategy interface {
	WithApiKey(string) InferenceStrategy
	WithModel(string) InferenceStrategy
	WithURL(string) InferenceStrategy
	WithReasoningLevel(string) InferenceStrategy
	WithStructuredOutput(any) InferenceStrategy
	SetSystemPrompt(string)
	Invoke(context.Context, []*Message, []*Tool) (chan LLMResponseChunk, error)
	CountTokens(context.Context, []*Message, []*Tool) (int, error)
	CompressContextWindow() error
	MaxContextWindow() (int, error)
	// SupportsMIME reports whether the currently selected model accepts the
	// given MIME type as user input. Empty and text/* are always true.
	// Probe representatives for ads (e.g. image/png); do not enumerate all types.
	SupportsMIME(mimeType string) bool
}

InferenceStrategy is the model provider interface used by the harness.

type Interrupt

type Interrupt = interrupt.Interrupt

Interrupt types re-exported for tool authors.

type ItemStatus

type ItemStatus = streaming.ItemStatus

type LLMResponseChunk

type LLMResponseChunk = streaming.LLMResponseChunk

type Message

type Message = streaming.Message

type MessageRole

type MessageRole = streaming.MessageRole

type ModelContextManager

type ModelContextManager struct {
	// contains filtered or unexported fields
}

ModelContextManager is the default ContextManager (name is historical).

func NewModelContextManager

func NewModelContextManager() *ModelContextManager

NewModelContextManager returns an empty ContextManager.

func (*ModelContextManager) Add

func (m *ModelContextManager) Add(msg *Message)

func (*ModelContextManager) InstallPlanDocument

func (m *ModelContextManager) InstallPlanDocument(planRaw string) error

func (*ModelContextManager) Messages

func (m *ModelContextManager) Messages() []*Message

func (*ModelContextManager) Replace

func (m *ModelContextManager) Replace(window []*Message)

func (*ModelContextManager) Restore

func (m *ModelContextManager) Restore(window []*Message)

func (*ModelContextManager) Snapshot

func (m *ModelContextManager) Snapshot() []*Message

type ModelTasks

type ModelTasks interface {
	// Turn streams the next model step for the current window and tools.
	Turn(ctx context.Context, tools []*Tool, systemPrompt string) (<-chan LLMResponseChunk, error)
	// Absorb adds msg under window pressure (may summarize).
	Absorb(ctx context.Context, msg *Message, tools []*Tool, systemPrompt string) (AbsorbResult, error)
	// Handoff rebuilds context after complete_todo or plan edit.
	Handoff(ctx context.Context, plan []Todo, planDoc string, tools []*Tool, systemPrompt string) error
}

ModelTasks is Turn, Absorb, and Handoff against InferenceStrategy and ContextManager.

type PayloadValidator

type PayloadValidator = interrupt.PayloadValidator

Interrupt types re-exported for tool authors.

type PermissionOption

type PermissionOption = interrupt.PermissionOption

Interrupt types re-exported for tool authors.

type ProviderStatus

type ProviderStatus interface {
	ProviderHTTPStatus() int
	ProviderErrorCode() string
}

ProviderStatus supplies HTTP status and error code from a provider error. Optional on InferenceStrategy errors for model-span attributes.

type StreamEvent

type StreamEvent = streaming.StreamEvent

type StreamEventType

type StreamEventType = streaming.StreamEventType

type SubAgent

type SubAgent struct {
	Tools        []*Tool
	Instructions string
	Model        InferenceStrategy
	WorkerName   string
	Description  string
	// SubAgents are nested workers available to this worker when it runs.
	SubAgents []*SubAgent
}

SubAgent describes a specialized worker that a harness can spawn via the spawn_worker tool. Specs may nest via SubAgents so interrupt propagation and orchestration stay self-similar at any depth.

type Todo

type Todo = streaming.Todo

Todo is one plan list item (also used in plan_update stream payloads).

type Tool

type Tool struct {
	DisplayName string
	Name        string
	Description string
	Namespace   string
	Category    streaming.ToolCategory
	Access      mapset.Set[ToolPermission]
	// Timeout is an optional per-invocation deadline. Zero means none.
	Timeout time.Duration
	// PermissionRequired asks the user to approve the tool before it runs.
	PermissionRequired bool
	// contains filtered or unexported fields
}

func NewTool

func NewTool(cfg ToolConfig) *Tool

func (*Tool) AsJson

func (t *Tool) AsJson() map[string]any

AsJson returns the OpenAI-style function tool definition for this tool. parameters is never nil on the returned map.

type ToolCall

type ToolCall = streaming.ToolCall

type ToolCallFunc

type ToolCallFunc func(ctx context.Context, inv ToolInvocation) (string, error)

ToolCallFunc is the next interceptor step or the final tool invoke.

type ToolConfig

type ToolConfig struct {
	Name        string
	Description string
	DisplayName string
	Namespace   string
	Category    streaming.ToolCategory
	Access      mapset.Set[ToolPermission]
	Timeout     time.Duration
	// PermissionRequired asks the user to approve the tool before it runs.
	PermissionRequired bool

	Handler any
}

type ToolHandlerFunc

type ToolHandlerFunc func(ctx context.Context, args map[string]any, runtime HarnessRuntime) (string, error)

type ToolInterceptor

type ToolInterceptor func(ctx context.Context, inv ToolInvocation, next ToolCallFunc) (string, error)

ToolInterceptor wraps a tool call. Call next to continue, or return early to short-circuit. Nil ToolInterceptors uses the built-in planning lock and permission gate; a non-nil slice replaces that chain.

type ToolInvocation

type ToolInvocation struct {
	Tool     *Tool
	ArgsJSON string
	Runtime  HarnessRuntime
}

ToolInvocation is one tool call in the interceptor chain.

type ToolNamespace

type ToolNamespace struct {
	Name        string
	Description string
}

type ToolPermission

type ToolPermission int
const (
	ReadPermission ToolPermission = iota
	WritePermission
	ExecutePermission
)

type ToolPermissionInterrupt

type ToolPermissionInterrupt = interrupt.ToolPermissionInterrupt

Interrupt types re-exported for tool authors.

type ToolResultDisposition

type ToolResultDisposition struct {
	Effect                ToolResultEffect
	SuppressWindowMessage bool
}

ToolResultDisposition is the window effect from a BuiltinResult or ToolResultHook.

type ToolResultEffect

type ToolResultEffect int

ToolResultEffect is applied once after a successful tool batch (no open interrupts).

const (
	EffectNone ToolResultEffect = iota
	// EffectInstallPlanDocument sets the window to [user, plan document].
	EffectInstallPlanDocument
	// EffectHandoff rebuilds the window for the next open todos.
	EffectHandoff
)

type ToolResultHook

type ToolResultHook func(ctx context.Context, obs ToolResultObservation) ToolResultDisposition

ToolResultHook runs after a successful host tool and before the tool result is emitted. Effects apply at batch end. Plan builtins use BuiltinResult instead.

type ToolResultObservation

type ToolResultObservation struct {
	Name     string
	ArgsJSON string
	Output   string
	Runtime  HarnessRuntime
}

ToolResultObservation is a successful tool result seen by a ToolResultHook.

type URLAnnotation

type URLAnnotation = streaming.URLAnnotation

type UserChoice

type UserChoice = interrupt.UserChoice

Interrupt types re-exported for tool authors.

type UserSelectionInterrupt

type UserSelectionInterrupt = interrupt.UserSelectionInterrupt

Interrupt types re-exported for tool authors.

Directories

Path Synopsis
Package brain is Tacklr's knowledge-base retrieval engine.
Package brain is Tacklr's knowledge-base retrieval engine.
helixgraph
Package helixgraph adapts HelixDB to brain.GraphReader / GraphWriter / searchers.
Package helixgraph adapts HelixDB to brain.GraphReader / GraphWriter / searchers.
cmd
agent-bench command
Command agent-bench drives a real tacklr agent through multi-turn scenarios aligned with industry agent/memory/tool evaluation shapes.
Command agent-bench drives a real tacklr agent through multi-turn scenarios aligned with industry agent/memory/tool evaluation shapes.
testserver command
Command testserver is a local ACP harness for exercising Tacklr’s built-in agent tooling (plan/todos, ask_user_choice, web_search/web_fetch when EXA_API_KEY is set, skills when configured).
Command testserver is a local ACP harness for exercising Tacklr’s built-in agent tooling (plan/todos, ask_user_choice, web_search/web_fetch when EXA_API_KEY is set, skills when configured).
internal
agentbench
Package agentbench runs multi-turn harness benchmarks aligned with industry agent/memory/tool evaluation shapes (LoCoMo-style memory, multi-hop QA, τ-bench-style domain end state, plan+interrupt, web-augmented).
Package agentbench runs multi-turn harness benchmarks aligned with industry agent/memory/tool evaluation shapes (LoCoMo-style memory, multi-hop QA, τ-bench-style domain end state, plan+interrupt, web-augmented).
exa
Package exa is a minimal REST client for Exa Search (https://api.exa.ai).
Package exa is a minimal REST client for Exa Search (https://api.exa.ai).
mcp
testkit
Package testkit provides shared test doubles for harness and server integration tests.
Package testkit provides shared test doubles for harness and server integration tests.
Package skills discovers and parses application-owned SKILL.md files.
Package skills discovers and parses application-owned SKILL.md files.
Package streaming holds protocol-agnostic conversation and stream types shared by inference, the agent harness, and server protocols (ACP, SSE, and future A2A).
Package streaming holds protocol-agnostic conversation and stream types shared by inference, the agent harness, and server protocols (ACP, SSE, and future A2A).
Package telemetry configures OpenTelemetry for Tacklr hosts and process tools.
Package telemetry configures OpenTelemetry for Tacklr hosts and process tools.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL