A Go framework for building agentic AI workflows, defined as autonomous systems that observe their environment, reason over it, select actions, execute them, and loop until a goal is reached. The framework is built around a composable graph engine with a provider-neutral LLM interface and a structured tool registry.
graph/ generic graph engine (state, nodes, edges, compile)
llm/ provider-neutral chat + streaming + tool-use interface
llm/openai/ OpenAI Chat Completions adapter (chat + streaming)
llm/anthropic/ Anthropic Messages adapter (chat + streaming)
llm/fake/ in-memory scripted client for deterministic tests
internal/sse/ shared Server-Sent Events parser for streaming
tool/ tool definition, registry, parallel invocation, Wrap
runtime/ graph executor, events, checkpoint integration
runtime/pgcheckpoint/ PostgreSQL-backed persistent checkpointer
ooda/ canonical OODA loop agent (built on top of graph/)
At its core, graph/ and runtime/ provide a general-purpose directed graph executor: you define nodes (functions), edges (transitions), and compile them into a runnable workflow. The llm/ package gives every node a uniform way to call any LLM with tool-use support, including streaming via iter.Seq2[Chunk, error]. The tool/ package manages tool definitions, schema validation, parallel execution of tool calls, and reflection-based schema generation via tool.Wrap.
On top of the graph engine, gagentic ships a canonical OODA loop agent (ooda/), which is a ready-to-use agentic workflow pattern grounded in John Boyd's Observe-Orient-Decide-Act loop. It is the recommended starting point for building agents that need to reason iteratively over a changing environment.
Theoretical Foundation - OODA loop, Boyd's Orient pentagon, and the full scientific basis for this design.
Each iteration of the OODA agent runs four phases:
- Observe -
Observer.Observe()collects new inputs (sensor data, API responses, user messages, prior action results) - Orient - all
FacetProviders run in parallel goroutines, each contributing a perspective on the situation; results are merged bySynthesizer.Synthesize()into anOrientation - Decide -
Decider.Decide()receives the orientation and history, and selects tool calls (typically via an LLM) - Act - the framework executes the selected tools and stores results as
Actions; the loop then returns to Observe with feedback
The loop continues until TerminateWhen returns true or MaxIterations is reached.
cfg := ooda.Config{
ImplicitGuidance string // system-level instructions passed to the LLM
Observer Observer // Observe phase implementation
Facets []FacetProvider // parallel Orient facets (Boyd's pentagon)
Synthesizer Synthesizer // merges facets into an Orientation
Decider Decider // Decide phase - typically wraps an LLM
Tools *tool.Registry // tools available to the agent
TerminateWhen func(State) bool // optional custom stop condition
MaxIterations int // hard cap on loop iterations
}
agent, err := ooda.NewAgent(cfg)
final, err := agent.Run(ctx, ooda.State{})All four phases are interfaces, to which you supply the implementations. The framework handles the loop, state threading, parallel Orient execution, tool dispatch, history tracking, and the audit trail.
Both OpenAI and Anthropic adapters support streaming via the Stream method, which returns an iter.Seq2[llm.Chunk, error] iterator:
seq, err := client.Stream(ctx, llm.Request{Model: "gpt-4o", Messages: msgs})
for chunk, err := range seq {
if err != nil { /* handle */ }
fmt.Print(chunk.ContentDelta)
}Chunks carry ContentDelta, ToolCalls, FinishReason, and Usage as they arrive.
The executor supports checkpoint-based persistence and resume. Every call to Save appends a new checkpoint (returning its unique ID), and Load retrieves either the latest or a specific historical checkpoint for a thread.
// In-memory (for tests / single-process)
cp := runtime.NewMemoryCheckpointer[MyState]()
// PostgreSQL (for production, bring your own driver, e.g. lib/pq)
db, _ := sql.Open("postgres", pgURL)
cp := pgcheckpoint.New[MyState](db)
cp.Migrate(ctx)
opts := runtime.Options{MaxSteps: 100, ThreadID: "thread-1"}
runtime.WithCheckpointer[MyState](cp, &opts)
final, err := runtime.Run(ctx, compiled, initial, opts)State is saved after each frontier and restored on resume via ThreadID.
Every Save appends a new checkpoint rather than overwriting the previous one. This gives you a full history per thread. To resume from a specific historical checkpoint instead of the latest, set CheckpointID in Options:
opts := runtime.Options{
MaxSteps: 100,
ThreadID: "thread-1",
CheckpointID: "3", // resume from checkpoint 3 instead of latest
}
runtime.WithCheckpointer[MyState](cp, &opts)
final, err := runtime.Run(ctx, compiled, MyState{}, opts)When CheckpointID is empty (the default), the executor loads the most recent checkpoint for the thread. This enables time-travel workflows: inspect or replay from any prior point in execution.
type Checkpointer[S any] interface {
Save(ctx context.Context, threadID string, step int, state S, frontier []string) (checkpointID string, err error)
Load(ctx context.Context, threadID string, checkpointID string) (state S, step int, frontier []string, ok bool, err error)
}The pgcheckpoint store uses an append-only table:
CREATE TABLE gagentic_checkpoints (
checkpoint_id BIGSERIAL PRIMARY KEY,
thread_id TEXT NOT NULL,
step INT NOT NULL,
state JSONB NOT NULL,
frontier JSONB NOT NULL DEFAULT '[]',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_gagentic_checkpoints_thread
ON gagentic_checkpoints (thread_id, checkpoint_id DESC);The examples/hitl demo shows checkpoint-based resume with a human-in-the-loop gate:
docker compose up -d
go run ./examples/hitl # first run, pauses at review
go run ./examples/hitl # resumes from latest checkpoint
go run ./examples/hitl -checkpoint 2 # time travel to checkpoint 2
go run ./examples/hitl -thread t2 # start a separate thread
docker compose downtool.Wrap creates a Tool from a typed Go function, generating JSON Schema from struct tags via reflection:
type WeatherIn struct {
City string `json:"city" desc:"City name" required:"true"`
Units string `json:"units" desc:"Temperature units"`
}
type WeatherOut struct {
Temp float64 `json:"temp"`
Desc string `json:"desc"`
}
t := tool.Wrap("get_weather", "Get current weather", func(ctx context.Context, in WeatherIn) (WeatherOut, error) {
return WeatherOut{Temp: 22.5, Desc: "sunny in " + in.City}, nil
})
registry.Register(t)Supported struct tags: json (property name), desc (description), required:"true". Nested structs and slices are handled recursively.
| Component | Fixed (deterministic) | LLM-controlled |
|---|---|---|
| Observer | iteration count, feedback wiring | - |
| Facets | context configuration | - |
| Synthesizer | aggregation logic | - |
| Decider (prompt) | system instructions, history replay | - |
| Decider (output) | - | tool choice, arguments, reasoning |
| Act | execution mechanics, result capture | - |
| Loop | MaxIterations, TerminateWhen |
- |
The only autonomous decision is what the LLM returns from the Decide phase. Everything else is deterministic scaffolding.
| Example | LLM | Description | Command |
|---|---|---|---|
| Fake pipeline | Scripted | Deterministic read/transform/write | go run ./examples/oodademo/fake_llm_client |
| Real pipeline | GPT-4o | Model picks tools autonomously | go run ./examples/oodademo/openai_client |
| Business strategy | Scripted | Competitive-response agent with fake LLM | go run ./examples/implementations/business |
| Checkpointing | None | Crash recovery + time travel with the in-memory checkpointer | go run ./examples/checkpointing |
| Typed tools | Scripted | tool.Wrap schema reflection + concurrent InvokeAll |
go run ./examples/typedtools |
| Streaming | GPT-4o / local | Token streaming via Client.Stream (offline fallback) |
go run ./examples/streaming |
| Fan-out | None | Parallel nodes, custom reducer, executor event timeline | go run ./examples/fanout |
| HITL | None | Human-in-the-loop with PG checkpointing + time travel | go run ./examples/hitl |
make test # all unit tests
make race # with race detector
make lint # golangci-lint- Go 1.26+
Pre-alpha. APIs may change.
- Multi-agent OODA loops with shared state
- Human-in-the-loop gates for high-risk decision functions
- Additional domain-specific agentic workflow abstractions