Metadata-Version: 2.5
Name: splox
Version: 0.6.0.dev1
Summary: Build, run, and evaluate Splox agent harnesses from Python
Project-URL: Homepage, https://splox.io
Project-URL: Documentation, https://docs.splox.io
Project-URL: Repository, https://github.com/splox-ai/python-sdk
Project-URL: Issues, https://github.com/splox-ai/python-sdk/issues
Author-email: Splox <support@splox.io>
License-Expression: MIT
License-File: LICENSE
Keywords: agent,ai,evals,harness,sdk,splox
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: attrs==26.1.0
Requires-Dist: dbos==3.2.0
Requires-Dist: httpx2==2.12.0
Requires-Dist: mcp-types==2.1.1
Requires-Dist: mcp==2.1.1
Requires-Dist: pydantic-ai-harness==0.52.1.dev1+0bdb572bf
Requires-Dist: pydantic-ai-slim[anthropic,google,mcp,openai,spec]==2.52.1.dev1+0bdb572bf
Requires-Dist: pydantic-graph==2.52.1.dev1+0bdb572bf
Requires-Dist: pydantic==2.13.5
Requires-Dist: pyyaml<7,>=6.0.2
Requires-Dist: sqlalchemy==2.1.1
Requires-Dist: sse-starlette==3.5.0
Requires-Dist: starlette==1.7.0
Requires-Dist: websockets==16.1.1
Provides-Extra: dev
Requires-Dist: aiohttp>=3.13; extra == 'dev'
Requires-Dist: hypothesis>=6; extra == 'dev'
Requires-Dist: import-linter>=2.1; extra == 'dev'
Requires-Dist: mypy>=2.2; extra == 'dev'
Requires-Dist: pip>=24; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest-httpx2>=1.0; extra == 'dev'
Requires-Dist: pytest-testmon<3,>=2.2; extra == 'dev'
Requires-Dist: pytest-timeout>=2.3; extra == 'dev'
Requires-Dist: pytest-xdist>=3.6; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: syrupy<7,>=4.9; extra == 'dev'
Description-Content-Type: text/markdown

# Splox Python SDK

The Python side of a Splox machine: the files that declare its agents, the code
that calls them, the MCP servers a script reaches, the command line that
evaluates a package, and `python -m splox.runtime` -- the process that links a
machine to the platform and runs its turns.

## Installation

```bash
pip install splox
```

Python 3.11 or newer.

## A machine is a repository

A machine's checkout is `~/harness`, and its root is a plugin in the Agent
Plugins format: `plugin.json`, `skills/`, `mcp.json`, `evals/`, and our own in
`io.splox/`. The plugins written on the machine are `packages/<name>/`; the ones
it installed are `plugins/<name>/`.

An agent is a file, `io.splox/agents/<file>.yaml` of any package -- a Pydantic
AI `AgentSpec` file, as `AgentSpec.to_file` writes one:

```yaml
# yaml-language-server: $schema=../../plugins/splox/schemas/agent.schema.json
name: Assistant
model: splox:default
capabilities:
  - Gateway
  - MachineTools: {include: [compute_exec]}
  - MCPServer: system:search
  - Delegates: [Executor]
  - Limits: {request_limit: 1000}
instructions: |-
  You answer questions and hand self-contained work to the Executor.
```

