Skip to content

Commit 9f076c7

Browse files
feat(agentos-server): HTTP telemetry ingest endpoint + Mongo projection
Add POST /agentos/api/ingest/events so the Python SDK posts telemetry instead of writing Mongo directly; the server now owns the projection. - routes/ingest.ts + ingest-auth.ts: batch ingest with a bearer guard (open if AGENTOS_INGEST_TOKEN unset; boot warning in that case). Mounted before the global 1mb parser + requireAuth with a 5mb body limit. - stores/telemetry-projection.ts: project each event into agent_registry, agent_logs, sessions, chat_sessions, agent_messages. Writes the canonical chat_sessions row (fixes library sessions missing from the dashboard list) and drops the dead slack_threads write. session_started is the sole creator of the sessions doc (no stub corruption on dropped/reordered events). Idempotent on event_id. + agent-source.ts (model-strip + library/inline source builders, ported from the Python sink). - Extend agent-log-store.ts (optional rollup fields + idempotent _id), widen SessionDoc/MessageDoc, mount + boot warning in index.ts. - vitest projection tests incl. out-of-order regression; CLAUDE.md updated to the HTTP-ingest model. Also snapshots in-progress AgentOS SPA, observability, and engine work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f53315f commit 9f076c7

42 files changed

Lines changed: 6346 additions & 593 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,20 +89,21 @@ Optional GitHub repo **variables** (build-time baked into the SPA bundle):
8989

9090
### 2.3 MongoDB Atlas — AgentOS runtime backend
9191

92-
The Mongo cluster is the source of truth for AgentOS. The Python SDK's `AgentRegistrySink` + `MongoMessageSink` and the AgentOS server both connect here.
92+
The Mongo cluster is the source of truth for AgentOS. **Only the `agentos-server` connects to Mongo.** As of the post-0.2.1 dev build, the Python SDK no longer writes Mongo directly — it POSTs telemetry to the server's ingest endpoint (`AgentOSHttpSink``POST /agentos/api/ingest/events`), and the server owns all writes. (The old `AgentRegistrySink` + `MongoMessageSink` and the `motor` dep were removed — see the Python SDK history below.)
9393

