Skip to content

Commit f2c4c25

Browse files
committed
feat: add Agno AgentOS adapter package
1 parent 539f1f5 commit f2c4c25

16 files changed

Lines changed: 1059 additions & 0 deletions

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Try it yourself in the [Playground](https://www.openui.com/playground): generate
9595
| :--------------------------------------------------------------------------------------------------------- | :----------------------------------------------- | :----------------------------------------------------------------------------------------------------------- |
9696
| [`@openuidev/lang-core`](./packages/lang-core) | Framework-agnostic parsing and prompt generation | Core parser, prompt-generation, runtime-evaluation, and type layer with no React, Vue, or Svelte dependency |
9797
| [`@openuidev/langchain`](./packages/langchain) | LangChain and LangGraph agents | Agent transformer and server helpers that stream OpenUI through AG-UI |
98+
| [`@openuidev/agno`](./packages/agno) | Agno AgentOS agents and teams | AG-UI streaming and AgentOS session adapters for OpenUI chat interfaces |
9899
| [`@openuidev/react-lang`](./packages/react-lang) | React rendering runtimes | Define component libraries, generate prompts, and render streamed OpenUI Lang in React |
99100
| [`@openuidev/react-headless`](./packages/react-headless) | Bring-your-own React chat UI | Headless chat state, streaming adapters, and message format converters |
100101
| [`@openuidev/react-ui`](./packages/react-ui) | Fastest path to a full React chat experience | Prebuilt chat layouts, standalone UI primitives, and two built-in component libraries |
@@ -117,6 +118,9 @@ npm install @openuidev/lang-core
117118
# LangChain/LangGraph agent and server integration
118119
npm install @openuidev/langchain @langchain/langgraph
119120

121+
# Agno AgentOS integration
122+
npm install @openuidev/agno @openuidev/react-ui
123+
120124
# Vue or Svelte runtime
121125
npm install @openuidev/vue-lang
122126
npm install @openuidev/svelte-lang
@@ -163,6 +167,7 @@ openui/
163167
│ ├── react-email/ # React Email component library for generated emails
164168
│ ├── lang-core/ # Framework-agnostic parser, prompt, and runtime layer
165169
│ ├── langchain/ # LangChain/LangGraph streaming integration
170+
│ ├── agno/ # Agno AgentOS streaming and session integration
166171
│ ├── vue-lang/ # Vue runtime bindings for OpenUI Lang
167172
│ ├── svelte-lang/ # Svelte runtime bindings for OpenUI Lang
168173
│ ├── browser-bundle/ # Script-tag bundle for CDN / iframe / no-build embeds

packages/agno/README.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# `@openuidev/agno`
2+
3+
Connect an Agno AgentOS to OpenUI without copying transport or persistence glue.
4+
5+
The integration is intentionally complementary:
6+
7+
- **AgentOS handles everything behind the UI:** agents, teams, models, tools,
8+
memory, knowledge, sessions, authorization, execution, and deployment.
9+
- **OpenUI handles the UI:** component instructions, streamed OpenUI Lang,
10+
rendering, interactions, forms, charts, theming, and the chat surface.
11+
- **AG-UI is the boundary** between the two systems.
12+
13+
## Install
14+
15+
```bash
16+
pnpm add @openuidev/agno @openuidev/react-ui
17+
```
18+
19+
Import OpenUI styles once:
20+
21+
```css
22+
@import "@openuidev/react-ui/layered/styles/index.css";
23+
```
24+
25+
## Connect AgentInterface to AgentOS
26+
27+
```tsx
28+
import { createAgnoLLM, agnoStorage } from "@openuidev/agno";
29+
import { AgentInterface } from "@openuidev/react-ui";
30+
31+
const llm = createAgnoLLM({
32+
url: "http://localhost:7777/agui",
33+
forwardedProps: { user_id: "demo-user" },
34+
});
35+
36+
const storage = agnoStorage({
37+
baseUrl: "http://localhost:7777",
38+
entityType: "agent",
39+
entityId: "openui-assistant",
40+
userId: "demo-user",
41+
});
42+
43+
export function Chat() {
44+
return <AgentInterface llm={llm} storage={storage} />;
45+
}
46+
```
47+
48+
For an authenticated AgentOS, pass a scoped bearer token through `token` and
49+
omit `userId`/`forwardedProps.user_id`; AgentOS derives identity from the token.
50+
51+
## What the package owns
52+
53+
- `createAgnoLLM()` adds AgentOS's AG-UI extension containers and configures
54+
the Agno-aware stream adapter.
55+
- `agnoAGUIAdapter()` removes non-chat lifecycle/state events and Agno's empty
56+
tool-parent text envelope while retaining streamed text, tools, and errors.
57+
- `agnoStorage()` maps OpenUI threads to AgentOS `/sessions` APIs and reloads
58+
messages from AgentOS `chat_history`.
59+
- The mapping is deliberately 1:1: the OpenUI `thread.id` is the AgentOS
60+
`session_id`, so chat, persistence, inspection, and operational tooling all
61+
address the same conversation without a translation table.
62+
- `agnoHistoryToMessages()` exposes the tolerant history conversion separately
63+
for custom storage implementations.
64+
65+
The package does not run an agent, proxy model keys, or create a second source
66+
of conversation truth.
67+
68+
## AgentOS backend
69+
70+
The backend remains normal Agno Python code. Generate the OpenUI component
71+
prompt from the frontend library, then include it in the agent instructions:
72+
73+
```python
74+
from agno.agent import Agent
75+
from agno.db.sqlite import SqliteDb
76+
from agno.models.openai import OpenAIResponses
77+
from agno.os import AgentOS
78+
from agno.os.interfaces.agui import AGUI
79+
80+
agent = Agent(
81+
id="openui-assistant",
82+
model=OpenAIResponses(id="gpt-5.5"),
83+
db=SqliteDb(id="openui", db_file="tmp/openui.db"),
84+
instructions=[openui_system_prompt],
85+
add_history_to_context=True,
86+
)
87+
88+
agent_os = AgentOS(agents=[agent], interfaces=[AGUI(agent=agent)])
89+
app = agent_os.get_app()
90+
```
91+
92+
The local `examples/agno-chat` workspace contains a runnable client, a real
93+
AgentOS server, and a deterministic no-key development harness.
94+
95+
## Current scope
96+
97+
This first local implementation supports streamed OpenUI responses, backend
98+
tool timelines, authentication headers, agents/teams, and AgentOS-backed
99+
conversation persistence. Native rendering and resumption of Agno HITL/client
100+
tools is the next integration layer rather than something this package claims
101+
to support already.

packages/agno/eslint.config.cjs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const tseslint = require("@typescript-eslint/eslint-plugin");
2+
const typescript = require("@typescript-eslint/parser");
3+
const prettier = require("eslint-config-prettier");
4+
const unusedImports = require("eslint-plugin-unused-imports");
5+
const eslintPluginPrettier = require("eslint-plugin-prettier");
6+
7+
module.exports = [
8+
{
9+
files: ["**/__tests__/**/*.{ts,tsx}", "**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"],
10+
languageOptions: {
11+
parser: typescript,
12+
parserOptions: {
13+
project: "./tsconfig.test.json",
14+
sourceType: "module",
15+
},
16+
},
17+
},
18+
{
19+
files: ["**/*.{ts,tsx}"],
20+
ignores: [
21+
"**/__tests__/**/*.{ts,tsx}",
22+
"**/*.test.{ts,tsx}",
23+
"**/*.spec.{ts,tsx}",
24+
"*.config.ts",
25+
],
26+
languageOptions: {
27+
parser: typescript,
28+
parserOptions: {
29+
project: "./tsconfig.json",
30+
sourceType: "module",
31+
},
32+
},
33+
plugins: {
34+
"@typescript-eslint": tseslint,
35+
"unused-imports": unusedImports,
36+
prettier: eslintPluginPrettier,
37+
},
38+
rules: {
39+
"@typescript-eslint/interface-name-prefix": "off",
40+
"@typescript-eslint/explicit-function-return-type": "off",
41+
"@typescript-eslint/explicit-module-boundary-types": "off",
42+
"@typescript-eslint/no-explicit-any": "off",
43+
"no-undefined": "off",
44+
"@typescript-eslint/no-unused-vars": [
45+
"error",
46+
{
47+
vars: "all",
48+
varsIgnorePattern: "^_",
49+
args: "after-used",
50+
argsIgnorePattern: "^_",
51+
},
52+
],
53+
"unused-imports/no-unused-imports": "error",
54+
...eslintPluginPrettier.configs.recommended.rules,
55+
},
56+
},
57+
prettier,
58+
];

packages/agno/package.json

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"name": "@openuidev/agno",
3+
"version": "0.0.1",
4+
"description": "OpenUI client and AgentOS session adapters for Agno",
5+
"license": "MIT",
6+
"engines": {
7+
"node": ">=18"
8+
},
9+
"type": "module",
10+
"main": "dist/index.cjs",
11+
"module": "dist/index.mjs",
12+
"types": "dist/index.d.cts",
13+
"sideEffects": false,
14+
"files": [
15+
"dist",
16+
"README.md"
17+
],
18+
"exports": {
19+
".": {
20+
"import": {
21+
"types": "./dist/index.d.mts",
22+
"default": "./dist/index.mjs"
23+
},
24+
"require": {
25+
"types": "./dist/index.d.cts",
26+
"default": "./dist/index.cjs"
27+
}
28+
}
29+
},
30+
"scripts": {
31+
"test": "vitest run",
32+
"build": "tsdown",
33+
"watch": "tsdown --watch",
34+
"typecheck": "tsc --noEmit",
35+
"lint:check": "eslint ./src",
36+
"lint:fix": "eslint ./src --fix",
37+
"format:fix": "prettier --write ./src README.md",
38+
"format:check": "prettier --check ./src README.md",
39+
"check:publint": "publint",
40+
"check:attw": "attw --pack .",
41+
"prepare": "pnpm run build",
42+
"prepublishOnly": "pnpm run check:publint && pnpm run check:attw",
43+
"ci": "pnpm run typecheck && pnpm run test && pnpm run lint:check && pnpm run format:check"
44+
},
45+
"keywords": [
46+
"openui",
47+
"agno",
48+
"agentos",
49+
"ag-ui",
50+
"generative-ui",
51+
"streaming"
52+
],
53+
"homepage": "https://openui.com",
54+
"repository": {
55+
"type": "git",
56+
"url": "https://github.com/thesysdev/openui.git",
57+
"directory": "packages/agno"
58+
},
59+
"bugs": {
60+
"url": "https://github.com/thesysdev/openui/issues"
61+
},
62+
"author": "engineering@thesys.dev",
63+
"peerDependencies": {
64+
"@openuidev/react-headless": "workspace:^"
65+
},
66+
"devDependencies": {
67+
"@openuidev/react-headless": "workspace:^",
68+
"typescript": "catalog:",
69+
"vitest": "^4.1.0"
70+
}
71+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { EventType } from "@openuidev/react-headless";
2+
import { describe, expect, it } from "vitest";
3+
import { agnoAGUIAdapter } from "../adapter";
4+
5+
function aguiResponse(events: object[]) {
6+
return new Response(events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(""), {
7+
headers: { "Content-Type": "text/event-stream" },
8+
});
9+
}
10+
11+
async function parse(events: object[]) {
12+
const parsed = [];
13+
for await (const event of agnoAGUIAdapter().parse(aguiResponse(events))) parsed.push(event);
14+
return parsed;
15+
}
16+
17+
describe("agnoAGUIAdapter", () => {
18+
it("removes AgentOS lifecycle, state, raw events, and an empty tool parent", async () => {
19+
const parsed = await parse([
20+
{ type: EventType.RUN_STARTED, threadId: "thread-1", runId: "run-1" },
21+
{ type: EventType.STATE_SNAPSHOT, snapshot: {} },
22+
{ type: EventType.TEXT_MESSAGE_START, messageId: "empty", role: "assistant" },
23+
{ type: EventType.TEXT_MESSAGE_END, messageId: "empty" },
24+
{
25+
type: EventType.TOOL_CALL_START,
26+
toolCallId: "call-1",
27+
toolCallName: "get_quarterly_revenue",
28+
parentMessageId: "empty",
29+
},
30+
{ type: EventType.TOOL_CALL_ARGS, toolCallId: "call-1", delta: "{}" },
31+
{ type: EventType.TOOL_CALL_END, toolCallId: "call-1" },
32+
{
33+
type: EventType.TOOL_CALL_RESULT,
34+
toolCallId: "call-1",
35+
messageId: "call-1",
36+
role: "tool",
37+
content: '{"quarters":[]}',
38+
},
39+
{ type: EventType.TEXT_MESSAGE_START, messageId: "answer", role: "assistant" },
40+
{ type: EventType.TEXT_MESSAGE_CONTENT, messageId: "answer", delta: "root = Card([])" },
41+
{ type: EventType.TEXT_MESSAGE_END, messageId: "answer" },
42+
{ type: EventType.RUN_FINISHED, threadId: "thread-1", runId: "run-1" },
43+
]);
44+
45+
expect(parsed.map((event) => event.type)).toEqual([
46+
EventType.TOOL_CALL_START,
47+
EventType.TOOL_CALL_ARGS,
48+
EventType.TOOL_CALL_END,
49+
EventType.TOOL_CALL_RESULT,
50+
EventType.TEXT_MESSAGE_START,
51+
EventType.TEXT_MESSAGE_CONTENT,
52+
EventType.TEXT_MESSAGE_END,
53+
]);
54+
});
55+
56+
it("keeps non-text events buffered inside an empty envelope", async () => {
57+
const parsed = await parse([
58+
{ type: EventType.TEXT_MESSAGE_START, messageId: "empty", role: "assistant" },
59+
{
60+
type: EventType.TOOL_CALL_START,
61+
toolCallId: "call-1",
62+
toolCallName: "lookup",
63+
},
64+
{ type: EventType.TEXT_MESSAGE_END, messageId: "empty" },
65+
]);
66+
67+
expect(parsed).toEqual([
68+
expect.objectContaining({ type: EventType.TOOL_CALL_START, toolCallId: "call-1" }),
69+
]);
70+
});
71+
72+
it("keeps a text envelope once meaningful content arrives", async () => {
73+
const parsed = await parse([
74+
{ type: EventType.TEXT_MESSAGE_START, messageId: "answer", role: "assistant" },
75+
{ type: EventType.TEXT_MESSAGE_CONTENT, messageId: "answer", delta: "hello" },
76+
{ type: EventType.TEXT_MESSAGE_END, messageId: "answer" },
77+
]);
78+
79+
expect(parsed.map((event) => event.type)).toEqual([
80+
EventType.TEXT_MESSAGE_START,
81+
EventType.TEXT_MESSAGE_CONTENT,
82+
EventType.TEXT_MESSAGE_END,
83+
]);
84+
});
85+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { createAgnoLLM } from "../llm";
3+
4+
describe("createAgnoLLM", () => {
5+
it("sends AgentOS extension containers and bearer authentication", async () => {
6+
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(new Response(null, { status: 200 }));
7+
const llm = createAgnoLLM({
8+
url: "/agui",
9+
token: "test-token",
10+
forwardedProps: { user_id: "user-1" },
11+
fetch: fetchMock,
12+
});
13+
14+
await llm.send({
15+
threadId: "thread-1",
16+
messages: [{ id: "message-1", role: "user", content: "Hello" }],
17+
signal: new AbortController().signal,
18+
});
19+
20+
const [url, request] = fetchMock.mock.calls[0]!;
21+
expect(url).toBe("/agui");
22+
expect(request?.headers).toMatchObject({
23+
Authorization: "Bearer test-token",
24+
"Content-Type": "application/json",
25+
});
26+
expect(JSON.parse(request?.body as string)).toMatchObject({
27+
threadId: "thread-1",
28+
state: {},
29+
forwardedProps: { user_id: "user-1" },
30+
tools: [],
31+
context: [],
32+
});
33+
});
34+
35+
it("lets an explicit Authorization header override the token", async () => {
36+
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(new Response(null, { status: 200 }));
37+
const llm = createAgnoLLM({
38+
url: "/agui",
39+
token: "ignored",
40+
headers: { Authorization: "Bearer custom" },
41+
fetch: fetchMock,
42+
});
43+
44+
await llm.send({
45+
threadId: "thread-1",
46+
messages: [{ id: "message-1", role: "user", content: "Hello" }],
47+
signal: new AbortController().signal,
48+
});
49+
50+
expect(fetchMock.mock.calls[0]?.[1]?.headers).toMatchObject({
51+
Authorization: "Bearer custom",
52+
});
53+
});
54+
});

0 commit comments

Comments
 (0)