ctxprotocol 0.16.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.16.0 → ctxprotocol-0.18.0}/.gitignore +1 -0
  2. ctxprotocol-0.18.0/CHANGELOG.md +7 -0
  3. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/PKG-INFO +36 -9
  4. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/README.md +35 -8
  5. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/__init__.py +13 -1
  6. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/client/__init__.py +12 -0
  7. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/client/resources/query.py +122 -7
  8. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/client/types.py +74 -15
  9. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/pyproject.toml +1 -1
  10. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/tests/test_query.py +111 -8
  11. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/uv.lock +1 -1
  12. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/.codexignore +0 -0
  13. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/auth/__init__.py +0 -0
  14. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/client/client.py +0 -0
  15. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/client/resources/__init__.py +0 -0
  16. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/client/resources/developer.py +0 -0
  17. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/client/resources/discovery.py +0 -0
  18. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/client/resources/tools.py +0 -0
  19. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/context/__init__.py +0 -0
  20. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/context/hyperliquid.py +0 -0
  21. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/context/polymarket.py +0 -0
  22. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/context/wallet.py +0 -0
  23. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/contrib/__init__.py +0 -0
  24. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/contrib/search/__init__.py +0 -0
  25. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/contrib/search/core.py +0 -0
  26. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/contrib/search/trace.py +0 -0
  27. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/contrib/search/types.py +0 -0
  28. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/contrib/search/validation.py +0 -0
  29. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/handshake/__init__.py +0 -0
  30. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/ctxprotocol/py.typed +0 -0
  31. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/docs/README.md +0 -0
  32. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/client/execute_client.py +0 -0
  33. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/client/polymarket_query_trace_validation.py +0 -0
  34. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/client/query_client.py +0 -0
  35. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/client/test_favorites_only.py +0 -0
  36. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/client/test_get_tool.py +0 -0
  37. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/client/two_surfaces_client.py +0 -0
  38. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/client/validation/generate_contributor_search_validation_artifacts.py +0 -0
  39. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/client/validation/named-regression-iran-boots-on-ground.json +0 -0
  40. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/client/validation/named-regression-supreme-court-tariffs.json +0 -0
  41. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/client/validation/shared-capability-miss-unsupported-venue.json +0 -0
  42. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/client/validation/shared-generic-overlap-best-match.json +0 -0
  43. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/client/validation/shared-still-ambiguous-shortlist.json +0 -0
  44. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/server/hummingbot-contributor/README.md +0 -0
  45. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/server/hummingbot-contributor/env.example +0 -0
  46. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/server/hummingbot-contributor/requirements.txt +0 -0
  47. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/examples/server/hummingbot-contributor/server.py +0 -0
  48. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/polymarket-query-trace-results-py.json +0 -0
  49. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/tests/__init__.py +0 -0
  50. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/tests/contrib_search_validation_cases.py +0 -0
  51. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/tests/test_client.py +0 -0
  52. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/tests/test_contrib_search.py +0 -0
  53. {ctxprotocol-0.16.0 → ctxprotocol-0.18.0}/tests/test_discovery.py +0 -0
  54. {ctxprotocol-0.16.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.16.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
@@ -129,7 +129,7 @@ print(result.session) # method_price, spent, remaining, max_spend, ...
129
129
  ```python
130
130
  answer = await client.query.run(
131
131
  query="What are the top whale movements on Base?",
132
- answer_model_id="glm-model", # optional: choose the final synthesis model
132
+ agent_model_id="kimi-k2.6-model", # optional: choose the main librarian agent model
133
133
  response_shape="answer_with_evidence", # optional: answer_with_evidence (default) | evidence_only
134
134
  include_data_url=True, # optional: persist full execution data to blob
135
135
  include_developer_trace=True, # optional: include runtime developer trace
@@ -149,6 +149,33 @@ 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
+
170
+ To see valid model slugs without guessing:
171
+
172
+ ```python
173
+ from ctxprotocol import AGENT_MODEL_IDS, DEFAULT_AGENT_MODEL_ID
174
+
175
+ print(DEFAULT_AGENT_MODEL_ID) # "kimi-k2.6-model"
176
+ print(AGENT_MODEL_IDS) # Supported agent_model_id values
177
+ ```
178
+
152
179
  > Mixed listings are first-class: one listing can expose methods to both surfaces. Methods without `_meta.pricing.executeUsd` remain query-only until priced.
153
180
  >
154
181
  > Compatibility: SDK/API payload fields such as `price` and `price_per_query` are retained for backward compatibility. In Query mode, they represent listing-level **price per response turn**.
@@ -157,7 +184,7 @@ print(answer.orchestration_metrics) # Optional first-pass / rediscovery metrics
157
184
  `response_shape` options:
158
185
 
159
186
  - `answer_with_evidence` (default): prose plus `summary`, `evidence`, `artifacts`, `freshness`, `confidence`, `usage`, `outcome`, and `controller`
160
- - `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)
161
188
 
162
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`.
163
190
 
@@ -297,7 +324,7 @@ closed = await client.tools.close_session("sess_123")
297
324
 
298
325
  ### Query (Pay-Per-Response)
299
326
 
300
- #### `client.query.run(query, tools?, answer_model_id?, include_data?, include_data_url?, include_developer_trace?, idempotency_key?)`
327
+ #### `client.query.run(query, tools?, agent_model_id?, include_data?, include_data_url?, include_developer_trace?, idempotency_key?)`
301
328
 
302
329
  Run an agentic query. The server applies the live librarian pipeline (`discover -> select -> iterative execute -> synthesize -> settle`) with up to 100 MCP calls per response turn, then returns the selected Query response contract (`answer_with_evidence` or `evidence_only`, default `answer_with_evidence`).
303
330
 
@@ -317,8 +344,8 @@ answer = await client.query.run("What are the top whale movements on Base?")
317
344
  answer = await client.query.run(
318
345
  query="Analyze whale activity on Base",
319
346
  tools=["tool-uuid-1", "tool-uuid-2"], # optional — auto-discover if omitted
320
- answer_model_id="kimi-model-thinking", # optional final synthesis model
321
- include_data=True, # optional: include execution data inline
347
+ agent_model_id="kimi-k2.6-model", # optional main librarian agent model
348
+ include_data=True, # optional: include bounded execution data inline
322
349
  include_data_url=True, # optional: include blob URL for full data
323
350
  include_developer_trace=True, # optional: include Developer Mode trace
324
351
  )
@@ -327,7 +354,7 @@ print(answer.response) # response text or summary
327
354
  print(answer.tools_used) # [QueryToolUsage(id, name, skill_calls)]
328
355
  print(answer.cost) # QueryCost(model_cost_usd, tool_cost_usd, total_cost_usd)
329
356
  print(answer.duration_ms) # Total time
330
- print(answer.data) # Optional execution data (when include_data=True)
357
+ print(answer.data) # Optional bounded data or truncation preview
331
358
  print(answer.data_url) # Optional blob URL (when include_data_url=True)
332
359
  print(answer.developer_trace.summary if answer.developer_trace else None)
333
360
  print(
@@ -338,9 +365,9 @@ print(
338
365
  print(answer.orchestration_metrics) # Optional first-pass / rediscovery metrics
339
366
  ```
340
367
 
341
- 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.
342
369
 
343
- #### `client.query.stream(query, tools?, answer_model_id?, include_data?, include_data_url?, include_developer_trace?, idempotency_key?)`
370
+ #### `client.query.stream(query, tools?, agent_model_id?, include_data?, include_data_url?, include_developer_trace?, idempotency_key?)`
344
371
 
345
372
  Same as `run()` but streams events in real-time via SSE.
346
373
 
@@ -91,7 +91,7 @@ print(result.session) # method_price, spent, remaining, max_spend, ...
91
91
  ```python
92
92
  answer = await client.query.run(
93
93
  query="What are the top whale movements on Base?",
94
- answer_model_id="glm-model", # optional: choose the final synthesis model
94
+ agent_model_id="kimi-k2.6-model", # optional: choose the main librarian agent model
95
95
  response_shape="answer_with_evidence", # optional: answer_with_evidence (default) | evidence_only
96
96
  include_data_url=True, # optional: persist full execution data to blob
97
97
  include_developer_trace=True, # optional: include runtime developer trace
@@ -111,6 +111,33 @@ 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
+
132
+ To see valid model slugs without guessing:
133
+
134
+ ```python
135
+ from ctxprotocol import AGENT_MODEL_IDS, DEFAULT_AGENT_MODEL_ID
136
+
137
+ print(DEFAULT_AGENT_MODEL_ID) # "kimi-k2.6-model"
138
+ print(AGENT_MODEL_IDS) # Supported agent_model_id values
139
+ ```
140
+
114
141
  > Mixed listings are first-class: one listing can expose methods to both surfaces. Methods without `_meta.pricing.executeUsd` remain query-only until priced.
115
142
  >
116
143
  > Compatibility: SDK/API payload fields such as `price` and `price_per_query` are retained for backward compatibility. In Query mode, they represent listing-level **price per response turn**.
@@ -119,7 +146,7 @@ print(answer.orchestration_metrics) # Optional first-pass / rediscovery metrics
119
146
  `response_shape` options:
120
147
 
121
148
  - `answer_with_evidence` (default): prose plus `summary`, `evidence`, `artifacts`, `freshness`, `confidence`, `usage`, `outcome`, and `controller`
122
- - `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)
123
150
 
124
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`.
125
152
 
@@ -259,7 +286,7 @@ closed = await client.tools.close_session("sess_123")
259
286
 
260
287
  ### Query (Pay-Per-Response)
261
288
 
262
- #### `client.query.run(query, tools?, answer_model_id?, include_data?, include_data_url?, include_developer_trace?, idempotency_key?)`
289
+ #### `client.query.run(query, tools?, agent_model_id?, include_data?, include_data_url?, include_developer_trace?, idempotency_key?)`
263
290
 
264
291
  Run an agentic query. The server applies the live librarian pipeline (`discover -> select -> iterative execute -> synthesize -> settle`) with up to 100 MCP calls per response turn, then returns the selected Query response contract (`answer_with_evidence` or `evidence_only`, default `answer_with_evidence`).
265
292
 
@@ -279,8 +306,8 @@ answer = await client.query.run("What are the top whale movements on Base?")
279
306
  answer = await client.query.run(
280
307
  query="Analyze whale activity on Base",
281
308
  tools=["tool-uuid-1", "tool-uuid-2"], # optional — auto-discover if omitted
282
- answer_model_id="kimi-model-thinking", # optional final synthesis model
283
- include_data=True, # optional: include execution data inline
309
+ agent_model_id="kimi-k2.6-model", # optional main librarian agent model
310
+ include_data=True, # optional: include bounded execution data inline
284
311
  include_data_url=True, # optional: include blob URL for full data
285
312
  include_developer_trace=True, # optional: include Developer Mode trace
286
313
  )
@@ -289,7 +316,7 @@ print(answer.response) # response text or summary
289
316
  print(answer.tools_used) # [QueryToolUsage(id, name, skill_calls)]
290
317
  print(answer.cost) # QueryCost(model_cost_usd, tool_cost_usd, total_cost_usd)
291
318
  print(answer.duration_ms) # Total time
292
- print(answer.data) # Optional execution data (when include_data=True)
319
+ print(answer.data) # Optional bounded data or truncation preview
293
320
  print(answer.data_url) # Optional blob URL (when include_data_url=True)
294
321
  print(answer.developer_trace.summary if answer.developer_trace else None)
295
322
  print(
@@ -300,9 +327,9 @@ print(
300
327
  print(answer.orchestration_metrics) # Optional first-pass / rediscovery metrics
301
328
  ```
302
329
 
303
- 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.
304
331
 
305
- #### `client.query.stream(query, tools?, answer_model_id?, include_data?, include_data_url?, include_developer_trace?, idempotency_key?)`
332
+ #### `client.query.stream(query, tools?, agent_model_id?, include_data?, include_data_url?, include_developer_trace?, idempotency_key?)`
306
333
 
307
334
  Same as `run()` but streams events in real-time via SSE.
308
335
 
@@ -31,7 +31,7 @@ Example:
31
31
  For more information, visit: https://ctxprotocol.com
