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:
| Field | Type | Description |
|---|---|---|
ID | string | Unique identifier for the agent |
Name | string | Human-readable display name |
Description | string | Optional description of the agent's purpose |
UserID | string | Optional user scope for multi-tenant scenarios |
Model | model.Provider | LLM backend for chat completions |
Tools | *tool.Registry | Registered tools for function calling |
Skills | *skill.Registry | Reusable skill definitions |
Memory | *memory.Store | Short-term and long-term memory store |
Storage | storage.Storage | Persistence for sessions, events, checkpoints |
Graph | *graph.CompiledGraph | Durable workflow graph (optional) |
Knowledge | knowledge.Knowledge | RAG knowledge base (optional) |
MemoryManager | *memory.Manager | LLM-powered memory extraction (optional) |
Hooks | hooks.Chain | Before/after middleware for execution events |
Guardrails | *guardrails.Engine | Input and output validation rules |
SessionState | map[string]any | Persistent cross-turn state |
OutputSchema | map[string]any | JSON Schema for structured output |
NumHistoryRuns | int | Number of past runs to inject into context |
ContextCfg | ContextConfig | Context window and summarization settings |
SystemPrompt | string | Base system prompt |
Instructions | []string | Additional system instructions |
SubAgents | []*Agent | Child agents for multi-agent orchestration |
Capabilities | []string | Advertised 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
| Method | Description |
|---|---|
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:
Executereturnsstring— use when you just need the text response (team orchestration, simple tasks).Chatreturns*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:
- 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'stext.format.json_schema— so the model is actually constrained while generating, not just asked nicely. Anthropic has no equivalent API parameter, soOutputSchemahas no request-side effect there; write an explicit schema-following instruction into the system prompt instead, or use tool-forcing for guaranteed structure. - Response-side validation, regardless of provider: the reply is parsed
as JSON and checked against
requiredfields and each property'stype. A mismatch (or non-JSON output) returns an error fromChat/ChatWithSession/Runrather 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:
-
Graph mode (when
GraphandStorageare set): Executes a StateGraph with checkpointing. Each node receives and returnsgraph.State. The runner persists checkpoints to storage, enabling resume after interrupts. -
Model-only mode (when only
Modelis set): Falls back toExecute— extracts a"message"key from the input, calls the model, and returns the result as a completedRunState. 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:
| Field | Type | Description |
|---|---|---|
MaxContextTokens | int | Override model default; 0 = use model default |
SummarizeThreshold | float64 | Fraction of context window to trigger summarization (default 0.8) |
PreserveRecentTurns | int | Number of recent user/assistant pairs to keep (default 5) |