Skip to content
Merged
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
8 changes: 4 additions & 4 deletions agentos/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function App() {
<Route path="observability" element={<ObservabilityTab />} />
<Route path="policies" element={<PoliciesPage />} />
<Route path="evals" element={<EvalsPage />} />
<Route path="agents/:name" element={<AgentDashboard />} />
<Route path="agents/:id" element={<AgentDashboard />} />
<Route path="*" element={<Navigate to="/home" replace />} />
</Route>
</Routes>
Expand Down Expand Up @@ -77,8 +77,8 @@ function HomeRoute() {
return (
<HomePage
agents={agents}
onLaunch={(name, message) => navigate(`/agents/${encodeURIComponent(name)}`, { state: { message } })}
onOpenDashboard={() => navigate(agents[0] ? `/agents/${encodeURIComponent(agents[0].name)}` : "/registry")}
onLaunch={(agentId, message) => navigate(`/agents/${encodeURIComponent(agentId)}`, { state: { message } })}
onOpenDashboard={() => navigate(agents[0] ? `/agents/${encodeURIComponent(agents[0].id)}` : "/registry")}
/>
);
}
Expand All @@ -92,7 +92,7 @@ function RegistryRoute() {
loaded={loaded}
err={err}
selected={null}
onOpenAgent={(name) => navigate(`/agents/${encodeURIComponent(name)}`)}
onOpenAgent={(agentId) => navigate(`/agents/${encodeURIComponent(agentId)}`)}
onReload={reload}
/>
);
Expand Down
46 changes: 25 additions & 21 deletions agentos/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export type IdentitySource =
| { type: "inline"; manifest: Record<string, unknown>; files?: Record<string, string> };

export interface Agent {
/** Registry surrogate key (Mongo ObjectId, stringified). The public/API
* identifier — routes are `/agents/:id` and all agent-scoped calls pass it.
* `name` is the human label/FK; never use it as the addressing key. */
id: string;
name: string;
label: string;
harness: string;
Expand Down Expand Up @@ -207,7 +211,7 @@ export interface Schedule {
lastResult?: string | null;
}
export interface NewSchedule {
agentName: string;
agentId: string;
prompt: string;
kind: "interval" | "daily";
intervalMinutes?: number;
Expand Down Expand Up @@ -336,35 +340,35 @@ export interface EvalRun {
export const api = {
agents: () => getJSON<{ agents: Agent[] }>("/agents").then((d) => d.agents),
registerAgent: (input: RegisterAgentInput) =>
postJSON<{ ok: boolean; name: string }>("/agents/register", input),
unregisterAgent: (name: string) =>
reqJSON<DeleteResult>("DELETE", `/agents/${encodeURIComponent(name)}`),
patchAgent: (name: string, fields: Partial<Omit<RegisterAgentInput, "name">>) =>
reqJSON<{ ok: boolean }>("PATCH", `/agents/${encodeURIComponent(name)}`, fields),
logs: (bot?: string, limit = 100) =>
getJSON<{ logs: LogEntry[] }>(`/logs?limit=${limit}${bot ? `&bot=${encodeURIComponent(bot)}` : ""}`).then((d) => d.logs),
sessions: (bot?: string, limit = 100) =>
getJSON<{ sessions: SessionSummary[] }>(`/sessions?limit=${limit}${bot ? `&bot=${encodeURIComponent(bot)}` : ""}`).then((d) => d.sessions),
postJSON<{ ok: boolean; id: string; name: string }>("/agents/register", input),
unregisterAgent: (agentId: string) =>
reqJSON<DeleteResult>("DELETE", `/agents/${encodeURIComponent(agentId)}`),
patchAgent: (agentId: string, fields: Partial<Omit<RegisterAgentInput, "name">>) =>
reqJSON<{ ok: boolean }>("PATCH", `/agents/${encodeURIComponent(agentId)}`, fields),
logs: (agentId?: string, limit = 100) =>
getJSON<{ logs: LogEntry[] }>(`/logs?limit=${limit}${agentId ? `&agentId=${encodeURIComponent(agentId)}` : ""}`).then((d) => d.logs),
sessions: (agentId?: string, limit = 100) =>
getJSON<{ sessions: SessionSummary[] }>(`/sessions?limit=${limit}${agentId ? `&agentId=${encodeURIComponent(agentId)}` : ""}`).then((d) => d.sessions),
session: (id: string) => getJSON<SessionDetail>(`/sessions/${encodeURIComponent(id)}`),
deleteSession: (id: string, bot?: string) =>
deleteSession: (id: string, agentId?: string) =>
reqJSON<DeleteResult>(
"DELETE",
`/sessions/${encodeURIComponent(id)}${bot ? `?bot=${encodeURIComponent(bot)}` : ""}`,
`/sessions/${encodeURIComponent(id)}${agentId ? `?agentId=${encodeURIComponent(agentId)}` : ""}`,
),
chatSandbox: (agent: string, opts?: { sessionId?: string; forceNew?: boolean }) =>
chatSandbox: (agentId: string, opts?: { sessionId?: string; forceNew?: boolean }) =>
postJSON<{ sandboxId: string; sessionId: string; bot: string }>(
`/agents/${encodeURIComponent(agent)}/chat-sandbox`,
`/agents/${encodeURIComponent(agentId)}/chat-sandbox`,
opts?.sessionId ? { sessionId: opts.sessionId } : opts?.forceNew ? { forceNew: true } : {},
),
logWebTurn: (entry: { bot: string; sessionId: string; query: string; reply: string; ok: boolean }) =>
postJSON<{ ok: boolean }>("/logs", { ...entry, requester: "web" }),
// SSE chat — caller reads the stream. Path goes through the same /api proxy.
chatStreamUrl: (sandboxId: string) => `/api/sandboxes/${encodeURIComponent(sandboxId)}/chat`,
// SSE one-shot run (deepagents). Server builds the /run body from {message}.
runStreamUrl: (agent: string) => `/api/agents/${encodeURIComponent(agent)}/run`,
runStreamUrl: (agentId: string) => `/api/agents/${encodeURIComponent(agentId)}/run`,
// Schedules
schedules: (agent?: string) =>
getJSON<{ schedules: Schedule[] }>(`/schedules${agent ? `?agent=${encodeURIComponent(agent)}` : ""}`).then((d) => d.schedules),
schedules: (agentId?: string) =>
getJSON<{ schedules: Schedule[] }>(`/schedules${agentId ? `?agentId=${encodeURIComponent(agentId)}` : ""}`).then((d) => d.schedules),
createSchedule: (s: NewSchedule) => postJSON<{ schedule: Schedule }>("/schedules", s).then((d) => d.schedule),
updateSchedule: (id: string, fields: Partial<NewSchedule> & { enabled?: boolean }) =>
reqJSON<{ schedule: Schedule }>("PATCH", `/schedules/${encodeURIComponent(id)}`, fields).then((d) => d.schedule),
Expand All @@ -379,10 +383,10 @@ export const api = {
deletePolicy: (id: string) =>
reqJSON<{ success?: boolean }>("DELETE", `/policies/${encodeURIComponent(id)}`),
// Per-agent policy binding (Mongo, ours).
getAgentPolicy: (agent: string) =>
getJSON<{ binding: AgentPolicyBinding | null }>(`/agents/${encodeURIComponent(agent)}/policy`).then((d) => d.binding),
setAgentPolicy: (agent: string, policyId: string | null) =>
reqJSON<{ binding: AgentPolicyBinding | null }>("PUT", `/agents/${encodeURIComponent(agent)}/policy`, { policy_id: policyId }).then((d) => d.binding),
getAgentPolicy: (agentId: string) =>
getJSON<{ binding: AgentPolicyBinding | null }>(`/agents/${encodeURIComponent(agentId)}/policy`).then((d) => d.binding),
setAgentPolicy: (agentId: string, policyId: string | null) =>
reqJSON<{ binding: AgentPolicyBinding | null }>("PUT", `/agents/${encodeURIComponent(agentId)}/policy`, { policy_id: policyId }).then((d) => d.binding),
// OPA rego policies (managed by SRS, referenced from RAI policies' opa_guardrail).
opaPolicies: () => getJSON<{ policies: OPAPolicyDoc[] } | OPAPolicyDoc[]>("/opa-policies").then((d) => (Array.isArray(d) ? d : d.policies)),
opaPolicy: (id: string) => getJSON<OPAPolicyDoc>(`/opa-policies/${encodeURIComponent(id)}`),
Expand Down
19 changes: 10 additions & 9 deletions agentos/src/components/AgentDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function TypeBadge({ agent, className = "" }: { agent: Agent; className?: string
}

export function AgentDashboard() {
const { name } = useParams<{ name: string }>();
const { id } = useParams<{ id: string }>();
const { agents, loaded } = useAgents();
const navigate = useNavigate();
const location = useLocation();
Expand All @@ -51,7 +51,7 @@ export function AgentDashboard() {
(location.state as { message?: string } | null)?.message ?? null,
);

const agent = agents.find((a) => a.name === name) ?? null;
const agent = agents.find((a) => a.id === id) ?? null;

if (!loaded) {
return <div className="flex-1 grid place-items-center text-muted-foreground">Loading…</div>;
Expand All @@ -60,7 +60,7 @@ export function AgentDashboard() {
return (
<div className="flex-1 grid place-items-center text-muted-foreground">
<div className="text-center space-y-2">
<div>Unknown agent <span className="font-mono">{name}</span>.</div>
<div>Unknown agent <span className="font-mono">{id}</span>.</div>
<Link to="/registry" className="text-primary hover:underline">Back to registry</Link>
</div>
</div>
Expand Down Expand Up @@ -98,24 +98,25 @@ export function AgentDashboard() {
<section className="flex-1 min-h-0">
{tab === "chat" && (
<WorkspaceTab
key={agent.name}
agent={agent.name}
key={agent.id}
agentId={agent.id}
agentName={agent.name}
sandboxCapable={agent.sandboxCapable}
liveChatCapable={agent.liveChatCapable !== false}
initialMessage={launchMessage}
onConsumedInitial={() => setLaunchMessage(null)}
/>
)}
{tab === "schedules" && <SchedulesTab key={agent.name} agent={agent.name} agentLabel={agent.label} />}
{tab === "schedules" && <SchedulesTab key={agent.id} agentId={agent.id} agentLabel={agent.label} />}
{tab === "policy" && (
<PolicyTab
key={agent.name}
agent={agent.name}
key={agent.id}
agentId={agent.id}
agentLabel={agent.label}
onManagePolicies={() => navigate("/policies")}
/>
)}
{tab === "logs" && <LogsTab key={agent.name} agent={agent.name} />}
{tab === "logs" && <LogsTab key={agent.id} agentId={agent.id} />}
</section>
</>
);
Expand Down
30 changes: 18 additions & 12 deletions agentos/src/components/ChatTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,19 @@ const CONTINUE_PROMPT =
"Continue from where you left off — keep building until the project is complete, then summarize what you built and give me the deploy URL.";

export function ChatTab({
agent,
agentId,
agentName,
sandboxCapable,
resumeSessionId,
onConsumedResume,
initialMessage,
onConsumedInitial,
onSessionStarted,
}: {
agent: string;
/** Registry ObjectId — used to address the agent in API calls. */
agentId: string;
/** Human name — used for display + the `bot` field on logged turns. */
agentName: string;
sandboxCapable: boolean;
resumeSessionId: string | null;
onConsumedResume: () => void;
Expand Down Expand Up @@ -61,7 +65,7 @@ export function ChatTab({
setSessionId(null);
setMsgs([]);
setErr(null);
}, [agent]);
}, [agentId]);

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

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

setBusy(false);
api.logWebTurn({
bot: agent,
sessionId: sessionId ?? `oneshot-${agent}`,
bot: agentName,
sessionId: sessionId ?? `oneshot-${agentName}`,
query: text,
reply: finalText,
ok: true,
Expand Down Expand Up @@ -219,7 +225,7 @@ export function ChatTab({
)}
{msgs.length === 0 && !err && (
<div className="h-full grid place-items-center text-muted-foreground text-sm">
Talk to {agent}. It runs with its configured identity and tools.
Talk to {agentName}. It runs with its configured identity and tools.
</div>
)}
{msgs.map((m, i) => {
Expand Down Expand Up @@ -248,7 +254,7 @@ export function ChatTab({
send();
}
}}
placeholder={`Message ${agent}… (Enter to send, Shift+Enter for newline)`}
placeholder={`Message ${agentName}… (Enter to send, Shift+Enter for newline)`}
rows={2}
className="flex-1 resize-none rounded-xl bg-card px-3.5 py-2.5"
/>
Expand Down
12 changes: 9 additions & 3 deletions agentos/src/components/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export function HomePage({
// mode agents (Python harness) have a non-resolvable source and so cannot
// start a live chat, even though their harness is sandboxable.
const resolveTarget = (): {
id: string | null;
name: string;
sandboxCapable: boolean;
liveChatCapable: boolean;
Expand All @@ -106,7 +107,7 @@ export function HomePage({
const found = agents.find((a) => a.name === name);
const sandboxCap = found ? found.sandboxCapable : selected.id !== "deep-agent";
const liveCap = found ? found.liveChatCapable !== false : sandboxCap;
return { name, sandboxCapable: sandboxCap, liveChatCapable: liveCap };
return { id: found?.id ?? null, name, sandboxCapable: sandboxCap, liveChatCapable: liveCap };
};

const submit = async () => {
Expand Down Expand Up @@ -167,10 +168,15 @@ export function HomePage({
setBusy(false);
return;
}
if (!target.id) {
setAssistant(`⚠️ "${target.name}" isn't a registered agent yet — register it first.`);
setBusy(false);
return;
}
if (target.sandboxCapable) {
let sb = sandboxId;
if (!sb) {
const created = await api.chatSandbox(target.name);
const created = await api.chatSandbox(target.id);
sb = created.sandboxId;
turnSession = created.sessionId;
setSandboxId(created.sandboxId);
Expand All @@ -179,7 +185,7 @@ export function HomePage({
streamUrl = api.chatStreamUrl(sb);
} else {
// One-shot agents (deepagents): no warm sandbox, no cross-turn memory.
streamUrl = api.runStreamUrl(target.name);
streamUrl = api.runStreamUrl(target.id);
}

await streamChat(streamUrl, msg, {
Expand Down
8 changes: 4 additions & 4 deletions agentos/src/components/LogsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const SOURCE_VARIANT: Record<LogEntry["source"], "default" | "secondary" | "warn
schedule: "warning",
};

export function LogsTab({ agent }: { agent: string }) {
export function LogsTab({ agentId }: { agentId: string }) {
const [logs, setLogs] = useState<LogEntry[]>([]);
const [loading, setLoading] = useState(true);
const [expanded, setExpanded] = useState<string | null>(null);
Expand All @@ -25,7 +25,7 @@ export function LogsTab({ agent }: { agent: string }) {
if (!quiet) setLoading(true);
else setRefreshing(true);
api
.logs(agent, 200)
.logs(agentId, 200)
.then((l) => {
setLogs(l);
setErr(null);
Expand All @@ -40,13 +40,13 @@ export function LogsTab({ agent }: { agent: string }) {
useEffect(() => {
load();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [agent]);
}, [agentId]);

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

const shown = filter === "schedule" ? logs.filter((l) => l.source === "schedule") : logs;
const scheduleCount = logs.filter((l) => l.source === "schedule").length;
Expand Down
10 changes: 5 additions & 5 deletions agentos/src/components/PolicyTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { api, type PolicyDoc, type AgentPolicyBinding } from "../api.ts";
* every chat sandbox the agent boots (see runtime SrsPolicyDecider).
*/
export function PolicyTab({
agent,
agentId,
agentLabel,
onManagePolicies,
}: {
agent: string;
agentId: string;
agentLabel: string;
onManagePolicies: () => void;
}) {
Expand All @@ -25,7 +25,7 @@ export function PolicyTab({
setLoading(true);
setErr(null);
try {
const [pols, b] = await Promise.all([api.policies(), api.getAgentPolicy(agent)]);
const [pols, b] = await Promise.all([api.policies(), api.getAgentPolicy(agentId)]);
setPolicies(pols);
setBinding(b);
} catch (e) {
Expand All @@ -36,12 +36,12 @@ export function PolicyTab({
};
useEffect(() => {
void load();
}, [agent]);
}, [agentId]);

const attach = async (policyId: string | null) => {
setSaving(true);
try {
const b = await api.setAgentPolicy(agent, policyId);
const b = await api.setAgentPolicy(agentId, policyId);
setBinding(b);
} catch (e) {
setErr(String(e));
Expand Down
Loading
Loading