Skip to content

Commit aafd334

Browse files
mikemolinetclaude
andcommitted
feat(agents): add roster() + presence() methods (cueapi #630 + #662 parity)
Adds two SDK methods to ``AgentsResource`` covering the Agent Directory v0/v1/v2 surface: - ``roster(*, if_none_match=None)`` — GET /v1/agents/roster Lists every agent owned by the calling key with a presence block (online, derived_status, bucketed_seen, default_live cue, labeled sessions, etag). Supports ``If-None-Match`` header so cheap-poll callers can skip the payload when the directory hasn't changed. - ``presence(ref)`` — GET /v1/agents/{ref}/presence 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 refreshing a single tile every few seconds without re-fetching the directory or full agent record. 4 new mock-based tests in test_agents_resource.py: - test_roster_no_etag — no If-None-Match header by default - test_roster_with_if_none_match — header flows as ``If-None-Match`` - test_presence_by_id — opaque agent_id path - test_presence_by_slug_form — slug@user path Source: drift audit handoff/cueapi-package-drift-2026-05-06; Backlog rows "Parity port: PR #630 (GET /v1/agents/roster) → cueapi-python" + "Parity port: PR #662 (GET /v1/agents/{ref}/presence) → cueapi-python" (both p2, CTO-SEC-DRIFT-AUDIT-AUTHORIZE 2026-05-06). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b30d23b commit aafd334

2 files changed

Lines changed: 103 additions & 0 deletions

File tree

cueapi/resources/agents.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,53 @@ def sent(
190190
"""List messages sent by this agent."""
191191
params: Dict[str, Any] = {"limit": limit, "offset": offset}
192192
return self._client._get(f"/v1/agents/{ref}/sent", params=params)
193+
194+
def roster(
195+
self,
196+
*,
197+
if_none_match: Optional[str] = None,
198+
) -> Dict[str, Any]:
199+
"""List the agent directory (Surface 6, cueapi #630).
200+
201+
Returns the user's agent directory — every agent owned by the
202+
calling key with a presence block (online state, derived status,
203+
bucketed last-seen, default-live cue, labeled live sessions).
204+
Used by Directory v0/v1/v2 UIs and by senders that want to
205+
choose recipients based on presence.
206+
207+
Args:
208+
if_none_match: Optional ETag from a prior call. Server
209+
returns ``304 Not Modified`` (raised as
210+
``CueAPIError`` with status 304) if the directory
211+
hasn't changed. Use to cheap-poll without re-fetching
212+
payloads.
213+
214+
Returns:
215+
Dict with ``agents`` list (each carrying presence block) and
216+
``etag`` for the next call.
217+
"""
218+
headers: Dict[str, str] = {}
219+
if if_none_match is not None:
220+
headers["If-None-Match"] = if_none_match
221+
kwargs: Dict[str, Any] = {}
222+
if headers:
223+
kwargs["headers"] = headers
224+
return self._client._get("/v1/agents/roster", **kwargs)
225+
226+
def presence(self, ref: str) -> Dict[str, Any]:
227+
"""Cheap-poll a single agent's presence block (cueapi #662).
228+
229+
Lighter than ``get(ref)`` — returns just the presence-relevant
230+
fields (online, derived_status, bucketed_seen, default_live,
231+
labeled_sessions, etag) without the full agent record.
232+
Designed for UIs that need to refresh a single tile every few
233+
seconds without re-fetching the full directory or agent record.
234+
235+
Args:
236+
ref: Agent opaque ID (``agt_<12 alnum>``) or slug-form
237+
(``slug@user``).
238+
239+
Returns:
240+
Presence dict.
241+
"""
242+
return self._client._get(f"/v1/agents/{ref}/presence")

tests/test_agents_resource.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,56 @@ def test_sent_basic(self):
217217
"/v1/agents/agt_x/sent",
218218
params={"limit": 50, "offset": 0},
219219
)
220+
221+
222+
class TestRoster:
223+
"""Agent directory roster — cueapi #630 parity."""
224+
225+
def test_roster_no_etag(self):
226+
mock_client = MagicMock()
227+
mock_client._get.return_value = {"agents": [], "etag": "abc"}
228+
r = AgentsResource(mock_client)
229+
230+
r.roster()
231+
232+
# No If-None-Match header when if_none_match is None
233+
mock_client._get.assert_called_once_with("/v1/agents/roster")
234+
235+
def test_roster_with_if_none_match(self):
236+
"""If-None-Match flows as a header (not a query param)."""
237+
mock_client = MagicMock()
238+
mock_client._get.return_value = {"agents": [], "etag": "v2"}
239+
r = AgentsResource(mock_client)
240+
241+
r.roster(if_none_match="W/\"abc\"")
242+
243+
mock_client._get.assert_called_once_with(
244+
"/v1/agents/roster",
245+
headers={"If-None-Match": 'W/"abc"'},
246+
)
247+
248+
249+
class TestPresence:
250+
"""Cheap-poll presence — cueapi #662 parity."""
251+
252+
def test_presence_by_id(self):
253+
mock_client = MagicMock()
254+
mock_client._get.return_value = {
255+
"online": True,
256+
"derived_status": "active",
257+
"bucketed_seen": "now",
258+
}
259+
r = AgentsResource(mock_client)
260+
261+
r.presence("agt_abcdef123456")
262+
263+
mock_client._get.assert_called_once_with("/v1/agents/agt_abcdef123456/presence")
264+
265+
def test_presence_by_slug_form(self):
266+
mock_client = MagicMock()
267+
mock_client._get.return_value = {"online": False}
268+
r = AgentsResource(mock_client)
269+
270+
r.presence("foo@me")
271+
272+
mock_client._get.assert_called_once_with("/v1/agents/foo@me/presence")

0 commit comments

Comments
 (0)