Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions cookbook/05_agent_os/16_agui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ default empty prefix those routes are `POST /agui` and `GET /status`.
| `human_in_the_loop.py` | Pause and resume a real backend tool that uses `requires_confirmation`. |
| `research_team.py` | Stream a coordinated Team and its member activity over AG-UI. |
| `multiple_instances.py` | Mount two independent AG-UI interfaces on one AgentOS. |
| `background_run.py` | Keep a run alive across a client disconnect and resume it from a cursor. |
| `openui/` | Render an Agent as streaming charts, follow-ups, and validated forms with OpenUI. |

## Prerequisites
Expand Down Expand Up @@ -51,6 +52,7 @@ Start one example at a time; every standalone server uses port 7777:
.venvs/demo/bin/python cookbook/05_agent_os/16_agui/human_in_the_loop.py
.venvs/demo/bin/python cookbook/05_agent_os/16_agui/research_team.py
.venvs/demo/bin/python cookbook/05_agent_os/16_agui/multiple_instances.py
.venvs/demo/bin/python cookbook/05_agent_os/16_agui/background_run.py
```

The [`openui/`](openui/) example includes its own React client. Follow its
Expand All @@ -71,6 +73,7 @@ endpoint:
| `human_in_the_loop.py` | `http://localhost:7777/human-in-the-loop/agui` | `http://localhost:7777/human-in-the-loop/status` |
| `research_team.py` | `http://localhost:7777/research-team/agui` | `http://localhost:7777/research-team/status` |
| `multiple_instances.py` | `http://localhost:7777/chat/agui` and `http://localhost:7777/analyst/agui` | `/chat/status` and `/analyst/status` |
| `background_run.py` | `http://localhost:7777/background/agui` | `http://localhost:7777/background/status` |
| `openui/server.py` | `http://localhost:7777/agui` | `http://localhost:7777/status` |

The old all-in-one showcase is intentionally gone: starting the file you are
Expand Down Expand Up @@ -149,3 +152,65 @@ snapshot.
Media belongs in the latest user message as an AG-UI image, audio, video, or
document content part. The adapter converts URL or base64 data sources into
Agno media objects before calling the Gemini agent in `agent_with_media.py`.

## Background runs and reconnection

By default a run streams inline: closing the connection ends the run. A client
can instead ask for a background run, which executes detached from the request
that started it and buffers its events for replay.

Opt in per request through `forwardedProps`:

```json
"forwardedProps": {"agnoBackground": {"enabled": true}}
```

Every event of a background run then carries its resume cursor:

```json
"metadata": {"agnoBackground": {"eventIndex": 12, "subIndex": 0}}
```

To reconnect, send the same `runId` again with the last cursor received. The
server replays whatever the client missed, once and in order, and then resumes
live streaming until the run finishes:

```json
"forwardedProps": {
"agnoBackground": {"enabled": true, "lastEventIndex": 12, "lastSubIndex": 0}
}
```

A reconnection has to reproduce the events the first connection would have
sent, which shows up in how state is reported. A request that sends a `state`
is bracketed by a `STATE_SNAPSHOT` when the run starts and another when it
finishes, with no `STATE_DELTA` events in between; a request that sends none
gets neither, exactly as it would streaming inline. A client that follows
shared state along the run, as it can with `shared_state.py`, therefore sees
the state of a background run only as those two snapshots, and has to keep
sending `state` the same way on every connection.

The buffer a background run replays from is finite. A run long enough to
overflow it keeps executing, and a connection that sends no resume position
still gets a well-formed stream starting from wherever the buffer now begins,
with a warning logged naming that event. A reconnection to such a run is refused with a
`RUN_ERROR`: the replay is rebuilt from what the buffer still holds, so a
message whose opening was trimmed would be opened again under a new identifier
while the client's own copy of it was never closed.

A client that never sees the `agnoBackground` metadata cannot resume the run.
That covers both a server too old to offer background runs and a server that
has them but cannot apply them to this particular agent; either way the run
still streams normally on the one connection.

Background execution needs a database, a readable run history so the server can
tell whose run a reconnection is naming, and an agent or team that executes in
this process, so a remote entity cannot use it. What happens
then depends on the request. A first connection runs in the foreground instead,
streaming normally without the ability to resume. A request that carries a
resume position is refused with a `RUN_ERROR` rather than downgraded, because
running it in the foreground would execute the whole run a second time.