32
32
  """
33
33
 
34
- __version__ = "0.15.0"
34
+ __version__ = "0.18.0"
35
35
 
36
36
  # Re-export everything from client module
37
37
  from ctxprotocol.client import (
@@ -43,8 +43,10 @@ from ctxprotocol.client import (
43
43
  Tools,
44
44
  )
45
45
  from ctxprotocol.client.types import (
46
+ AGENT_MODEL_IDS,
46
47
  ContextClientOptions,
47
48
  ContextErrorCode,
49
+ DEFAULT_AGENT_MODEL_ID,
48
50
  ExecuteApiErrorResponse,
49
51
  ExecuteApiSuccessResponse,
50
52
  ExecuteMethodInfo,
@@ -68,6 +70,9 @@ from ctxprotocol.client.types import (
68
70
  QueryDeveloperTraceSummary,
69
71
  QueryDeveloperTraceStep,
70
72
  QueryDeveloperTraceToolRef,
73
+ QueryJobStartResult,
74
+ QueryJobStatus,
75
+ QueryJobStatusResult,
71
76
  QueryOrchestrationMetrics,
72
77
  QueryDeveloperTraceLoopInfo,
73
78
  QueryStreamDeveloperTraceEvent,
@@ -83,6 +88,7 @@ from ctxprotocol.client.types import (
83
88
  SearchResponse,
84
89
  Tool,
85
90
  ToolInfo,
91
+ AgentModelId,
86
92
  )
87
93
 
88
94
  # Context types for portfolio injection
@@ -178,6 +184,9 @@ __all__ = [
178
184
  "ExecuteApiErrorResponse",
179
185
  "ExecuteSessionApiSuccessResponse",
180
186
  "ToolInfo",
187
+ "AGENT_MODEL_IDS",
188
+ "DEFAULT_AGENT_MODEL_ID",
189
+ "AgentModelId",
181
190
  # Query types (pay-per-response)
182
191
  "QueryOptions",
183
192
  "QueryResult",
@@ -189,6 +198,9 @@ __all__ = [
189
198
  "QueryDeveloperTraceSummary",
190
199
  "QueryDeveloperTraceStep",
191
200
  "QueryDeveloperTraceToolRef",
201
+ "QueryJobStartResult",
202
+ "QueryJobStatus",
203
+ "QueryJobStatusResult",
192
204
  "QueryOrchestrationMetrics",
193
205
  "QueryDeveloperTraceLoopInfo",
194
206
  "QueryApiSuccessResponse",
@@ -10,9 +10,11 @@ from ctxprotocol.client.resources.discovery import Discovery
10
10
  from ctxprotocol.client.resources.query import Query
11
11
  from ctxprotocol.client.resources.tools import Tools
12
12
  from ctxprotocol.client.types import (
13
+ AGENT_MODEL_IDS,
13
14
  ContextClientOptions,
14
15
  ContextError,
15
16
  ContextErrorCode,
17
+ DEFAULT_AGENT_MODEL_ID,
16
18
  ExecuteApiErrorResponse,
17
19
  ExecuteApiSuccessResponse,
18
20
  ExecuteMethodInfo,
@@ -49,6 +51,9 @@ from ctxprotocol.client.types import (
49
51
  QueryDeveloperTraceSummary,
50
52
  QueryDeveloperTraceStep,
51
53
  QueryDeveloperTraceToolRef,
54
+ QueryJobStartResult,
55
+ QueryJobStatus,
56
+ QueryJobStatusResult,
52
57
  QueryOrchestrationMetrics,
53
58
  QueryDeveloperTraceLoopInfo,
54
59
  QueryStreamDeveloperTraceEvent,
@@ -64,6 +69,7 @@ from ctxprotocol.client.types import (
64
69
  SearchResponse,
65
70
  Tool,
66
71
  ToolInfo,
72
+ AgentModelId,
67
73
  )
68
74
 
69
75
  __all__ = [
@@ -76,6 +82,9 @@ __all__ = [
76
82
  "Query",
77
83
  # Types
78
84
  "ContextClientOptions",
85
+ "AGENT_MODEL_IDS",
86
+ "DEFAULT_AGENT_MODEL_ID",
87
+ "AgentModelId",
79
88
  "Tool",
80
89
  "McpTool",
81
90
  "McpToolMeta",
@@ -105,6 +114,9 @@ __all__ = [
105
114
  "QueryDeveloperTraceSummary",
106
115
  "QueryDeveloperTraceStep",
107
116
  "QueryDeveloperTraceToolRef",
117
+ "QueryJobStartResult",
118
+ "QueryJobStatus",
119
+ "QueryJobStatusResult",
108
120
  "QueryOrchestrationMetrics",
109
121
  "QueryDeveloperTraceLoopInfo",
110
122
  "QueryApiSuccessResponse",
@@ -9,14 +9,18 @@ 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
 
15
16
  from ctxprotocol.client.types import (
16
17
  ContextError,
18
+ AgentModelId,
17
19
  QueryAttemptReference,
18
20
  QueryDeveloperTrace,
19
21
  QueryForkReference,
22
+ QueryJobStartResult,
23
+ QueryJobStatusResult,
20
24
  QueryResponseShape,
21
25
  QueryResult,
22
26
  QueryStreamDeveloperTraceEvent,
@@ -42,6 +46,52 @@ class Query:
42
46
  """
43
47
  self._client = client
44
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
+
45
95
  @staticmethod
46
96
  def _merge_developer_trace(
47
97
  first: QueryDeveloperTrace | None,
@@ -193,7 +243,7 @@ class Query:
193
243
  query: str,
194
244
  tools: list[str] | None = None,
195
245
  favorites_only: bool | None = None,
196
- answer_model_id: str | None = None,
246
+ agent_model_id: AgentModelId | str | None = None,
197
247
  response_shape: QueryResponseShape | None = None,
198
248
  include_data: bool | None = None,
199
249
  include_data_url: bool | None = None,
@@ -215,7 +265,7 @@ class Query:
215
265
  query: The natural-language question to answer
216
266
  tools: Optional tool IDs to use (auto-discover if not provided)
217
267
  favorites_only: Restrict auto-discovery to favorite tools for this request
218
- answer_model_id: Optional answer model ID for final synthesis
268
+ agent_model_id: Optional model ID for the main librarian agent loop
219
269
  response_shape: Structured response mode (`answer_with_evidence` or
220
270
  `evidence_only`; server default `answer_with_evidence`)
221
271
  include_data: Include execution data inline in the query response
@@ -253,7 +303,7 @@ class Query:
253
303
  query=query,
254
304
  tools=tools,
255
305
  favorites_only=favorites_only,
256
- answer_model_id=answer_model_id,
306
+ agent_model_id=agent_model_id,
257
307
  response_shape=response_shape,
258
308
  include_data=include_data,
259
309
  include_data_url=include_data_url,
@@ -277,12 +327,77 @@ class Query:
277
327
 
278
328
  raise ContextError("Streaming query ended before done event")
279
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
+
280
395
  async def stream(
281
396
  self,
282
397
  query: str,
283
398
  tools: list[str] | None = None,
284
399
  favorites_only: bool | None = None,
285
- answer_model_id: str | None = None,
400
+ agent_model_id: AgentModelId | str | None = None,
286
401
  response_shape: QueryResponseShape | None = None,
287
402
  include_data: bool | None = None,
288
403
  include_data_url: bool | None = None,
@@ -304,7 +419,7 @@ class Query:
304
419
  query: The natural-language question to answer
305
420
  tools: Optional tool IDs to use (auto-discover if not provided)
306
421
  favorites_only: Restrict auto-discovery to favorite tools for this request
307
- answer_model_id: Optional answer model ID for final synthesis
422
+ agent_model_id: Optional model ID for the main librarian agent loop
308
423
  response_shape: Structured response mode (`answer_with_evidence` or
309
424
  `evidence_only`; server default `answer_with_evidence`)
310
425
  include_data: Include execution data inline in the query response
@@ -345,8 +460,8 @@ class Query:
345
460
  )
346
461
  if favorites_only is not None:
347
462
  request_body["favoritesOnly"] = favorites_only
348
- if answer_model_id is not None:
349
- request_body["answerModelId"] = answer_model_id
463
+ if agent_model_id is not None:
464
+ request_body["agentModelId"] = agent_model_id
350
465
  if response_shape is not None:
351
466
  request_body["responseShape"] = response_shape
352
467
  if include_data is not None:
@@ -43,6 +43,27 @@ McpToolSurface = Literal["answer", "execute", "both"]
43
43
  McpToolLatencyClass = Literal["instant", "fast", "slow", "streaming"]
44
44
  ExecuteSessionStatus = Literal["open", "closed", "expired"]
45
45
  SuggestedPromptSource = Literal["contributor", "platform", "sdk"]
46
+ DEFAULT_AGENT_MODEL_ID = "kimi-k2.6-model"
47
+ AGENT_MODEL_IDS = (
48
+ "kimi-k2.6-model",
49
+ "glm-5.2-model",
50
+ "deepseek-v4-pro-model",
51
+ "deepseek-v4-flash-model",
52
+ "qwen-3.7-plus-model",
53
+ "qwen-3.7-max-model",
54
+ "gpt-5.5-model",
55
+ "claude-opus-model",
56
+ )
57
+ AgentModelId = Literal[
58
+ "kimi-k2.6-model",
59
+ "glm-5.2-model",
60
+ "deepseek-v4-pro-model",
61
+ "deepseek-v4-flash-model",
62
+ "qwen-3.7-plus-model",
63
+ "qwen-3.7-max-model",
64
+ "gpt-5.5-model",
65
+ "claude-opus-model",
66
+ ]
46
67
 
47
68
 
48
69
  class McpToolRateLimitHints(BaseModel):
@@ -674,7 +695,7 @@ class QueryOptions(BaseModel):
674
695
  Attributes:
675
696
  query: The natural-language question to answer
676
697
  tools: Optional tool IDs to use (auto-discover if not provided)
677
- answer_model_id: Optional answer model ID for final synthesis
698
+ agent_model_id: Optional model ID for the main librarian agent loop
678
699
  include_data: Include execution data inline in the query response
679
700
  include_data_url: Persist execution data to blob and return URL
680
701
  include_developer_trace: Include machine-readable developer runtime traces
@@ -703,10 +724,14 @@ class QueryOptions(BaseModel):
703
724
  "Ignored when explicit tools are provided."
704
725
  ),
