splox 0.3.0__tar.gz → 0.3.2__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 (50) hide show
  1. splox-0.3.2/CHANGELOG.md +101 -0
  2. splox-0.3.2/PKG-INFO +469 -0
  3. splox-0.3.2/README.md +436 -0
  4. {splox-0.3.0 → splox-0.3.2}/pyproject.toml +1 -1
  5. splox-0.3.2/src/splox/__init__.py +110 -0
  6. {splox-0.3.0 → splox-0.3.2}/src/splox/_client.py +40 -36
  7. splox-0.3.2/src/splox/_discovery.py +102 -0
  8. splox-0.3.2/src/splox/_ids.py +70 -0
  9. splox-0.3.2/src/splox/_interactions.py +172 -0
  10. splox-0.3.2/src/splox/_mcp.py +426 -0
  11. splox-0.3.2/src/splox/_models.py +1074 -0
  12. splox-0.3.2/src/splox/_resources.py +42 -0
  13. splox-0.3.2/src/splox/_runs.py +714 -0
  14. splox-0.3.2/src/splox/_transport.py +501 -0
  15. splox-0.3.2/src/splox/_workflows.py +222 -0
  16. splox-0.3.2/src/splox/builder/__init__.py +542 -0
  17. splox-0.3.2/src/splox/evals/__init__.py +558 -0
  18. splox-0.3.2/src/splox/exceptions.py +229 -0
  19. splox-0.3.2/tests/integration_test.py +126 -0
  20. splox-0.3.2/tests/integration_test_mcp.py +124 -0
  21. splox-0.3.2/tests/integration_test_v2.py +315 -0
  22. splox-0.3.2/tests/test_builder.py +432 -0
  23. splox-0.3.2/tests/test_client.py +284 -0
  24. splox-0.3.2/tests/test_codemode_contract.py +204 -0
  25. splox-0.3.2/tests/test_discovery.py +194 -0
  26. splox-0.3.2/tests/test_evals.py +307 -0
  27. splox-0.3.2/tests/test_exceptions.py +92 -0
  28. splox-0.3.2/tests/test_ids.py +65 -0
  29. splox-0.3.2/tests/test_models.py +336 -0
  30. splox-0.3.2/tests/test_runs.py +637 -0
  31. splox-0.3.2/tests/test_sse.py +119 -0
  32. splox-0.3.2/tests/test_workflows.py +388 -0
  33. splox-0.3.0/PKG-INFO +0 -236
  34. splox-0.3.0/README.md +0 -203
  35. splox-0.3.0/src/splox/__init__.py +0 -76
  36. splox-0.3.0/src/splox/_models.py +0 -730
  37. splox-0.3.0/src/splox/_resources.py +0 -1029
  38. splox-0.3.0/src/splox/_transport.py +0 -220
  39. splox-0.3.0/src/splox/exceptions.py +0 -70
  40. splox-0.3.0/tests/integration_test.py +0 -322
  41. splox-0.3.0/tests/test_client.py +0 -434
  42. splox-0.3.0/tests/test_exceptions.py +0 -43
  43. splox-0.3.0/tests/test_models.py +0 -217
  44. splox-0.3.0/tests/test_sse.py +0 -65
  45. {splox-0.3.0 → splox-0.3.2}/.github/workflows/publish.yml +0 -0
  46. {splox-0.3.0 → splox-0.3.2}/.github/workflows/test.yml +0 -0
  47. {splox-0.3.0 → splox-0.3.2}/.gitignore +0 -0
  48. {splox-0.3.0 → splox-0.3.2}/LICENSE +0 -0
  49. {splox-0.3.0 → splox-0.3.2}/src/splox/py.typed +0 -0
  50. {splox-0.3.0 → splox-0.3.2}/tests/__init__.py +0 -0
