ctxprotocol 0.17.0__tar.gz → 0.18.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.17.0 → ctxprotocol-0.18.0}/.gitignore +1 -0
  2. ctxprotocol-0.18.0/CHANGELOG.md +7 -0
  3. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/PKG-INFO +23 -5
  4. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/README.md +22 -4
  5. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/__init__.py +7 -1
  6. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/client/__init__.py +6 -0
  7. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/client/resources/query.py +114 -0
  8. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/client/types.py +47 -19
  9. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/pyproject.toml +1 -1
  10. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/tests/test_query.py +101 -0
  11. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/uv.lock +1 -1
  12. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/.codexignore +0 -0
  13. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/auth/__init__.py +0 -0
  14. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/client/client.py +0 -0
  15. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/client/resources/__init__.py +0 -0
  16. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/client/resources/developer.py +0 -0
  17. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/client/resources/discovery.py +0 -0
  18. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/client/resources/tools.py +0 -0
  19. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/context/__init__.py +0 -0
  20. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/context/hyperliquid.py +0 -0
  21. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/context/polymarket.py +0 -0
  22. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/context/wallet.py +0 -0
  23. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/contrib/__init__.py +0 -0
  24. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/contrib/search/__init__.py +0 -0
  25. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/contrib/search/core.py +0 -0
  26. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/contrib/search/trace.py +0 -0
  27. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/contrib/search/types.py +0 -0
  28. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/contrib/search/validation.py +0 -0
  29. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/handshake/__init__.py +0 -0
  30. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/ctxprotocol/py.typed +0 -0
  31. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/docs/README.md +0 -0
  32. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/client/execute_client.py +0 -0
  33. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/client/polymarket_query_trace_validation.py +0 -0
  34. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/client/query_client.py +0 -0
  35. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/client/test_favorites_only.py +0 -0
  36. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/client/test_get_tool.py +0 -0
  37. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/client/two_surfaces_client.py +0 -0
  38. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/client/validation/generate_contributor_search_validation_artifacts.py +0 -0
  39. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/client/validation/named-regression-iran-boots-on-ground.json +0 -0
  40. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/client/validation/named-regression-supreme-court-tariffs.json +0 -0
  41. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/client/validation/shared-capability-miss-unsupported-venue.json +0 -0
  42. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/client/validation/shared-generic-overlap-best-match.json +0 -0
  43. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/client/validation/shared-still-ambiguous-shortlist.json +0 -0
  44. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/server/hummingbot-contributor/README.md +0 -0
  45. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/server/hummingbot-contributor/env.example +0 -0
  46. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/server/hummingbot-contributor/requirements.txt +0 -0
  47. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/examples/server/hummingbot-contributor/server.py +0 -0
  48. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/polymarket-query-trace-results-py.json +0 -0
  49. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/tests/__init__.py +0 -0
  50. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/tests/contrib_search_validation_cases.py +0 -0
  51. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/tests/test_client.py +0 -0
  52. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/tests/test_contrib_search.py +0 -0
  53. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/tests/test_discovery.py +0 -0
  54. {ctxprotocol-0.17.0 → ctxprotocol-0.18.0}/tests/test_tools.py +0 -0
@@ -43,6 +43,7 @@ htmlcov/
43
43
  # md files
44
44
  *.md
45
45
  !README.md
46
+ !CHANGELOG.md
46
47
  !docs/mcp-builder-template.md
47
48
  !docs/mcp-server-analysis-prompt.md
48
49
  !docs/mcp-contributor-deep-validation-system-prompt.md
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 0.18.0
4
+
5
+ - Added durable async Query jobs with `client.query.start()`, `client.query.get_status()`, and `client.query.poll()`.
6
+ - Exported `QueryJobStartResult`, `QueryJobStatusResult`, and `QueryJobStatus` from the root and client entry points.
7
+ - Kept existing `client.query.run()` and `client.query.stream()` behavior unchanged.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctxprotocol
3
- Version: 0.17.0
3
+ Version: 0.18.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,6 +149,24 @@ 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:
153
+
154
+ ```python
155
+ job = await client.query.start(
156
+ query="Build a chart-ready dataset of Base whale movements over the last 30 days",
157
+ response_shape="evidence_only",
158
+ include_data_url=True,
159
+ )
160
+
161
+ completed = await client.query.poll(
162
+ job.job_id,
163
+ interval_ms=2000,
164
+ timeout_ms=15 * 60_000,
165
+ )
166
+
167
+ print(completed.result.data_url if completed.result else None)
168
+ ```
169
+
152
170
  To see valid model slugs without guessing:
153
171
 
154
172
  ```python
@@ -166,7 +184,7 @@ print(AGENT_MODEL_IDS) # Supported agent_model_id values
166
184
  `response_shape` options:
167
185
 
168
186
  - `answer_with_evidence` (default): prose plus `summary`, `evidence`, `artifacts`, `freshness`, `confidence`, `usage`, `outcome`, and `controller`
169
- - `evidence_only`: raw fetched data, computed artifacts, and provenance for downstream agents (no prose synthesis)
187
+ - `evidence_only`: bounded evidence, computed artifacts, and full-data references for downstream agents (no prose synthesis)
170
188
 
171
189
  Premium wedge answers can also expose `evidence.market_intelligence`, `view.rows`, `view.columns`, and the top-level controller fields `stop_reason`, `issue_class`, and `actions_taken`.
172
190
 
@@ -327,7 +345,7 @@ answer = await client.query.run(
327
345
  query="Analyze whale activity on Base",
328
346
  tools=["tool-uuid-1", "tool-uuid-2"], # optional — auto-discover if omitted
329
347
  agent_model_id="kimi-k2.6-model", # optional main librarian agent model
330
- include_data=True, # optional: include execution data inline
348
+ include_data=True, # optional: include bounded execution data inline
331
349
  include_data_url=True, # optional: include blob URL for full data
332
350
  include_developer_trace=True, # optional: include Developer Mode trace
333
351
  )
@@ -336,7 +354,7 @@ print(answer.response) # response text or summary
336
354
  print(answer.tools_used) # [QueryToolUsage(id, name, skill_calls)]
337
355
  print(answer.cost) # QueryCost(model_cost_usd, tool_cost_usd, total_cost_usd)
338
356
  print(answer.duration_ms) # Total time
339
- print(answer.data) # Optional execution data (when include_data=True)
357
+ print(answer.data) # Optional bounded data or truncation preview
340
358
  print(answer.data_url) # Optional blob URL (when include_data_url=True)
341
359
  print(answer.developer_trace.summary if answer.developer_trace else None)
342
360
  print(
@@ -347,7 +365,7 @@ print(
347
365
  print(answer.orchestration_metrics) # Optional first-pass / rediscovery metrics
348
366
  ```
349
367
 
350
- When retrieval-first synthesis rollout is enabled server-side, full-data or truncation-sensitive query requests can switch to retrieval-first context assembly using private stage artifacts and canonical execution data slices. `include_data` and `include_data_url` continue to reference the same canonical dataset used for synthesis.
368
+ When retrieval-first synthesis rollout is enabled server-side, full-data or truncation-sensitive query requests can switch to retrieval-first context assembly using private stage artifacts and canonical execution data slices. `include_data` returns a bounded inline preview when needed, and `include_data_url`/`artifacts.canonical_data_ref` reference the same canonical dataset used for synthesis.
351
369
 
352
370
  #### `client.query.stream(query, tools?, agent_model_id?, include_data?, include_data_url?, include_developer_trace?, idempotency_key?)`
353
371
 
@@ -111,6 +111,24 @@ 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:
115
+
116
+ ```python
117
+ job = await client.query.start(
118
+ query="Build a chart-ready dataset of Base whale movements over the last 30 days",
119
+ response_shape="evidence_only",
120
+ include_data_url=True,
121
+ )
122
+
123
+ completed = await client.query.poll(
124
+ job.job_id,
125
+ interval_ms=2000,
126
+ timeout_ms=15 * 60_000,
127
+ )
128
+
129
+ print(completed.result.data_url if completed.result else None)
130
+ ```
131
+
114
132
  To see valid model slugs without guessing:
115
133
 