705
726
  )
706
- answer_model_id: str | None = Field(
727
+ agent_model_id: AgentModelId | str | None = Field(
707
728
  default=None,
708
- alias="answerModelId",
709
- description="Optional answer model ID for final synthesis",
729
+ alias="agentModelId",
730
+ description=(
731
+ "Optional model ID for the main librarian agent loop. Controls the "
732
+ "merged iterative execution + final response stage; internal tool "
733
+ "selection remains managed by the server."
734
+ ),
710
735
  )
711
736
  response_shape: QueryResponseShape | None = Field(
712
737
  default=None,
@@ -714,21 +739,21 @@ class QueryOptions(BaseModel):
714
739
  description=(
715
740
  "Structured response mode. Defaults to `answer_with_evidence` on the "
716
741
  "server when omitted. The runtime always produces a grounded result "
717
- "(raw data + computed artifacts + provenance); this controls whether "
718
- "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. "
719
744
  "`answer_with_evidence` returns prose plus the structured grounding "
720
745
  "(chat parity); `evidence_only` returns grounding only with no prose "
721
- "(the agent-harness shape, with raw data + computed_artifacts + "
722
- "grounding by default)."
746
+ "(the agent-harness shape, with bounded evidence, computed_artifacts, "
747
+ "and data_url/canonical_data_ref references)."
723
748
  ),
724
749
  )
725
750
  include_data: bool | None = Field(
726
751
  default=None,
727
752
  alias="includeData",
728
753
  description=(
729
- "Include raw execution data inline. Defaults to true for "
730
- "response_shape `evidence_only` (its primary payload is the raw "
731
- "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."
732
757
  ),
733
758
  )
734
759
  include_data_url: bool | None = Field(
@@ -1672,7 +1697,7 @@ class QueryResult(BaseModel):
1672
1697
  tools_used: Tools that were used to answer the query
1673
1698
  cost: Cost breakdown
1674
1699
  duration_ms: Total duration in milliseconds
1675
- data: Raw execution data (default for evidence_only; else when include_data is enabled)
1700
+ data: Bounded execution data when include_data is enabled
1676
1701
  data_url: Optional blob URL for execution data (when include_data_url is enabled)
1677
1702
  computed_artifacts: Optional chart artifacts emitted by code interpreter
1678
1703
  developer_trace: Optional machine-readable Developer Mode trace
@@ -1690,9 +1715,9 @@ class QueryResult(BaseModel):
1690
1715
  data: Any | None = Field(
1691
1716
  default=None,
1692
1717
  description=(
1693
- "Raw execution data from tools. Returned by default for "
1694
- "response_shape `evidence_only` (its primary payload); otherwise "
1695
- "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."
1696
1721
  ),
1697
1722
  )
1698
1723
  data_url: str | None = Field(
@@ -1861,6 +1886,40 @@ class QueryApiSuccessResponse(BaseModel):
1861
1886
  model_config = {"populate_by_name": True}
1862
1887
 
1863
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
+
1864
1923
  class QueryStreamToolStatusEvent(BaseModel):
1865
1924
  """Emitted when a tool starts or changes execution status."""
1866
1925
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ctxprotocol"
7
- version = "0.16.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"
@@ -468,7 +468,7 @@ class TestQueryRun:
468
468
  mock_stream.return_value = _make_done_stream_response(success_with_data)
469
469
  result = await client.query.run(
470
470
  query="Analyze whale activity",
471
- answer_model_id="glm-model",
471
+ agent_model_id="kimi-k2.6-model",
472
472
  response_shape="answer_with_evidence",
473
473
  include_data=True,
474
474
  include_data_url=True,
@@ -482,7 +482,7 @@ class TestQueryRun:
482
482
  "query": "Analyze whale activity",
483
483
  "tools": None,
484
484
  "stream": True,
485
- "answerModelId": "glm-model",
485
+ "agentModelId": "kimi-k2.6-model",
486
486
  "responseShape": "answer_with_evidence",
487
487
  "includeData": True,
488
488
  "includeDataUrl": True,
@@ -571,7 +571,9 @@ class TestQueryRun:
571
571
  def test_query_options_supports_public_aliases(self) -> None:
572
572
  """QueryOptions accepts supported public aliases."""
573
573
  trace_alias = QueryOptions(query="test", includeDeveloperTrace=True)
574
- answer_model_alias = QueryOptions(query="test", answerModelId="glm-model")
574
+ agent_model_alias = QueryOptions(
575
+ query="test", agentModelId="kimi-k2.6-model"
576
+ )
575
577
  resume_alias = QueryOptions(
576
578
  query="test",
577
579
  resumeFrom={
@@ -596,10 +598,10 @@ class TestQueryRun:
596
598
  assert (
597
599
  trace_alias.model_dump(by_alias=True)["includeDeveloperTrace"] is True
598
600
  )
599
- assert answer_model_alias.answer_model_id == "glm-model"
601
+ assert agent_model_alias.agent_model_id == "kimi-k2.6-model"
600
602
  assert (
601
- answer_model_alias.model_dump(by_alias=True)["answerModelId"]
602
- == "glm-model"
603
+ agent_model_alias.model_dump(by_alias=True)["agentModelId"]
604
+ == "kimi-k2.6-model"
603
605
  )
604
606
  assert response_shape_alias.response_shape == "evidence_only"
605
607
  assert (
@@ -1216,7 +1218,7 @@ class TestQueryStream:
1216
1218
  events = []
1217
1219
  async for event in client.query.stream(
1218
1220
  query="test",
1219
- answer_model_id="claude-sonnet-model",
1221
+ agent_model_id="claude-sonnet-model",
1220
1222
  include_data=True,
1221
1223
  include_data_url=True,
1222
1224
  include_developer_trace=True,
@@ -1225,7 +1227,7 @@ class TestQueryStream:
1225
1227
 
1226
1228
  call_kwargs = mock_stream.call_args
1227
1229
  assert (
1228
- call_kwargs[1]["json_body"]["answerModelId"]
1230
+ call_kwargs[1]["json_body"]["agentModelId"]
1229
1231
  == "claude-sonnet-model"
1230
1232
  )
1231
1233
  assert call_kwargs[1]["json_body"]["includeData"] is True
@@ -1464,3 +1466,104 @@ class TestQueryStream:
1464
1466
  events.append(event)
1465
1467
 
1466
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.14.0"
208
+ version = "0.18.0"
209
209
  source = { editable = "." }
210
210
  dependencies = [
211
211
  { name = "cryptography" },
File without changes