Typed LLM tool execution framework for Go. The same role rocco plays for HTTP, ago plays for LLM tool dispatch.
ago ("I do" in Latin) is where an LLM's decisions become validated, scoped, auditable actions in the host system. It sits alongside cogito ("I think") and zyn (typed LLM interactions) in the zoobzio AI stack.
- Typed tool handlers with generic input/output via
Tool[In, Out] - Automatic JSON Schema generation from Go types via sentinel
- Input validation with JSON deserialization and
Validatableinterface - Middleware chain for cross-cutting concerns (auth, logging, rate limiting)
- Typed errors distinguishing tool errors (LLM can retry) from dispatch errors (system failure)
- Observability via capitan lifecycle signals
- Panic recovery on every tool invocation
go get github.com/zoobz-io/ago// Define input and output types.
type SearchInput struct {
Query string `json:"query" desc:"The search query"`
Limit int `json:"limit" desc:"Max results to return"`
}
type SearchOutput struct {
Results []string `json:"results"`
Total int `json:"total"`
}
// Create a typed tool.
search := ago.NewTool[SearchInput, SearchOutput]("search",
func(ctx context.Context, inv *ago.Invocation) (SearchOutput, error) {
input, _ := ago.TypedInput[SearchInput](inv)
results := doSearch(ctx, input.Query, input.Limit)
return SearchOutput{Results: results, Total: len(results)}, nil
},
).WithDescription("Search for items by query")
// Register and invoke.
registry := ago.NewRegistry()
registry.Register(search)
result, err := registry.Invoke(ctx, "search", []byte(`{"query":"go","limit":10}`), identity)Generate tool schemas for LLM APIs directly from registered tools:
schemas := ago.GenerateSchemas(registry)
// schemas is []ToolSchema with name, description, and JSON Schema for each tool// Global middleware applies to all tools.
registry.WithMiddleware(func(next ago.HandlerFunc) ago.HandlerFunc {
return func(ctx context.Context, inv *ago.Invocation) (*ago.Result, error) {
log.Printf("tool=%s id=%s", inv.ToolName, inv.ID)
return next(ctx, inv)
}
})
// Per-tool middleware.
tool.WithMiddleware(rateLimitMiddleware, auditMiddleware)Tool errors are returned to the LLM as structured results. Dispatch errors are system failures.
// Define a tool error the LLM can act on.
var ErrNotFound = ago.NewError[NotFoundDetails]("NOT_FOUND", "resource not found")
// Return it from a handler — it becomes a tool_result, not a crash.
func handler(ctx context.Context, inv *ago.Invocation) (Output, error) {
return Output{}, ErrNotFound.WithDetails(NotFoundDetails{Resource: "user"})
}All lifecycle events are emitted as capitan signals:
| Signal | When |
|---|---|
ToolRegistered |
Tool added to registry |
ToolExecutionStarted |
Invocation dispatched |
ToolExecutionCompleted |
Invocation succeeded |
ToolExecutionFailed |
Invocation failed |
| Package | Role |
|---|---|
| capitan | Lifecycle signal emission |
| sentinel | Type metadata for schema generation |