Skip to content

GiesN/gagentic

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gagentic

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.


Framework Architecture

Core Packages

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.


OODA Agent

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.

How It Works

Each iteration of the OODA agent runs four phases:

  1. Observe - Observer.Observe() collects new inputs (sensor data, API responses, user messages, prior action results)
  2. Orient - all FacetProviders run in parallel goroutines, each contributing a perspective on the situation; results are merged by Synthesizer.Synthesize() into an Orientation
  3. Decide - Decider.Decide() receives the orientation and history, and selects tool calls (typically via an LLM)
  4. 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.

Configuration

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.


LLM Streaming

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.


Checkpointing

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.

Basic Usage

// 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.

Checkpoint History and Time Travel

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.

Interface

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)
}

PostgreSQL Schema

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);

HITL Example with Checkpointing

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 down

tool.Wrap

tool.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.

What the LLM Controls vs. What is Fixed

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.


Examples

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

Build & Test

make test   # all unit tests
make race   # with race detector
make lint   # golangci-lint

Requirements

  • Go 1.26+

Status

Pre-alpha. APIs may change.

Roadmap

  • Multi-agent OODA loops with shared state
  • Human-in-the-loop gates for high-risk decision functions
  • Additional domain-specific agentic workflow abstractions

About

A Go framework for building agentic AI workflows

Resources

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors