Skip to content

Commit f9b9111

Browse files
Merge pull request #24 from open-gitagent/feat/agentos-overhaul
feat(agentos): registry ObjectId addressing, cascade delete, session-…
2 parents 65c4afd + bf783c5 commit f9b9111

26 files changed

Lines changed: 115478 additions & 103779 deletions

‎agentos/src/App.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function App() {
2020
<Route path="observability" element={<ObservabilityTab />} />
2121
<Route path="policies" element={<PoliciesPage />} />
2222
<Route path="evals" element={<EvalsPage />} />
23-
<Route path="agents/:name" element={<AgentDashboard />} />
23+
<Route path="agents/:id" element={<AgentDashboard />} />
2424
<Route path="*" element={<Navigate to="/home" replace />} />
2525
</Route>
2626
</Routes>
@@ -77,8 +77,8 @@ function HomeRoute() {
7777
return (
7878
<HomePage
7979
agents={agents}
80-
onLaunch={(name, message) => navigate(`/agents/${encodeURIComponent(name)}`, { state: { message } })}
81-
onOpenDashboard={() => navigate(agents[0] ? `/agents/${encodeURIComponent(agents[0].name)}` : "/registry")}
80+
onLaunch={(agentId, message) => navigate(`/agents/${encodeURIComponent(agentId)}`, { state: { message } })}
81+
onOpenDashboard={() => navigate(agents[0] ? `/agents/${encodeURIComponent(agents[0].id)}` : "/registry")}
8282
/>
8383
);
8484
}
@@ -92,7 +92,7 @@ function RegistryRoute() {
9292
loaded={loaded}
9393
err={err}
9494
selected={null}
95-
onOpenAgent={(name) => navigate(`/agents/${encodeURIComponent(name)}`)}
95+
onOpenAgent={(agentId) => navigate(`/agents/${encodeURIComponent(agentId)}`)}
9696
onReload={reload}
9797
/>
9898
);

‎agentos/src/api.ts‎

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export type IdentitySource =
1010
| { type: "inline"; manifest: Record<string, unknown>; files?: Record<string, string> };
1111

1212
export interface Agent {
13+
/** Registry surrogate key (Mongo ObjectId, stringified). The public/API
14+
* identifier — routes are `/agents/:id` and all agent-scoped calls pass it.
15+
* `name` is the human label/FK; never use it as the addressing key. */
16+
id: string;
1317
name: string;
1418
label: string;
1519
harness: string;
@@ -207,7 +211,7 @@ export interface Schedule {
207211
lastResult?: string | null;
208212
}
209213
export interface NewSchedule {
210-
agentName: string;
214+
agentId: string;
211215
prompt: string;
212216
kind: "interval" | "daily";
213217
intervalMinutes?: number;
@@ -336,35 +340,35 @@ export interface EvalRun {
336340
export const api = {
337341
agents: () => getJSON<{ agents: Agent[] }>("/agents").then((d) => d.agents),
338342
registerAgent: (input: RegisterAgentInput) =>
339-
postJSON<{ ok: boolean; name: string }>("/agents/register", input),
340-
unregisterAgent: (name: string) =>
341-
reqJSON<DeleteResult>("DELETE", `/agents/${encodeURIComponent(name)}`),
342-
patchAgent: (name: string, fields: Partial<Omit<RegisterAgentInput, "name">>) =>
343-
reqJSON<{ ok: boolean }>("PATCH", `/agents/${encodeURIComponent(name)}`, fields),
344-
logs: (bot?: string, limit = 100) =>
345-
getJSON<{ logs: LogEntry[] }>(`/logs?limit=${limit}${bot ? `&bot=${encodeURIComponent(bot)}` : ""}`).then((d) => d.logs),
346-
sessions: (bot?: string, limit = 100) =>
347-
getJSON<{ sessions: SessionSummary[] }>(`/sessions?limit=${limit}${bot ? `&bot=${encodeURIComponent(bot)}` : ""}`).then((d) => d.sessions),
343+
postJSON<{ ok: boolean; id: string; name: string }>("/agents/register", input),
344+
unregisterAgent: (agentId: string) =>
345+
reqJSON<DeleteResult>("DELETE", `/agents/${encodeURIComponent(agentId)}`),
346+
patchAgent: (agentId: string, fields: Partial<Omit<RegisterAgentInput, "name">>) =>
347+
reqJSON<{ ok: boolean }>("PATCH", `/agents/${encodeURIComponent(agentId)}`, fields),
348+
logs: (agentId?: string, limit = 100) =>
349+
getJSON<{ logs: LogEntry[] }>(`/logs?limit=${limit}${agentId ? `&agentId=${encodeURIComponent(agentId)}` : ""}`).then((d) => d.logs),
350+
sessions: (agentId?: string, limit = 100) =>
351+
getJSON<{ sessions: SessionSummary[] }>(`/sessions?limit=${limit}${agentId ? `&agentId=${encodeURIComponent(agentId)}` : ""}`).then((d) => d.sessions),
348352
session: (id: string) => getJSON<SessionDetail>(`/sessions/${encodeURIComponent(id)}`),
349-
deleteSession: (id: string, bot?: string) =>
353+
deleteSession: (id: string, agentId?: string) =>
350354
reqJSON<DeleteResult>(
351355
"DELETE",
352-
`/sessions/${encodeURIComponent(id)}${bot ? `?bot=${encodeURIComponent(bot)}` : ""}`,
356+
`/sessions/${encodeURIComponent(id)}${agentId ? `?agentId=${encodeURIComponent(agentId)}` : ""}`,
353357
),
354-
chatSandbox: (agent: string, opts?: { sessionId?: string; forceNew?: boolean }) =>
358+
chatSandbox: (agentId: string, opts?: { sessionId?: string; forceNew?: boolean }) =>
355359
postJSON<{ sandboxId: string; sessionId: string; bot: string }>(
356-
`/agents/${encodeURIComponent(agent)}/chat-sandbox`,
360+
`/agents/${encodeURIComponent(agentId)}/chat-sandbox`,
357361
opts?.sessionId ? { sessionId: opts.sessionId } : opts?.forceNew ? { forceNew: true } : {},
358362
),
359363
logWebTurn: (entry: { bot: string; sessionId: string; query: string; reply: string; ok: boolean }) =>
360364
postJSON<{ ok: boolean }>("/logs", { ...entry, requester: "web" }),
361365
// SSE chat — caller reads the stream. Path goes through the same /api proxy.
362366
chatStreamUrl: (sandboxId: string) => `/api/sandboxes/${encodeURIComponent(sandboxId)}/chat`,
363367
// SSE one-shot run (deepagents). Server builds the /run body from {message}.
364-
runStreamUrl: (agent: string) => `/api/agents/${encodeURIComponent(agent)}/run`,
368+
runStreamUrl: (agentId: string) => `/api/agents/${encodeURIComponent(agentId)}/run`,
365369
// Schedules
366-
schedules: (agent?: string) =>
367-
getJSON<{ schedules: Schedule[] }>(`/schedules${agent ? `?agent=${encodeURIComponent(agent)}` : ""}`).then((d) => d.schedules),
370+
schedules: (agentId?: string) =>
371+
getJSON<{ schedules: Schedule[] }>(`/schedules${agentId ? `?agentId=${encodeURIComponent(agentId)}` : ""}`).then((d) => d.schedules),
368372
createSchedule: (s: NewSchedule) => postJSON<{ schedule: Schedule }>("/schedules", s).then((d) => d.schedule),
369373
updateSchedule: (id: string, fields: Partial<NewSchedule> & { enabled?: boolean }) =>
370374
reqJSON<{ schedule: Schedule }>("PATCH", `/schedules/${encodeURIComponent(id)}`, fields).then((d) => d.schedule),
@@ -379,10 +383,10 @@ export const api = {
379383
deletePolicy: (id: string) =>
380384
reqJSON<{ success?: boolean }>("DELETE", `/policies/${encodeURIComponent(id)}`),
381385
// Per-agent policy binding (Mongo, ours).
382-
getAgentPolicy: (agent: string) =>
383-
getJSON<{ binding: AgentPolicyBinding | null }>(`/agents/${encodeURIComponent(agent)}/policy`).then((d) => d.binding),
384-
setAgentPolicy: (agent: string, policyId: string | null) =>
385-
reqJSON<{ binding: AgentPolicyBinding | null }>("PUT", `/agents/${encodeURIComponent(agent)}/policy`, { policy_id: policyId }).then((d) => d.binding),
386+
getAgentPolicy: (agentId: string) =>
387+
getJSON<{ binding: AgentPolicyBinding | null }>(`/agents/${encodeURIComponent(agentId)}/policy`).then((d) => d.binding),
388+
setAgentPolicy: (agentId: string, policyId: string | null) =>
389+
reqJSON<{ binding: AgentPolicyBinding | null }>("PUT", `/agents/${encodeURIComponent(agentId)}/policy`, { policy_id: policyId }).then((d) => d.binding),
386390
// OPA rego policies (managed by SRS, referenced from RAI policies' opa_guardrail).
387391
opaPolicies: () => getJSON<{ policies: OPAPolicyDoc[] } | OPAPolicyDoc[]>("/opa-policies").then((d) => (Array.isArray(d) ? d : d.policies)),
388392
opaPolicy: (id: string) => getJSON<OPAPolicyDoc>(`/opa-policies/${encodeURIComponent(id)}`),

‎agentos/src/components/AgentDashboard.tsx‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function TypeBadge({ agent, className = "" }: { agent: Agent; className?: string
4040
}
4141

4242
export function AgentDashboard() {
43-
const { name } = useParams<{ name: string }>();
43+
const { id } = useParams<{ id: string }>();
4444
const { agents, loaded } = useAgents();
4545
const navigate = useNavigate();
4646
const location = useLocation();
@@ -51,7 +51,7 @@ export function AgentDashboard() {
5151
(location.state as { message?: string } | null)?.message ?? null,
5252
);
5353

54-
const agent = agents.find((a) => a.name === name) ?? null;
54+
const agent = agents.find((a) => a.id === id) ?? null;
5555

5656
if (!loaded) {
5757
return <div className="flex-1 grid place-items-center text-muted-foreground">Loading…</div>;
@@ -60,7 +60,7 @@ export function AgentDashboard() {
6060
return (
6161
<div className="flex-1 grid place-items-center text-muted-foreground">
6262
<div className="text-center space-y-2">
63-
<div>Unknown agent <span className="font-mono">{name}</span>.</div>
63+
<div>Unknown agent <span className="font-mono">{id}</span>.</div>
6464
<Link to="/registry" className="text-primary hover:underline">Back to registry</Link>
6565
</div>
6666
</div>
@@ -98,24 +98,25 @@ export function AgentDashboard() {
9898
<section className="flex-1 min-h-0">
9999
{tab === "chat" && (
100100
<WorkspaceTab
101-
key={agent.name}
102-
agent={agent.name}
101+
key={agent.id}
102+
agentId={agent.id}
103+
agentName={agent.name}
103104
sandboxCapable={agent.sandboxCapable}
104105
liveChatCapable={agent.liveChatCapable !== false}
105106
initialMessage={launchMessage}
106107
onConsumedInitial={() => setLaunchMessage(null)}
107108
/>
108109
)}
109-
{tab === "schedules" && <SchedulesTab key={agent.name} agent={agent.name} agentLabel={agent.label} />}
110+
{tab === "schedules" && <SchedulesTab key={agent.id} agentId={agent.id} agentLabel={agent.label} />}
110111
{tab === "policy" && (
111112
<PolicyTab
112-
key={agent.name}
113-
agent={agent.name}
113+
key={agent.id}
114+
agentId={agent.id}
114115
agentLabel={agent.label}
115116
onManagePolicies={() => navigate("/policies")}
116117
/>
117118
)}
118-
{tab === "logs" && <LogsTab key={agent.name} agent={agent.name} />}
119+
{tab === "logs" && <LogsTab key={agent.id} agentId={agent.id} />}
119120
</section>
120121
</>
121122
);

‎agentos/src/components/ChatTab.tsx‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,19 @@ const CONTINUE_PROMPT =
2525
"Continue from where you left off — keep building until the project is complete, then summarize what you built and give me the deploy URL.";
2626

2727
export function ChatTab({
28-
agent,
28+
agentId,
29+
agentName,
2930
sandboxCapable,
3031
resumeSessionId,
3132
onConsumedResume,
3233
initialMessage,
3334
onConsumedInitial,
3435
onSessionStarted,
3536
}: {
36-
agent: string;
37+
/** Registry ObjectId — used to address the agent in API calls. */
38+
agentId: string;
39+
/** Human name — used for display + the `bot` field on logged turns. */
40+
agentName: string;
3741
sandboxCapable: boolean;
3842
resumeSessionId: string | null;
3943
onConsumedResume: () => void;
@@ -61,7 +65,7 @@ export function ChatTab({
6165
setSessionId(null);
6266
setMsgs([]);
6367
setErr(null);
64-
}, [agent]);
68+
}, [agentId]);
6569

6670
useEffect(() => {
6771
if (!resumeSessionId) return;
@@ -97,12 +101,14 @@ export function ChatTab({
97101
try {
98102
// No `resume` → this is a fresh "New chat": force a brand-new session so
99103
// the server doesn't silently resume the agent's pinned (last) session.
100-
const r = await api.chatSandbox(agent, resume ? { sessionId: resume } : { forceNew: true });
104+
const r = await api.chatSandbox(agentId, resume ? { sessionId: resume } : { forceNew: true });
101105
setSandboxId(r.sandboxId);
102106
setSessionId(r.sessionId);
103-
// Tell the parent a session now exists so the sidebar can show/highlight
104-
// it — a freshly-created chat has no row in the list until this fires.
105-
onSessionStarted?.(r.sessionId);
107+
// Only notify the parent for a genuinely NEW session — it adds the row to
108+
// the sidebar. Resuming an existing session must NOT fire this: the row
109+
// already exists and re-fetching the list on every click is the bug that
110+
// refetched sessions repeatedly.
111+
if (!resume) onSessionStarted?.(r.sessionId);
106112
return r.sandboxId;
107113
} catch (e) {
108114
setErr(String(e));
@@ -125,7 +131,7 @@ export function ChatTab({
125131
}
126132
streamUrl = api.chatStreamUrl(curSandbox);
127133
} else {
128-
streamUrl = api.runStreamUrl(agent);
134+
streamUrl = api.runStreamUrl(agentId);
129135
}
130136

131137
if (textArg === undefined) setInput("");
@@ -173,8 +179,8 @@ export function ChatTab({
173179

174180
setBusy(false);
175181
api.logWebTurn({
176-
bot: agent,
177-
sessionId: sessionId ?? `oneshot-${agent}`,
182+
bot: agentName,
183+
sessionId: sessionId ?? `oneshot-${agentName}`,
178184
query: text,
179185
reply: finalText,
180186
ok: true,
@@ -219,7 +225,7 @@ export function ChatTab({
219225
)}
220226
{msgs.length === 0 && !err && (
221227
<div className="h-full grid place-items-center text-muted-foreground text-sm">
222-
Talk to {agent}. It runs with its configured identity and tools.
228+
Talk to {agentName}. It runs with its configured identity and tools.
223229
</div>
224230
)}
225231
{msgs.map((m, i) => {
@@ -248,7 +254,7 @@ export function ChatTab({
248254
send();
249255
}
250256
}}
251-
placeholder={`Message ${agent}… (Enter to send, Shift+Enter for newline)`}
257+
placeholder={`Message ${agentName}… (Enter to send, Shift+Enter for newline)`}
252258
rows={2}
253259
className="flex-1 resize-none rounded-xl bg-card px-3.5 py-2.5"
254260
/>

‎agentos/src/components/HomePage.tsx‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export function HomePage({
9898
// mode agents (Python harness) have a non-resolvable source and so cannot
9999
// start a live chat, even though their harness is sandboxable.
100100
const resolveTarget = (): {
101+
id: string | null;
101102
name: string;
102103
sandboxCapable: boolean;
103104
liveChatCapable: boolean;
@@ -106,7 +107,7 @@ export function HomePage({
106107
const found = agents.find((a) => a.name === name);
107108
const sandboxCap = found ? found.sandboxCapable : selected.id !== "deep-agent";
108109
const liveCap = found ? found.liveChatCapable !== false : sandboxCap;
109-
return { name, sandboxCapable: sandboxCap, liveChatCapable: liveCap };
110+
return { id: found?.id ?? null, name, sandboxCapable: sandboxCap, liveChatCapable: liveCap };
110111
};
111112

112113
const submit = async () => {
@@ -167,10 +168,15 @@ export function HomePage({
167168
setBusy(false);
168169
return;
169170
}
171+
if (!target.id) {
172+
setAssistant(`⚠️ "${target.name}" isn't a registered agent yet — register it first.`);
173+
setBusy(false);
174+
return;
175+
}
170176
if (target.sandboxCapable) {
171177
let sb = sandboxId;
172178
if (!sb) {
173-
const created = await api.chatSandbox(target.name);
179+
const created = await api.chatSandbox(target.id);
174180
sb = created.sandboxId;
175181
turnSession = created.sessionId;
176182
setSandboxId(created.sandboxId);
@@ -179,7 +185,7 @@ export function HomePage({
179185
streamUrl = api.chatStreamUrl(sb);
180186
} else {
181187
// One-shot agents (deepagents): no warm sandbox, no cross-turn memory.
182-
streamUrl = api.runStreamUrl(target.name);
188+
streamUrl = api.runStreamUrl(target.id);
183189
}
184190

185191
await streamChat(streamUrl, msg, {

‎agentos/src/components/LogsTab.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const SOURCE_VARIANT: Record<LogEntry["source"], "default" | "secondary" | "warn
1313
schedule: "warning",
1414
};
1515

16-
export function LogsTab({ agent }: { agent: string }) {
16+
export function LogsTab({ agentId }: { agentId: string }) {
1717
const [logs, setLogs] = useState<LogEntry[]>([]);
1818
const [loading, setLoading] = useState(true);
1919
const [expanded, setExpanded] = useState<string | null>(null);
@@ -25,7 +25,7 @@ export function LogsTab({ agent }: { agent: string }) {
2525
if (!quiet) setLoading(true);
2626
else setRefreshing(true);
2727
api
28-
.logs(agent, 200)
28+
.logs(agentId, 200)
2929
.then((l) => {
3030
setLogs(l);
3131
setErr(null);
@@ -40,13 +40,13 @@ export function LogsTab({ agent }: { agent: string }) {
4040
useEffect(() => {
4141
load();
4242
// eslint-disable-next-line react-hooks/exhaustive-deps
43-
}, [agent]);
43+
}, [agentId]);
4444

4545
useEffect(() => {
4646
const t = setInterval(() => load(true), 15_000);
4747
return () => clearInterval(t);
4848
// eslint-disable-next-line react-hooks/exhaustive-deps
49-
}, [agent]);
49+
}, [agentId]);
5050

5151
const shown = filter === "schedule" ? logs.filter((l) => l.source === "schedule") : logs;
5252
const scheduleCount = logs.filter((l) => l.source === "schedule").length;

‎agentos/src/components/PolicyTab.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import { api, type PolicyDoc, type AgentPolicyBinding } from "../api.ts";
77
* every chat sandbox the agent boots (see runtime SrsPolicyDecider).
88
*/
99
export function PolicyTab({
10-
agent,
10+
agentId,
1111
agentLabel,
1212
onManagePolicies,
1313
}: {
14-
agent: string;
14+
agentId: string;
1515
agentLabel: string;
1616
onManagePolicies: () => void;
1717
}) {
@@ -25,7 +25,7 @@ export function PolicyTab({
2525
setLoading(true);
2626
setErr(null);
2727
try {
28-
const [pols, b] = await Promise.all([api.policies(), api.getAgentPolicy(agent)]);
28+
const [pols, b] = await Promise.all([api.policies(), api.getAgentPolicy(agentId)]);
2929
setPolicies(pols);
3030
setBinding(b);
3131
} catch (e) {
@@ -36,12 +36,12 @@ export function PolicyTab({
3636
};
3737
useEffect(() => {
3838
void load();
39-
}, [agent]);
39+
}, [agentId]);
4040

4141
const attach = async (policyId: string | null) => {
4242
setSaving(true);
4343
try {
44-
const b = await api.setAgentPolicy(agent, policyId);
44+
const b = await api.setAgentPolicy(agentId, policyId);
4545
setBinding(b);
4646
} catch (e) {
4747
setErr(String(e));

0 commit comments

Comments
 (0)