agent

package module
v1.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 17 Imported by: 0

README

agent

Hanzo Agent SDK

hanzo-agent runs agents in Python. An agent is a model reached over an OpenAI-compatible /v1 API, its instructions, and the Python functions it may call as tools. It installs as hanzo-agent and imports as agents.

Install

Python 3.9 or newer.

pip install hanzo-agent

First agent

from openai import AsyncOpenAI
from agents import Agent, Runner, set_default_openai_api, set_default_openai_client

set_default_openai_client(AsyncOpenAI(base_url="http://localhost:11434/v1", api_key="ollama"))
set_default_openai_api("chat_completions")

agent = Agent(name="Assistant", instructions="You are a helpful assistant.", model="qwen3:0.6b")
result = Runner.run_sync(agent, "Write a haiku about recursion in programming.")
print(result.final_output)

One run printed:

Recursive function deferring calls,
A loop unrolled with grace,
Ends with a loop.

The two calls before the agent decide where it runs:

  • set_default_openai_client names the endpoint and the key. Above it is a local server on port 11434, Ollama with qwen3:0.6b pulled. For the hosted API pass base_url="https://api.hanzo.ai/v1", a Hanzo API key as api_key, and a model id from https://api.hanzo.ai/v1/models.
  • set_default_openai_api("chat_completions") sends POST /v1/chat/completions. Without it the SDK sends POST /v1/responses instead.

Tools

A decorated function becomes a tool the model can call:

from agents import function_tool

@function_tool
def get_weather(city: str) -> str:
    return f"The weather in {city} is sunny."

agent = Agent(
    name="Weather",
    instructions="Answer with the get_weather tool.",
    tools=[get_weather],
    model="qwen3:0.6b",
)
print(Runner.run_sync(agent, "What is the weather in Tokyo?").final_output)

With the setup above it printed The weather in Tokyo is sunny.

Errors and retries

Model calls go through the openai client and follow its rules. A 401, 402 or 403 raises on the first answer (AuthenticationError, APIStatusError, PermissionDeniedError). A 429 or 5xx is retried twice and then raises; AsyncOpenAI(..., max_retries=0) sends it once. A 200 whose body is an error object raises openai.APIError carrying the server's message, as the client does for an error inside a stream. A <think>…</think> block at the start of a reply is dropped from final_output and from the history the next turn sends; result.raw_responses keeps it.

Tracing

Runs create traces and spans and send them nowhere. To export them, add a processor:

from agents import add_trace_processor
from agents.tracing.processors import BackendSpanExporter, BatchTraceProcessor

exporter = BackendSpanExporter(endpoint="https://collector.example/ingest", api_key="...")
add_trace_processor(BatchTraceProcessor(exporter))

The exporter posts batches of JSON to that endpoint with the key as a bearer token. set_tracing_disabled(True) or OPENAI_AGENTS_DISABLE_TRACING=1 stops creating them.

Extras

pip install "hanzo-agent[web3]"   # agents.extensions.web3: AgentWallet, Web3Wallet, MpcClient
pip install "hanzo-agent[tee]"    # agents.extensions.tee: ConfidentialAgent, TEEProvider

agents.network routes a request among several agents and agents.orchestration runs workflows over registered agents; both are in the base package.

This repository

The root is also a Go module, github.com/hanzoai/agent, and sdk/ holds Python, TypeScript and Go clients published under other names. This README covers hanzo-agent only.

Development

make sync    # uv sync --all-extras --all-packages --group dev
make tests   # uv run pytest

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

View Source
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.

View Source
const DefaultPreset = "graph"

DefaultPreset is used when a request omits preset (and its capability alias).

Variables

This section is empty.

Functions

func Register

func Register(p Preset)

Register adds (or replaces) a preset in the library. Call from init() or before Mount. Replacing an existing id is allowed so a host can re-skin a builtin.

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.

func Presets

func Presets() []Preset

Presets returns every registered preset, sorted by ID for a stable listing.

type Principal

type Principal struct {
	Org     string
	Project string
	User    string
	Cred    map[string]string
}

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 Scope

type Scope struct {
	Org     string
	Project string
}

Scope is the (org, project) a tool listing is resolved for.

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 Mount

func Mount(app *zip.App, deps Deps, completer Completer, plane ToolPlane) (*Service, error)

Mount registers the routes at DefaultPrefix. See MountAt.

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

func (*Service) Close

func (s *Service) Close() error

Close releases every open per-org store. Idempotent.

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

type UpstreamError struct {
	Status int
	Body   []byte
}

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL