Skip to main content

Agents

The Chronos agent is the central abstraction for building AI-powered applications. Agents combine a language model, tools, memory, knowledge, and optional graph-based workflows into a single, configurable unit.

Agent Struct​

The Agent struct holds all configuration and runtime components:

FieldTypeDescription
IDstringUnique identifier for the agent
NamestringHuman-readable display name
DescriptionstringOptional description of the agent's purpose
UserIDstringOptional user scope for multi-tenant scenarios
Modelmodel.ProviderLLM backend for chat completions
Tools*tool.RegistryRegistered tools for function calling
Skills*skill.RegistryReusable skill definitions
Memory*memory.StoreShort-term and long-term memory store
Storagestorage.StoragePersistence for sessions, events, checkpoints
Graph*graph.CompiledGraphDurable workflow graph (optional)
Knowledgeknowledge.KnowledgeRAG knowledge base (optional)
MemoryManager*memory.ManagerLLM-powered memory extraction (optional)
Hookshooks.ChainBefore/after middleware for execution events
Guardrails*guardrails.EngineInput and output validation rules
SessionStatemap[string]anyPersistent cross-turn state
OutputSchemamap[string]anyJSON Schema for structured output
NumHistoryRunsintNumber of past runs to inject into context
ContextCfgContextConfigContext window and summarization settings
SystemPromptstringBase system prompt
Instructions[]stringAdditional system instructions
SubAgents[]*AgentChild agents for multi-agent orchestration
Capabilities[]stringAdvertised capabilities for the protocol bus

Builder API​

Use agent.New(id, name) to create a builder. All methods return *Builder for chaining.

Core Configuration​

package main

import (
"context"
"log"
"os"

"github.com/spawn08/chronos/engine/model"
"github.com/spawn08/chronos/sdk/agent"
"github.com/spawn08/chronos/sdk/knowledge"
"github.com/spawn08/chronos/sdk/memory"
"github.com/spawn08/chronos/storage/adapters/sqlite"
)

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

store, err := sqlite.New("agent.db")
if err != nil {
log.Fatal(err)
}
defer store.Close()
if err := store.Migrate(ctx); err != nil {
log.Fatal(err)
}

provider := model.NewOpenAI(os.Getenv("OPENAI_API_KEY"))
memStore := memory.NewStore("my-agent", store)
mgr := memory.NewManager("my-agent", "user-123", memStore, provider)
var kb knowledge.Knowledge // wire up a knowledge.VectorKnowledge in real use

schema := map[string]any{
"type": "object",
"properties": map[string]any{
"answer": map[string]any{"type": "string"},
},
}

a, err := agent.New("my-agent", "My Agent").
Description("A helpful assistant for technical questions").
WithUserID("user-123").
WithModel(provider).
WithStorage(store).
WithMemory(memStore).
WithKnowledge(kb).
WithMemoryManager(mgr).
WithOutputSchema(schema).
WithHistoryRuns(3).
WithContextConfig(agent.ContextConfig{
MaxContextTokens: 128000,
SummarizeThreshold: 0.8,
PreserveRecentTurns: 5,
}).
WithSystemPrompt("You are a senior engineer.").
AddInstruction("Always cite sources when possible.").
AddCapability("chat").
Build()
if err != nil {
log.Fatal(err)
}
_ = a
}

Builder Methods​

MethodDescription
Description(d string)Set agent description
WithUserID(id string)Set user scope
WithModel(p model.Provider)Set LLM provider
WithStorage(s storage.Storage)Set persistence backend
WithMemory(m *memory.Store)Set memory store
WithKnowledge(k knowledge.Knowledge)Set RAG knowledge base
WithMemoryManager(m *memory.Manager)Set LLM-powered memory manager
WithOutputSchema(s map[string]any)Set JSON Schema for structured output
WithHistoryRuns(n int)Set number of past runs to inject
WithContextConfig(cfg ContextConfig)Set context window and summarization
WithSystemPrompt(prompt string)Set base system prompt
AddInstruction(instruction string)Append system instruction
AddCapability(capability string)Add advertised capability
AddTool(def *tool.Definition)Register a tool
AddSkill(s *skill.Skill)Register a skill
AddSubAgent(sub *Agent)Add child agent
AddHook(h hooks.Hook)Add execution hook
AddInputGuardrail(name string, g guardrails.Guardrail)Add input validation
AddOutputGuardrail(name string, g guardrails.Guardrail)Add output validation
WithGraph(g *graph.StateGraph)Set workflow graph
Build()Compile and return *Agent

Execute (Lightweight Task Execution)​