Continuing a paused run is not a resume: it starts a new leg, so it always
takes the foreground path and any resume position still being echoed alongside
it is ignored rather than refused.
111 changes: 105 additions & 6 deletions cookbook/05_agent_os/16_agui/TEST_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ Tested on 2026-07-24 against Agno source commit
The OpenUI addition was tested on 2026-08-18 against Agno source commit
`32e5fb9c2203fa98de19ca72750133a57a075899`.

`background_run.py` was re-tested on 2026-09-04 and last re-confirmed on
2026-09-08 against the tree of the commit that carries this entry, on a machine
with no `OPENAI_API_KEY`. That run is scoped accordingly and its entry says what
it could not reach. The entry was rewritten from scratch because the previous
one, written against `8f76f52f41b4366dce9b6def7f4f687ede6c5229`, described
paused-run continuation behavior that has since changed.

Each checked-in server was first booted on its default port 7777. The sweep
asserted `GET /health`, `GET /config`, every mounted AG-UI status route, and a
clean shutdown. Capability-specific POST tests then used
Expand Down Expand Up @@ -165,6 +172,96 @@ points, and both streams closed with `RUN_FINISHED`.

---

### background_run.py

**Status:** PASS (routing, cursor stamping, and resume gating only; no model
call, so this is not a full-capability PASS like the entries above)

**Test mode:** LIVE SERVER, NO MODEL KEY

**Description:** Booted the checked-in server on its default port 7777 with no
`OPENAI_API_KEY` in the environment, and with `PYTHONPATH` pointed at this
worktree's `libs/agno` so the AG-UI interface under test is this tree's and not
another checkout's editable install. Checked `/health`, `/config`, and
`/background/status`, then sent ten `POST /background/agui` requests: a
background opt-in, two reconnections with the same `runId` from different
resume positions, a reconnection naming a `runId` no run uses, a second
background run seeded on a different thread plus a cross-thread reconnection to
it, a resume position with `enabled` set to `false`, two paused-run
continuations echoing a resume position, and a plain foreground run.

**Result:** Health returned `ok`; config returned OS `agui-background-os`,
agent `agui-background-agent`, model `gpt-5.6-luna`, database
`agui-background-db`, and one AG-UI interface at route `/background`;
`/background/status` returned `available`.

The background opt-in, sent as `forwardedProps.agnoBackground` set to the
boolean `true` shorthand rather than the `{"enabled": true}` form the README
documents, streamed five events. Both spellings are accepted. `RUN_STARTED` and `STATE_SNAPSHOT` came at cursors
`{"eventIndex": -1, "subIndex": 0}` and `{"eventIndex": -1, "subIndex": 1}`,
the snapshot carrying the submitted state. Two buffered Agno events with no
AG-UI handler arrived as `RAW` at cursors 0 and 1, wrapping `RunStarted` and
`ModelRequestStarted`. The stream ended at cursor 2 with a `RUN_ERROR` reading
`OPENAI_API_KEY not set. Please set the OPENAI_API_KEY environment variable.`

Reconnecting with the same `runId` and `lastEventIndex` 1 replayed exactly one
event, the `RUN_ERROR` at cursor 2. Reconnecting with the same `runId` at
`lastEventIndex` -1 and `lastSubIndex` 0 replayed four events, everything from
the `STATE_SNAPSHOT` at cursor -1/1 onward, dropping only the `RUN_STARTED` at
-1/0. The cursor filter therefore discriminates within one event index as well
as across indices.

A reconnection sending `lastEventIndex` 1 with `runId` `agui-bg-run-other`,
which no run uses, was refused with `Run agui-bg-run-other not found in this
session`, and that refusal was itself stamped at cursor 2, one past the
position the client sent. A second background run `agui-bg-run-2` was then
started on thread `agui-bg-thread-2`; reconnecting to it from thread
`agui-bg-thread-1` with `lastEventIndex` 0 was refused the same way, with
`Run agui-bg-run-2 not found in this session` stamped at cursor 1. Every
refusal observed carried a resume marker one event index past the client's own.

A resume position sent as
`{"enabled": false, "lastEventIndex": 1, "lastSubIndex": 0}` was refused before
anything ran, with `A resume position was sent with background execution
disabled` stamped at cursor 2.

A paused-run continuation, meaning a request carrying a trailing AG-UI tool
message, was not refused. With `agnoBackground` carrying `lastEventIndex` 1 the
server logged the warning `Background execution does not apply to a paused-run
continuation; continuing in the foreground` and took the foreground
continuation path, emitting `RUN_STARTED`, `STATE_SNAPSHOT`, and then
`RUN_ERROR` reading `No paused run matching the provided tool results found in
session agui-bg-thread-1`. None of those three events carried any
`agnoBackground` metadata. The same continuation with `agnoBackground` set to
`true` and no resume position behaved identically. That terminal error is the
continuation finding no paused run to resume in this key-less environment, not
a background refusal.