It is loaded by Pydantic itself, `Agent.from_file`, and by nothing else; from
code -- a program, a script -- it is one import, the agent run through the
platform, each run a conversation of its own (a script a chat's `compute_exec`
started makes it that chat's agent, under the call):

```python
from splox.agents import executor            # io.splox/agents/ of this tree
from splox.agents.engineering import reviewer  # the plugin engineering's

print(executor.run_sync("summarise the open invoices").output)
```

A bare name is the tree's own agent, `<plugin>:<Name>` a plugin's, in a
`Delegates` list and in `plugin.json` alike. The agent has exactly the
capabilities its file lists: the registry `splox.capabilities` (`Gateway`,
`MachineTools`, `MCPServer`, `Delegates`, `Plugins`, `CommandHooks`,
`Compaction`, `Memory`, `ToolOutputLimits`, `Limits`, `CodeMode`, `Catalogue`)
and the machine's own (`io.splox/capabilities/`). `Gateway` is what makes
`model: splox:default` a model: Pydantic knows no provider `splox`, and
`Gateway` turns a model string into the provider's Pydantic model on the
platform's gateway (`{api}/v1/llm/{provider}`), spoken to with the run's grant,
and serves the person's pick in the composer.

What a capability needs of the machine -- its tree, the grant, the platform's
address -- it reads from the environment a machine's processes have
(`SPLOX_TREE_DIR`, else `~/harness`; `SPLOX_GRANT`; `SPLOX_API_URL`); the
runtime binds its own (`splox.capabilities.machine`). `MachineTools` acts on
the machine of the process that calls it, and a command of a script that goes
on in the background is handed to the runtime, which brings its end to the
chat that ran the script.

The agent a chat is answered by is named in `plugin.json`:

```json
{"name": "machine", "extensions": {"io.splox": {"chat": "Assistant"}}}
```

## Calling agents from code

A program is `io.splox/programs/<name>/main.py` of any package: a plain Python
module. It -- or any script run on the machine -- imports its agents from
`splox.agents` as above; another script imports the program (its functions)
or the same agents the same way.

## MCP servers

```python
from splox import plugins

with plugins.connect("system:search") as search:
    out = search.call_tool("search", {"query": "mcp registry", "max_results": 5})
    print(out["content"])   # the full MCP result, isError and _meta included
```

An address carries its scheme, and the scheme says who answers:

- `system:<server>` -- a server of the platform's own (`system:memory`,
  `system:media`, ...), through the gateway at `{SPLOX_API_URL}/v1/mcp/<server>`.
- `app:<integration>` or `app:<connection id>` -- an app of the person's
  (Nango's catalogue, `GET /v1/apps/catalogue`; their connections, `GET /v1/apps`).
- `https://...` -- a service. The URL of an app's server as the apps'
  catalogue lists it goes through the gateway on the account its person
  connected; one nobody connected raises `plugins.NotConnectedError` carrying
  `authorize_url` for the person to open. Any other server is dialled directly
  and is sent none of the platform's credentials.
- `<package>/<server>` -- a server a package of this machine declares,
  started here when it is a command.

The credential is the calling turn's grant. A process the runtime starts for a
turn is given `SPLOX_GRANT` and `SPLOX_API_URL` (with `SPLOX_CHAT_ID`,
`SPLOX_TURN_ID` and `SPLOX_TOOL_CALL_ID`); the SDK reads nothing else, and no
account credential ever enters it.

## Hooks

A hook is a function a declaration hands a point of the loop to
(`io.splox/hooks.json`, or the file an agent's `hooks:` names). What it is
handed as `t`:

| | |
|---|---|
| `t.llm(prompt, model=None)` | one model call through `POST /v1/llm`; the turn's model when none is named |
| `t.history.tail(n)`, `.tags`, `.get(tag)` | the chat's messages, read-only |
| `t.store.get(key)`, `.set(key, value)` | the machine's own key/value store (`~/.splox/store.db`) |

`splox/toolkit/CONTRACT.md` is the whole contract.

## The runtime

```bash
SPLOX_API_URL=https://splox.io/api SPLOX_MACHINE_ID=... SPLOX_MACHINE_TOKEN=... \
    python -m splox.runtime
```

The machine's image runs it under a supervisor. It links to the platform over
`/v1/link`, prepares `~/harness`, and runs each turn it is handed: the loop,
the machine's compute tools, the sub-agents, the calls. The token may instead be
in `~/.splox/machine.json`, which `splox login` writes. It exits 0 on SIGTERM (the turns it held are resumed
by the next process) and when another process took the machine's link, 3 when
the machine token is refused, and 75 when it installed a new build and wants to
be restarted on it.

## The command line

`splox plugin eval` runs the cases under a package's `evals/` -- a directory per
case holding `case.yaml`, or `prompt.md` with `graders/*.md` beside it -- against
the agent this machine answers a chat with, and grades what comes back with
`regex`, `tool_order`, `tool_used`, `file_exists`, `llm` and `baseline`:

```bash
splox plugin eval --package crm --cases asks-the-crm --runs 5 --threshold 0.8
```

Name a package and each case runs twice, once with the package and once with it
refused to the agent, so the delta says what the package is worth. It reads the
checkout it runs on (`SPLOX_TREE_DIR`, or `~/harness`), writes
`evals/results/<timestamp>/aggregate-result.json`, and exits non-zero when the
suite missed its threshold.

## License

MIT
