Skip to main content
This example is a support agent built with the Upsonic framework. It looks orders up with a tool, and when the model decides to refund an order, Upsonic pauses the run and AgentOS asks a person. The reviewer approves the refund, changes the amount, or rejects it, and the agent continues with that decision.
Tested with Upsonic 0.77.3 and Python 3.13, using the conformance kit: 25 checks passed, including an approved, an edited and a rejected refund.

The bundle

The SDK wheel goes in the bundle’s vendor/ folder; see SDKs. The sqlite-storage extra is what Upsonic needs for SqliteDatabase.

How the model is wired

Upsonic’s OpenAIChatModel takes any OpenAI client, so the agent hands it one made from inv.llm.openai_kwargs(asynchronous=True): the gateway’s base URL, the run token in the x-agentos-run-token header, and an HTTP client that never sends the token anywhere else. The model name from config.models reaches the gateway exactly as written (anthropic/claude-sonnet-4-5 here); Upsonic doesn’t read a provider out of it. The agent is built inside the handler, on every invocation, because the token belongs to one invocation. max_retries=0 stops the OpenAI client from retrying a call a guardrail blocked; if one does, agentos.guardrail_block(error) finds the block inside Upsonic’s ModelHTTPError and the run fails with blocked_by_guardrail and the guardrail’s message. See Guardrails.

How the approval works

issue_refund is an Upsonic external-execution tool: when the model calls it, Upsonic pauses the run instead of running the function.
1

The run pauses

do_async returns a paused output. The agent keeps what Upsonic needs to continue in the run’s state (Upsonic’s SQLite database, the run id and the requirements), then asks one AgentOS hold with an item per refund call: its arguments are the item’s data, so a reviewer can edit the amount. The invocation answers suspended.
2

A person decides

The run is held until someone decides in the Review queue or through the Holds API. The container may stop meanwhile.
3

The run continues

AgentOS invokes the agent again with the decision. It writes the database back, issues each refund as decided (or explains why it didn’t), hands the results to Upsonic with set_external_execution_result, and calls continue_run_async. The model reads the results and answers.
Upsonic’s @tool(requires_confirmation=True) pauses too, but on a resume in a new process it runs the tool with the arguments it stored, so a reviewer’s edit wouldn’t reach the refund. An external-execution tool lets the agent run the refund with the decided arguments.

Run it

1

Zip and deploy

Put the SDK wheel in vendor/ (see SDKs), set config.models.main to a model from your Models page, then zip the folder’s contents and upload them:
2

Ask a question

3

Ask for a refund

The run is held: the refund waits in the Review queue. A reviewer approves it, edits the amount, or rejects it there, or through the Holds API. The item’s id is the model’s tool call id: read it from the hold (GET …/holds/h_12).

Test it before you deploy

The conformance kit builds the bundle and runs it against a mock platform whose model follows call:<tool> {arguments} in a message, so these cases drive the tools and every decision without a real model:
conformance.yaml

Things to know

  • The database travels in run state. A run’s state value can be at most 1 MiB. Upsonic’s SQLite file for a short conversation was about 320 KB (430 KB as base64). For long conversations, give Agent a database outside the container instead, such as Upsonic’s Postgres database, and allow its address in network.allow (see Network access).
  • Keep the default retry=1. A higher retry on Agent runs the whole task again after an error, model calls and tools included.
  • Upsonic’s own memory and storage live wherever its database lives. For memory that outlasts a run, use AgentOS memory.

Next steps

Human approvals

Everything an ask can carry: several items, details, files, an expiry.

More frameworks

The same agent in other frameworks and languages.