Goal: Learn to wire up and configure a runnable Agent.
Prerequisite: 01-overview.md
Next: 03-agent-loop.md for loop internals, 04-tools.md for tool development.
devkit.Build(ctx, opts) is the single entry point to assemble a fully wired *Kit.
- Validate Options (Model required, APIKey or OAuth required)
- Create TapeStore (memory, file, or database)
- Create LLMCore, TapeManager, ToolExecutor, ChatClient
- Compose system prompt (supports plugin fragment injection)
- Create and return Agent
kit, err := devkit.Build(ctx, devkit.Options{
Model: "gpt-4o",
APIKey: os.Getenv("AI_API_KEY"),
})
if err != nil {
log.Fatal(err)
}
defer kit.Close(ctx)type Kit struct {
Agent *agent.Agent
TapeManager *tape.TapeManager
Store tape.TapeStore
Client *client.ChatClient
Hooks agent.Hooks
}
// Close runs OnClose from Options if set (e.g. plugin manager shutdown)
kit.Close(ctx)| Field | Type | Description |
|---|---|---|
Model |
string |
Model ID, e.g. "gpt-4o", "claude-sonnet-4-6" |
APIKey |
string |
API key (optional when using OAuth) |
| Field | Type | Description |
|---|---|---|
APIBase |
string |
Custom API base URL |
TokenURL |
string |
OAuth2 token endpoint |
ClientID |
string |
OAuth2 client ID |
ClientSecret |
string |
OAuth2 client secret |
Headers |
map[string]string |
Extra HTTP headers |
| Field | Type | Description |
|---|---|---|
TapeStore |
tape.TapeStore |
Pre-created store instance (highest priority) |
TapeConfig |
tape.StoreConfig |
Store factory config |
Priority: TapeStore > TapeConfig > in-memory fallback
Store config examples:
// File store
opts.TapeConfig = tape.StoreConfig{
Driver: "file",
Workspace: "/path/to/workspace",
}
// SQLite store
opts.TapeConfig = tape.StoreConfig{
Driver: "sqlite",
Database: "/path/to/tape.db",
}
// PostgreSQL store
opts.TapeConfig = tape.StoreConfig{
Driver: "postgres",
Host: "localhost",
Port: 5432,
Database: "dmr",
User: "dmr",
Password: "secret",
}| Field | Type | Description |
|---|---|---|
SystemPromptExtra |
string |
Appended after default system prompt |
SystemPromptBase |
string |
Replaces default+extra merge |
// Append to default
opts.SystemPromptExtra = "Keep answers concise. Speak in Chinese."
// Full replacement
opts.SystemPromptBase = "You are a code reviewer. Be strict."| Field | Type | Default | Description |
|---|---|---|---|
MaxSteps |
int |
20 | Max loop steps per run |
Verbose |
int |
0 | Log verbosity (0=warn, 1=info, 2=debug) |
Workspace |
string |
"" | Working directory passed to tools |
Tools |
[]*tool.Tool |
nil | Core tools to register |
Hooks |
agent.Hooks |
nil | Extension hooks (e.g. dmr plugin.Manager) |
OnClose |
func(context) error |
nil | Cleanup callback |
Models |
[]config.ModelConfig |
nil | Multi-model configuration |
HTTPResponseHeaderTimeout |
int |
0 (10min) | Response header timeout (seconds) |
HTTPClientTimeout |
int |
0 (15min) | Overall request timeout (seconds) |
devkit.EnvOptions() reads configuration from environment variables:
| Variable | Maps To |
|---|---|
AI_MODEL |
Options.Model |
AI_API_KEY |
Options.APIKey |
AI_API_BASE |
Options.APIBase |
Empty values are skipped and do not override existing Options.
opts := devkit.EnvOptions()
opts.Verbose = 1
opts.Tools = myTools
kit, err := devkit.Build(ctx, opts)res, err := kit.Agent.Run(ctx, devkit.DefaultTapeName, "Hello", 0)
fmt.Println(res.Output)// Resume from entry ID 42
res, err := kit.Agent.Run(ctx, "my-tape", "Continue", 42)// Pass JSON context to tools
res, err := kit.Agent.RunWithContext(ctx, tape, prompt, 0, `{"user_id":123}`)// Only allow specific tools
allowed := []string{"read_file", "write_file"}
res, err := kit.Agent.RunWithOptsAndTools(ctx, tape, prompt, 0, 0, &allowed, "")Devkit provides convenience methods for workflow integration:
// Create a workflow node wrapping the agent with a dedicated tape
node := kit.AsAgentNodeWithTape("writer", "wf-writer")
node.SystemPrompt = "You are a technical writer."
// Use in Sequential workflow
seq := &workflow.Sequential{
WorkflowName: "content_pipeline",
Nodes: []workflow.Node{node},
}
res, err := kit.RunWorkflow(ctx, seq, "topic")// Execute any workflow runner
result, err := kit.RunWorkflow(ctx, runner, input)
fmt.Printf("Completed in %d steps\n", result.Steps)package main
import (
"context"
"fmt"
"log"
"os"
"github.com/seanly/dmr-devkit/devkit"
"github.com/seanly/dmr-devkit/tool"
)
func main() {
ctx := context.Background()
opts := devkit.EnvOptions()
if opts.APIKey == "" || opts.Model == "" {
log.Fatal("AI_API_KEY and AI_MODEL required")
}
opts.Tools = []*tool.Tool{{
Spec: tool.ToolSpec{
Name: "echo",
Description: "Echo a message back.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"message": map[string]any{"type": "string"},
},
"required": []any{"message"},
},
},
Handler: func(_ *tool.ToolContext, args map[string]any) (any, error) {
return args["message"], nil
},
}}
kit, err := devkit.Build(ctx, opts)
if err != nil {
log.Fatal(err)
}
defer kit.Close(ctx)
res, err := kit.Agent.Run(ctx, devkit.DefaultTapeName,
"Echo 'hello world'", 0)
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Output)
}opts := devkit.EnvOptions()
opts.TapeConfig = tape.StoreConfig{
Driver: "sqlite",
Database: "./tape.db",
Workspace: "./workspace",
}
kit, err := devkit.Build(ctx, opts)// dmr's plugin.Manager implements agent.Hooks
manager := plugin.NewManager(...)
opts := devkit.EnvOptions()
opts.Hooks = manager
opts.OnClose = manager.ShutdownAll
kit, err := devkit.Build(ctx, opts)| Approach | Best For |
|---|---|
| devkit | Embedding in your own program; quick prototypes; tests needing full loop |
dmr (cmd/dmr) |
Production: config file, workspace, tape drivers, packaged plugins, webserver |
republic |
Tape-first LLM client only — no multi-step loop or hooks. See examples/basic_demo.go |