A plain foreground run with empty `forwardedProps` produced the same five-event
shape as the opt-in run, but with no `agnoBackground` key on any event, so the
resume marker appears only on background responses.

The server shut down cleanly on SIGINT.

**Changed since the previous entry:** the previous version of this entry
recorded a paused-run continuation carrying a resume position as being refused
with `Background execution does not apply to a paused-run continuation` and
starting nothing. At this commit it is not refused. The router decides the
continuation downgrade before it decides the resume gate, so that sentence is
now only a server-side warning and the continuation proceeds in the foreground.

**Not verified:** everything that needs a model call. No assistant text was
produced, so no long-running stream was disconnected and resumed mid-run, no
`TEXT_MESSAGE_*` or tool-call events were ever buffered or replayed, the
terminal `STATE_SNAPSHOT` and the absence of mid-run `STATE_DELTA` events were
not observed, and the buffer-overflow replay refusal was never reached. Those
behaviors are read from `agno/os/interfaces/agui/background.py` rather than
witnessed here. A genuinely paused run was also never reached, so the
foreground continuation path was observed only as far as its "no paused run"
rejection.

---

### openui/server.py and frontend

**Status:** PASS
Expand All @@ -190,11 +287,13 @@ TypeScript compilation, and the Vite production build passed.

## Validation

- All 9 standalone files booted, exposed their expected `/status` route, and
- All 10 standalone files booted, exposed their expected `/status` route, and
shut down cleanly.
- All 9 files completed a real capability-specific AG-UI POST flow.
- Recursive pattern validation checked exactly 9 Python files with 0
violations.
- 9 of the 10 completed a real capability-specific AG-UI POST flow. The
exception is `background_run.py`, whose entry records what was verified
without a model call.
- Recursive pattern validation checked all 11 Python files in the folder, the
10 standalone servers plus `openui/server.py`, with 0 violations.
- Targeted Ruff format and check passed.
- Python compilation, banned-model, stale-route, scope, Unicode/emoji,
non-PASS status, and `git diff --check` gates passed.
Expand All @@ -204,5 +303,5 @@ TypeScript compilation, and the Vite production build passed.
check. Its frontend passed five tests, TypeScript compilation, and a
production build.
- Repository-wide Ruff, agnoctl mypy, and cookbook pattern checks passed. The
core Agno mypy step reported 27 existing errors in six files outside this
integration's diff.
core Agno mypy step now reports no issues across 1034 source files, so the 27
pre-existing errors this section previously recorded are no longer present.
62 changes: 62 additions & 0 deletions cookbook/05_agent_os/16_agui/background_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
Resume a Background AG-UI Run
=============================

Run an agent detached from the request that started it, so the run keeps going
after the client disconnects and a reconnecting client picks up exactly where
it left off.

A client opts in per request with forwardedProps.agnoBackground. Every event of
such a run carries its resume cursor under metadata.agnoBackground.

Resuming is addressed by run id, so the client has to choose the runId when it
starts the run and send that same runId back with the last cursor it received.
A reconnect that carries a different runId names a run the session does not
have, and is refused rather than resumed.

Prerequisites: OPENAI_API_KEY
Run: .venvs/demo/bin/python cookbook/05_agent_os/16_agui/background_run.py
Try: POST a background run at http://localhost:7777/background/agui
"""

from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIResponses
from agno.os import AgentOS
from agno.os.interfaces.agui import AGUI

# ---------------------------------------------------------------------------
# Create Background-Capable Agent
# ---------------------------------------------------------------------------

# Detached execution persists run status, so a database is required.
db = SqliteDb(
id="agui-background-db",
db_file="tmp/agui_background.db",
)

long_form_agent = Agent(
id="agui-background-agent",
name="AG-UI Background Agent",
model=OpenAIResponses(id="gpt-5.6-luna"),
db=db,
instructions=[
"Answer at length so the stream lasts long enough to disconnect from.",
"Write at least six paragraphs.",
],
)

agent_os = AgentOS(
id="agui-background-os",
description="AgentOS serving resumable background AG-UI runs.",
agents=[long_form_agent],
interfaces=[AGUI(agent=long_form_agent, prefix="/background")],
)
app = agent_os.get_app()

# ---------------------------------------------------------------------------
# Run Background Server
# ---------------------------------------------------------------------------

if __name__ == "__main__":
agent_os.serve(app=app)
Loading
Loading