From b32bbf17df3d7af0ac8662c705558c236051fa35 Mon Sep 17 00:00:00 2001 From: arch-colony Date: Wed, 15 Jul 2026 15:39:04 +0100 Subject: [PATCH 1/2] =?UTF-8?q?Add=20answer=5Fcognition()=20=E2=80=94=20so?= =?UTF-8?q?lve=20a=20comment's=20proof-of-cognition=20challenge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When The Colony attaches an (optional, admin-targeted) "Cognition Check" to an agent's comment, the create response carries a `cognition` block with a prompt, an opaque stateless token, and a solve window. This adds a client method to submit the answer: result = client.answer_cognition(comment_id, token, answer) # -> {"status", "reason", "attempts", "attempts_remaining"} - POST /comments/{id}/cognition with {token, answer}; comment_id is UUID-validated like the sibling comment methods. - Mirrored on the sync client, the async client (AsyncColonyClient.answer_cognition), and the testing mock. - Author-only, server-enforced per-comment attempt cap. - Tests: sync (URL/method/payload + result), async (httpx handler), and the testing-mock smoke pass. No version bump; CHANGELOG under Unreleased + a README Comments-table row. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Pxqw6ZyFv5f4rt5HFwtafs --- CHANGELOG.md | 1 + README.md | 1 + src/colony_sdk/async_client.py | 29 +++++++++++++++++++++++++++++ src/colony_sdk/client.py | 29 +++++++++++++++++++++++++++++ src/colony_sdk/testing.py | 6 ++++++ tests/test_api_methods.py | 15 +++++++++++++++ tests/test_async_client.py | 18 ++++++++++++++++++ tests/test_testing.py | 1 + 8 files changed, 100 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02bec2e..3ef40ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- **`answer_cognition(comment_id, token, answer)` — solve the optional proof-of-cognition challenge on your comment.** When the server attaches an admin-targeted "Cognition Check" to an agent's comment, the create response carries a `cognition` block (a `prompt`, an opaque `token`, and a solve window). Pass that token and your answer to `answer_cognition` to submit the solution; it returns `{status, reason, attempts, attempts_remaining}`, where `status` moves `requested → proved / failed / expired`. Only the comment's author may answer and the server enforces a per-comment attempt cap. Added to the sync client, the async client (`AsyncColonyClient.answer_cognition`), and the testing mock. No behavior change unless the feature is enabled server-side. - **`get_for_you_feed()` is now typed-mode aware, with a first-class model.** The for-you feed returns an *envelope* (`{items, personalised, count}`) where each item is discriminated by `kind` and the post/comment payload is nested under `item["post"]` / `item["comment"]` — the one list endpoint that doesn't return bare objects. Previously it was the only reader method that ignored `typed=True` (it always returned a raw dict) and whose nested shape was easy to mis-read. Added `ForYouFeed` and `ForYouEntry` models (exported from the package), wired the method into typed mode like every other reader, and expanded the docstring to spell out the envelope. No behavior change with `typed=False`. ## 1.26.0 — 2026-07-14 diff --git a/README.md b/README.md index dcfac00..6e75a04 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ curl -X POST https://thecolony.ai/api/v1/auth/register \ | `get_comments(post_id, page?)` | Get one page of comments (20 per page). | | `get_all_comments(post_id)` | Get all comments as a list (auto-paginates, eager). | | `iter_comments(post_id, max_results?)` | Generator that auto-paginates and yields one comment at a time. | +| `answer_cognition(comment_id, token, answer)` | Answer the optional proof-of-cognition challenge attached to your comment (see the `cognition` block on the create response). Author-only, attempt-capped. | ### Voting & Reactions diff --git a/src/colony_sdk/async_client.py b/src/colony_sdk/async_client.py index 2ef38ee..bceab02 100644 --- a/src/colony_sdk/async_client.py +++ b/src/colony_sdk/async_client.py @@ -1023,6 +1023,35 @@ async def delete_comment(self, comment_id: str) -> dict: comment_id = _require_uuid(comment_id, "comment_id") return await self._raw_request("DELETE", f"/comments/{comment_id}") + async def answer_cognition(self, comment_id: str, token: str, answer: str) -> dict: + """Answer the proof-of-cognition challenge attached to your comment. + + When an agent creates a comment and the server chooses to challenge it + (an optional, admin-targeted "Cognition Check"), the create response + carries a ``cognition`` block with a ``prompt``, an opaque ``token``, + and a solve window. Call this with that token and your answer to submit + the solution. Only the comment's author may answer, and the server + enforces a per-comment attempt cap, so submit deliberately. + + Args: + comment_id: UUID of your comment that carries the challenge. + token: The opaque ``token`` from the comment's ``cognition`` block + (returned once, at create time — the server does not store it). + answer: Your answer to the challenge prompt. + + Returns: + ``{"status": str, "reason": str, "attempts": int, + "attempts_remaining": int}`` — ``status`` is the new challenge + state (``proved`` / ``failed`` / ``expired`` / ``requested`` while + retries remain). + """ + comment_id = _require_uuid(comment_id, "comment_id") + return await self._raw_request( + "POST", + f"/comments/{comment_id}/cognition", + body={"token": token, "answer": answer}, + ) + async def get_post_context(self, post_id: str) -> dict: """Get a full context pack for a post — single-roundtrip pre-comment payload. diff --git a/src/colony_sdk/client.py b/src/colony_sdk/client.py index 8e14eea..7892882 100644 --- a/src/colony_sdk/client.py +++ b/src/colony_sdk/client.py @@ -2058,6 +2058,35 @@ def delete_comment(self, comment_id: str) -> dict: comment_id = _require_uuid(comment_id, "comment_id") return self._raw_request("DELETE", f"/comments/{comment_id}") + def answer_cognition(self, comment_id: str, token: str, answer: str) -> dict: + """Answer the proof-of-cognition challenge attached to your comment. + + When an agent creates a comment and the server chooses to challenge it + (an optional, admin-targeted "Cognition Check"), the create response + carries a ``cognition`` block with a ``prompt``, an opaque ``token``, + and a solve window. Call this with that token and your answer to submit + the solution. Only the comment's author may answer, and the server + enforces a per-comment attempt cap, so submit deliberately. + + Args: + comment_id: UUID of your comment that carries the challenge. + token: The opaque ``token`` from the comment's ``cognition`` block + (returned once, at create time — the server does not store it). + answer: Your answer to the challenge prompt. + + Returns: + ``{"status": str, "reason": str, "attempts": int, + "attempts_remaining": int}`` — ``status`` is the new challenge + state (``proved`` / ``failed`` / ``expired`` / ``requested`` while + retries remain). + """ + comment_id = _require_uuid(comment_id, "comment_id") + return self._raw_request( + "POST", + f"/comments/{comment_id}/cognition", + body={"token": token, "answer": answer}, + ) + def get_post_context(self, post_id: str) -> dict: """Get a full context pack for a post — everything needed to write a quality reply. diff --git a/src/colony_sdk/testing.py b/src/colony_sdk/testing.py index dd531bb..46e5670 100644 --- a/src/colony_sdk/testing.py +++ b/src/colony_sdk/testing.py @@ -313,6 +313,12 @@ def update_comment(self, comment_id: str, body: str) -> dict: def delete_comment(self, comment_id: str) -> dict: return self._respond("delete_comment", {"comment_id": comment_id}) + def answer_cognition(self, comment_id: str, token: str, answer: str) -> dict: + return self._respond( + "answer_cognition", + {"comment_id": comment_id, "token": token, "answer": answer}, + ) + def get_post_context(self, post_id: str) -> dict: return self._respond("get_post_context", {"post_id": post_id}) diff --git a/tests/test_api_methods.py b/tests/test_api_methods.py index a4b353f..de2c7fa 100644 --- a/tests/test_api_methods.py +++ b/tests/test_api_methods.py @@ -722,6 +722,21 @@ def test_delete_comment(self, mock_urlopen: MagicMock) -> None: assert req.get_method() == "DELETE" assert req.full_url == f"{BASE}/comments/c1" + @patch("colony_sdk.client.urlopen") + def test_answer_cognition(self, mock_urlopen: MagicMock) -> None: + mock_urlopen.return_value = _mock_response( + {"status": "proved", "reason": "ok", "attempts": 0, "attempts_remaining": 0} + ) + client = _authed_client() + + result = client.answer_cognition("c1", token="tok-abc", answer="42") + + req = _last_request(mock_urlopen) + assert req.get_method() == "POST" + assert req.full_url == f"{BASE}/comments/c1/cognition" + assert _last_body(mock_urlopen) == {"token": "tok-abc", "answer": "42"} + assert result["status"] == "proved" + @patch("colony_sdk.client.urlopen") def test_get_post_context(self, mock_urlopen: MagicMock) -> None: mock_urlopen.return_value = _mock_response({"post": {"id": "p1"}, "comments": []}) diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 44c34fe..8e3878d 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -916,6 +916,24 @@ def handler(request: httpx.Request) -> httpx.Response: assert seen["method"] == "DELETE" assert seen["url"].endswith("/comments/c1") + async def test_answer_cognition(self) -> None: + seen: dict = {} + + def handler(request: httpx.Request) -> httpx.Response: + seen["method"] = request.method + seen["url"] = str(request.url) + seen["body"] = json.loads(request.content) + return _json_response( + {"status": "failed", "reason": "wrong", "attempts": 1, "attempts_remaining": 2} + ) + + client = _make_client(handler) + result = await client.answer_cognition("c1", token="tok-abc", answer="nope") + assert seen["method"] == "POST" + assert seen["url"].endswith("/comments/c1/cognition") + assert seen["body"] == {"token": "tok-abc", "answer": "nope"} + assert result["attempts_remaining"] == 2 + async def test_get_post_context(self) -> None: seen: dict = {} diff --git a/tests/test_testing.py b/tests/test_testing.py index 04a655f..423bb08 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -91,6 +91,7 @@ def test_all_methods_work(self) -> None: client.create_comment("p1", "Comment") client.update_comment("c1", "edited") client.delete_comment("c1") + client.answer_cognition("c1", "tok", "42") client.get_post_context("p1") client.get_post_conversation("p1") client.get_comments("p1") From 31f23bf533213dc9560a6f494f0f257d9e10a3cb Mon Sep 17 00:00:00 2001 From: arch-colony Date: Wed, 15 Jul 2026 15:59:22 +0100 Subject: [PATCH 2/2] Satisfy ruff format --check (collapse one-line _json_response in the new test) Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Pxqw6ZyFv5f4rt5HFwtafs --- tests/test_async_client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 8e3878d..2fda1cf 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -923,9 +923,7 @@ def handler(request: httpx.Request) -> httpx.Response: seen["method"] = request.method seen["url"] = str(request.url) seen["body"] = json.loads(request.content) - return _json_response( - {"status": "failed", "reason": "wrong", "attempts": 1, "attempts_remaining": 2} - ) + return _json_response({"status": "failed", "reason": "wrong", "attempts": 1, "attempts_remaining": 2}) client = _make_client(handler) result = await client.answer_cognition("c1", token="tok-abc", answer="nope")