Documentation
¶
Overview ¶
Package agent is a reusable agentic-chat orchestrator: one POST runs a single LLM tool-calling round that lets a model manage a system through tools, and it PERSISTS conversation history (per-org SQLite via hanzoai/orm). It is a library — a host (hanzoai/ai / hanzoai/cloud) mounts it on its OWN zip router so the round registers in the SAME router as /v1/chat/completions (a distinct path, no route-precedence gamble), and injects the two dependency seams:
- Completer — the host's in-process LLM completion (the ONLY path that both returns tool_calls and carries per-org billing);
- ToolPlane — the host's unified tool registry (list / exists / dispatch).
A round yields four things: the assistant's text (reply), the tool calls the server executed against the registry (actions), the tool calls the client must apply itself — a graph/UI mutation the server cannot run (ops), and the id of the conversation the turn was appended to (conversationId).
This package DELIBERATELY does not import hanzoai/cloud or hanzoai/ai — that coupling is the thing it exists to break. It builds directly on zip (routing), hanzoai/orm (per-org SQLite persistence) and go-openai (the request/response shapes); the host wraps Mount with a thin adapter that supplies the seams.
Index ¶
Constants ¶
const DefaultPrefix = "/v1/agent"
DefaultPrefix is where the standalone daemon answers. A host whose own router already spends that address folds this surface somewhere else with MountAt; nothing in the round depends on which address it was given.
const DefaultPreset = "graph"
DefaultPreset is used when a request omits preset (and its capability alias).
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Completer ¶
type Completer interface {
Complete(ctx context.Context, cred map[string]string, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error)
}
Completer runs one chat completion (with tools) and returns the parsed response. It is the seam onto the host's in-process LLM path: the real impl replays the request carrying the caller's credential (so the host's per-org billing runs); tests inject a fake so the round is exercised without a live model. agent never imports the package that provides the real Completer.
type Conversation ¶
type Conversation struct {
orm.Model[Conversation]
Org string `json:"org"`
User string `json:"user"`
Title string `json:"title"`
}
Conversation is one persisted chat thread. Org is the owning org — physical isolation (one SQLite file per org) already scopes it; Org is stored for clarity and defense-in-depth. User is the member who opened it: a thread is one person's inside a shared org, so it lists and opens for them alone. A thread recorded before users were kept has none, and stays the org's.
type Deps ¶
type Deps struct {
// Logger is the canonical Hanzo logger (luxfi/log). Required.
Logger luxlog.Logger
// DataDir is the per-deployment data root. Per-org SQLite files land at
// {DataDir}/orgs/{orgSlug}/agent.db. Required.
DataDir string
// Brand is the white-label brand identifier (logged only).
Brand string
// Model is the served model used when a request supplies none.
Model string
// Principal resolves the validated caller from a request. Defaults to
// header-based resolution (X-Org-Id / X-User-Id) when nil.
Principal func(*zip.Ctx) (Principal, bool)
}
Deps is agent's OWN small dependency surface — deliberately not cloud.Deps. It carries only what this package needs: a logger, the per-org SQLite data root, a brand tag, the default served model, and an optional principal resolver (defaults to gateway-header identity when nil).
type Message ¶
type Message struct {
orm.Model[Message]
ConversationId string `json:"conversationId"`
Org string `json:"org"`
Role string `json:"role"`
Content string `json:"content"`
ToolCalls json.RawMessage `json:"toolCalls,omitempty"`
}
Message is one persisted turn. ConversationId is deliberately spelled with a lowercase-d so orm's PascalCase→camelCase filter (ToJSONFieldName lowercases only the first rune) maps Filter("ConversationId=") onto the stored "conversationId" JSON key. ToolCalls is the marshaled model tool_calls (nil for a plain user/assistant turn).
type Preset ¶
type Preset struct {
ID string `json:"id"`
Title string `json:"title"`
SystemPrompt string `json:"systemPrompt"`
BuiltinTools []openai.Tool `json:"-"`
// ServerExecuted gates the tool-call split: when true, a call the tool
// registry knows is dispatched server-side and reported in actions; when
// false the round is advisory — every call is returned as an op for the
// client to apply.
ServerExecuted bool `json:"serverExecuted"`
}
Preset is one named agent type in the preset LIBRARY — a first-class, extensible catalog the round is created from and the presets route lists. It frames a tool-calling round: the system prompt that instructs the model, the builtin tool defs offered alongside the caller's and the org's registered tools, and whether the model's tool calls are EXECUTED server-side (registry Dispatch → actions) or handed back to the client as ops (a graph/UI mutation the server cannot perform). Adding an agent type is one Register call — no other change.
type Principal ¶
Principal is the VALIDATED caller a round runs as. Org scopes persistence AND the tool listing (never a client-supplied field); Cred is the caller's own credential headers, opaque to agent, replayed by the injected Completer / ToolPlane so an in-process call carries exactly the caller's identity.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the mounted orchestrator handle. Its handlers are plain zip handlers; Close releases the per-org SQLite stores at host shutdown.
func MountAt ¶ added in v1.0.6
func MountAt(app *zip.App, prefix string, deps Deps, completer Completer, plane ToolPlane) (*Service, error)
MountAt registers the routes under prefix using the injected Completer and ToolPlane, and returns the Service so the host can Close it on shutdown. The per-org SQLite models are auto-migrated (schema created) on first per-org open. prefix is an absolute path chosen by whoever composes the router:
POST {prefix} — run one tool-calling round
GET {prefix}/presets — list the preset library
POST {prefix}/conversations — record turns in a conversation
GET {prefix}/conversations — list the caller-org's conversations
GET {prefix}/conversations/:id — one conversation's messages
type Tool ¶
type Tool struct {
Name string
Description string
Schema json.RawMessage
Activated bool
Dispatchable bool
}
Tool is the agent-facing projection of one registered tool offered to the model. Schema is the JSON-Schema of the call arguments; Dispatchable is false for a listing-only entry; Activated is true only for tools the org turned on.
type ToolPlane ¶
type ToolPlane interface {
List(ctx context.Context, scope Scope) []Tool
Exists(ctx context.Context, scope Scope, name string) bool
Dispatch(c *zip.Ctx, name string, args map[string]any) (any, error)
}
ToolPlane is the org's registered tool registry seam: list the tools offered to the model, test whether a call is server-known, and dispatch a call. Dispatch takes the live request so the host resolves the caller the ONE canonical way (its PrincipalFrom) — the credential is never reconstructed or passed as a value, only read from the validated request. List/Exists take a plain (org, project) Scope; that carries no credential, so it is safe by value. The host injects its unified tool plane; tests inject a stub.
type UpstreamError ¶ added in v0.1.2
UpstreamError is returned by a Completer when the completion service refuses the request for the CALLER's OWN reason — a 4xx like 402 insufficient_balance, 429 rate-limit, or 403 — that must reach the caller intact, not be masked as a gateway 502. Body is the completion's verbatim error payload; the round passes Status + Body straight through so a client shows the real message ("add credits") instead of an opaque "agent: completion" wrapper. A non-4xx completion failure (5xx, transport) stays a 502 — that IS a gateway fault.
func (*UpstreamError) Error ¶ added in v0.1.2
func (e *UpstreamError) Error() string