Skip to content

Commit 8d829b8

Browse files
feat(agentos): liveChatCapable — hide chat-sandbox button for library agents
The chat-sandbox button + landing-chat input on the AgentOS SPA both trigger `POST /agents/<name>/chat-sandbox`, which proxies to a remote harness server at `${CA_BASE}/sandboxes`. For agents that run in their host process (Python `ComputerAgent` harness, etc.) there's no remote sandbox to proxy to — clicking the button gets a 502 (no :8787) or a 400 (LIBRARY_AGENT_NO_LIVE_CHAT when source.type = "library"). This patch adds UI-side gating so the button never renders for these agents: routes/agents.ts - Import `hasResolvableSource`. - Add a derived `liveChatCapable: sCap && hasResolvableSource(source)` to each item in `GET /agents` so the SPA can decide without duplicating server-side logic. api.ts - Add `liveChatCapable?: boolean` to the `Agent` interface. Optional + defaults-true everywhere a value is missing — every legacy registry doc keeps rendering the button. App.tsx - Forward `liveChatCapable={agent.liveChatCapable !== false}` to `WorkspaceTab`. WorkspaceTab.tsx - New `liveChatCapable?: boolean` prop (default `true`). - Wrap both "New chat" buttons (collapsed + expanded sidebar) in `{liveChatCapable && (...)}`. HomePage.tsx - `resolveTarget()` returns `liveChatCapable` alongside `sandboxCapable`. - When the user submits a prompt for a sandbox-capable but non-live-chat-capable agent, show a friendly inline message ("library-mode agent — invoke from your code") instead of POSTing to `/chat-sandbox` and surfacing the 400. Type-check: SPA + agentos-server both compile clean.
1 parent af47a08 commit 8d829b8

5 files changed

Lines changed: 62 additions & 11 deletions

File tree

