Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
project adheres to [Semantic Versioning](https://semver.org/).

## [0.5.0rc1] - 2026-09-18

Internal release candidate for 0.5.0. See [MIGRATION.md](MIGRATION.md) before
upgrading from any released 0.3.x version or current main's 0.4.0 surface.

### Added

- `AsyncPineAI` now supports `async with` and `aclose()`. `disconnect()` only
closes Socket.IO; `aclose()` also closes SDK-owned HTTP resources.
- Typed REST resources: `auth.me()`, ticket mint/redeem, and validated session
list/get responses.
- Explicit `api_base_path`, `http_client`, and `http_transport` options.
Injected HTTP clients remain caller-owned.
- `PineAI` now has real synchronous REST `auth` and `sessions` resources built
on `httpx.Client`.

### Changed

- HTTP errors expose a stable error code and `status_code` without reproducing
upstream response bodies.
- `sessions.list()` sends `ensure_copilot=false` by default when the backend
supports that read-only query option.

### Removed

- `PineAI` no longer offers synchronous Socket.IO methods. Use `AsyncPineAI`
for realtime connections and streams; the former wrapper owned an event loop
and exposed coroutine-backed resources from a synchronous client.

## [0.4.0] - 2026-08-08

Aligned to the supported protocol scope: the subset of the task-session
Expand Down
54 changes: 54 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Migrating to 0.5.0

Version 0.5.0 keeps the supported 0.4.0 asynchronous realtime API on
`AsyncPineAI` and adds typed REST resources. This release candidate is for
validation; do not use `0.5.0rc1` as a default production dependency.

## 0.3.3 to 0.5.0

Current main contains unreleased 0.4.0 protocol-scope changes. Read the 0.4.0
entry in `CHANGELOG.md` first: it removed unsupported events and narrowed the
realtime contract. On 0.5.0, use `async with AsyncPineAI(...)` or call
`await client.aclose()` after realtime and REST work. `disconnect()` still only
ends Socket.IO.

Session reads now return typed models:

```python
# Before
listed = await client.sessions.list(limit=20)
session_id = listed["sessions"][0]["id"]

# 0.5.0
listed = await client.sessions.list(limit=20)
session_id = listed.sessions[0].id
```

`sessions.list()` now requests `ensure_copilot=false`. This is read-only only
against a backend that implements the option; older backends may ignore it.
Pass `ensure_copilot=True` only when the legacy side effect is wanted.

## Synchronous client

`PineAI.auth` and `PineAI.sessions` now return synchronous values rather than
coroutines. Synchronous Socket.IO methods (`connect`, `chat_sync`, and related
streaming helpers) are removed because a safe synchronous realtime client needs
an explicit event-loop ownership model. Move those calls to `AsyncPineAI`.

```python
# Before: this exposed an async resource from PineAI
result = await PineAI(access_token="...").sessions.list()

# 0.5.0
with PineAI(access_token="...") as client:
result = client.sessions.list()
```

## Errors and HTTP configuration

Errors preserve `code` and `status_code`, but no longer include upstream
response text. Configure a nonstandard prefix with `api_base_path`; `/api`
remains the default. An injected `httpx.Client` or `httpx.AsyncClient` remains
open after the SDK client closes.

The SDK continues to support Python 3.10 and later.
104 changes: 95 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,107 @@ pip install pine-assistant[cli] # SDK + CLI
from pine_assistant import AsyncPineAI

client = AsyncPineAI(access_token="...", user_id="...")
await client.connect()

session = await client.sessions.create()
await client.join_session(session["id"])
await client.rebuild(session["id"]) # load the session's messages
async with client:
await client.connect()

async for event in client.chat(session["id"], "Negotiate my Comcast bill",
turn_timeout=120):
print(event.type, event.data)
session = await client.sessions.create()
await client.join_session(session["id"])
await client.rebuild(session["id"]) # load the session's messages

await client.disconnect()
async for event in client.chat(session["id"], "Negotiate my Comcast bill",
turn_timeout=120):
print(event.type, event.data)
```

A client tracks one session. Concurrent sessions need one client each.

`disconnect()` only ends the real-time connection. `aclose()` (including the
end of `async with`) also releases the SDK-owned HTTP client. If you pass an
`httpx.AsyncClient`, it remains your responsibility to close it. Use
`api_base_path` to select an API prefix; it defaults to `/api` for compatibility.

## REST identity and sessions

```python
async with AsyncPineAI(access_token="...") as client:
identity = await client.auth.me() # AuthIdentity(user_id="...")
sessions = await client.sessions.list(limit=20) # SessionListResponse
session = await client.sessions.get("123") # SessionInfo
```

The list call sends `ensure_copilot=false` by default. It is read-only when
used with a backend that supports this query option; older backends may ignore
it and retain their legacy Copilot behavior.

Use `await client.sessions.end_task(session_id)` to request a user-ended task;
the synchronous client provides the same method without `await`. The backend
checks ownership and eligibility, and the method returns the updated
`SessionInfo`. This is not proof that the task objective succeeded or that an
external action has already stopped. The SDK never retries this write: after a
timeout or server error, query the session and its history before deciding
whether to retry, because the state may already have changed.

Use `sessions.send_message()` when an application needs the REST write
acknowledgement without joining Socket.IO. It returns a typed status: `received`
means the message was persisted, `delivered` means it was handed to the Agent,
and `delivery_failed` means that handoff failed. None means that a task has
finished. A timeout or connection error leaves persistence unknown, so the SDK
never retries a send; recover Socket history before deciding what to do next.

```python
status = await client.sessions.send_message(
"123", "Continue the task", request_id="ui-click-42",
)
if status.status == "delivered":
print(status.message_id, status.revision)

outcomes = await client.sessions.outcomes("123")
for outcome in outcomes.items:
print(outcome.outcome_id, outcome.outcome_narrative)
```

`outcomes()` returns newest-first persisted Outcomes. Follow `next_cursor` to
request older pages with `before=...`; `total` is a first-page snapshot and is
not a pagination signal.

For structured form answers, use the async method on a connected client:

```python
await client.connect()
try:
receipt = await client.submit_form_response(
"123", "456", {"contact_name": "Example User"},
)
finally:
await client.disconnect()
```

The method re-reads the original agent form from authenticated history, preserves
its message and request IDs, validates visible required fields, and JSON-encodes
array answers like the web app. Callers cannot override field privacy levels.
The backend's `session:message_status` receipt supplies the persisted reply ID:
`delivered` means it reached the agent and `received` means it was persisted but
delivery was not observed before the deadline. `unknown` requires checking
history before deciding whether to submit again. Each Socket.IO connection sends
a form only once because a late receipt cannot identify an attempt; reconnects
never retry or replay form submissions. A transport ACK is not a delivery
receipt. The legacy synchronous `send_form_response()` is deprecated because it
cannot verify the original form request.

## Quick Start (Sync REST)

`PineAI` is a synchronous REST client. It returns values directly for auth and
session resources; use `AsyncPineAI` for Socket.IO and streaming.

```python
from pine_assistant import PineAI

with PineAI(access_token="...") as client:
print(client.auth.me().user_id)
for session in client.sessions.list(limit=20).sessions:
print(session.id, session.title)
```

## Quick Start (CLI)

```bash
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pine-assistant"
version = "0.4.0"
version = "0.5.0rc1"
description = "Pine AI SDK — Let Pine AI handle your digital chores. Socket.IO + REST client."
readme = "README.md"
license = "MIT"
Expand All @@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Typing :: Typed",
"Topic :: Software Development :: Libraries :: Python Modules",
]
Expand Down
26 changes: 23 additions & 3 deletions src/pine_assistant/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,44 @@
them. `is_supported_event` tells the two apart.
"""

from pine_assistant.auth import Auth
from pine_assistant.auth import Auth, SyncAuth
from pine_assistant.chat import ChatEvent
from pine_assistant.client import AsyncPineAI, PineAI
from pine_assistant.errors import AuthError, ConnectionError, PineAIError, SessionError
from pine_assistant.models.auth import AuthIdentity, AuthTicket, RedeemedTicket
from pine_assistant.models.events import (
SUPPORTED_EVENTS,
C2SEvent,
S2CEvent,
is_supported_event,
)
from pine_assistant.sessions import SessionsAPI
from pine_assistant.models.session import (
SessionInfo,
SessionListResponse,
SessionMessageStatus,
SessionOutcome,
SessionOutcomeRating,
SessionOutcomesPage,
)
from pine_assistant.sessions import SessionsAPI, SyncSessionsAPI

__version__ = "0.4.0"
__version__ = "0.5.0rc1"
__all__ = [
"PineAI",
"AsyncPineAI",
"Auth",
"SyncAuth",
"SessionsAPI",
"SyncSessionsAPI",
"AuthIdentity",
"AuthTicket",
"RedeemedTicket",
"SessionInfo",
"SessionListResponse",
"SessionMessageStatus",
"SessionOutcome",
"SessionOutcomeRating",
"SessionOutcomesPage",
"ChatEvent",
"PineAIError",
"AuthError",
Expand Down
Loading
Loading