Apps on OZ

Go SDK

The typed Go client of the runtime API, a module of its own, with typed agents per tenant.

For: application developers.

Use package oz0 from module go.oz0.ai/oz0/sdk/go to start agents and flows, wait for answers, watch jobs, answer questions, decide approvals, read the catalog and inspect cost.

The SDK depends on the Connect client in its gen directory, connect-go, protobuf-go and the standard library. It does not depend on the server.

The module is not published yet: until the first public release, point a replace directive at sdk/go in a checkout of the repository. Its tags will be sdk/go/vX.Y.Z.

Connect

main.go
c, err := oz0.New("https://edge.example.com:7443",
    oz0.WithAPIKey(os.Getenv("OZ0_API_KEY")), oz0.WithTenant("acme"), oz0.WithCAFile("ca.crt"))

oz0.NewFromEnv() reads OZ0_URL, OZ0_API_KEY, OZ0_TENANT and OZ0_CA_FILE. WithClientCertificate takes an operator's certificate, WithHTTPClient a client of your own, and WithConnectOptions connect-go's options. Calls take their context's deadline.

Run an agent

main.go
type review struct {
    Verdict string `json:"verdict"`
}
out, job, err := oz0.RunAs[review](ctx, c, "acme-reviewer", map[string]any{"repo": "acme/api", "pr": 42},
    oz0.WithTimeout(15*time.Minute), oz0.WithBudget(0.5), oz0.WithTags("ci"))

Run returns the job and a *oz0.JobFailed error when it did not complete; Start, Wait and Get are the steps, and oz0.Output[T] and oz0.OutputText read an answer. Errors from the API are connect-go's, so connect.CodeOf(err) is connect.CodeInvalidArgument for an input that does not fit the agent's schema.

Conversations

oz0.WithContinueFrom(job) makes the job the next turn of that job's conversation, and oz0.WithConversation(id) the next turn of the conversation with that ID (its first, when there is none yet): the agent gets the earlier turns (Conversations). The job says which turn it is.

main.go
first, err := c.Run(ctx, "acme-assistant", "Our release is on Friday.")
// ...
second, err := c.Run(ctx, "acme-assistant", "When is the release?", oz0.WithContinueFrom(first.GetId()))
fmt.Println(second.GetTurn(), second.GetConversation(), oz0.OutputText(second))

Start and Run retry a start that meets unavailable twice, with the same key: without oz0.WithID, each start sends an Idempotency-Key of its own, so the server gives the retries the same job, and with an ID the retries repeat it. A start again under an ID with the same request returns that job, running or ended; another request under it fails with already_exists (job IDs and retries).

oz0.WithCandidate(id) starts a job on a private candidate instead of the installed version. Every RPC of the API, GetConversation and ListConversations included, is typed on c.Runtime.

Watch, questions and approvals

main.go
job, _ := c.Start(ctx, "acme-asker", "Pick a colour for the report")
for ev, err := range c.Watch(ctx, job.GetId()) {
    if err != nil {
        return err
    }
    if a := ev.GetApprovalRequested(); a.GetKind() == "question" {
        _ = c.Answer(ctx, a, "blue", "")
    }
    if f := ev.GetFinished(); f != nil {
        fmt.Println(oz0.OutputText(f))
    }
}

Approvals, Questions, Approve, Deny, Answer, Cancel, Jobs, Catalog, Cost and OpenAPI do what the Python SDK's methods do, and c.Runtime has every method of the API, typed.

Watch resumes a broken stream by itself after the last event it yielded (since_seq and the resume token), so no event comes twice; an event with GetResync() says a gap could not be resumed over, and the job is fetched again. WatchFrom(ctx, id, seq, token) resumes a watch an earlier process made.

Feeds

c.OpenFeed(oz0.ConversationFeed(id), oz0.JobFeed(id), oz0.MyActivity()) follows many chats and jobs on one stream; feed.Events(ctx) yields its events and resumes it by itself on any edge, feed.Subscribe and feed.Unsubscribe change what it follows while it runs, and feed.State() with c.ResumeFeed(...) resumes it in another process. See Live feeds.

Stream tickets and apps that act as their people

c.StreamTicket(ctx, oz0.StreamTarget{Job: id}) asks for a one-time ticket that lets a browser's EventSource open a job's (or, with Feed, a feed's) Server-Sent Events without the key.

A client made with the app's key exchanges a person's ID token for an app token with c.ExchangeToken(ctx, idToken) (apps that act as their people); oz0.WithToken(func(ctx) (string, error)) then makes a client that sends the person's current token on every call. A refused exchange is a *oz0.TokenExchangeError with OAuth's Code.

The client follows a redirect within the runtime API's origin; one to another origin, which would carry the request's body there (a person's token, a job's input), is an error.

Lists, page after page

The runtime API's lists that can grow come a page at a time. AllJobs, AllApprovals, Conversations, Connections and RoutineFires are iterators over every row that read the next page only when the loop reaches it, so a loop that stops early reads no further; Jobs(ctx, n) is the first n of AllJobs (20 when n is 0 or less), read in pages of n up to 100, and Approvals reads every page. oz0.Pages does the same for any other list method of c.Runtime.

for job, err := range c.AllJobs(ctx, true) { // only the jobs the caller started
    if err != nil {
        return err
    }
    fmt.Println(job.GetId(), job.GetStatus())
}

A person's chats

MyChats yields the caller's own chats and those shared with them, page after page; UpdateChat pins, archives, marks read, renames (the owner) or removes one from the caller's list, which erases nothing. Both need a person's credentials: an app's key or an operator's certificate has no chats.

for chat, err := range c.MyChats(ctx, oz0.ChatFilter{Agent: "helper"}) {
    if err != nil {
        return err
    }
    fmt.Println(chat.GetId(), chat.GetTitle(), chat.GetUnread())
}

Typed agents

Terminal
oz0 client generate --tenant acme --lang go --out internal/agents
main.go
review, job, err := agents.New(c).RunAcmeReviewer(ctx, agents.AcmeReviewerInput{Repo: "acme/api", Pr: 42})

The package has a struct per object of the agents' schemas (optional fields as pointers) and per agent and flow a method that starts it and one that runs it, typed. It is named after its directory (--package to choose), records the catalog's ETag and the generator's version, and is left alone until the catalog changes or a newer generator writes it again. A property name Go's encoding/json cannot carry in a struct tag (a comma, a quote, a back quote, a backslash, an empty name) makes its object a map[string]any.

A type that refers to itself works: a required field whose struct would hold, by value, the struct it is in (a node whose next is a node) is a pointer too, so the package compiles, and a schema that is a list of itself with no object between is []any.

Copyright © 2026