# stdiobus

Python SDK for stdio_bus - the AI agent transport layer.

[![PyPI](https://img.shields.io/pypi/v/stdiobus)](https://pypi.org/project/stdiobus/)

## Features

- **Cross-platform** - Works on Windows, macOS, Linux via Docker backend
- **Native backend** - Direct FFI to C kernel on Linux/macOS (cffi)
- **Async/Sync API** - Both `AsyncStdioBus` and `StdioBus` classes
- **Type hints** - Full typing support for IDE autocompletion
- **Context managers** - Clean resource management with `with`/`async with`

## Installation

```bash
pip install stdiobus
```

Requirements:
- Python 3.10+
- [Docker Desktop](https://www.docker.com/products/docker-desktop) (for Docker backend)

### Native Backend (Optional)

For better performance on Linux/macOS, build the native extension:

```bash
# Build libstdio_bus.a first
git clone https://github.com/stdiobus/python.git
cd stdio-bus
make lib

# Build Python extension
cd sdk/python
pip install cffi
python -m stdiobus._native.build_ffi
```

## Quick Start

### Synchronous API

```python
from stdiobus import StdioBus

with StdioBus(config_path='./config.json') as bus:
    result = bus.request('tools/list', {})
    print('Tools:', result)
```

### Async API

```python
import asyncio
from stdiobus import AsyncStdioBus

async def main():
    async with AsyncStdioBus(config_path='./config.json') as bus:
        result = await bus.request('tools/list', {})
        print('Tools:', result)

asyncio.run(main())
```

## Configuration

Create a `config.json` file:

```json
{
  "pools": [
    {
      "id": "mcp-worker",
      "command": "node",
      "args": ["./worker.js"],
      "instances": 4
    }
  ]
}
```

## API Reference

### StdioBus / AsyncStdioBus

```python
bus = StdioBus(
    config_path='./config.json',  # Required
    backend='auto',               # 'auto', 'native', or 'docker'
    timeout_ms=30000,             # Default request timeout
    docker=DockerOptions(         # Docker backend options
        image='stdiobus/stdiobus:node20',
        pull_policy='if-missing',
    ),
)
```

### Methods

| Method | Description |
|--------|-------------|
| `start()` | Start the bus and spawn workers |
| `stop(timeout_sec=30)` | Stop the bus gracefully |
| `request(method, params, timeout_ms=None)` | Send request and wait for response |
| `notify(method, params)` | Send notification (no response) |
| `send(message)` | Send raw JSON-RPC message |
| `on_message(handler)` | Register message handler |
| `get_state()` | Get current bus state |
| `get_stats()` | Get runtime statistics |
| `is_running()` | Check if bus is running |
| `destroy()` | Release all resources |

### States

```python
from stdiobus import BusState

BusState.CREATED   # 0 - Created but not started
BusState.STARTING  # 1 - Workers being spawned
BusState.RUNNING   # 2 - Running and accepting messages
BusState.STOPPING  # 3 - Graceful shutdown in progress
BusState.STOPPED   # 4 - Fully stopped
```

### Errors

```python
from stdiobus import (
    StdioBusError,        # Base exception
    InvalidArgumentError, # Invalid parameter
    InvalidStateError,    # Operation not valid in current state
    TimeoutError,         # Request timeout
    TransportError,       # Transport-level failure
    PolicyDeniedError,    # Operation denied by policy
)
```

## Docker Backend

The Docker backend runs stdio_bus in a container and communicates via TCP.

```python
from stdiobus import StdioBus
from stdiobus.types import DockerOptions

bus = StdioBus(
    config_path='./config.json',
    backend='docker',
    docker=DockerOptions(
        image='stdiobus/stdiobus:node20',
        pull_policy='if-missing',  # never, if-missing, always
        startup_timeout_sec=15.0,
        extra_args=['-v', '/path/to/worker.js:/worker.js:ro'],
        env={'DEBUG': '1'},
    ),
)
```

## License

Apache-2.0
