Skip to main content
This example is a support agent built with LlamaIndex’s FunctionAgent. It looks orders up with a tool, and its refund tool waits for a person with the workflow’s own wait_for_event. The handler keeps the workflow’s context in the run’s state, asks through AgentOS, and on the resume sends the decision back in as a HumanResponseEvent.
Tested with llama-index-core 0.14.25 (workflows 2.26.1) and llama-index-llms-openai-like 0.8.1 on 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.

How the model is wired

OpenAILike gets the gateway’s base URL, the run token in default_headers (x-agentos-run-token) and inv.llm.httpx_client(asynchronous=True), an HTTP client that never follows redirects and never sends the token to another host. is_chat_model and is_function_calling_model must be on for FunctionAgent to use tools, and context_window tells LlamaIndex how much it can send. The model name from config.models reaches the gateway exactly as written (anthropic/claude-sonnet-4-5). FunctionAgent streams its model calls, which the gateway supports. The agent is built inside the handler, on every invocation, because the token belongs to one invocation. With max_retries=0 a guardrail block isn’t retried; agentos.guardrail_block(error) finds it inside the OpenAI client’s BadRequestError, and the run fails with blocked_by_guardrail. See Guardrails.

How the approval works

1

The tool waits

issue_refund calls ctx.wait_for_event(HumanResponseEvent, waiter_event=InputRequiredEvent(...)). The handler sees the InputRequiredEvent in the event stream, keeps handler.ctx.to_dict(...) in the run’s state, stops the workflow and asks one AgentOS hold. The invocation answers suspended.
2

A person decides

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

The workflow continues

On the resume the handler rebuilds the context with Context.from_dict, runs the agent on it and sends the decision as a HumanResponseEvent. The tool’s step runs again from its start, wait_for_event returns the decision, and the tool refunds as approved or edited, or reports the rejection.
Use JsonPickleSerializer for the context. JsonSerializer leaves out values it can’t write as JSON, the agent’s memory among them, and the resumed workflow then fails with Path 'memory' not found in state.

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 order id.

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 context travels in run state. A run’s state value can be at most 1 MiB, so a very long conversation needs trimming. The pickled context is written and read only by your agent, with its own run token.
  • Everything before wait_for_event runs again on the resume: keep it free of side effects.
  • One refund per model answer. A run has one open hold at a time, and this example asks for the first InputRequiredEvent it sees. It was tested with one refund call per answer; if your model calls the tool several times at once, collect the events before you ask, as one hold with an item per call.

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.