Skip to content

Improve optional/conditional agent connection pattern in useAgent #820

@threepointone

Description

@threepointone

(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 available

Option 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 useAgent behavior 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 - useAgent hook implementation
  • examples/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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions