Metadata-Version: 2.4
Name: pouchy-companion
Version: 0.1.6
Summary: Pouchy companion SDK for Python — thin httpx/SSE client + pydantic models for server-side integrators (game backends, bots, services).
Project-URL: Homepage, https://pouchy.ai/sdk
Project-URL: Documentation, https://pouchy.ai/sdk
Project-URL: Repository, https://github.com/oviswang/pouchy
Author: Pouchy
License-Expression: LicenseRef-Proprietary
License-File: LICENSE
Keywords: agent,ai-companion,pouchy,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.5
Description-Content-Type: text/markdown

# pouchy-companion — Pouchy Companion SDK for Python

A thin, typed client for the [Pouchy](https://pouchy.ai) companion REST/SSE
plane — the same wire contract the JS SDK (`@pouchy_ai/companion-sdk`) speaks,
aimed at **server-side integrators**: game backends, bots, services.

- **httpx** (sync) + minimal SSE — no framework assumptions
- **pydantic v2** models for every envelope/payload
- `protocol.py` is **generated from the TypeScript source of truth** and
  drift-tested against it in CI — the two SDKs cannot silently diverge
- Voice/WebRTC and browser-only surfaces are deliberately out of scope
  (use the JS SDK for embeds)

## Install

```bash
pip install pouchy-companion
```

## Quickstart

Mint a session token from your backend (one per end user):

```bash
curl -X POST https://pouchy.ai/v1/sessions \
  -H "Authorization: Bearer pchy_sk_…" \
  -H "Content-Type: application/json" \
  -d '{ "agent": "<agentId>", "external_user_id": "player_42" }'
```

Then talk:

```python
from pouchy_companion import CompanionClient

with CompanionClient(base_url="https://pouchy.ai", token=session_token) as client:
    ack = client.connect()
    print("scopes:", ack.grantedScopes)

    # Request/response in one call — streams server-side, returns the reply.
    out = client.send_text("what should I do about the boss on floor 3?", await_reply=True)
    print(out["text"])
```

### Streaming deltas

```python
client.send_text(
    "tell me a story",
    await_reply=True,
    on_delta=lambda chunk, reset: print(chunk, end="", flush=True),
)
```

### Idempotent retries

`send_text` accepts an optional `turn_id` — a client-chosen id the server dedupes
on for 10 minutes (the last 8 turns per session). Reusing the same `turn_id` after
a network failure is safe: a turn the server already saw returns
`{"duplicate": True, ...}` instead of running twice (with `await_reply=True` the
recorded reply text rides back too).

```python
tid = "order-42"
client.send_text("place the order", await_reply=True, turn_id=tid)
# ...connection dropped, unsure if it landed — safe to retry with the same id:
client.send_text("place the order", await_reply=True, turn_id=tid)
```

### The event stream (proactive messages, confirms, tool calls)

```python
from pouchy_companion import ConfirmRequestPayload, MessagePayload, ToolCallPayload

for env in client.events():           # SSE; resumes from the session cursor
    if isinstance(env.payload, MessagePayload):
        print("companion:", env.payload.text)
    elif isinstance(env.payload, ConfirmRequestPayload):
        # Platform session tokens may resolve confirms directly — your end
        # user IS this instance's human. Show your own confirm UI, then:
        res = client.confirm_action(env.payload.confirmId, approve=True)
        print("outcome:", res.outcome)
    elif isinstance(env.payload, ToolCallPayload):
        # An app-declared tool (declared via CompanionClient(tools=[...])).
        client.send_tool_result(env.payload.id, ok=True, result={"answer": 42})
```

### World state (play-along context)

```python
client.send_world_state({
    "type": "game.event.boss_spawned",
    "data": {"name": "Ashen Knight", "floor": 3},
    "salience": 0.9,
})
```

### Memory / history / wallet

```python
client.recall(query="player preferences", limit=10)
client.history(limit=20)
client.get_wallet()          # read-only (wallet.read)
client.end_session()
```

## Errors

Every failed call raises `CompanionError` with `.status`, `.code`
(machine-switchable — `rate_limited`, `turn_pending`, `confirm_resolved`, …
see `COMPANION_ERROR_CODES`) and `.retry_after` seconds when the server said
when to come back. Transport failures (connect refused, timeouts, dropped
streams) are wrapped too — `status` is `0` and `code` is `request_timeout`
(deadline) or `aborted` (any other transport failure), with the original
httpx exception preserved as `__cause__`.

```python
import time

from pouchy_companion import CompanionError

try:
    client.send_text("hi")
except CompanionError as e:
    if e.code == "rate_limited":
        time.sleep(e.retry_after or 1)
```

## Reference

The full REST semantics (scopes, confirm flow, world-state salience, limits)
are documented at [pouchy.ai/sdk](https://pouchy.ai/sdk) and in
`docs/companion-api-reference.md` — the wire contract is identical across
SDKs. Protocol vocabulary lives in `pouchy_companion.protocol`
(`PROTOCOL_VERSION`, `OUTBOUND_TYPES`, error code lists), generated from the
TypeScript source of truth.

## Development

```bash
# Regenerate protocol.py after a protocol.ts change (CI enforces parity):
node scripts/generate-protocol.mjs

# Tests (offline, MockTransport):
pip install -e . pytest && pytest
```