@@ -0,0 +1,101 @@
1
+ # Changelog
2
+
3
+ ## 0.2.1 — v2 API (breaking)
4
+
5
+ The run-execution surface now targets the Splox Core API v2
6
+ (`/v2/runs`, `/v2/interactions`, RFC 9457 problem+json, cursor pagination,
7
+ durable SSE event journals). Workflow CRUD reads, chats, memory, billing,
8
+ webhooks, LLM and the **entire MCP module are unchanged**.
9
+
10
+ ### Added
11
+
12
+ - `client.runs` (`Runs` / `AsyncRuns`):
13
+ - `create(workflow_id, input, *, metadata=None, conversation_id=None, workflow_version=None, idempotency_key=None)`
14
+ — returns a bound `Run` handle; `input` accepts a `str` shorthand, a full
15
+ MessageInput dict, or a list of content parts; an `Idempotency-Key`
16
+ (UUIDv4) is generated automatically when omitted; `workflow_id` accepts both
17
+ `wf_...` public ids and raw workflow UUIDs.
18
+ - `get`, `list` (cursor pagination: `{data, page:{has_more, next_cursor}}`,
19
+ filters: `status`, `workflow_id`, `created_after`, `created_before`),
20
+ `cancel` (idempotent), `wait(timeout=, poll_interval=)`.
21
+ - `list_events` (JSON pages) and `stream_events` (SSE with automatic
22
+ `Last-Event-ID` reconnect, at-least-once dedupe by event id, terminates
23
+ after the terminal `run.status_changed` event).
24
+ - `messages`, `outputs`, `tree`, `usage`, `pending_interactions`.
25
+ - `Run` handle methods: `wait`, `cancel`, `events(stream=True|False)`,
26
+ `messages`, `outputs`, `tree`, `usage`, `pending_interactions`, `refresh`.
27
+ - `client.interactions` (`Interactions` / `AsyncInteractions`):
28
+ `list(status=, run_id=, ...)`, `get`, and
29
+ `respond(id, type=..., **fields)` / `respond(id, response={...})` with an
30
+ auto-generated idempotency key.
31
+ - ID helpers: `splox.encode_id(prefix, uuid)` / `splox.decode_id(public_id)`
32
+ (Crockford base32 / ULID alphabet).
33
+ - v2 models: `Run`, `RunFailure`, `RunEvent`, `Message`, `ContentPart`,
34
+ `Output`, `RunTree`, `RunTreeNode`, `RunUsage`, `Interaction`,
35
+ `InteractionResponse`, `PageInfo` and page wrappers (`RunPage`,
36
+ `RunEventPage`, `MessagePage`, `OutputPage`, `InteractionPage`), plus
37
+ `TERMINAL_RUN_STATUSES`.
38
+ - Exceptions: `SploxBadRequestError` (400), `SploxConflictError` (409),
39
+ `SploxValidationError` (422, carries `errors[]` with JSON Pointer names),
40
+ `SploxServerError` (5xx). All API errors now expose `code`, `trace_id` and
41
+ `problem` parsed from RFC 9457 bodies. `SploxRateLimitError.retry_after`
42
+ is populated from `Retry-After`.
43
+ - Transport retries: GETs are retried up to 3 times with exponential backoff
44
+ on connection errors / 429 / 5xx; POSTs are retried only when they carry an
45
+ `Idempotency-Key` header (same key on every attempt).
46
+ - v2 URL resolution: `/v2/...` paths resolve against the server origin
47
+ (base URL with a trailing `/api/v1` or `/v1` stripped), so the default
48
+ base URL keeps working for both API generations.
49
+
50
+ ### Changed (BREAKING)
51
+
52
+ - **"Agent" was renamed to "workflow" across the v2 wire contract** and the
53
+ SDK follows: `runs.create(workflow_id=..., workflow_version=...)`,
54
+ `Run.workflow_id`, `runs.list(workflow_id=...)`, public id prefix `wf_`
55
+ (was `agt_`), error pointers `/workflow_id` / `/workflow_version`, and
56
+ codes `workflow_not_runnable` / `workflow_mismatch`.
57
+ - **`client.workflows` lost the run-execution surface.** Removed:
58
+ `workflows.run`, `workflows.listen`, `workflows.get_execution_tree`,
59
+ `workflows.get_history`, `workflows.stop`, `workflows.run_and_wait`
60
+ (sync and async). Replacement: `client.runs.create(...)`,
61
+ `run.events()` / `client.runs.stream_events(...)`, `client.runs.tree(...)`,
62
+ `client.runs.list(...)`, `client.runs.cancel(...)`,
63
+ `client.runs.create(...).wait(...)` respectively. Workflow CRUD reads
64
+ (`list`, `get`, `get_latest_version`, `list_versions`, `get_entry_nodes`)
65
+ and secrets management remain on v1 and are unchanged.
66
+ - **Removed models** (v1 run-execution): `RunResponse`,
67
+ `ExecutionTreeResponse`, `HistoryResponse`, `ExecutionTree`,
68
+ `ExecutionNode`, `ChildExecution`. The v2 equivalents are `Run`, `RunTree`,
69
+ `RunTreeNode` and `RunPage`. (`WorkflowRequest`, `NodeExecution` and
70
+ `SSEEvent` remain for the kept v1 chat streams.)
71
+ - **Removed: the `agents.*` surface entirely** (`client.agents`,
72
+ `Agents`/`AsyncAgents`, `AgentRun`/`AsyncAgentRun`, `AgentResult`/
73
+ `AgentGatherResult`, `AgentError`, `spawn`/`gather`/`stop`) — use
74
+ `client.runs.create(...)` + `run.wait()`/`runs.cancel()` instead; in-run
75
+ subagent spawning is a runtime concern (executor tool), not an SDK API.
76
+ `spawn`'s `SPLOX_RUN_ID`/`parent_run_id` mechanics are gone with it.
77
+ - **Run state names are the v2 state machine**: terminal is
78
+ `succeeded|failed|cancelled` (previously `completed|failed|stopped`);
79
+ cancellation passes through `cancelling`.
80
+ - **Timeouts**: `run.wait()` raises `SploxTimeoutError` (subclass of both
81
+ `SploxError` and `TimeoutError`), so existing `except TimeoutError` code
82
+ keeps working.
83
+ - **500-level responses** now raise `SploxServerError` (a `SploxAPIError`
84
+ subclass) instead of the generic `SploxAPIError`; 400 responses raise
85
+ `SploxBadRequestError`, 409 `SploxConflictError`, 422
86
+ `SploxValidationError`. Code that caught `SploxAPIError` keeps working.
87
+ - GET requests are now retried automatically (previously no retries); disable
88
+ by catching errors yourself — retry semantics never re-send an unkeyed POST.
89
+
90
+ ### Unchanged (explicitly)
91
+
92
+ - `client.mcp` — catalog, connections, `execute_tool(mcp_server_id=, tool_slug=, args=)`,
93
+ server tools, OAuth/connection links, `generate_connection_token/link`.
94
+ - `client.chats`, `client.memory`, `client.billing`, `client.llm`,
95
+ `client.events` (webhooks), `SploxClient()`/`AsyncSploxClient()`
96
+ construction and the `SPLOX_API_KEY` / `SPLOX_BASE_URL` env fallbacks.
97
+ - Workflow provisioning reads and workflow secrets management.
98
+
99
+ ## 0.0.18
100
+
101
+ - Last release of the v1 run-execution surface.
splox-0.3.2/PKG-INFO ADDED
@@ -0,0 +1,469 @@
1
+ Metadata-Version: 2.4
2
+ Name: splox
3
+ Version: 0.3.2
4
+ Summary: Official Python SDK for the Splox API — run workflows, manage chats, and monitor execution
5
+ Project-URL: Homepage, https://splox.io
6
+ Project-URL: Documentation, https://docs.splox.io
7
+ Project-URL: Repository, https://github.com/splox-ai/python-sdk
8
+ Project-URL: Issues, https://github.com/splox-ai/python-sdk/issues
9
+ Author-email: Splox <support@splox.io>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,ai,sdk,splox,workflow
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Requires-Dist: httpx>=0.25.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.0; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
29
+ Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
30
+ Requires-Dist: pytest>=7.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.4; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Splox Python SDK
35
+
36
+ Official Python SDK for the [Splox API](https://docs.splox.io) — create and observe workflow **runs**, resolve human-in-the-loop **interactions**, browse the MCP catalog, and manage workflows programmatically.
37
+
38
+ > **v0.1.0 is a breaking release.** The run-execution surface moved to the v2 API
39
+ > (`client.runs` / `client.interactions`). Workflow CRUD reads, chats, memory,
40
+ > billing and the **MCP module are unchanged**. See [CHANGELOG.md](CHANGELOG.md)
41
+ > for the full breaking-change list.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install splox
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```python
52
+ from splox import SploxClient
53
+
54
+ client = SploxClient(api_key="your-api-key")
55
+
56
+ # Create a run: workflow id ("wf_...") or a raw workflow UUID both work.
57
+ run = client.runs.create(
58
+ "wf_01JAZ6Y5M3Q8F7N2R4T6V9W0XC",
59
+ "Summarize the latest sales report",
60
+ )
61
+ print(run.id, run.status) # run_01... queued
62
+
63
+ # Block until the run finishes (raises SploxTimeoutError on timeout).
64
+ run = run.wait(timeout=300)
65
+ print(run.status) # succeeded | failed | cancelled
66
+
67
+ # Read the transcript and outputs.
68
+ for message in run.messages().data:
69
+ print(f"[{message.role}] {message.text}")
70
+ for output in run.outputs().data:
71
+ print(output.type, output.value)
72
+
73
+ # Usage (includes descendant runs; amount is a decimal string).
74
+ usage = run.usage()
75
+ print(usage.input_tokens, usage.output_tokens, usage.amount, usage.currency)
76
+ ```
77
+
78
+ ### Creating runs
79
+
80
+ ```python
81
+ run = client.runs.create(
82
+ workflow_id, # "wf_..." or raw UUID
83
+ input, # str shorthand, or a full MessageInput dict,
84
+ # or a list of content parts
85
+ metadata={"ticket": "T-123"}, # optional client metadata
86
+ conversation_id="conv_...", # optional: continue a conversation (multi-turn)
87
+ workflow_version=3, # optional: pin a workflow version
88
+ idempotency_key="my-key", # optional: auto-generated UUIDv4 when omitted
89
+ )
90
+ ```
91
+
92
+ `input` accepts:
93
+
94
+ ```python
95
+ "What is 2+2?" # text shorthand
96
+ {"role": "user", "content": [{"type": "text", "text": "hi"}]} # full MessageInput
97
+ [{"type": "text", "text": "hi"}, {"type": "json", "value": {"x": 1}}] # parts
98
+ ```
99
+
100
+ Every `POST /v2/runs` carries an `Idempotency-Key` (auto UUIDv4). Retrying with
101
+ the same key and body returns the same run; the SDK only ever retries POSTs
102
+ that carry an idempotency key.
103
+
104
+ ### Streaming events (SSE)
105
+
106
+ ```python
107
+ # Live event stream: auto-reconnects with Last-Event-ID, dedupes by event id,
108
+ # and ends after the terminal run.status_changed event.
109
+ for event in run.events(): # stream=True is the default
110
+ print(event.sequence, event.type, event.data)
111
+
112
+ # Durable JSON pages instead of a stream:
113
+ page = run.events(stream=False, limit=100)
114
+ for event in page.data:
115
+ print(event.sequence, event.type)
116
+ ```
117
+
118
+ ### Listing and cancelling
119
+
120
+ ```python
121
+ page = client.runs.list(status=["running", "waiting"], limit=20)
122
+ for run in page.data:
123
+ print(run.id, run.status)
124
+ if page.page.has_more:
125
+ page = client.runs.list(status=["running", "waiting"], cursor=page.page.next_cursor)
126
+
127
+ run = client.runs.cancel(run_id) # idempotent; terminal runs are returned unchanged
128
+ tree = client.runs.tree(run_id) # run + descendants snapshot
129
+ ```
130
+
131
+ ### Human-in-the-loop interactions
132
+
133
+ ```python
134
+ # Pending questions raised by a run:
135
+ for interaction in run.pending_interactions():
136
+ print(interaction.type, interaction.prompt, interaction.payload)
137
+
138
+ # Respond (variant must match the interaction type):
139
+ client.interactions.respond(interaction.id, type="approval", approved=True)
140
+ client.interactions.respond(interaction.id, type="text", text="blue")
141
+ client.interactions.respond(interaction.id, type="choice", option_ids=["opt_a"])
142
+ client.interactions.respond(interaction.id, type="confirmation", confirmed=True)
143
+ # ...or pass a prebuilt body:
144
+ client.interactions.respond(interaction.id, response={"type": "approval", "approved": False})
145
+
146
+ # Inbox-style listing:
147
+ page = client.interactions.list(status="pending", limit=50)
148
+ ```
149
+
150
+ ### Public IDs
151
+
152
+ v2 IDs are `<prefix>_<26-char Crockford base32>` (`run_`, `wf_`, `int_`, ...).
153
+ The SDK accepts raw UUIDs for workflow ids and encodes them client-side:
154
+
155
+ ```python
156
+ from splox import encode_id, decode_id
157
+
158
+ encode_id("wf", "019f455e-a84c-7d4c-87b0-c951d38bc224") # -> "wf_01KX2NXA2C..."
159
+ decode_id("wf_01KX2NXA2CFN68FC69A79RQGH4") # -> UUID(...)
160
+ ```
161
+
162
+ ## Async Support
163
+
164
+ Every resource has an async twin with the same shape:
165
+
166
+ ```python
167
+ import asyncio
168
+ from splox import AsyncSploxClient
169
+
170
+ async def main():
171
+ async with AsyncSploxClient(api_key="your-api-key") as client:
172
+ run = await client.runs.create("wf_01JAZ6Y5M3Q8F7N2R4T6V9W0XC", "Hello!")
173
+
174
+ async for event in run.events(): # SSE with auto-reconnect
175
+ print(event.type, event.data)
176
+
177
+ run = await run.wait(timeout=300)
178
+ page = await run.messages()
179
+ print([m.text for m in page.data])
180
+
181
+ asyncio.run(main())
182
+ ```
183
+
184
+ ## Error Handling
185
+
186
+ All non-2xx v2 responses are RFC 9457 `problem+json` and map onto a typed
187
+ hierarchy; `code` carries the stable machine-readable error code and
188
+ `trace_id` correlates with server logs.
189
+
190
+ ```python
191
+ from splox.exceptions import (
192
+ SploxAPIError, # base for HTTP errors (.status_code/.code/.trace_id/.problem)
193
+ SploxBadRequestError, # 400
194
+ SploxAuthError, # 401
195
+ SploxForbiddenError, # 403
196
+ SploxNotFoundError, # 404
197
+ SploxConflictError, # 409 (idempotency_key_conflict, interaction_not_pending, ...)
198
+ SploxGoneError, # 410 (event_cursor_expired, ...)
199
+ SploxValidationError, # 422 (.errors = [{name, reason, ...}] with JSON Pointers)
200
+ SploxRateLimitError, # 429 (.retry_after)
201
+ SploxServerError, # 5xx
202
+ SploxTimeoutError, # run.wait()/result() timeouts (also a TimeoutError)
203
+ SploxConnectionError, # network failures
204
+ SploxStreamError, # SSE stream gave up reconnecting
205
+ )
206
+
207
+ try:
208
+ run = client.runs.create(workflow_id, "hi")
209
+ except SploxValidationError as e:
210
+ for item in e.errors:
211
+ print(item["name"], item["reason"])
212
+ except SploxRateLimitError as e:
213
+ print(f"Rate limited. Retry after: {e.retry_after}")
214
+ except SploxAPIError as e:
215
+ print(f"API error {e.status_code} ({e.code}): {e.message}")
216
+ ```
217
+
218
+ **Retries:** GET requests are retried up to 3 times with exponential backoff on
219
+ connection errors, 429 and 5xx. POSTs are retried only when they carry an
220
+ `Idempotency-Key` (always true for `runs.create` and `interactions.respond`),
221
+ reusing the same key so replays are safe.
222
+
223
+ ## Discovery & building an agent
224
+
225
+ Three resource surfaces cover discovery: `client.llm_endpoints`
226
+ (`GET /v2/llm-endpoints`, `.models(id)`) and `client.tool_servers`
227
+ (`GET /v2/tool-servers`, `.tools(id)`). One endpoint carries
228
+ `is_default=True` — the endpoint new agent nodes get automatically; model
229
+ choice is equally optional (omit `text_llm_model` in node data → server
230
+ default).
231
+
232
+ ```python
233
+ # 1. LLM endpoints: pick one (or rely on the default).
234
+ endpoints = client.llm_endpoints.list()
235
+ default_ep = next(e for e in endpoints if e.is_default)
236
+ model = client.llm_endpoints.models(default_ep.id)[0] # Model, all operations
237
+ # model.input_schema is the JSON Schema of the agent node's
238
+ # "additional_llm_config"; model.id goes into "text_llm_model".
239
+
240
+ # 2. Tool servers: system servers (kind="system") + your own (kind="user").
241
+ servers = client.tool_servers.list()
242
+ compute = next(s for s in servers if s.id == "system:compute")
243
+ tool_names = [t.name for t in client.tool_servers.tools(compute.id)]
244
+
245
+ # 3. Build a workflow: an agent wired to compute tools via a tool edge.
246
+ wf = client.workflows.create("researcher")
247
+ client.workflows.set_graph(
248
+ wf.id,
249
+ nodes=[
250
+ {"id": "agent", "type": "agent", "data": {
251
+ "system_prompt": "You are a research assistant.",
252
+ # optional — omit for server defaults:
253
+ "text_llm_endpoint_id": default_ep.id,
254
+ "text_llm_model": model.id,
255
+ }},
256
+ {"id": "tools", "type": "tool", "data": {
257
+ "mcp_server_id": compute.id, # "system:compute"
258
+ "allowed_tools": ["compute_exec", "compute_read_file"],
259
+ }},
260
+ ],
261
+ edges=[{"source": "agent", "target": "tools"}], # tool edge (inferred)
262
+ )
263
+
264
+ run = client.runs.create(wf.id, "How many CPUs does this sandbox have?").wait()
265
+ ```
266
+
267
+ ### `splox.builder` — declare a graph in Python
268
+
269
+ `splox.builder` turns nested `agent()` / `tool()` specs into one `upsert()`
270
+ call (idempotent by workflow name). Two pure helpers configure the agent
271
+ node's context knobs, mirroring the editor's "Rule-based Context" and
272
+ "Summarization" panels:
273
+
274
+ ```python
275
+ from splox.builder import agent, tool, upsert, summarization, context_rule
276
+
277
+ root = agent(
278
+ "assistant",
279
+ "You help the user.",
280
+ tools=[tool("system:compute", allow=["compute_exec"])],
281
+ # Token-based memory: summarize trimmed history instead of dropping it.
282
+ context_memory=summarization(
283
+ "Summarize the conversation so far, keeping open questions.",
284
+ max_tokens=70000, trim_target_percent=30,
285
+ ),
286
+ # Rule-based context: each rule fires only when its condition matches
287
+ # (keyword/regex are instant; intent asks the model).
288
+ context_providers=[
289
+ context_rule(text="Today is 2025-01-01.", keywords=["date", "today"]),
290
+ context_rule(
291
+ tool_server="system:compute", tool_name="compute_exec",
292
+ tool_args={"command": "uname -a"},
293
+ intent="the user asks about the machine",
294
+ ),
295
+ ],
296
+ )
297
+ res = upsert(client, "Assistant", root) # create or new version, one call
298
+ ```
299
+
300
+ ### `splox.evals` — grade a workflow in code
301
+
302
+ `splox.evals` runs scenario batches and lets you grade them with ordinary
303
+ Python: `run_cases()` fans out `k` attempts per input and folds each run's
304
+ output / tools / usage into a `RunResult`; `judge()` scores text with the
305
+ platform judge workflow; `compare()` diffs two result matrices (pass@k,
306
+ regressions, exact binomial p-value).
307
+
308
+ ```python
309
+ from splox.evals import run_cases, judge, compare
310
+
311
+ runs = run_cases(client, res.workflow_id, inputs=["Run uname -sm"], k=3)
312
+ graded = [[r.status == "succeeded" and "compute_exec" in r.tools_used
313
+ for r in case] for case in runs]
314
+ ```
315
+
316
+ ## Workflow provisioning (v1, unchanged)
317
+
318
+ Workflow CRUD reads remain available for provisioning:
319
+
320
+ ```python
321
+ workflows = client.workflows.list(search="Test")
322
+ full = client.workflows.get(workflow_id)
323
+ version = client.workflows.get_latest_version(workflow_id)
324
+ entry_nodes = client.workflows.get_entry_nodes(version.id)
325
+ versions = client.workflows.list_versions(workflow_id)
326
+ # plus workflow secrets management: list_secrets / set_env_secret / ...
327
+ ```
328
+
329
+ Chats (`client.chats`), memory (`client.memory`), billing (`client.billing`)
330
+ and webhooks (`client.events`) are also unchanged.
331
+
332
+ ## MCP (Model Context Protocol) — unchanged
333
+
334
+ The MCP module is fully supported and **unchanged in this release**: catalog,
335
+ connections, tool execution, OAuth and connection links work exactly as in 0.0.x.
336
+
337
+ ### Catalog
338
+
339
+ ```python
340
+ # Search the MCP catalog
341
+ catalog = client.mcp.list_catalog(search="github", per_page=10)
342
+ for server in catalog.mcp_servers:
343
+ print(f"{server.name} — {server.url}")
344
+
345
+ # Get featured servers
346
+ featured = client.mcp.list_catalog(featured=True)
347
+
348
+ # Get a single catalog item
349
+ item = client.mcp.get_catalog_item("mcp-server-id")
350
+ print(item.name, item.auth_type)
351
+ ```
352
+
353
+ ### Connections & tools
354
+
355
+ ```python
356
+ conns = client.mcp.list_connections()
357
+ owner_servers = client.mcp.list_connections(scope="owner_user")
358
+
359
+ tools = client.mcp.get_server_tools("mcp-server-id")
360
+
361
+ result = client.mcp.execute_tool(
362
+ mcp_server_id="mcp-server-id",
363
+ tool_slug="list_servers",
364
+ args={"query": "x"},
365
+ )
366
+ print(result.result.content, result.result.structured_content, result.result.is_error)
367
+
368
+ client.mcp.delete_connection("connection-id")
369
+ ```
370
+
371
+ ### Connection Token & Link
372
+
373
+ ```python
374
+ from splox import generate_connection_token, generate_connection_link
375
+
376
+ token = generate_connection_token(
377
+ mcp_server_id="mcp-server-id",
378
+ owner_user_id="owner-user-id",
379
+ end_user_id="end-user-id",
380
+ credentials_encryption_key="your-credentials-encryption-key",
381
+ )
382
+
383
+ link = generate_connection_link(
384
+ base_url="https://app.splox.io",
385
+ mcp_server_id="mcp-server-id",
386
+ owner_user_id="owner-user-id",
387
+ end_user_id="end-user-id",
388
+ credentials_encryption_key="your-credentials-encryption-key",
389
+ )
390
+ # → https://app.splox.io/tools/connect?token=eyJhbG...
391
+ ```
392
+
393
+ ## Webhooks
394
+
395
+ ```python
396
+ from splox import SploxClient
397
+
398
+ client = SploxClient() # No API key needed for webhooks
399
+
400
+ result = client.events.send(
401
+ webhook_id="your-webhook-id",
402
+ payload={"order_id": "12345", "status": "paid"},
403
+ )
404
+ print(result.event_id)
405
+ ```
406
+
407
+ ## Custom Base URL
408
+
409
+ ```python
410
+ client = SploxClient(
411
+ api_key="your-api-key",
412
+ base_url="https://your-self-hosted-instance.com/api/v1",
413
+ )
414
+ ```
415
+
416
+ v1 endpoints use the base URL as-is; v2 endpoints (`/v2/...`) are resolved
417
+ against the server origin (the base URL with its `/api/v1` suffix stripped).
418
+
419
+ ## API Reference
420
+
421
+ ### `SploxClient` / `AsyncSploxClient`
422
+
423
+ | Parameter | Type | Default | Description |
424
+ |-----------|------|---------|-------------|
425
+ | `api_key` | `str \| None` | `SPLOX_API_KEY` env | API authentication token |
426
+ | `base_url` | `str` | `SPLOX_BASE_URL` env, then `https://splox.io/api/v1` | API base URL |
427
+ | `timeout` | `float` | `300.0` | Request timeout in seconds |
428
+
429
+ ### `client.runs` (v2)
430
+
431
+ | Method | Description |
432
+ |--------|-------------|
433
+ | `create(workflow_id, input, *, metadata=, conversation_id=, workflow_version=, idempotency_key=)` | Create a run; returns a bound `Run` handle |
434
+ | `get(run_id)` | Get a run |
435
+ | `list(*, status=, workflow_id=, created_after=, created_before=, cursor=, limit=)` | Cursor-paged listing (newest first) |
436
+ | `cancel(run_id)` | Idempotent cancellation |
437
+ | `wait(run_id, *, timeout=, poll_interval=)` | Poll until terminal |
438
+ | `stream_events(run_id, *, cursor=)` | SSE stream with auto-reconnect + dedupe |
439
+ | `list_events(run_id, *, cursor=, limit=)` | Durable JSON event pages |
440
+ | `messages / outputs / tree / usage / pending_interactions` | Run reads |
441
+
442
+ `Run` handles expose the same operations instance-bound: `run.wait()`,
443
+ `run.cancel()`, `run.events()`, `run.messages()`, `run.outputs()`,
444
+ `run.tree()`, `run.usage()`, `run.pending_interactions()`, `run.refresh()`.
445
+
446
+ ### `client.interactions` (v2)
447
+
448
+ | Method | Description |
449
+ |--------|-------------|
450
+ | `list(*, status=, run_id=, cursor=, limit=)` | Cursor-paged listing (newest first) |
451
+ | `get(interaction_id)` | Get an interaction |
452
+ | `respond(interaction_id, *, type=, ..., response=, idempotency_key=)` | Resolve a pending interaction |
453
+
454
+ ### `client.workflows` (v1 provisioning reads)
455
+
456
+ | Method | Description |
457
+ |--------|-------------|
458
+ | `list(...)` / `get(id)` | List/get workflows |
459
+ | `get_latest_version(id)` / `list_versions(id)` | Version reads |
460
+ | `get_entry_nodes(version_id)` | Entry nodes for a version |
461
+ | `list_secrets / set_env_secret / set_file_secret / delete_secret / ...` | Secrets management |
462
+
463
+ ### `client.chats`, `client.memory`, `client.billing`, `client.mcp`
464
+
465
+ Unchanged from 0.0.x — see the sections above.
466
+
467
+ ## License
468
+
469
+ MIT