94-
**Collections (database = `AGENTOS_MONGO_DB`):**
95-
- `agent_registry` — one doc per registered agent (the SDK writes `source.type="library"` for harness-mode agents → AgentOS UI hides the chat-sandbox button for those, see commit `8d829b8`)
94+
**Collections (database = the server's `MONGO_DATABASE`):**
95+
- `agent_registry` — one doc per registered agent (the server writes `source.type="library"` for harness-mode agents → AgentOS UI hides the chat-sandbox button for those, see commit `8d829b8`)
9696
- `agent_logs` — one doc per conversation (one `ComputerAgent` instance = one log row, multi-turn collapses correctly since the 0.2.0 session-id refactor)
97-
- `sessions` — ordered chat transcript (one doc per session_id, entries appended in order)
97+
- `sessions` — ordered chat transcript (one doc per session_id, entries appended in order; **`session_started` is the sole creator** of the doc, so a dropped/reordered start can't stub it)
98+
- `chat_sessions` — the session-index row (`{_id, agent, createdAt, lastMessageAt}`) the dashboard's session list + per-agent `sessionCount`/`lastActivity` read. The server projection writes this so library-mode sessions show up (the old Python sink omitted it).
9899
- `agent_messages` — per-event audit trail (every assistant_message / tool_use / tool_result lands here)
99-
- `slack_threads` — chat-channel state
100+
- `slack_threads`Slack-bot chat-channel state only; **not** written by the ingest projection (it was dead/legacy for library agents).
100101

101-
**Credentials required (runtime env, anywhere the SDK or server runs):**
102-
- `AGENTOS_MONGO_URL``mongodb+srv://<user>:<pass>@<cluster>.mongodb.net`
103-
- `AGENTOS_MONGO_DB` — usually `computeragent` / `computeragent-prod` / `computeragent-test` per env
102+
**Credentials required:**
103+
- On the **SDK** side: `AGENTOS_INGEST_URL` (e.g. `https://<host>/agentos/api/ingest/events`) + optional `AGENTOS_INGEST_TOKEN` (sent as `Authorization: Bearer …`). No Mongo creds.
104+
- On the **server** side: `MONGO_URL` + `MONGO_DATABASE` (this is the DB the collections above live in).
104105

105-
**Behaviour:** when `AGENTOS_MONGO_URL` is set, the default telemetry pipeline auto-attaches **both** the registry sink and the message sink. Pre-0.2.0 only the registry sink auto-attached and `agent_messages` was empty — that bug is fixed.
106+
**Behaviour:** when `AGENTOS_INGEST_URL` is set, the SDK's default telemetry pipeline auto-attaches `AgentOSHttpSink` (gated on the `[agentos]` extra, which is now `httpx`-based). Each event carries a stable `event_id` so the server's writes are idempotent on retry. ⚠️ When the server's `AGENTOS_INGEST_TOKEN` is unset the ingest route is **open** (anonymous writes) — set it on any network-exposed deployment.
106107

107108
---
108109

@@ -253,7 +254,7 @@ Top-level directories (`pnpm` workspace, `turbo` for the build graph):
253254
└──────────────────┘
254255
```
255256

256-
The Python SDK (`computer-agent-py`) re-implements the harness layer in Python with the same four orthogonal axes. It writes to the same MongoDB collections via `AgentRegistrySink` + `MongoMessageSink` so library-mode Python agents show up in the AgentOS UI alongside TS harness-server-hosted ones.
257+
The Python SDK (`computer-agent-py`) re-implements the harness layer in Python with the same four orthogonal axes. It POSTs telemetry to the AgentOS server (`AgentOSHttpSink``POST /agentos/api/ingest/events`), which projects it into the same MongoDB collections so library-mode Python agents show up in the AgentOS UI alongside TS harness-server-hosted ones. (Through 0.2.x the SDK wrote Mongo directly via `AgentRegistrySink` + `MongoMessageSink`; that was removed in favour of HTTP ingest so the SDK needs no Mongo creds and the schema lives server-side.)
257258

258259
---
259260

@@ -271,9 +272,13 @@ ANTHROPIC_API_KEY=sk-ant-...
271272
GITCLAW_MODEL_BASE_URL=https://api.lyzr.ai/v1
272273
OPENAI_API_KEY=sk-...
273274

274-
# AgentOS Mongo (auto-attaches both sinks when set)
275-
AGENTOS_MONGO_URL=mongodb+srv://user:pass@cluster.mongodb.net
276-
AGENTOS_MONGO_DB=computeragent
275+
# AgentOS persistence — SDK POSTs telemetry to the server; the server writes Mongo.
276+
# On the SDK (library/worker) side:
277+
AGENTOS_INGEST_URL=https://<agentos-host>/agentos/api/ingest/events
278+
AGENTOS_INGEST_TOKEN=<shared-secret> # optional; must match the server's
279+
# On the agentos-server side (NOT the SDK):
280+
MONGO_URL=mongodb+srv://user:pass@cluster.mongodb.net
281+
MONGO_DATABASE=computeragent
277282

278283
# OTel → New Relic
279284
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net
@@ -356,6 +361,7 @@ pnpm build && pnpm start # node dist/index.js
356361
| `COOKIE_SECURE` | derived from `NODE_ENV` | Force `true` / `false` explicitly |
357362
| `AGENTOS_SESSION_SECRET` | random per boot | Cookie-session secret. **Set to a stable value in prod** or sessions are invalidated on restart |
358363
| `API_AUTH_USER` + `API_AUTH_PASS` | unset | Basic-auth gate on the API. When unset the API is open (relies on network policy) |
364+
| `AGENTOS_INGEST_TOKEN` | unset | Bearer token guarding `POST /agentos/api/ingest/events` (the Python SDK's telemetry ingest). When unset the route is **open** (anonymous writes to registry/logs/sessions) — set it on any network-exposed pod. The SDK must send the same value as `AGENTOS_INGEST_TOKEN`. |
359365
| `AGENTOS_RUNTIME` | unset | Default substrate name used by the "Register agent" form (`local` / `bwrap` / `e2b` / `vzvm`) |
360366
| `AGENTOS_SEED_DEFAULT` | unset | Set to `1` to auto-seed a default agent into the registry on first boot |
361367
| `AGENTOS_DEFAULT_SOURCE` | `github.com/shreyas-lyzr/general-agent` | Used by the seed agent |

agentos/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"react-hook-form": "^7.76.1",
3535
"react-markdown": "^10.1.0",
3636
"react-resizable-panels": "^4.11.2",
37+
"react-router-dom": "^7",
3738
"recharts": "^3.8.1",
3839
"remark-gfm": "^4.0.1",
3940
"sonner": "^2.0.7",

agentos/src/App.tsx

Lines changed: 70 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,34 @@
1-
import { useCallback, useEffect, useState } from "react";
21
import { Home as HomeIcon, Activity, Shield, Boxes } from "lucide-react";
3-
import { api, type Agent } from "./api.ts";
4-
import { LogsTab } from "./components/LogsTab.tsx";
5-
import { WorkspaceTab } from "./components/WorkspaceTab.tsx";
6-
import { SchedulesTab } from "./components/SchedulesTab.tsx";
2+
import { NavLink, Navigate, Outlet, Route, Routes, useLocation, useNavigate } from "react-router-dom";
73
import { HomePage } from "./components/HomePage.tsx";
8-
import { PolicyTab } from "./components/PolicyTab.tsx";
94
import { PoliciesPage } from "./components/PoliciesPage.tsx";
105
import { ObservabilityTab } from "./components/observability/ObservabilityTab.tsx";
116
import { RegistryPage } from "./components/RegistryPage.tsx";
12-
import { Tabs, TabsList, TabsTrigger } from "./components/ui/tabs.tsx";
13-
import { Badge } from "./components/ui/badge.tsx";
7+
import { AgentDashboard } from "./components/AgentDashboard.tsx";
148
import { Separator } from "./components/ui/separator.tsx";
15-
import { PageHeader } from "./components/composite/PageHeader.tsx";
9+
import { useAgents } from "./context/AgentsContext.tsx";
1610
import { cn } from "./lib/cn.ts";
1711

18-
type Tab = "chat" | "schedules" | "policy" | "logs";
19-
type View = "home" | "observability" | "policies" | "registry" | "dashboard";
20-
21-
const NAME_OVERRIDES: Record<string, string> = {
22-
"general-agent": "General Agent",
23-
"agentos-builder": "AgentOS Builder",
24-
"gap-promoter": "GAP Promoter",
25-
"framework-translator-agent": "Framework Translator",
26-
};
27-
function agentNameFromSource(source: string): string {
28-
const slug = source.split("/").pop() ?? source;
29-
return NAME_OVERRIDES[slug] ?? slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
30-
}
31-
32-
function typeLogo(harness: string): string | null {
33-
if (harness === "gitagent") return "/logos/gitagent.png";
34-
if (harness === "claude-agent-sdk") return "/logos/claude.svg";
35-
if (harness === "deepagents") return "/logos/langchain.svg";
36-
return null;
37-
}
38-
39-
function TypeBadge({ agent, className = "" }: { agent: Agent; className?: string }) {
40-
const logo = typeLogo(agent.harness);
12+
export default function App() {
4113
return (
42-
<Badge variant="secondary" className={cn("gap-1.5 pl-1 pr-2 py-0.5 text-[10px] font-normal", className)}>
43-
{logo && (
44-
<span className="h-3.5 w-3.5 grid place-items-center rounded-sm bg-background shrink-0">
45-
<img src={logo} alt="" className="h-2.5 w-2.5 object-contain" />
46-
</span>
47-
)}
48-
<span className="truncate">{agent.label}</span>
49-
</Badge>
14+
<Routes>
15+
<Route element={<Layout />}>
16+
<Route index element={<Navigate to="/home" replace />} />
17+
<Route path="home" element={<HomeRoute />} />
18+
<Route path="registry" element={<RegistryRoute />} />
19+
<Route path="observability" element={<ObservabilityTab />} />
20+
<Route path="policies" element={<PoliciesPage />} />
21+
<Route path="agents/:name" element={<AgentDashboard />} />
22+
<Route path="*" element={<Navigate to="/home" replace />} />
23+
</Route>
24+
</Routes>
5025
);
5126
}
5227

53-
export default function App() {
54-
const [view, setView] = useState<View>("home");
55-
const [agents, setAgents] = useState<Agent[]>([]);
56-
const [agentsLoaded, setAgentsLoaded] = useState(false);
57-
const [selected, setSelected] = useState<string | null>(null);
58-
const [tab, setTab] = useState<Tab>("chat");
59-
const [err, setErr] = useState<string | null>(null);
60-
const [launchMessage, setLaunchMessage] = useState<string | null>(null);
61-
62-
const reloadAgents = useCallback(() => {
63-
api
64-
.agents()
65-
.then(setAgents)
66-
.catch((e) => setErr(String(e)))
67-
.finally(() => setAgentsLoaded(true));
68-
}, []);
69-
70-
useEffect(() => {
71-
reloadAgents();
72-
}, [reloadAgents]);
73-
74-
const agent = agents.find((a) => a.name === selected) ?? null;
75-
76-
const openAgent = (name: string) => {
77-
setSelected(name);
78-
setTab("chat");
79-
setView("dashboard");
80-
};
81-
82-
const launchFromHome = (agentName: string, message: string) => {
83-
setSelected(agentName);
84-
setTab("chat");
85-
setLaunchMessage(message);
86-
setView("dashboard");
87-
};
28+
function Layout() {
29+
const { pathname } = useLocation();
30+
// "Agent Registry" stays active for both the registry list and any open agent.
31+
const registryActive = pathname.startsWith("/registry") || pathname.startsWith("/agents");
8832

8933
return (
9034
<div className="flex h-full bg-background text-foreground">
@@ -101,30 +45,10 @@ export default function App() {
10145
</div>
10246

10347
<nav className="px-2 pt-2 space-y-1">
104-
<RailButton
105-
icon={HomeIcon}
106-
label="Home"
107-
active={view === "home"}
108-
onClick={() => setView("home")}
109-
/>
110-
<RailButton
111-
icon={Boxes}
112-
label="Agent Registry"
113-
active={view === "registry" || view === "dashboard"}
114-
onClick={() => setView("registry")}
115-
/>
116-
<RailButton
117-
icon={Activity}
118-
label="Observability"
119-
active={view === "observability"}
120-
onClick={() => setView("observability")}
121-
/>
122-
<RailButton
123-
icon={Shield}
124-
label="Policies"
125-
active={view === "policies"}
126-
onClick={() => setView("policies")}
127-
/>
48+
<RailLink to="/home" icon={HomeIcon} label="Home" />
49+
<RailLink to="/registry" icon={Boxes} label="Agent Registry" active={registryActive} />
50+
<RailLink to="/observability" icon={Activity} label="Observability" />
51+
<RailLink to="/policies" icon={Shield} label="Policies" />
12852
</nav>
12953

13054
<div className="flex-1" />
@@ -135,103 +59,68 @@ export default function App() {
13559

13660
{/* Main */}
13761
<main className="flex-1 flex flex-col min-w-0">
138-
{view === "policies" ? (
139-
<PoliciesPage />
140-
) : view === "home" ? (
141-
<HomePage agents={agents} onLaunch={launchFromHome} onOpenDashboard={() => agents[0] && openAgent(agents[0].name)} />
142-
) : view === "observability" ? (
143-
<ObservabilityTab />
144-
) : view === "registry" ? (
145-
<RegistryPage
146-
agents={agents}
147-
loaded={agentsLoaded}
148-
err={err}
149-
selected={selected}
150-
onOpenAgent={openAgent}
151-
onReload={reloadAgents}
152-
/>
153-
) : agent ? (
154-
<>
155-
<PageHeader
156-
title={
157-
<span className="flex items-center gap-2">
158-
{agentNameFromSource(agent.sourceUrl ?? "")}
159-
<TypeBadge agent={agent} />
160-
</span>
161-
}
162-
description={
163-
<>
164-
{agent.harness} · {agent.model ?? "default model"}
165-
{!agent.sandboxCapable && (
166-
<span className="ml-2 text-warning">one-shot · no memory across turns</span>
167-
)}
168-
</>
169-
}
170-
actions={
171-
<Tabs value={tab} onValueChange={(v) => setTab(v as Tab)}>
172-
<TabsList>
173-
<TabsTrigger value="chat">Chat</TabsTrigger>
174-
<TabsTrigger value="schedules">Schedules</TabsTrigger>
175-
<TabsTrigger value="policy">Policy</TabsTrigger>
176-
<TabsTrigger value="logs">Logs</TabsTrigger>
177-
</TabsList>
178-
</Tabs>
179-
}
180-
/>
181-
<section className="flex-1 min-h-0">
182-
{tab === "chat" && (
183-
<WorkspaceTab
184-
key={agent.name}
185-
agent={agent.name}
186-
sandboxCapable={agent.sandboxCapable}
187-
liveChatCapable={agent.liveChatCapable !== false}
188-
initialMessage={launchMessage}
189-
onConsumedInitial={() => setLaunchMessage(null)}
190-
/>
191-
)}
192-
{tab === "schedules" && <SchedulesTab key={agent.name} agent={agent.name} agentLabel={agent.label} />}
193-
{tab === "policy" && (
194-
<PolicyTab
195-
key={agent.name}
196-
agent={agent.name}
197-
agentLabel={agent.label}
198-
onManagePolicies={() => setView("policies")}
199-
/>
200-
)}
201-
{tab === "logs" && <LogsTab key={agent.name} agent={agent.name} />}
202-
</section>
203-
</>
204-
) : (
205-
<div className="flex-1 grid place-items-center text-muted-foreground">
206-
{err ? <span className="text-destructive">{err}</span> : "Select an agent"}
207-
</div>
208-
)}
62+
<Outlet />
20963
</main>
21064
</div>
21165
);
21266
}
21367

214-
function RailButton({
68+
// ── Route wrappers — inject navigate-based callbacks so the page components
69+
// keep their existing prop contracts and need no router awareness. ──
70+
71+
function HomeRoute() {
72+
const { agents } = useAgents();
73+
const navigate = useNavigate();
74+
return (
75+
<HomePage
76+
agents={agents}
77+
onLaunch={(name, message) => navigate(`/agents/${encodeURIComponent(name)}`, { state: { message } })}
78+
onOpenDashboard={() => navigate(agents[0] ? `/agents/${encodeURIComponent(agents[0].name)}` : "/registry")}
79+
/>
80+
);
81+
}
82+
83+
function RegistryRoute() {
84+
const { agents, loaded, err, reload } = useAgents();
85+
const navigate = useNavigate();
86+
return (
87+
<RegistryPage
88+
agents={agents}
89+
loaded={loaded}
90+
err={err}
91+
selected={null}
92+
onOpenAgent={(name) => navigate(`/agents/${encodeURIComponent(name)}`)}
93+
onReload={reload}
94+
/>
95+
);
96+
}
97+
98+
function RailLink({
99+
to,
215100
icon: Icon,
216101
label,
217102
active,
218-
onClick,
219103
}: {
104+
to: string;
220105
icon: React.ComponentType<{ className?: string }>;
221106
label: string;
222-
active: boolean;
223-
onClick: () => void;
107+
/** Optional override; defaults to react-router's own active match. */
108+
active?: boolean;
224109
}) {
225110
return (
226-
<button
227-
onClick={onClick}
228-
className={cn(
229-
"w-full text-left rounded-md px-3 py-2 text-sm transition-colors flex items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
230-
active ? "bg-muted ring-1 ring-primary/40 text-foreground" : "hover:bg-muted/60 text-muted-foreground",
231-
)}
111+
<NavLink
112+
to={to}
113+
className={({ isActive }) =>
114+
cn(
115+
"w-full text-left rounded-md px-3 py-2 text-sm transition-colors flex items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
116+
(active ?? isActive)
117+
? "bg-muted ring-1 ring-primary/40 text-foreground"
118+
: "hover:bg-muted/60 text-muted-foreground",
119+
)
120+
}
232121
>
233122
<Icon className="h-4 w-4" />
234123
<span>{label}</span>
235-
</button>
124+
</NavLink>
236125
);
237126
}

0 commit comments

Comments
 (0)