116
134
  ```python
@@ -128,7 +146,7 @@ print(AGENT_MODEL_IDS) # Supported agent_model_id values
128
146
  `response_shape` options:
129
147
 
130
148
  - `answer_with_evidence` (default): prose plus `summary`, `evidence`, `artifacts`, `freshness`, `confidence`, `usage`, `outcome`, and `controller`
131
- - `evidence_only`: raw fetched data, computed artifacts, and provenance for downstream agents (no prose synthesis)
149
+ - `evidence_only`: bounded evidence, computed artifacts, and full-data references for downstream agents (no prose synthesis)
132
150
 
133
151
  Premium wedge answers can also expose `evidence.market_intelligence`, `view.rows`, `view.columns`, and the top-level controller fields `stop_reason`, `issue_class`, and `actions_taken`.
134
152
 
@@ -289,7 +307,7 @@ answer = await client.query.run(
289
307
  query="Analyze whale activity on Base",
290
308
  tools=["tool-uuid-1", "tool-uuid-2"], # optional — auto-discover if omitted
291
309
  agent_model_id="kimi-k2.6-model", # optional main librarian agent model
292
- include_data=True, # optional: include execution data inline
310
+ include_data=True, # optional: include bounded execution data inline
293
311
  include_data_url=True, # optional: include blob URL for full data
294
312
  include_developer_trace=True, # optional: include Developer Mode trace
295
313
  )
@@ -298,7 +316,7 @@ print(answer.response) # response text or summary
298
316
  print(answer.tools_used) # [QueryToolUsage(id, name, skill_calls)]
299
317
  print(answer.cost) # QueryCost(model_cost_usd, tool_cost_usd, total_cost_usd)
300
318
  print(answer.duration_ms) # Total time
301
- print(answer.data) # Optional execution data (when include_data=True)
319
+ print(answer.data) # Optional bounded data or truncation preview
302
320
  print(answer.data_url) # Optional blob URL (when include_data_url=True)
303
321
  print(answer.developer_trace.summary if answer.developer_trace else None)
304
322
  print(
@@ -309,7 +327,7 @@ print(
309
327
  print(answer.orchestration_metrics) # Optional first-pass / rediscovery metrics
310
328
  ```
311
329
 
312
- When retrieval-first synthesis rollout is enabled server-side, full-data or truncation-sensitive query requests can switch to retrieval-first context assembly using private stage artifacts and canonical execution data slices. `include_data` and `include_data_url` continue to reference the same canonical dataset used for synthesis.
330
+ When retrieval-first synthesis rollout is enabled server-side, full-data or truncation-sensitive query requests can switch to retrieval-first context assembly using private stage artifacts and canonical execution data slices. `include_data` returns a bounded inline preview when needed, and `include_data_url`/`artifacts.canonical_data_ref` reference the same canonical dataset used for synthesis.
313
331
 
314
332
  #### `client.query.stream(query, tools?, agent_model_id?, include_data?, include_data_url?, include_developer_trace?, idempotency_key?)`
315
333
 
@@ -31,7 +31,7 @@ Example:
31
31
  For more information, visit: https://ctxprotocol.com
32
32
  """
33
33
 
34
- __version__ = "0.17.0"
34
+ __version__ = "0.18.0"
35
35
 
36
36
  # Re-export everything from client module
37
37
  from ctxprotocol.client import (
@@ -70,6 +70,9 @@ from ctxprotocol.client.types import (
70
70
  QueryDeveloperTraceSummary,
71
71
  QueryDeveloperTraceStep,
72
72
  QueryDeveloperTraceToolRef,
73
+ QueryJobStartResult,
74
+ QueryJobStatus,
75
+ QueryJobStatusResult,
73
76
  QueryOrchestrationMetrics,
74
77
  QueryDeveloperTraceLoopInfo,
75
78
  QueryStreamDeveloperTraceEvent,
@@ -195,6 +198,9 @@ __all__ = [
195
198
  "QueryDeveloperTraceSummary",
196
199
  "QueryDeveloperTraceStep",
197
200
  "QueryDeveloperTraceToolRef",
201
+ "QueryJobStartResult",
202
+ "QueryJobStatus",
203
+ "QueryJobStatusResult",
198
204
  "QueryOrchestrationMetrics",
199
205
  "QueryDeveloperTraceLoopInfo",
200
206
  "QueryApiSuccessResponse",
@@ -51,6 +51,9 @@ from ctxprotocol.client.types import (
51
51
  QueryDeveloperTraceSummary,
52
52
  QueryDeveloperTraceStep,
53
53
  QueryDeveloperTraceToolRef,
54
+ QueryJobStartResult,
55
+ QueryJobStatus,
56
+ QueryJobStatusResult,
54
57
  QueryOrchestrationMetrics,
55
58
  QueryDeveloperTraceLoopInfo,
56
59
  QueryStreamDeveloperTraceEvent,
@@ -111,6 +114,9 @@ __all__ = [
111
114
  "QueryDeveloperTraceSummary",
112
115
  "QueryDeveloperTraceStep",
113
116
  "QueryDeveloperTraceToolRef",
117
+ "QueryJobStartResult",
118
+ "QueryJobStatus",
119
+ "QueryJobStatusResult",
114
120
  "QueryOrchestrationMetrics",
115
121
  "QueryDeveloperTraceLoopInfo",
116
122
  "QueryApiSuccessResponse",
@@ -9,6 +9,7 @@ execute -> synthesize -> settle) and AI synthesis — all for one flat fee.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import asyncio
12
13
  import json
13
14
  from typing import TYPE_CHECKING, Any, AsyncGenerator
14
15
 
@@ -18,6 +19,8 @@ from ctxprotocol.client.types import (
18
19
  QueryAttemptReference,
19
20
  QueryDeveloperTrace,
20
21
  QueryForkReference,
22
+ QueryJobStartResult,
23
+ QueryJobStatusResult,
21
24
  QueryResponseShape,
22
25
  QueryResult,
23
26
  QueryStreamDeveloperTraceEvent,
@@ -43,6 +46,52 @@ class Query:
43
46
  """