Execute is the simplest way to use an agent. It takes a text task, calls the model, and returns the text response. No graph, no storage, no session management — just a model call with the agent's system prompt and configuration applied.

This is the recommended entry point for agents used in multi-agent teams.

package main

import (
"context"
"fmt"
"log"
"os"

"github.com/spawn08/chronos/engine/model"
"github.com/spawn08/chronos/sdk/agent"
)

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

a, err := agent.New("helper", "Helper Agent").
WithModel(model.NewOpenAI(os.Getenv("OPENAI_API_KEY"))).
WithSystemPrompt("You are a helpful assistant. Be concise.").
Build()
if err != nil {
log.Fatal(err)
}

response, err := a.Execute(ctx, "What is the capital of France?")
if err != nil {
log.Fatal(err)
}
fmt.Println(response) // "The capital of France is Paris."
}

When to use Execute vs Chat:

  • Execute returns string — use when you just need the text response (team orchestration, simple tasks).
  • Chat returns *model.ChatResponse — use when you need the full response object (usage stats, tool calls, stop reason).

Chat (Single Turn)​

Chat sends a single user message and returns the full model response. It is stateless: no session is created, and conversation history is not persisted.

The agent builds messages from: system prompt, instructions, long-term memories (via MemoryManager), relevant knowledge (via RAG), and the user message. It runs input guardrails, calls the model, handles tool calls automatically, checks output guardrails, and extracts memories.

package main

import (
"context"
"fmt"
"log"
"os"

"github.com/spawn08/chronos/engine/model"
"github.com/spawn08/chronos/sdk/agent"
)

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

a, err := agent.New("chat-agent", "Chat Agent").
WithModel(model.NewOpenAI(os.Getenv("OPENAI_API_KEY"))).
WithSystemPrompt("You are a helpful assistant.").
Build()
if err != nil {
log.Fatal(err)
}

resp, err := a.Chat(ctx, "What is the capital of France?")
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Content)
}

Structured (JSON) Output​

WithOutputSchema(schema map[string]any) (or output_schema: in YAML) asks the model to return JSON conforming to a JSON Schema, and applies to Chat, ChatWithSession, and Run.

schema := map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"age": map[string]any{"type": "number"},
},
"required": []any{"name", "age"},
}

a, err := agent.New("extractor", "Extractor").
WithModel(model.NewOpenAI(os.Getenv("OPENAI_API_KEY"))).
WithOutputSchema(schema).
Build()

Two things happen together:

  1. Request-side enforcement. The schema is sent to the provider as its native structured-output parameter — OpenAI's, Azure OpenAI's, any OpenAI-compatible provider's (including Ollama and Mistral), and Gemini's response_format/responseSchema, or the Responses API's text.format.json_schema — so the model is actually constrained while generating, not just asked nicely. Anthropic has no equivalent API parameter, so OutputSchema has no request-side effect there; write an explicit schema-following instruction into the system prompt instead, or use tool-forcing for guaranteed structure.
  2. Response-side validation, regardless of provider: the reply is parsed as JSON and checked against required fields and each property's type. A mismatch (or non-JSON output) returns an error from Chat/ ChatWithSession/Run rather than silently passing through — this is your safety net on providers (or schemas) the request-side enforcement doesn't fully cover.

See examples/structured_output/ for a full runnable example decoding the reply into a typed Go struct.

From the CLI​

--output-schema <file> (a global flag — works before or after the subcommand, like --stream/--debug) points at a JSON Schema file and applies it the same way WithOutputSchema does, overriding any output_schema: already set in the agent's YAML config:

chronos --output-schema schema.json run --agent extractor "Extract: Alice is 30"

CHRONOS_OUTPUT_SCHEMA=<file> is the equivalent environment variable. The file must be plain JSON (a JSON Schema document), not YAML.

ChatWithSession (Multi-Turn)​

ChatWithSession maintains a persistent, multi-turn conversation. Messages are stored in the event ledger. When the context window approaches its limit, older messages are automatically summarized to stay within budget.

Requires Storage and Model. The session is created on first use if it does not exist.

package main

import (
"context"
"fmt"
"log"
"os"

"github.com/spawn08/chronos/engine/model"
"github.com/spawn08/chronos/sdk/agent"
"github.com/spawn08/chronos/storage/adapters/sqlite"
)

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

store, err := sqlite.New("session.db")
if err != nil {
log.Fatal(err)
}
defer store.Close()
if err := store.Migrate(ctx); err != nil {
log.Fatal(err)
}

