-
Notifications
You must be signed in to change notification settings - Fork 344
Description
(via cursor/opus, not sure we should do it yet)
Summary
The current useAgent hook requires awkward workarounds when you need a conditional connection (e.g., connect to a room agent only when the user has selected a room). Developers must provide placeholder names and add redundant null checks throughout their code.
Current Behavior
When you want to conditionally connect to an agent, the current pattern looks like this:
const [currentRoom, setCurrentRoom] = useState<string | null>(null);
const room = useAgent<RoomAgent, RoomState>({
agent: "room-agent",
name: currentRoom || "unused", // ← Must provide a name even when disabled
enabled: !!currentRoom, // ← Controls whether to actually connect
onOpen: async () => {
if (currentRoom) { // ← Must check again inside callback
addLog("info", "room_connected", currentRoom);
await room.call("join", [username]);
}
},
onClose: () => {
if (currentRoom) { // ← Must check again
addLog("info", "room_disconnected");
}
},
onMessage: (message) => {
if (currentRoom) { // ← Must check again
// handle message
}
}
});Problems
1. Fake/placeholder name required
Even when enabled: false, you must provide a valid name prop. This leads to patterns like:
name: currentRoom || "unused"
name: selectedAgent ?? "placeholder"
name: roomId || "no-room"These placeholder strings are confusing and could accidentally be used if there's a bug in the enabled logic.
2. Repeated null checks in callbacks
Every callback (onOpen, onClose, onMessage, onStateUpdate) needs to re-check the condition because TypeScript doesn't know the callbacks won't fire when disabled:
onOpen: () => {
if (currentRoom) { // Redundant but necessary
// ...
}
}3. Confusing return type
The hook always returns an agent object with methods like call(), setState(), etc. When enabled: false:
- What happens if you call
room.call("someMethod")? - Is it a no-op? Does it throw? Does it queue?
This ambiguity leads to defensive coding.
4. No TypeScript help
TypeScript can't narrow the type based on enabled. You get the same return type whether the agent is connected or not.
Real-world use case
From the playground's ChatRoomsDemo.tsx:
// User browses a list of rooms (connected to lobby agent)
// User clicks a room → now need to connect to that specific room agent
// User leaves room → disconnect from room agent
const [currentRoom, setCurrentRoom] = useState<string | null>(null);
// This connection should only exist when currentRoom is set
const room = useAgent({
agent: "room-agent",
name: currentRoom || "unused", // Awkward
enabled: !!currentRoom,
// ...
});Proposed Solutions
Option A: Allow name to be undefined
const room = useAgent({
agent: "room-agent",
name: currentRoom, // undefined = don't connect, no placeholder needed
});
// Hook returns null or a "disconnected" state when name is undefined
if (!room.connected) {
return <div>Select a room</div>;
}
// TypeScript now knows room is connected
await room.call("join", [username]);Option B: Separate useOptionalAgent hook
const room = useOptionalAgent({
agent: "room-agent",
name: currentRoom, // null/undefined = returns null
});
if (!room) {
return <div>Select a room</div>;
}
// TypeScript knows room is non-null and connected
await room.call("join", [username]);Option C: Discriminated union return type
const room = useAgent({
agent: "room-agent",
name: currentRoom || "unused",
enabled: !!currentRoom,
});
if (room.status === "disabled") {
// room.call, room.setState are not available in this branch
return <div>Select a room</div>;
}
// room.status === "connected" | "connecting" | "disconnected"
// room.call is availableOption D: Remove enabled, infer from name
// If name is nullish, don't connect
const room = useAgent({
agent: "room-agent",
name: currentRoom, // null = disabled
});Recommendation
Option B (separate hook) or Option D (infer from name) seem cleanest:
- No placeholder names
- Clear semantics
- TypeScript can narrow the type
- Existing
useAgentbehavior unchanged (backward compatible)
Additional Context
This pattern appears in any multi-agent scenario where connections are dynamic:
- Chat applications (join/leave rooms)
- Collaborative editing (connect to document when opened)
- Gaming (join/leave game sessions)
- Support tickets (agent per ticket, connect when viewing)
Related Files
packages/agents/src/react.tsx-useAgenthook implementationexamples/playground/src/demos/multi-agent/ChatRoomsDemo.tsx- Real example of the awkward pattern
Backward Compatibility
Any solution should be backward compatible. Existing code using enabled should continue to work.