Skip to content

Commit 0bc2968

Browse files
noah-databricksIsaac
andcommitted
feat(appkit): add the omnigent plugin (1/4)
Embeds Omnigent, the open-source agent meta-harness, in an AppKit app: Claude Agent SDK, Pi and OpenAI Agents SDK sessions, with model calls going through Unity AI Gateway as the app's service principal. - One Omnigent server on loopback in header-auth mode (random header name) and one host per active user, supervised with restart backoff and idle shutdown; a parent-death watchdog so no child outlives Node. - A loopback model gateway: children hold a placeholder token, the gateway forwards only model-serving paths with the real token. - Harnesses are offered the UC system.ai models whose API types they speak, plus the workspace's chat serving endpoints. - Session routes under /api/omnigent, scoped to the signed-in user. - docs/docs/plugins/omnigent.md, extended by the later PRs. Co-authored-by: Isaac <no-reply@databricks.com> Signed-off-by: Noah Solomon <noah@databricks.com>
1 parent ec26243 commit 0bc2968

19 files changed

Lines changed: 2054 additions & 0 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ internal
2222
.isaac/
2323

2424
.codex-tmp/
25+
__pycache__/

‎docs/docs/plugins/omnigent.md‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Omnigent
2+
3+
<!-- AUTO-GENERATED: stability-banner-start -->
4+
:::warning Beta plugin
5+
This plugin is currently **beta**. APIs may change between minor releases. Import from `@databricks/appkit/beta`. See [Plugin Stability Tiers](./stability.md).
6+
:::
7+
<!-- AUTO-GENERATED: stability-banner-end -->
8+
9+
The `omnigent` plugin embeds [Omnigent](https://github.com/omnigent-ai/omnigent), the open-source agent meta-harness, in a Databricks AppKit app. Your users get Claude Agent SDK, Pi and OpenAI Agents SDK sessions inside the app, with model calls going through Unity AI Gateway as the app's service principal.
10+
11+
Omnigent owns the agent loop: sessions, history, approvals. The plugin runs it safely inside a Databricks App. Use the [`agents`](./agents.md) plugin when you want AppKit to own the loop against one serving endpoint. Use `omnigent` when you want the harnesses themselves.
12+
13+
## Install
14+
15+
```ts
16+
import { createApp, server } from "@databricks/appkit";
17+
import { omnigent } from "@databricks/appkit/beta";
18+
19+
await createApp({
20+
plugins: [
21+
server(),
22+
omnigent({ instructions: "You help the team explore order data." }),
23+
],
24+
});
25+
```
26+
27+
The app also needs:
28+
29+
- **Python.** Add a `pyproject.toml` with `requires-python = ">=3.12,<3.13"` and `omnigent[databricks]`, plus a `uv.lock`. Databricks Apps then builds a Python 3.12 venv next to `npm install`. A plain `requirements.txt` gets Python 3.11, which Omnigent does not support. The lockfile must reference public PyPI.
30+
- **Harness CLIs.** Add them to your `package.json`, so you pin their versions: `@anthropic-ai/claude-code` and `@earendil-works/pi-coding-agent`. The plugin only offers harnesses whose CLI is installed. The Claude Agent SDK's bundled CLI is too old for current Claude models, so add `@anthropic-ai/claude-code` even though the SDK ships one.
31+
- **Start with `node`.** In `app.yaml`, use `command: ["node", "server.js"]`, not `npm run start`. npm does not pass SIGTERM on, so the plugin never gets to stop its child processes. As a backstop, each child also exits when Node does, so a redeploy still succeeds.
32+
33+
## Configuration
34+
35+
| Option | Default | |
36+
|---|---|---|
37+
| `instructions` | a generic assistant prompt | System instructions for every session |
38+
| `harnesses` | `claude-sdk`, `pi`, `openai-agents` | Harnesses users may pick |
39+
| `defaultHarness`, `defaultModel` | first available | |
40+
| `runtime.python` | `./.venv/bin/python` | |
41+
| `runtime.home` | `$TMPDIR/appkit-omnigent` | The plugin's state directory |
42+
| `runtime.hostIdleMs` | 30 min | Stop a user's Omnigent host after this long idle |
43+
44+
Each harness is offered the Unity AI Gateway `system.ai` models whose API it speaks: Claude Agent SDK the Anthropic Messages API, Pi the Codex Responses API, OpenAI Agents SDK the OpenAI Responses API.
45+
46+
## Routes
47+
48+
All routes are under `/api/omnigent` and scoped to the signed-in user.
49+
50+
| Route | |
51+
|---|---|
52+
| `GET /harnesses` | Harnesses available to the user, each with the gateway models it can drive |
53+
| `GET /sessions`, `POST /sessions` | List, create (`harness`, `model`, `message`, `title`) |
54+
| `GET /sessions/:id`, `DELETE /sessions/:id` | Snapshot (items, status), delete |
55+
| `POST /sessions/:id/messages` | `{ "text": "..." }` |
56+
| `GET /sessions/:id/stream` | Server-sent events (Omnigent's session stream, which replays from `Last-Event-ID`) |
57+
| `POST /sessions/:id/interrupt` | Stop the current turn |
58+
| `GET /status` | Runtime health: hosts, gateway counters |
59+
60+
## How it works
61+
62+
- **One Omnigent server, one host per user.** The server listens on loopback in Omnigent's header-auth mode, under a random header name only the plugin knows. Each active user gets their own host process, registered as that user; Omnigent only runs a session on its owner's host. An idle host stops after `runtime.hostIdleMs`.
63+
- **No credential in any child process.** The harnesses' Databricks profile points at a model gateway inside the plugin, on loopback, with a placeholder token. The gateway forwards only model-serving paths to the workspace and adds the service principal's token. Every other path returns 403.
64+
65+
## Limits
66+
67+
- **Sessions live in the container.** Omnigent keeps them in SQLite under the plugin's state directory, so they survive a process restart but not a redeploy.
68+
- **Only gateway harnesses.** Harnesses that sign in with their vendor (Cursor, Copilot, Gemini and others) bypass Unity AI Gateway, so they are not offered. Native terminal harnesses (`claude-native`, `pi-native`) are not offered yet: they need tmux and a terminal client.
69+
- **Shared gateway limits.** All users' model traffic uses the service principal's gateway limits.

‎packages/appkit/src/plugins/beta-exports.generated.ts‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { gzipSync } from "node:zlib";
2+
3+
interface BundleSpec {
4+
name: string;
5+
description?: string;
6+
harness: string;
7+
model: string;
8+
/** Databricks profile the harness uses for model calls. */
9+
profile: string;
10+
instructions: string;
11+
}
12+
13+
/**
14+
* An Omnigent agent bundle: `config.yaml` (the agent spec) and `AGENTS.md`
15+
* (the instructions), as a tar.gz. The YAML is written as JSON, which is
16+
* valid YAML.
17+
*/
18+
export function buildBundle(spec: BundleSpec): Buffer {
19+
const config = {
20+
spec_version: 1,
21+
name: spec.name,
22+
description: spec.description ?? spec.name,
23+
executor: {
24+
type: "omnigent",
25+
model: spec.model,
26+
profile: spec.profile,
27+
config: { harness: spec.harness },
28+
auth: { type: "databricks", profile: spec.profile },
29+
},
30+
instructions: "AGENTS.md",
31+
};
32+
const files: Record<string, string> = {
33+
"config.yaml": `${JSON.stringify(config, null, 2)}\n`,
34+
"AGENTS.md": spec.instructions,
35+
};
36+
return gzipSync(tar(files));
37+
}
38+
39+
/** Minimal ustar writer: regular files only, names under 100 bytes. */
40+
function tar(files: Record<string, string | Buffer>): Buffer {
41+
const blocks: Buffer[] = [];
42+
const mtime = Math.floor(Date.now() / 1000);
43+
for (const [name, content] of Object.entries(files)) {
44+
if (Buffer.byteLength(name) >= 100)
45+
throw new Error(`tar: name too long: ${name}`);
46+
const data = Buffer.isBuffer(content) ? content : Buffer.from(content);
47+
const header = Buffer.alloc(512);
48+
header.write(name, 0, 100);
49+
header.write("0000644\0", 100);
50+
header.write("0000000\0", 108);
51+
header.write("0000000\0", 116);
52+
header.write(`${data.length.toString(8).padStart(11, "0")}\0`, 124);
53+
header.write(`${mtime.toString(8).padStart(11, "0")}\0`, 136);
54+
header.write(" ", 148); // checksum placeholder
55+
header.write("0", 156);
56+
header.write("ustar\u000000", 257);
57+
let sum = 0;
58+
for (const b of header) sum += b;
59+
header.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148);
60+
blocks.push(header, data, Buffer.alloc((512 - (data.length % 512)) % 512));
61+
}
62+
blocks.push(Buffer.alloc(1024));
63+
return Buffer.concat(blocks);
64+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { OmnigentHarness } from "./types";
2+
3+
interface HarnessInfo {
4+
label: string;
5+
/**
6+
* Unity AI Gateway API types it calls (a UC model service's
7+
* `supported_api_types`); a model must support one of them.
8+
*/
9+
apis: string[];
10+
/** Default models, best first, when the workspace has them. */
11+
preferred: string[];
12+
}
13+
14+
/**
15+
* Harnesses whose model calls Omnigent routes through a Databricks profile
16+
* (`AuthModel.OMNIGENT_CREDENTIAL`), so they go through Unity AI Gateway.
17+
* The other harnesses Omnigent supports sign in with their vendor, so they
18+
* are not offered.
19+
*/
20+
const GATEWAY_HARNESSES: Readonly<Record<string, HarnessInfo>> = {
21+
"claude-sdk": {
22+
label: "Claude Agent SDK",
23+
apis: ["anthropic/v1/messages"],
24+
preferred: ["system.ai.claude-sonnet-5", "system.ai.claude-opus-5-5"],
25+
},
26+
pi: {
27+
label: "Pi",
28+
// Pi reaches Databricks models through the Codex responses API; on the
29+
// chat-completions path Gemini fails with an empty 400 (tested).
30+
apis: ["codex/v1/responses"],
31+
preferred: ["system.ai.gpt-5-5"],
32+
},
33+
"openai-agents": {
34+
label: "OpenAI Agents SDK",
35+
apis: ["openai/v1/responses"],
36+
preferred: ["system.ai.gpt-5-5"],
37+
},
38+
};
39+
40+
export const DEFAULT_HARNESSES = Object.keys(GATEWAY_HARNESSES);
41+
42+
/** A Unity AI Gateway model, e.g. `system.ai.claude-sonnet-5`. */
43+
export interface GatewayModel {
44+
name: string;
45+
/** The model service's `supported_api_types`. */
46+
apiTypes: string[];
47+
}
48+
49+
export interface HarnessOffer extends OmnigentHarness {
50+
models: string[];
51+
defaultModel?: string;
52+
}
53+
54+
/**
55+
* What a user can pick: the allowed gateway harnesses their host reports as
56+
* usable, each with the models whose API it speaks.
57+
*/
58+
export function offerHarnesses(opts: {
59+
allowed: string[];
60+
configured: Record<string, boolean | string>;
61+
models: GatewayModel[];
62+
}): HarnessOffer[] {
63+
const out: HarnessOffer[] = [];
64+
for (const id of opts.allowed) {
65+
const info = GATEWAY_HARNESSES[id];
66+
const ready = opts.configured[id];
67+
if (!info) continue;
68+
// "needs-auth" is advisory for gateway harnesses: their auth comes from the profile.
69+
if (ready === undefined || ready === false || ready === "binary-missing")
70+
continue;
71+
const models = opts.models
72+
.filter((m) => m.apiTypes.some((t) => info.apis.includes(t)))
73+
.map((m) => m.name);
74+
out.push({
75+
id,
76+
label: info.label,
77+
ready,
78+
models,
79+
defaultModel: info.preferred.find((m) => models.includes(m)) ?? models[0],
80+
});
81+
}
82+
return out;
83+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./omnigent";
2+
export * from "./types";
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"$schema": "https://databricks.github.io/appkit/schemas/plugin-manifest.schema.json",
3+
"name": "omnigent",
4+
"displayName": "Omnigent Plugin",
5+
"description": "Embedded Omnigent agent runtime: Claude Agent SDK, Pi and OpenAI Agents SDK harnesses through Unity AI Gateway",
6+
"stability": "beta",
7+
"resources": {
8+
"required": [],
9+
"optional": []
10+
},
11+
"scaffolding": {
12+
"rules": {
13+
"must": [
14+
"Add pyproject.toml (python >=3.12,<3.13) with omnigent[databricks] and a uv.lock that uses public PyPI",
15+
"Add harness CLIs to package.json: @anthropic-ai/claude-code, @earendil-works/pi-coding-agent",
16+
"Start the app with node, not npm run, so SIGTERM reaches the plugin and it can stop its child processes"
17+
]
18+
}
19+
},
20+
"config": {
21+
"schema": {
22+
"type": "object",
23+
"properties": {
24+
"instructions": {
25+
"type": "string",
26+
"description": "System instructions for every session"
27+
},
28+
"harnesses": {
29+
"type": "array",
30+
"items": {
31+
"type": "string"
32+
},
33+
"description": "Harnesses users may pick"
34+
},
35+
"defaultHarness": {
36+
"type": "string"
37+
},
38+
"defaultModel": {
39+
"type": "string"
40+
}
41+
}
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)