a, err := agent.New("session-agent", "Session Agent").
WithModel(model.NewOpenAI(os.Getenv("OPENAI_API_KEY"))).
WithStorage(store).
WithSystemPrompt("You are a helpful assistant.").
WithContextConfig(agent.ContextConfig{
SummarizeThreshold: 0.8,
PreserveRecentTurns: 5,
}).
Build()
if err != nil {
log.Fatal(err)
}

sessionID := "user-123-conv-1"

// First turn
resp1, err := a.ChatWithSession(ctx, sessionID, "My name is Alice.")
if err != nil {
log.Fatal(err)
}
fmt.Println(resp1.Content)

// Second turn (agent remembers context)
resp2, err := a.ChatWithSession(ctx, sessionID, "What is my name?")
if err != nil {
log.Fatal(err)
}
fmt.Println(resp2.Content)
}

Run (Graph or Model Execution)​

Run starts an execution session. It works in two modes:

  1. Graph mode (when Graph and Storage are set): Executes a StateGraph with checkpointing. Each node receives and returns graph.State. The runner persists checkpoints to storage, enabling resume after interrupts.

  2. Model-only mode (when only Model is set): Falls back to Execute — extracts a "message" key from the input, calls the model, and returns the result as a completed RunState. This is how lightweight agents work in multi-agent teams.

Graph mode requires Graph and Storage. Model-only mode requires just Model.

package main

import (
"context"
"fmt"
"log"

"github.com/spawn08/chronos/engine/graph"
"github.com/spawn08/chronos/sdk/agent"
"github.com/spawn08/chronos/storage/adapters/sqlite"
)

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

store, err := sqlite.New("run.db")
if err != nil {
log.Fatal(err)
}
defer store.Close()
if err := store.Migrate(ctx); err != nil {
log.Fatal(err)
}

g := graph.New("workflow").
AddNode("greet", func(_ context.Context, s graph.State) (graph.State, error) {
s["greeting"] = fmt.Sprintf("Hello, %s!", s["user"])
return s, nil
}).
AddNode("classify", func(_ context.Context, s graph.State) (graph.State, error) {
s["intent"] = "general_question"
return s, nil
}).
AddNode("respond", func(_ context.Context, s graph.State) (graph.State, error) {
s["response"] = fmt.Sprintf("Intent: %s. How can I help?", s["intent"])
return s, nil
}).
SetEntryPoint("greet").
AddEdge("greet", "classify").
AddEdge("classify", "respond").
SetFinishPoint("respond")

a, err := agent.New("run-agent", "Run Agent").
WithStorage(store).
WithGraph(g).
Build()
if err != nil {
log.Fatal(err)
}

result, err := a.Run(ctx, map[string]any{"user": "World"})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Result: %v\n", result.State)
}

Resume (Continue from Checkpoint)​

Resume continues a paused session from its last checkpoint. Use this when a graph contains an interrupt node that requires human approval, or when execution was stopped for any reason.

package main

import (
"context"
"fmt"
"log"

"github.com/spawn08/chronos/engine/graph"
"github.com/spawn08/chronos/sdk/agent"
"github.com/spawn08/chronos/storage/adapters/sqlite"
)

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

store, err := sqlite.New("resume.db")
if err != nil {
log.Fatal(err)
}
defer store.Close()
if err := store.Migrate(ctx); err != nil {
log.Fatal(err)
}

g := graph.New("approval-workflow").
AddNode("draft", func(_ context.Context, s graph.State) (graph.State, error) {
s["draft"] = fmt.Sprintf("Draft for %s", s["topic"])
return s, nil
}).
AddInterruptNode("approve", func(_ context.Context, s graph.State) (graph.State, error) {
s["approved"] = true
return s, nil
}).
SetEntryPoint("draft").
AddEdge("draft", "approve").
SetFinishPoint("approve")

a, err := agent.New("resume-agent", "Resume Agent").
WithStorage(store).
WithGraph(g).
Build()
if err != nil {
log.Fatal(err)
}

// Run pauses at the "approve" interrupt node and checkpoints to storage.
runResult, err := a.Run(ctx, map[string]any{"topic": "Q3 roadmap"})
if err != nil {
log.Fatal(err)
}

// Later — e.g. after a human approves via the dashboard — resume from the
// checkpoint using the session ID captured on the paused RunState.
result, err := a.Resume(ctx, runResult.SessionID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Resumed result: %v\n", result.State)
}

ContextConfig​

ContextConfig controls context window management and automatic summarization in ChatWithSession:

FieldTypeDescription
MaxContextTokensintOverride model default; 0 = use model default
SummarizeThresholdfloat64Fraction of context window to trigger summarization (default 0.8)
PreserveRecentTurnsintNumber of recent user/assistant pairs to keep (default 5)