ctxprotocol 0.19.1__tar.gz → 0.20.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/PKG-INFO +21 -7
  2. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/README.md +20 -6
  3. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/query.py +76 -6
  4. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/pyproject.toml +1 -1
  5. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/test_query.py +41 -1
  6. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/uv.lock +1 -1
  7. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/.codexignore +0 -0
  8. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/.gitignore +0 -0
  9. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/CHANGELOG.md +0 -0
  10. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/__init__.py +0 -0
  11. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/auth/__init__.py +0 -0
  12. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/__init__.py +0 -0
  13. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/client.py +0 -0
  14. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/__init__.py +0 -0
  15. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/developer.py +0 -0
  16. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/discovery.py +0 -0
  17. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/resources/tools.py +0 -0
  18. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/client/types.py +0 -0
  19. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/context/__init__.py +0 -0
  20. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/context/hyperliquid.py +0 -0
  21. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/context/polymarket.py +0 -0
  22. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/context/wallet.py +0 -0
  23. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/contrib/__init__.py +0 -0
  24. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/__init__.py +0 -0
  25. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/core.py +0 -0
  26. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/trace.py +0 -0
  27. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/types.py +0 -0
  28. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/contrib/search/validation.py +0 -0
  29. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/handshake/__init__.py +0 -0
  30. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/ctxprotocol/py.typed +0 -0
  31. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/docs/README.md +0 -0
  32. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/execute_client.py +0 -0
  33. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/polymarket_query_trace_validation.py +0 -0
  34. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/query_client.py +0 -0
  35. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/test_favorites_only.py +0 -0
  36. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/test_get_tool.py +0 -0
  37. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/two_surfaces_client.py +0 -0
  38. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/validation/generate_contributor_search_validation_artifacts.py +0 -0
  39. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/validation/named-regression-iran-boots-on-ground.json +0 -0
  40. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/validation/named-regression-supreme-court-tariffs.json +0 -0
  41. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/validation/shared-capability-miss-unsupported-venue.json +0 -0
  42. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/validation/shared-generic-overlap-best-match.json +0 -0
  43. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/client/validation/shared-still-ambiguous-shortlist.json +0 -0
  44. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/README.md +0 -0
  45. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/env.example +0 -0
  46. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/requirements.txt +0 -0
  47. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/examples/server/hummingbot-contributor/server.py +0 -0
  48. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/polymarket-query-trace-results-py.json +0 -0
  49. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/__init__.py +0 -0
  50. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/contrib_search_validation_cases.py +0 -0
  51. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/test_client.py +0 -0
  52. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/test_contrib_search.py +0 -0
  53. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/test_discovery.py +0 -0
  54. {ctxprotocol-0.19.1 → ctxprotocol-0.20.0}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctxprotocol
3
- Version: 0.19.1
3
+ Version: 0.20.0
4
4
  Summary: Official Python SDK for the Context Protocol - Discover and execute AI tools programmatically
5
5
  Project-URL: Homepage, https://ctxprotocol.com
6
6
  Project-URL: Documentation, https://docs.ctxprotocol.com