44
47
  self._client = client
45
48
 
49
+ @staticmethod
50
+ def _build_request_body(
51
+ query: str,
52
+ tools: list[str] | None = None,
53
+ favorites_only: bool | None = None,
54
+ agent_model_id: AgentModelId | str | None = None,
55
+ response_shape: QueryResponseShape | None = None,
56
+ include_data: bool | None = None,
57
+ include_data_url: bool | None = None,
58
+ include_developer_trace: bool | None = None,
59
+ resume_from: QueryAttemptReference | dict[str, Any] | None = None,
60
+ fork_from: QueryForkReference | dict[str, Any] | None = None,
61
+ stream: bool | None = None,
62
+ ) -> dict[str, Any]:
63
+ request_body: dict[str, Any] = {
64
+ "query": query,
65
+ "tools": tools,
66
+ }
67
+ if stream is not None:
68
+ request_body["stream"] = stream
69
+ if resume_from is not None:
70
+ request_body["resumeFrom"] = (
71
+ resume_from.model_dump(by_alias=True, exclude_none=True)
72
+ if isinstance(resume_from, QueryAttemptReference)
73
+ else resume_from
74
+ )
75
+ if fork_from is not None:
76
+ request_body["forkFrom"] = (
77
+ fork_from.model_dump(by_alias=True, exclude_none=True)
78
+ if isinstance(fork_from, QueryForkReference)
79
+ else fork_from
80
+ )
81
+ if favorites_only is not None:
82
+ request_body["favoritesOnly"] = favorites_only
83
+ if agent_model_id is not None:
84
+ request_body["agentModelId"] = agent_model_id
85
+ if response_shape is not None:
86
+ request_body["responseShape"] = response_shape
87
+ if include_data is not None:
88
+ request_body["includeData"] = include_data
89
+ if include_data_url is not None:
90
+ request_body["includeDataUrl"] = include_data_url
91
+ if include_developer_trace is not None:
92
+ request_body["includeDeveloperTrace"] = include_developer_trace
93
+ return request_body
94
+
46
95
  @staticmethod
