Skip to content
Draft
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Below is the list of packages currently included in this repository.

| Package | PyPI Status | Description |
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| [`bub-ag-ui`](./packages/bub-ag-ui/README.md) | | Adds an AG-UI HTTP channel that translates Bub runs and stream events into AG-UI server-sent events. |
| [`bub-codex`](./packages/bub-codex/README.md) | | Provides a `run_model` hook that delegates model execution to the Codex CLI. |
| [`bub-cursor`](./packages/bub-cursor/README.md) | | Provides a `run_model` hook that delegates model execution to the Cursor CLI, plus `bub login cursor`. |
| [`bub-acp-server`](./packages/bub-acp-server/README.md) | | Exposes Bub as an Agent Client Protocol agent with `bub acp serve` for ACP-compatible editors. |
Expand Down
79 changes: 79 additions & 0 deletions packages/bub-ag-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# bub-ag-ui

`bub-ag-ui` adds an [AG-UI](https://docs.ag-ui.com/) HTTP channel to the Bub gateway. It accepts AG-UI `RunAgentInput` requests, forwards them through Bub's normal channel pipeline, and translates Bub stream events into AG-UI server-sent events.

The package targets Bub 0.4.0's public extension contracts. Bub loads it as the `ag-ui` entry point, so the package intentionally does not declare Bub as a runtime dependency.

## Install

From a Bub project:

```bash
bub install bub-ag-ui@main
```

## Configure

The channel is available under the `ag-ui` key in Bub configuration:

```yaml
ag-ui:
host: 127.0.0.1
port: 8088
path: /agent
health_path: /agent/health
```

Environment variables with the `BUB_AG_UI_` prefix override these values:

| Variable | Default | Purpose |
| --- | --- | --- |
| `BUB_AG_UI_HOST` | `127.0.0.1` | HTTP bind host |
| `BUB_AG_UI_PORT` | `8088` | HTTP bind port |
| `BUB_AG_UI_PATH` | `/agent` | AG-UI request endpoint |
| `BUB_AG_UI_HEALTH_PATH` | `/agent/health` | Health endpoint |

Set `BUB_STREAM_OUTPUT=true` to receive model output as live AG-UI text events. Without it, successful runs still complete over SSE, but output usually arrives as one final text block.

## Run

```bash
export BUB_STREAM_OUTPUT=true
bub gateway --enable-channel ag-ui
```

The default endpoints are:

- `POST /agent` for AG-UI runs
- `GET /agent/health` for channel health

For a runnable, credential-free walkthrough that starts the real Bub gateway
and sends a protocol-native AG-UI request, see the
[`examples`](examples/README.md) directory.

## Event mapping

| Bub stream event | AG-UI event |
| --- | --- |
| `text` | `TEXT_MESSAGE_START`, `TEXT_MESSAGE_CONTENT`, `TEXT_MESSAGE_END` |
| `tool_call` | `TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_END` |
| `tool_result` | `TOOL_CALL_RESULT` |
| `usage` | `CUSTOM` named `bub.usage` |
| `error` | `RUN_ERROR` |

The channel emits `RUN_STARTED` before streamed work and `RUN_FINISHED` only after Bub successfully dispatches the final output. Public frontend state becomes normal Bub turn state; AG-UI transport metadata is preserved under the private `_ag_ui` key for downstream integrations.

## Development

From the `bub-contrib` repository root:

```bash
uv run --group test pytest -q packages/bub-ag-ui/tests
uv build --package bub-ag-ui
```

## Current limitations

- Session mapping is primarily aligned by AG-UI `thread_id`.
- Resume and interrupt semantics are not yet connected to Bub.
- The prompt fallback uses the last user message and plain scalar context values. Structured transport metadata remains under `_ag_ui`.
79 changes: 79 additions & 0 deletions packages/bub-ag-ui/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Bub + AG-UI web example

This example pairs a CopilotKit chat frontend with the real Bub AG-UI gateway.
A small deterministic model plugin keeps the default walkthrough local and
repeatable, so no model-provider credential is required.

The complete path is:

```text
CopilotChat -> Copilot Runtime -> AG-UI HttpAgent -> bub-ag-ui -> Bub turn
CopilotChat <- Copilot Runtime <- AG-UI events <- bub-ag-ui <- Bub output
```

The frontend is adapted from the original Apache-2.0 Bub template. Run the
commands below from the `bub-contrib` repository root.

## 1. Start the Bub gateway

```bash
BUB_STREAM_OUTPUT=true uv run --isolated --python 3.12 --no-project \
--with 'bub @ git+https://github.com/bubbuild/bub.git' \
--with-editable packages/bub-ag-ui \
--with-editable packages/bub-ag-ui/examples/echo-plugin \
bub gateway --enable-channel ag-ui
```

The temporary editable install exposes the example model through Bub's normal
`bub` entry-point group. It overrides only `run_model_stream`; the gateway,
channel routing, turn orchestration, streaming, and outbound dispatch all use
Bub's real runtime path.

Wait until Uvicorn is listening on `http://127.0.0.1:8088`.

## 2. Start the web frontend

In another terminal:

```bash
cd packages/bub-ag-ui/examples/frontend
npm install
npm run dev
```

Open `http://127.0.0.1:5173`, enter a message, and submit it. The page talks to
the Copilot Runtime on port 4000, whose AG-UI `HttpAgent` forwards the run to
the Bub endpoint at `http://127.0.0.1:8088/agent`.

The response begins with `Bub received through AG-UI:`. Both the browser UI and
Bub gateway stream the same AG-UI run. Stop the frontend and gateway with
Ctrl-C when finished.

## Configuration

Copy `frontend/.env.example` to `frontend/.env` to override any endpoint or
port:

| Variable | Default | Purpose |
| --- | --- | --- |
| `BUB_AG_UI_AGENT_URL` | `http://127.0.0.1:8088/agent` | Copilot Runtime → Bub |
| `COPILOTKIT_PORT` | `4000` | Copilot Runtime port |
| `VITE_COPILOTKIT_RUNTIME_PROXY` | `http://127.0.0.1:4000` | Vite → Copilot Runtime |
| `FRONTEND_PORT` | `5173` | Vite frontend port |

## Use a real model instead

Omit the example plugin and provide the normal Bub model settings:

```bash
export BUB_MODEL=openai:gpt-5-mini
export BUB_API_KEY=...
export BUB_STREAM_OUTPUT=true
uv run --isolated --python 3.12 --no-project \
--with 'bub @ git+https://github.com/bubbuild/bub.git' \
--with-editable packages/bub-ag-ui \
bub gateway --enable-channel ag-ui
```

Keep the frontend running unchanged. Any provider supported by Bub can replace
the deterministic example model.
13 changes: 13 additions & 0 deletions packages/bub-ag-ui/examples/echo-plugin/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[project]
name = "bub-ag-ui-example"
version = "0.1.0"
description = "Deterministic model plugin for the bub-ag-ui example"
requires-python = ">=3.12"
dependencies = []

[project.entry-points."bub"]
ag-ui-example-echo = "bub_ag_ui_example.plugin:main"

[build-system]
requires = ["uv_build>=0.10.4,<0.11.0"]
build-backend = "uv_build"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Deterministic model used by the bub-ag-ui end-to-end example."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from __future__ import annotations

import json
from collections.abc import AsyncIterator
from typing import Any

from bub import hookimpl
from bub.streaming import AsyncStreamEvents, StreamEvent, StreamState


class EchoModel:
"""Return a deterministic response while exercising Bub's streaming path."""

@hookimpl
async def run_model_stream(
self,
prompt: str | list[dict[str, Any]],
session_id: str,
state: dict[str, Any],
) -> AsyncStreamEvents:
del session_id, state
response = f"Bub received through AG-UI: {_prompt_text(prompt)}"

async def events() -> AsyncIterator[StreamEvent]:
yield StreamEvent("text", {"delta": response})
yield StreamEvent("final", {"text": response, "ok": True})

return AsyncStreamEvents(events(), state=StreamState())


def main(framework: Any) -> EchoModel:
del framework
return EchoModel()


def _prompt_text(prompt: str | list[dict[str, Any]]) -> str:
if isinstance(prompt, str):
return prompt
return json.dumps(prompt, ensure_ascii=False)
4 changes: 4 additions & 0 deletions packages/bub-ag-ui/examples/frontend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
COPILOTKIT_PORT=4000
BUB_AG_UI_AGENT_URL=http://127.0.0.1:8088/agent
VITE_COPILOTKIT_RUNTIME_PROXY=http://127.0.0.1:4000
FRONTEND_PORT=5173
3 changes: 3 additions & 0 deletions packages/bub-ag-ui/examples/frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist/
node_modules/
*.tsbuildinfo
12 changes: 12 additions & 0 deletions packages/bub-ag-ui/examples/frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bub AG-UI Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading