From a7b939dabeb8793785fb14dfe1c6a36350960d8d Mon Sep 17 00:00:00 2001 From: George-iam Date: Sun, 1 Mar 2026 09:04:21 +0000 Subject: [PATCH] feat: add intents.get and inbox action helpers to Python SDK Close final Track C family gaps by exposing intent read and inbox thread/delegate/decision/delete operations with tests and quickstart coverage. Made-with: Cursor --- README.md | 31 +++++++++++ axme_sdk/client.py | 91 +++++++++++++++++++++++++++++++ tests/test_client.py | 124 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+) diff --git a/README.md b/README.md index 5e30e9b..04f2221 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,11 @@ with AxmeClient(config) as client: idempotency_key="create-intent-001", ) print(result) + print(client.get_intent(result["intent_id"])["intent"]["status"]) inbox = client.list_inbox(owner_agent="agent://example/receiver", trace_id="trace-inbox-001") print(inbox) + thread = client.get_inbox_thread("11111111-1111-4111-8111-111111111111", owner_agent="agent://example/receiver") + print(thread["thread"]["status"]) changes = client.list_inbox_changes(owner_agent="agent://example/receiver", limit=50) print(changes["next_cursor"], changes["has_more"]) replied = client.reply_inbox_thread( @@ -42,6 +45,34 @@ with AxmeClient(config) as client: idempotency_key="reply-001", ) print(replied) + delegated = client.delegate_inbox_thread( + "11111111-1111-4111-8111-111111111111", + {"delegate_to": "agent://example/delegate", "note": "handoff"}, + owner_agent="agent://example/receiver", + idempotency_key="delegate-001", + ) + print(delegated["thread"]["status"]) + approved = client.approve_inbox_thread( + "11111111-1111-4111-8111-111111111111", + {"comment": "approved"}, + owner_agent="agent://example/receiver", + idempotency_key="approve-001", + ) + print(approved["thread"]["status"]) + rejected = client.reject_inbox_thread( + "11111111-1111-4111-8111-111111111111", + {"comment": "rejected"}, + owner_agent="agent://example/receiver", + idempotency_key="reject-001", + ) + print(rejected["thread"]["status"]) + deleted = client.delete_inbox_messages( + "11111111-1111-4111-8111-111111111111", + {"mode": "self", "limit": 1}, + owner_agent="agent://example/receiver", + idempotency_key="delete-001", + ) + print(deleted["deleted_count"]) approval = client.decide_approval( "55555555-5555-4555-8555-555555555555", decision="approve", diff --git a/axme_sdk/client.py b/axme_sdk/client.py index 1c05da1..61deebc 100644 --- a/axme_sdk/client.py +++ b/axme_sdk/client.py @@ -75,6 +75,9 @@ def create_intent( retryable=idempotency_key is not None, ) + def get_intent(self, intent_id: str, *, trace_id: str | None = None) -> dict[str, Any]: + return self._request_json("GET", f"/v1/intents/{intent_id}", trace_id=trace_id, retryable=True) + def list_inbox(self, *, owner_agent: str | None = None, trace_id: str | None = None) -> dict[str, Any]: params: dict[str, str] | None = None if owner_agent is not None: @@ -138,6 +141,94 @@ def reply_inbox_thread( retryable=idempotency_key is not None, ) + def delegate_inbox_thread( + self, + thread_id: str, + payload: dict[str, Any], + *, + owner_agent: str | None = None, + idempotency_key: str | None = None, + trace_id: str | None = None, + ) -> dict[str, Any]: + params: dict[str, str] | None = None + if owner_agent is not None: + params = {"owner_agent": owner_agent} + return self._request_json( + "POST", + f"/v1/inbox/{thread_id}/delegate", + params=params, + json_body=payload, + idempotency_key=idempotency_key, + trace_id=trace_id, + retryable=idempotency_key is not None, + ) + + def approve_inbox_thread( + self, + thread_id: str, + payload: dict[str, Any], + *, + owner_agent: str | None = None, + idempotency_key: str | None = None, + trace_id: str | None = None, + ) -> dict[str, Any]: + params: dict[str, str] | None = None + if owner_agent is not None: + params = {"owner_agent": owner_agent} + return self._request_json( + "POST", + f"/v1/inbox/{thread_id}/approve", + params=params, + json_body=payload, + idempotency_key=idempotency_key, + trace_id=trace_id, + retryable=idempotency_key is not None, + ) + + def reject_inbox_thread( + self, + thread_id: str, + payload: dict[str, Any], + *, + owner_agent: str | None = None, + idempotency_key: str | None = None, + trace_id: str | None = None, + ) -> dict[str, Any]: + params: dict[str, str] | None = None + if owner_agent is not None: + params = {"owner_agent": owner_agent} + return self._request_json( + "POST", + f"/v1/inbox/{thread_id}/reject", + params=params, + json_body=payload, + idempotency_key=idempotency_key, + trace_id=trace_id, + retryable=idempotency_key is not None, + ) + + def delete_inbox_messages( + self, + thread_id: str, + payload: dict[str, Any], + *, + owner_agent: str | None = None, + idempotency_key: str | None = None, + trace_id: str | None = None, + ) -> dict[str, Any]: + params: dict[str, str] | None = None + if owner_agent is not None: + params = {"owner_agent": owner_agent} + return self._request_json( + "POST", + f"/v1/inbox/{thread_id}/messages/delete", + params=params, + json_body=payload, + idempotency_key=idempotency_key, + trace_id=trace_id, + retryable=idempotency_key is not None, + ) + def decide_approval( self, approval_id: str, diff --git a/tests/test_client.py b/tests/test_client.py index 52f6cc1..b0693d3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -155,6 +155,33 @@ def test_create_intent_raises_for_mismatched_correlation_id() -> None: ) +def test_get_intent_success() -> None: + intent_id = "22222222-2222-4222-8222-222222222222" + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "GET" + assert request.url.path == f"/v1/intents/{intent_id}" + return httpx.Response( + 200, + json={ + "ok": True, + "intent": { + "intent_id": intent_id, + "status": "accepted", + "created_at": "2026-02-28T00:00:00Z", + "intent_type": "notify.message.v1", + "correlation_id": "11111111-1111-1111-1111-111111111111", + "from_agent": "agent://from", + "to_agent": "agent://to", + "payload": {"text": "hello"}, + }, + }, + ) + + client = _client(handler) + assert client.get_intent(intent_id)["intent"]["intent_id"] == intent_id + + def test_list_inbox_success() -> None: thread = _thread_payload() @@ -231,6 +258,103 @@ def handler(request: httpx.Request) -> httpx.Response: } +def test_delegate_inbox_thread_success() -> None: + thread = _thread_payload() + thread_id = str(thread["thread_id"]) + payload = {"delegate_to": "agent://example/delegate", "note": "handoff"} + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == f"/v1/inbox/{thread_id}/delegate" + assert request.url.params.get("owner_agent") == "agent://owner" + assert request.headers["idempotency-key"] == "delegate-1" + assert json.loads(request.read().decode("utf-8")) == payload + return httpx.Response(200, json={"ok": True, "thread": thread}) + + client = _client(handler) + assert client.delegate_inbox_thread( + thread_id, + payload, + owner_agent="agent://owner", + idempotency_key="delegate-1", + ) == {"ok": True, "thread": thread} + + +def test_approve_inbox_thread_success() -> None: + thread = _thread_payload() + thread_id = str(thread["thread_id"]) + payload = {"comment": "approved"} + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == f"/v1/inbox/{thread_id}/approve" + assert request.url.params.get("owner_agent") == "agent://owner" + assert request.headers["idempotency-key"] == "approve-1" + assert json.loads(request.read().decode("utf-8")) == payload + return httpx.Response(200, json={"ok": True, "thread": thread}) + + client = _client(handler) + assert client.approve_inbox_thread( + thread_id, + payload, + owner_agent="agent://owner", + idempotency_key="approve-1", + ) == {"ok": True, "thread": thread} + + +def test_reject_inbox_thread_success() -> None: + thread = _thread_payload() + thread_id = str(thread["thread_id"]) + payload = {"comment": "rejected"} + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == f"/v1/inbox/{thread_id}/reject" + assert request.url.params.get("owner_agent") == "agent://owner" + assert request.headers["idempotency-key"] == "reject-1" + assert json.loads(request.read().decode("utf-8")) == payload + return httpx.Response(200, json={"ok": True, "thread": thread}) + + client = _client(handler) + assert client.reject_inbox_thread( + thread_id, + payload, + owner_agent="agent://owner", + idempotency_key="reject-1", + ) == {"ok": True, "thread": thread} + + +def test_delete_inbox_messages_success() -> None: + thread = _thread_payload() + thread_id = str(thread["thread_id"]) + payload = {"mode": "self", "limit": 1} + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == f"/v1/inbox/{thread_id}/messages/delete" + assert request.url.params.get("owner_agent") == "agent://owner" + assert request.headers["idempotency-key"] == "delete-1" + assert json.loads(request.read().decode("utf-8")) == payload + return httpx.Response( + 200, + json={ + "ok": True, + "thread": thread, + "mode": "self", + "deleted_count": 1, + "message_ids": ["msg-1"], + }, + ) + + client = _client(handler) + assert client.delete_inbox_messages( + thread_id, + payload, + owner_agent="agent://owner", + idempotency_key="delete-1", + )["deleted_count"] == 1 + + def test_decide_approval_success() -> None: approval_id = "55555555-5555-4555-8555-555555555555"