47
96
  def _merge_developer_trace(
48
97
  first: QueryDeveloperTrace | None,
@@ -278,6 +327,71 @@ class Query:
278
327
 
279
328
  raise ContextError("Streaming query ended before done event")
280
329
 
330
+ async def start(
331
+ self,
332
+ query: str,
333
+ tools: list[str] | None = None,
334
+ favorites_only: bool | None = None,
335
+ agent_model_id: AgentModelId | str | None = None,
336
+ response_shape: QueryResponseShape | None = None,
337
+ include_data: bool | None = None,
338
+ include_data_url: bool | None = None,
339
+ include_developer_trace: bool | None = None,
340
+ resume_from: QueryAttemptReference | dict[str, Any] | None = None,
341
+ fork_from: QueryForkReference | dict[str, Any] | None = None,
342
+ idempotency_key: str | None = None,
343
+ ) -> QueryJobStartResult:
344
+ """Start a durable async query job and return its job id immediately."""
345
+ request_body = self._build_request_body(
346
+ query=query,
347
+ tools=tools,
348
+ favorites_only=favorites_only,
349
+ agent_model_id=agent_model_id,
350
+ response_shape=response_shape,
351
+ include_data=include_data,
352
+ include_data_url=include_data_url,
353
+ include_developer_trace=include_developer_trace,
354
+ resume_from=resume_from,
355
+ fork_from=fork_from,
356
+ stream=False,
357
+ )
358
+ response = await self._client.fetch(
359
+ "/api/v1/query/jobs",
360
+ method="POST",
361
+ json_body=request_body,
362
+ extra_headers=(
363
+ {"Idempotency-Key": idempotency_key}
364
+ if idempotency_key
365
+ else None
366
+ ),
367
+ )
368
+ return QueryJobStartResult.model_validate(response)
369
+
370
+ async def get_status(self, job_id: str) -> QueryJobStatusResult:
371
+ """Fetch the current status for a durable async query job."""
372
+ response = await self._client.fetch(f"/api/v1/query/jobs/{job_id}")
373
+ return QueryJobStatusResult.model_validate(response)
374
+
375
+ async def poll(
376
+ self,
377
+ job_id: str,
378
+ interval_ms: int = 2000,
379
+ timeout_ms: int = 15 * 60_000,
380
+ ) -> QueryJobStatusResult:
381
+ """Poll a durable async query job until completion or failure."""
382
+ loop = asyncio.get_running_loop()
383
+ deadline = loop.time() + timeout_ms / 1000
384
+
385
+ while loop.time() <= deadline:
386
+ status = await self.get_status(job_id)
387
+ if status.status == "completed":
388
+ return status
389
+ if status.status == "failed":
390
+ raise ContextError(status.error or "Context query job failed")
391
+ await asyncio.sleep(interval_ms / 1000)
392
+
393
+ raise ContextError(f"Context query job polling timed out after {timeout_ms}ms")
394
+
281
395
  async def stream(
282
396
  self,
283
397
  query: str,
@@ -46,12 +46,9 @@ SuggestedPromptSource = Literal["contributor", "platform", "sdk"]
46
46
  DEFAULT_AGENT_MODEL_ID = "kimi-k2.6-model"
47
47
  AGENT_MODEL_IDS = (
48
48
  "kimi-k2.6-model",
49
+ "glm-5.2-model",
49
50
  "deepseek-v4-pro-model",
50
51
  "deepseek-v4-flash-model",
51
- "gemini-3.5-flash-model",
52
- "gemini-flash-model",
53
- "gemini-lite-model",
54
- "minimax-m3-model",
55
52
  "qwen-3.7-plus-model",
56
53
  "qwen-3.7-max-model",
57
54
  "gpt-5.5-model",
@@ -59,12 +56,9 @@ AGENT_MODEL_IDS = (
59
56
  )
60
57
  AgentModelId = Literal[
61
58
  "kimi-k2.6-model",
59
+ "glm-5.2-model",
62
60
  "deepseek-v4-pro-model",
63
61
  "deepseek-v4-flash-model",
64
- "gemini-3.5-flash-model",
65
- "gemini-flash-model",
66
- "gemini-lite-model",
67
- "minimax-m3-model",
68
62
  "qwen-3.7-plus-model",
69
63
  "qwen-3.7-max-model",
70
64
  "gpt-5.5-model",
@@ -745,21 +739,21 @@ class QueryOptions(BaseModel):
745
739
  description=(
746
740
  "Structured response mode. Defaults to `answer_with_evidence` on the "
747
741
  "server when omitted. The runtime always produces a grounded result "
748
- "(raw data + computed artifacts + provenance); this controls whether "
749
- "a prose synthesis layer is added on top. "
742
+ "(bounded evidence + computed artifacts + full-data references); "
743
+ "this controls whether a prose synthesis layer is added on top. "
750
744
  "`answer_with_evidence` returns prose plus the structured grounding "
751
745
  "(chat parity); `evidence_only` returns grounding only with no prose "
752
- "(the agent-harness shape, with raw data + computed_artifacts + "
753
- "grounding by default)."
746
+ "(the agent-harness shape, with bounded evidence, computed_artifacts, "
747
+ "and data_url/canonical_data_ref references)."
754
748
  ),
755
749
  )
756
750
  include_data: bool | None = Field(
757
751
  default=None,
758
752
  alias="includeData",
759
753
  description=(
760
- "Include raw execution data inline. Defaults to true for "
761
- "response_shape `evidence_only` (its primary payload is the raw "
762
- "fetched data); false otherwise. Set explicitly to override."
754
+ "Include bounded execution data inline. Defaults to false for every "
755
+ "response_shape. Large payloads are returned as a preview object "
756
+ "with fullData.dataUrl/canonicalDataRef instead of unbounded raw rows."
763
757
  ),
764
758
  )
765
759
  include_data_url: bool | None = Field(
@@ -1703,7 +1697,7 @@ class QueryResult(BaseModel):
1703
1697
  tools_used: Tools that were used to answer the query
1704
1698
  cost: Cost breakdown
1705
1699
  duration_ms: Total duration in milliseconds
1706
- data: Raw execution data (default for evidence_only; else when include_data is enabled)
1700
+ data: Bounded execution data when include_data is enabled
1707
1701
  data_url: Optional blob URL for execution data (when include_data_url is enabled)
1708
1702
  computed_artifacts: Optional chart artifacts emitted by code interpreter
1709
1703
  developer_trace: Optional machine-readable Developer Mode trace
@@ -1721,9 +1715,9 @@ class QueryResult(BaseModel):
1721
1715
  data: Any | None = Field(
1722
1716
  default=None,
1723
1717
  description=(
1724
- "Raw execution data from tools. Returned by default for "
1725
- "response_shape `evidence_only` (its primary payload); otherwise "
1726
- "only when include_data is true."
1718
+ "Bounded execution data from tools. Returned only when include_data "
1719
+ "is true. Small payloads may be direct data; large payloads are a "
1720
+ "truncation object with preview and fullData references."
1727
1721
  ),
1728
1722
  )
1729
1723
  data_url: str | None = Field(
@@ -1892,6 +1886,40 @@ class QueryApiSuccessResponse(BaseModel):
1892
1886
  model_config = {"populate_by_name": True}
1893
1887
 
1894
1888
 
1889
+ QueryJobStatus = Literal["queued", "running", "completed", "failed"]
1890
+
1891
+
1892
+ class QueryJobStartResult(BaseModel):
1893
+ """Response from starting a durable async query job."""
1894
+
1895
+ status: QueryJobStatus
1896
+ job_id: str = Field(..., alias="jobId")
1897
+ polling_tool: str | None = Field(default=None, alias="pollingTool")
1898
+ message: str | None = None
1899
+ progress: Any | None = None
1900
+ query_session: Any | None = Field(default=None, alias="querySession")
1901
+ created_at: str = Field(..., alias="createdAt")
1902
+ updated_at: str = Field(..., alias="updatedAt")
1903
+
1904
+ model_config = {"populate_by_name": True}
1905
+
1906
+
1907
+ class QueryJobStatusResult(BaseModel):
1908
+ """Current status for a durable async query job."""
1909
+
1910
+ status: QueryJobStatus
1911
+ job_id: str = Field(..., alias="jobId")
1912
+ progress: Any | None = None
1913
+ query_session: Any | None = Field(default=None, alias="querySession")
1914
+ result: QueryResult | None = None
1915
+ error: str | None = None
1916
+ created_at: str = Field(..., alias="createdAt")
1917
+ updated_at: str = Field(..., alias="updatedAt")
1918
+ completed_at: str | None = Field(default=None, alias="completedAt")
1919
+
1920
+ model_config = {"populate_by_name": True}
1921
+
1922
+
1895
1923
  class QueryStreamToolStatusEvent(BaseModel):
1896
1924
  """Emitted when a tool starts or changes execution status."""
1897
1925
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ctxprotocol"
7
- version = "0.17.0"
7
+ version = "0.18.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"
@@ -1466,3 +1466,104 @@ class TestQueryStream:
1466
1466
  events.append(event)
1467
1467
 
1468
1468
  assert len(events) == 1
1469
+
1470
+
1471
+ class TestQueryJobs:
1472
+ """Tests for durable async query job helpers."""
1473
+
1474
+ async def test_start_creates_query_job(self) -> None:
1475
+ client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
1476
+ payload = {
1477
+ "status": "running",
1478
+ "jobId": "11111111-1111-4111-8111-111111111111",
1479
+ "pollingTool": "context_query_status",
1480
+ "message": "running",
1481
+ "progress": None,
1482
+ "querySession": None,
1483
+ "createdAt": "2026-06-14T00:00:00.000Z",
1484
+ "updatedAt": "2026-06-14T00:00:00.000Z",
1485
+ }
1486
+
1487
+ with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
1488
+ mock_fetch.return_value = payload
1489
+ job = await client.query.start(
1490
+ query="long query",
1491
+ response_shape="evidence_only",
1492
+ include_data_url=True,
1493
+ idempotency_key="21118fda-33be-4d66-8df5-0e50b3371f54",
1494
+ )
1495
+
1496
+ assert job.job_id == "11111111-1111-4111-8111-111111111111"
1497
+ mock_fetch.assert_called_once_with(
1498
+ "/api/v1/query/jobs",
1499
+ method="POST",
1500
+ json_body={
1501
+ "query": "long query",
1502
+ "tools": None,
1503
+ "stream": False,
1504
+ "responseShape": "evidence_only",
1505
+ "includeDataUrl": True,
1506
+ },
1507
+ extra_headers={
1508
+ "Idempotency-Key": "21118fda-33be-4d66-8df5-0e50b3371f54",
1509
+ },
1510
+ )
1511
+
1512
+ async def test_get_status_fetches_query_job(self) -> None:
1513
+ client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
1514
+ payload = {
1515
+ "status": "completed",
1516
+ "jobId": "11111111-1111-4111-8111-111111111111",
1517
+ "progress": None,
1518
+ "querySession": None,
1519
+ "result": MOCK_SUCCESS_RESPONSE,
1520
+ "error": None,
1521
+ "createdAt": "2026-06-14T00:00:00.000Z",
1522
+ "updatedAt": "2026-06-14T00:01:00.000Z",
1523
+ "completedAt": "2026-06-14T00:01:00.000Z",
1524
+ }
1525
+
1526
+ with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
1527
+ mock_fetch.return_value = payload
1528
+ status = await client.query.get_status(
1529
+ "11111111-1111-4111-8111-111111111111"
1530
+ )
1531
+
1532
+ assert status.status == "completed"
1533
+ assert status.result is not None
1534
+ assert status.result.response == MOCK_SUCCESS_RESPONSE["response"]
1535
+ mock_fetch.assert_called_once_with(
1536
+ "/api/v1/query/jobs/11111111-1111-4111-8111-111111111111"
1537
+ )
1538
+
1539
+ async def test_poll_waits_until_query_job_completes(self) -> None:
1540
+ client = ContextClient(api_key="ctx_test_key_1234567890abcdef12345678")
1541
+ running_payload = {
1542
+ "status": "running",
1543
+ "jobId": "11111111-1111-4111-8111-111111111111",
1544
+ "progress": None,
1545
+ "querySession": None,
1546
+ "result": None,
1547
+ "error": None,
1548
+ "createdAt": "2026-06-14T00:00:00.000Z",
1549
+ "updatedAt": "2026-06-14T00:00:30.000Z",
1550
+ "completedAt": None,
1551
+ }
1552
+ completed_payload = {
1553
+ **running_payload,
1554
+ "status": "completed",
1555
+ "result": MOCK_SUCCESS_RESPONSE,
1556
+ "updatedAt": "2026-06-14T00:01:00.000Z",
1557
+ "completedAt": "2026-06-14T00:01:00.000Z",
1558
+ }
1559
+
1560
+ with patch.object(client, "fetch", new_callable=AsyncMock) as mock_fetch:
1561
+ mock_fetch.side_effect = [running_payload, completed_payload]
1562
+ status = await client.query.poll(
1563
+ "11111111-1111-4111-8111-111111111111",
1564
+ interval_ms=1,
1565
+ timeout_ms=1000,
1566
+ )
1567
+
1568
+ assert status.status == "completed"
1569
+ assert mock_fetch.await_count == 2
@@ -205,7 +205,7 @@ wheels = [
205
205
 
206
206
  [[package]]
207
207
  name = "ctxprotocol"
208
- version = "0.17.0"
208
+ version = "0.18.0"
209
209
  source = { editable = "." }
210
210
  dependencies = [
211
211
  { name = "cryptography" },
File without changes