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
50 changes: 50 additions & 0 deletions cueapi/resources/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,53 @@ def sent(
"""List messages sent by this agent."""
params: Dict[str, Any] = {"limit": limit, "offset": offset}
return self._client._get(f"/v1/agents/{ref}/sent", params=params)

def roster(
self,
*,
if_none_match: Optional[str] = None,
) -> Dict[str, Any]:
"""List the agent directory (Surface 6, cueapi #630).

Returns the user's agent directory — every agent owned by the
calling key with a presence block (online state, derived status,
bucketed last-seen, default-live cue, labeled live sessions).
Used by Directory v0/v1/v2 UIs and by senders that want to
choose recipients based on presence.

Args:
if_none_match: Optional ETag from a prior call. Server
returns ``304 Not Modified`` (raised as
``CueAPIError`` with status 304) if the directory
hasn't changed. Use to cheap-poll without re-fetching
payloads.

Returns:
Dict with ``agents`` list (each carrying presence block) and
``etag`` for the next call.
"""
headers: Dict[str, str] = {}
if if_none_match is not None:
headers["If-None-Match"] = if_none_match
kwargs: Dict[str, Any] = {}
if headers:
kwargs["headers"] = headers
return self._client._get("/v1/agents/roster", **kwargs)

def presence(self, ref: str) -> Dict[str, Any]:
"""Cheap-poll a single agent's presence block (cueapi #662).

Lighter than ``get(ref)`` — returns just the presence-relevant
fields (online, derived_status, bucketed_seen, default_live,
labeled_sessions, etag) without the full agent record.
Designed for UIs that need to refresh a single tile every few
seconds without re-fetching the full directory or agent record.

Args:
ref: Agent opaque ID (``agt_<12 alnum>``) or slug-form
(``slug@user``).

Returns:
Presence dict.
"""
return self._client._get(f"/v1/agents/{ref}/presence")
53 changes: 53 additions & 0 deletions tests/test_agents_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,56 @@ def test_sent_basic(self):
"/v1/agents/agt_x/sent",
params={"limit": 50, "offset": 0},
)


class TestRoster:
"""Agent directory roster — cueapi #630 parity."""

def test_roster_no_etag(self):
mock_client = MagicMock()
mock_client._get.return_value = {"agents": [], "etag": "abc"}
r = AgentsResource(mock_client)

r.roster()

# No If-None-Match header when if_none_match is None
mock_client._get.assert_called_once_with("/v1/agents/roster")

def test_roster_with_if_none_match(self):
"""If-None-Match flows as a header (not a query param)."""
mock_client = MagicMock()
mock_client._get.return_value = {"agents": [], "etag": "v2"}
r = AgentsResource(mock_client)

r.roster(if_none_match="W/\"abc\"")

mock_client._get.assert_called_once_with(
"/v1/agents/roster",
headers={"If-None-Match": 'W/"abc"'},
)


class TestPresence:
"""Cheap-poll presence — cueapi #662 parity."""

def test_presence_by_id(self):
mock_client = MagicMock()
mock_client._get.return_value = {
"online": True,
"derived_status": "active",
"bucketed_seen": "now",
}
r = AgentsResource(mock_client)

r.presence("agt_abcdef123456")

mock_client._get.assert_called_once_with("/v1/agents/agt_abcdef123456/presence")

def test_presence_by_slug_form(self):
mock_client = MagicMock()
mock_client._get.return_value = {"online": False}
r = AgentsResource(mock_client)

r.presence("foo@me")

mock_client._get.assert_called_once_with("/v1/agents/foo@me/presence")
Loading