@@ -149,21 +149,35 @@ print(
149
149
  print(answer.orchestration_metrics) # Optional first-pass / rediscovery metrics
150
150
  ```
151
151
 
152
- For long-running questions, start a durable job and poll until it completes:
152
+ For long-running questions from LLM agents, use `run_or_poll()` so the entire wait happens inside one SDK call (one model turn):
153
153
 
154
154
  ```python
155
- job = await client.query.start(
155
+ answer = await client.query.run_or_poll(
156
156
  query="Build a chart-ready dataset of Base whale movements over the last 30 days",
157
157
  response_shape="evidence_only",
158
158
  include_data_url=True,
159
159
  )
160
160
 
161
- completed = await client.query.poll(
162
- job.job_id,
163
- interval_ms=2000,
164
- timeout_ms=15 * 60_000,
161
+ print(answer.data_url)
162
+ ```
163
+
164
+ The defaults are already agent-friendly: the SDK checks status every 5 seconds
165
+ over plain HTTP (this costs no model tokens — token cost comes from model
166
+ *turns*, not HTTP polls) and waits up to 31 minutes, slightly beyond the
167
+ hosted 1800-second compute ceiling that bounds every query path. Treat a
168
+ failed job-window status as terminal and start a fresh query.
169
+
170
+ For programmatic workflows that want the job handle, start a durable job and poll until it completes:
171
+
172
+ ```python
173
+ job = await client.query.start(
174
+ query="Build a chart-ready dataset of Base whale movements over the last 30 days",
175
+ response_shape="evidence_only",
176
+ include_data_url=True,
165
177
  )
166
178
 
179
+ completed = await client.query.poll(job.job_id)
180
+
167
181
  print(completed.result.data_url if completed.result else None)
168
182
  ```
169
183
 
@@ -111,21 +111,35 @@ print(
111
111
  print(answer.orchestration_metrics) # Optional first-pass / rediscovery metrics
112
112
  ```
113
113
 
114
- For long-running questions, start a durable job and poll until it completes:
114
+ For long-running questions from LLM agents, use `run_or_poll()` so the entire wait happens inside one SDK call (one model turn):
115
115
 
116
116
  ```python
117
- job = await client.query.start(
117
+ answer = await client.query.run_or_poll(
118
118
  query="Build a chart-ready dataset of Base whale movements over the last 30 days",
119
119
  response_shape="evidence_only",
120
120
  include_data_url=True,
121
121
  )
122
122
 
123
- completed = await client.query.poll(
124
- job.job_id,
125
- interval_ms=2000,
126
- timeout_ms=15 * 60_000,
123
+ print(answer.data_url)
124
+ ```
125
+
126
+ The defaults are already agent-friendly: the SDK checks status every 5 seconds
127
+ over plain HTTP (this costs no model tokens — token cost comes from model
128
+ *turns*, not HTTP polls) and waits up to 31 minutes, slightly beyond the
129
+ hosted 1800-second compute ceiling that bounds every query path. Treat a
130
+ failed job-window status as terminal and start a fresh query.
131
+
132
+ For programmatic workflows that want the job handle, start a durable job and poll until it completes:
133
+
134
+ ```python
135
+ job = await client.query.start(
136
+ query="Build a chart-ready dataset of Base whale movements over the last 30 days",
137
+ response_shape="evidence_only",
138
+ include_data_url=True,
127
139
  )
128
140
 
141
+ completed = await client.query.poll(job.job_id)
142
+
129
143
  print(completed.result.data_url if completed.result else None)
130
144
  ```
131
145
 
@@ -11,11 +11,12 @@ from __future__ import annotations
11
11
 
12
12
  import asyncio
13
13
  import json
14
- from typing import TYPE_CHECKING, Any, AsyncGenerator
14
+ from collections.abc import AsyncGenerator
15
+ from typing import TYPE_CHECKING, Any
15
16
 
16
17
  from ctxprotocol.client.types import (
17
- ContextError,
18
18
  AgentModelId,
19
+ ContextError,
19
20
  QueryAttemptReference,
20
21
  QueryDeveloperTrace,
21
22
  QueryForkReference,
@@ -34,6 +35,18 @@ from ctxprotocol.client.types import (
34
35
  if TYPE_CHECKING:
35
36
  from ctxprotocol.client.client import ContextClient
36
37
 
38
+ # Internal HTTP status-check cadence for poll()/run_or_poll(). This is plain
39
+ # HTTP polling below any LLM boundary — it costs no model tokens no matter how
40
+ # frequent it is; a slower interval only delays completion detection. To keep
41
+ # LLM-agent token costs down, minimize *model turns* (use run_or_poll()), not
42
+ # HTTP polls.
43
+ DEFAULT_QUERY_POLL_INTERVAL_MS = 5_000
44
+ # Default client-side wait: slightly beyond the hosted 1800s compute ceiling
45
+ # (Vercel extended max duration) that bounds every query path, so the client
46
+ # observes the job's terminal state instead of giving up while the server is
47
+ # still legitimately working.
48
+ DEFAULT_QUERY_POLL_TIMEOUT_MS = 31 * 60_000
49
+
37
50
 
38
51
  class Query:
39
52
  """Query resource for pay-per-response agentic queries."""
@@ -376,10 +389,18 @@ class Query:
376
389
  async def poll(
377
390
  self,
378
391
  job_id: str,
379
- interval_ms: int = 2000,
380
- timeout_ms: int = 15 * 60_000,
392
+ interval_ms: int = DEFAULT_QUERY_POLL_INTERVAL_MS,
393
+ timeout_ms: int = DEFAULT_QUERY_POLL_TIMEOUT_MS,
381
394
  ) -> QueryJobStatusResult:
382
- """Poll a durable async query job until completion or failure."""
395
+ """Poll a durable async query job until completion or failure.
396
+
397
+ ``timeout_ms`` controls how long this client waits; the hosted job
398
+ itself is bounded by the 1800s server compute ceiling. If the status
399
+ endpoint reports the job exceeded the server-side window, the job is
400
+ terminal and should not be polled again. ``interval_ms`` is an HTTP
401
+ check cadence with no effect on LLM token usage — leave it at the
402
+ fast default.
403
+ """
383
404
  loop = asyncio.get_running_loop()
384
405
  deadline = loop.time() + timeout_ms / 1000
385
406
 
@@ -389,10 +410,59 @@ class Query:
389
410
  return status
390
411
  if status.status == "failed":
391
412
  raise ContextError(status.error or "Context query job failed")
392
- await asyncio.sleep(interval_ms / 1000)
413
+ remaining_s = deadline - loop.time()
414
+ if remaining_s <= 0:
415
+ break
416
+ await asyncio.sleep(min(interval_ms / 1000, remaining_s))
393
417
 
394
418
  raise ContextError(f"Context query job polling timed out after {timeout_ms}ms")
395
419
 
420
+ async def run_or_poll(
421
+ self,
422
+ query: str,
423
+ tools: list[str] | None = None,
424
+ favorites_only: bool | None = None,
425
+ agent_model_id: AgentModelId | str | None = None,
426
+ response_shape: QueryResponseShape | None = None,
427
+ include_data: bool | None = None,
428
+ include_data_url: bool | None = None,
429
+ include_developer_trace: bool | None = None,
430
+ resume_from: QueryAttemptReference | dict[str, Any] | None = None,
431
+ fork_from: QueryForkReference | dict[str, Any] | None = None,
432
+ idempotency_key: str | None = None,
433
+ interval_ms: int = DEFAULT_QUERY_POLL_INTERVAL_MS,
434
+ timeout_ms: int = DEFAULT_QUERY_POLL_TIMEOUT_MS,
435
+ ) -> QueryResult:
436
+ """Run a durable query job and wait internally for completion.
437
+
438
+ This is the recommended one-call helper for LLM agents: the entire
439
+ wait happens inside this single call (one model turn), instead of one
440
+ turn per ``get_status()`` check. It also avoids starting a duplicate
441
+ paid query after a client-side streaming timeout. The job is bounded
442
+ by the 1800s hosted compute ceiling.
443
+ """
444
+ job = await self.start(
445
+ query=query,
446
+ tools=tools,
447
+ favorites_only=favorites_only,
448
+ agent_model_id=agent_model_id,
449
+ response_shape=response_shape,
450
+ include_data=include_data,
451
+ include_data_url=include_data_url,
452
+ include_developer_trace=include_developer_trace,
453
+ resume_from=resume_from,
454
+ fork_from=fork_from,
455
+ idempotency_key=idempotency_key,
456
+ )
457
+ completed = await self.poll(
458
+ job.job_id,
459
+ interval_ms=interval_ms,
460
+ timeout_ms=timeout_ms,
461
+ )
462
+ if completed.status == "completed" and completed.result is not None:
463
+ return completed.result
464
+ raise ContextError(completed.error or "Context query job completed without a result")
465
+
396
466
  async def stream(
397
467
  self,
398
468
  query: str,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ctxprotocol"
7
- version = "0.19.1"
7
+ version = "0.20.0"
8
8
  description = "Official Python SDK for the Context Protocol - Discover and execute AI tools programmatically"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -11,7 +11,7 @@ from __future__ import annotations
11
11
  import json
12
12
  from pathlib import Path
13
13
  from typing import Any
14
- from unittest.mock import AsyncMock, MagicMock, patch
14
+ from unittest.mock import AsyncMock, patch
15
15
 
16
16
  import httpx
17
17
  import pytest
@@ -1631,3 +1631,43 @@ class TestQueryJobs:
1631
1631
 
1632
1632
  assert status.status == "completed"
1633
1633
  assert mock_fetch.await_count == 2
1634
+
1635
+ async def test_run_or_poll_returns_completed_query_result(self) -> None:
1636
+ client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
1637
+ start_payload = {
1638
+ "status": "running",
1639
+ "jobId": "11111111-1111-4111-8111-111111111111",
1640
+ "pollingTool": "context_query_poll",
1641
+ "message": "running",
1642
+ "progress": None,
1643
+ "querySession": None,
1644
+ "createdAt": "2026-06-14T00:00:00.000Z",
1645
+ "updatedAt": "2026-06-14T00:00:00.000Z",
1646
+ }
1647
+ completed_payload = {
1648
+ "status": "completed",
1649
+ "jobId": "11111111-1111-4111-8111-111111111111",
1650
+ "progress": None,
1651
+ "querySession": None,
1652
+ "result": MOCK_SUCCESS_RESPONSE,
1653
+ "error": None,
1654
+ "createdAt": "2026-06-14T00:00:00.000Z",
1655
+ "updatedAt": "2026-06-14T00:01:00.000Z",
1656
+ "completedAt": "2026-06-14T00:01:00.000Z",
1657
+ }
1658
+
1659
+ with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
1660
+ mock_fetch.side_effect = [start_payload, completed_payload]
1661
+ result = await client.query.run_or_poll(
1662
+ "Analyze whale activity",
1663
+ interval_ms=1,
1664
+ timeout_ms=1000,
1665
+ )
1666
+
1667
+ assert result.response == MOCK_SUCCESS_RESPONSE["response"]
1668
+ assert mock_fetch.await_count == 2
1669
+ assert mock_fetch.await_args_list[0].args[0] == "/api/v1/query/jobs"
1670
+ assert (
1671
+ mock_fetch.await_args_list[1].args[0]
1672
+ == "/api/v1/query/jobs/11111111-1111-4111-8111-111111111111"
1673
+ )
@@ -205,7 +205,7 @@ wheels = [
205
205
 
206
206
  [[package]]
207
207
  name = "ctxprotocol"
208
- version = "0.19.1"
208
+ version = "0.20.0"
209
209
  source = { editable = "." }
210
210
  dependencies = [
211
211
  { name = "cryptography" },
File without changes
File without changes
File without changes