agentos/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export default function App() {
184184
key={agent.name}
185185
agent={agent.name}
186186
sandboxCapable={agent.sandboxCapable}
187+
liveChatCapable={agent.liveChatCapable !== false}
187188
initialMessage={launchMessage}
188189
onConsumedInitial={() => setLaunchMessage(null)}
189190
/>

agentos/src/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export interface Agent {
2323
sourceUrl: string | null;
2424
model: string | null;
2525
sandboxCapable: boolean;
26+
/** True when the agent can actually spin up a live chat sandbox.
27+
* Equals `sandboxCapable && hasResolvableSource(source)`. Library-mode
28+
* agents (Python harness, etc.) have `false` so the UI hides the
29+
* "New chat" button instead of triggering a 400 on click. */
30+
liveChatCapable?: boolean;
2631
sessionCount: number;
2732
activeSandboxes: number;
2833
lastActivity: string | null;

agentos/src/components/HomePage.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,19 @@ export function HomePage({
9494

9595
// Explicit framework → its mapped agent. sandboxCapable comes from the
9696
// registry when the agent is known, else inferred (deepagents run one-shot).
97-
const resolveTarget = (): { name: string; sandboxCapable: boolean } => {
97+
// liveChatCapable additionally checks the source can be resolved — library-
98+
// mode agents (Python harness) have a non-resolvable source and so cannot
99+
// start a live chat, even though their harness is sandboxable.
100+
const resolveTarget = (): {
101+
name: string;
102+
sandboxCapable: boolean;
103+
liveChatCapable: boolean;
104+
} => {
98105
const name = selected.agent ?? agents[0]?.name ?? "gitagent";
99106
const found = agents.find((a) => a.name === name);
100-
return { name, sandboxCapable: found ? found.sandboxCapable : selected.id !== "deep-agent" };
107+
const sandboxCap = found ? found.sandboxCapable : selected.id !== "deep-agent";
108+
const liveCap = found ? found.liveChatCapable !== false : sandboxCap;
109+
return { name, sandboxCapable: sandboxCap, liveChatCapable: liveCap };
101110
};
102111

103112
const submit = async () => {
@@ -140,6 +149,24 @@ export function HomePage({
140149
let streamUrl: string;
141150
let turnSession = sessionId;
142151

152+
if (!target.liveChatCapable && target.sandboxCapable) {
153+
// Library-mode agent: sandbox-capable harness but source isn't
154+
// resolvable into a workdir (Python harness, etc.). Surface a
155+
// friendly explanation instead of POSTing to /chat-sandbox where
156+
// the server would 400 with LIBRARY_AGENT_NO_LIVE_CHAT.
157+
setMessages((cur) => [
158+
...cur,
159+
{
160+
role: "assistant",
161+
text:
162+
`${target.name} is a library-mode agent — it runs in its host ` +
163+
`process (e.g. a Python SDK). AgentOS shows its telemetry but ` +
164+
`can't start a new live chat from here. Invoke it from your code.`,
165+
},
166+
]);
167+
setBusy(false);
168+
return;
169+
}
143170
if (target.sandboxCapable) {
144171
let sb = sandboxId;
145172
if (!sb) {

agentos/src/components/WorkspaceTab.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@ import { cn } from "../lib/cn.ts";
1111
export function WorkspaceTab({
1212
agent,
1313
sandboxCapable,
14+
liveChatCapable = true,
1415
initialMessage,
1516
onConsumedInitial,
1617
}: {
1718
agent: string;
1819
sandboxCapable: boolean;
20+
// True when the agent can spin up a live chat sandbox. False for
21+
// library-mode agents (Python harness etc.) whose ``source`` doesn't
22+
// resolve into something the server can clone. Defaults to true so
23+
// existing callers and registry docs without the field keep working.
24+
liveChatCapable?: boolean;
1925
initialMessage?: string | null;
2026
onConsumedInitial?: () => void;
2127
}) {
@@ -61,9 +67,11 @@ export function WorkspaceTab({
6167
>
6268
<PanelLeftClose className="h-4 w-4 rotate-180" />
6369
</Button>
64-
<Button onClick={newChat} size="icon" title="New chat" className="h-8 w-8">
65-
<Plus className="h-4 w-4" />
66-
</Button>
70+
{liveChatCapable && (
71+
<Button onClick={newChat} size="icon" title="New chat" className="h-8 w-8">
72+
<Plus className="h-4 w-4" />
73+
</Button>
74+
)}
6775
<div className="mt-1 text-[10px] text-muted-foreground [writing-mode:vertical-rl] rotate-180 tracking-wide">
6876
{sessions.length} sessions
6977
</div>
@@ -104,10 +112,12 @@ export function WorkspaceTab({
104112
<span className="text-xs text-muted-foreground truncate min-w-0">
105113
{sessions.length} session{sessions.length !== 1 ? "s" : ""}
106114
</span>
107-
<Button variant="default" size="sm" onClick={newChat} className="ml-auto shrink-0 gap-1">
108-
<Plus className="h-3 w-3" />
109-
New
110-
</Button>
115+
{liveChatCapable && (
116+
<Button variant="default" size="sm" onClick={newChat} className="ml-auto shrink-0 gap-1">
117+
<Plus className="h-3 w-3" />
118+
New
119+
</Button>
120+
)}
111121
</div>
112122
<div className="flex-1 overflow-y-auto min-w-0">
113123
{loading && (

packages/agentos-server/src/routes/agents.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { registryColl, threadsColl, type RegistryDoc } from "../mongo.js";
1616
import { caBase } from "../upstream.js";
1717
import { caAuthHeader } from "../auth.js";
1818
import { agentLogStore } from "../stores/agent-log-store.js";
19-
import { normalizeSource, registryDocToAgentDef, sandboxCapable } from "../agent-defs.js";
19+
import { hasResolvableSource, normalizeSource, registryDocToAgentDef, sandboxCapable } from "../agent-defs.js";
2020

2121
export const agentsRouter: IRouter = Router();
2222

@@ -60,6 +60,7 @@ agentsRouter.get("/agents", async (_req, res, next) => {
6060
return t && (!acc || t > acc) ? t : acc;
6161
}, null);
6262
const { source, sourceUrl } = normalizeSource(r.source);
63+
const sCap = sandboxCapable(agent.harness);
6364
out.push({
6465
name: agent.name,
6566
label: agent.label,
@@ -70,7 +71,14 @@ agentsRouter.get("/agents", async (_req, res, next) => {
7071
origin: "registry" as const,
7172
registeredBy: r.registeredBy ?? null,
7273
lastSeen: r.lastSeen ? r.lastSeen.toISOString() : null,
73-
sandboxCapable: sandboxCapable(agent.harness),
74+
sandboxCapable: sCap,
75+
// True when this agent can actually spin up a live chat sandbox.
76+
// ``sandboxCapable`` is true for everything except deepagents;
77+
// adding the source-resolvability check hides the chat button for
78+
// library-mode (Python harness) agents whose ``source.type`` is
79+
// neither git/local nor inline-with-files. UI uses this to
80+
// conditionally render the "New chat" button.
81+
liveChatCapable: sCap && hasResolvableSource(agent.source),
7482
sessionCount: sessionIds.size,
7583
activeSandboxes: active,
7684
lastActivity: lastActivity ? lastActivity.toISOString() : null,

0 commit comments

Comments
 (0)