Skip to content

feat(protocol)!: align the SDK to the supported protocol scope - #1

Merged
a7vinx merged 1 commit into
mainfrom
feat/protocol-scope
Aug 8, 2026
Merged

feat(protocol)!: align the SDK to the supported protocol scope#1
a7vinx merged 1 commit into
mainfrom
feat/protocol-scope

Conversation

@a7vinx

@a7vinx a7vinx commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

The SDK modelled 33 server-to-client events selected from roughly 60 the backend emits, with nothing marking which of them carry a compatibility guarantee. The supported protocol scope names 19, and the two sets were not in a containment relationship.

This aligns the modelled surface with that scope, implements the requirements the scope states as MUST, and adds a test layer that pins both.

Protocol surface

Four supported events were absent and are added, with models:

Event What it carries
session:llm_thinking Reasoning and tool-call trace
session:tool_status The record of one asynchronous operation, including an outbound call
session:required_action Whether the session is awaiting a user response
session:restriction An account restriction — the only statement that a task will not complete

The reasoning stream previously ran on session:work_log, which is outside the scope. It is a distinct event from session:llm_thinking, not a former name for it.

Events outside the scope are unmodelled but still delivered, unchanged and in order. is_supported_event() distinguishes the two surfaces; emit_event() sends an unmodelled event. This follows the scope's own position: tolerating an unsupported event is required, depending on one is not.

Requirements implemented

Recovery. session:join carries since_revision "0" and ignores the incremental-synchronisation fields. rebuild() pages through history until the cursor is exhausted, on every join and every reconnect. A short or empty page does not indicate exhaustion.

Deduplication. Events are keyed on the event identifier together with the message type. Identifiers collide across types, so keying on the identifier alone discards valid events.

Blocking conditions. InputState exposes the reason from session:input_state, including insufficient credits and outstanding phone verification.

Turn control no longer depends on unsupported events. It previously hinged on session:ask_for_location, session:interactive_auth_confirmation, session:three_way_call and session:reward.

Tests

Three layers, separated because they fail for different reasons.

tests/protocol/fixtures holds one envelope per supported event, with provenance recorded: 14 captured from live sessions, 4 derived from the protocol definition for conditions an ordinary session does not reach. test_contract.py validates one envelope at a time — a failure means a payload shape moved. test_flow.py drives sequences through the real transport and pins each MUST by name, including that an unrecognised event arrives verbatim without disturbing ordering.

tests/integration was two overlapping copies of one live script, one of which asserted behaviour the SDK had already dropped. It is now a single suite that also serves as the fixture recorder. It places one call to a number in the range NANP reserves for fiction, which is what makes session:task_ready, session:tool_status and session:task_finished observable. It remains outside CI: it requires a token and consumes credits.

CI gains a lint step. ruff was configured but never invoked.

Fixed

Sessions joined through join_session() were never re-joined after a reconnect. Membership was tracked on the fire-and-forget emit path, while joining is a request/response call.

Breaking changes

Removed: send_auth_confirmation(), send_location_response(), send_location_selection(), NotificationEvent, the session:reward and session:payment models, the unsupported event constants, the action argument on chat(), and request_work_log on get_history(). Each remains reachable through emit_event().

pine-mcp-server calls the three removed methods and declares pine-assistant>=0.3.2. It requires a corresponding change before this is released.

Verification

  • 96 offline tests and 9 live tests pass. ruff check clean. python -m build and twine check pass. CI green on Python 3.10–3.13.
  • Every event name and payload field was checked against the protocol definitions in the backend repository.
  • Four fixtures remain derived: session:error, session:message, session:restriction and session:rich_content. Nothing in an ordinary session reaches an account restriction or produces a rich content document.

🤖 Generated with Claude Code

@a7vinx
a7vinx force-pushed the feat/protocol-scope branch from 63ae2cd to 9982abc Compare August 8, 2026 13:49
@a7vinx a7vinx changed the title feat(protocol)!: align the SDK to the Business API protocol scope feat(protocol)!: align the SDK to the supported protocol scope Aug 8, 2026
@a7vinx
a7vinx force-pushed the feat/protocol-scope branch 2 times, most recently from 858256f to 218c39e Compare August 8, 2026 14:58
The SDK modelled 33 server-to-client events selected from roughly 60 the
backend emits, with nothing marking which of them carry a compatibility
guarantee. The supported scope names 19, and the two sets were not in a
containment relationship.

## Protocol surface

The modelled surface is now the supported scope and nothing else.

Four supported events were absent and are added, with models:
`session:llm_thinking`, `session:tool_status`, `session:required_action` and
`session:restriction`. Between them they carry what a task is doing and why it
stopped. The reasoning stream previously ran on `session:work_log`, which is
outside the scope and is a distinct event from `session:llm_thinking` rather
than a former name for it.

Events outside the scope are unmodelled but still delivered, unchanged and in
order. `is_supported_event()` distinguishes the two surfaces and
`emit_event()` sends an unmodelled event. This follows the scope's own
position: tolerating an unsupported event is required, depending on one is not.

## Requirements

Three requirements the scope states as MUST were unmet:

- **Recovery.** `session:join` now carries `since_revision` `"0"` and ignores
  the incremental-synchronisation fields. `rebuild()` pages through history
  until the cursor is exhausted, on every join and every reconnect. A short or
  empty page does not indicate exhaustion.
- **Deduplication.** Events are keyed on the event identifier together with the
  message type. Identifiers collide across types, so keying on the identifier
  alone discards valid events.
- **Blocking conditions.** `InputState` exposes the reason from
  `session:input_state`, including the two conditions the scope calls out.

Turn control no longer depends on unsupported events. It previously hinged on
`session:ask_for_location`, `session:interactive_auth_confirmation`,
`session:three_way_call` and `session:reward`.

## Tests

Three layers. `tests/protocol/fixtures` holds one envelope per supported event,
with provenance recorded — 14 captured from live sessions, 4 derived from the
protocol definition for conditions an ordinary session does not reach.
`test_contract.py` validates one envelope at a time. `test_flow.py` drives
sequences through the real transport and pins each MUST by name, including that
an unrecognised event arrives verbatim without disturbing ordering.

`tests/integration` was two overlapping copies of one live script, one of which
asserted behaviour the SDK had already dropped. It is now a single suite that
also serves as the fixture recorder. It places one call to a number in the
range NANP reserves for fiction, which is what makes `session:task_ready`,
`session:tool_status` and `session:task_finished` observable. It stays out of
CI: it requires a token and consumes credits.

CI gains a lint step. `ruff` was configured but never invoked.

## Fixed

Sessions joined through `join_session()` were never re-joined after a
reconnect. Membership was tracked on the fire-and-forget emit path, while
joining is a request/response call.

## Breaking

`send_auth_confirmation()`, `send_location_response()` and
`send_location_selection()` are removed, along with `NotificationEvent`, the
`session:reward` and `session:payment` models, the unsupported event constants,
the `action` argument on `chat()`, and `request_work_log` on `get_history()`.
Each remains reachable through `emit_event()`.

`pine-mcp-server` calls the three removed methods and depends on
`pine-assistant>=0.3.2`. It requires a corresponding change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@a7vinx
a7vinx force-pushed the feat/protocol-scope branch from 218c39e to 329b16d Compare August 8, 2026 15:04
@a7vinx
a7vinx merged commit 0e3046f into main Aug 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant