Skip to content

Shared event stream and typed plugin RPC - #13

Merged
shrijayan merged 2 commits into
mainfrom
feat/shared-events-typed-rpc
Oct 22, 2026
Merged

shrijayan merged 2 commits into
mainfrom
feat/shared-events-typed-rpc

Conversation

@shrijayan

Copy link
Copy Markdown
Owner

Brings event and RPC support to parity with the OpenCode TS client (v2.1.2: shared-events.ts, promise/rpc.ts, promise/client.ts), and goes further in a few places.

What's added

  • client.events (EventHub / AsyncEventHub, new module _events.py): one GET /api/event connection shared by every subscriber.
    • Opens with the first subscriber and closes when the last one leaves. Each subscriber gets a bounded queue (4096 events); a subscriber that overflows fails alone with EventOverflowError. The latest server.connected is replayed to late subscribers.
    • Filters: of_type(*classes) (narrows the type, e.g. Subscription[SessionIdleEvent]), for_session(id), where(pred). Use with for/async for, get(timeout=), with/async with.
    • on_activity callback fires on every chunk, keepalive comments included.
    • Cancellation: sync close() works from any thread (it shuts down the socket to unblock the reader). Async waits can be cancelled under asyncio and trio, and the subscription stays usable afterwards.
    • Opt-in reconnect=True | Reconnect(...): jittered exponential backoff after connection errors, 5xx and 429. Duplicate server.connected events are dropped. Documented as volatile.
    • The generated client.event.subscribe() is unchanged.
  • client.rpc(Definition) (_rpc.py): declare a plugin RPC with class Counter(RpcDefinition, id="counter"), rpc_method(In, Out, errors=...) and rpc_event(Data).
    • call(Counter.add, input) is typed through pydantic.
    • RpcError maps to RpcCallError and RpcInternalError maps to RpcCallInternalError (output that fails validation counts as rpc.invalid_output). Data from declared errors is validated.
    • subscribe(Counter.changed) yields typed RpcEventPayloads, and events() yields every raw rpc.counter.* event.
    • client.rpc.call(...) still works.
  • Stream.on_activity and Stream.interrupt().

Generated code is untouched (codegen --check passes). In __init__.py, the new imports sit after an # isort: split so ruff doesn't sort them into the generated error-import block.

Parity versus TS

TS behavior Python
Shared connection; opens lazily, stops when the last subscriber leaves ✅
4096-event per-subscriber capacity; overflow fails that subscriber and discards its queue ✅
server.connected replayed to late subscribers ✅
Queued events delivered before the end or error ✅
onActivity on every chunk, keepalives included ✅ on_activity
signal abort ✅ close() (thread-safe) and native async cancellation (asyncio and trio)
client.rpc(definition) methods and events.subscribe(name) ✅ call(method, input) and subscribe(event); events.on(handler) not ported, use a loop or a task
RPC errors mapped to {type, message, data}, with decoding of declared errors (rpc-runtime) ✅ RpcCallError
— ➕ opt-in reconnect with backoff; type and session filters; with joins eagerly so no events are missed; get(timeout)

Difference: calling a method uses counter.call(Counter.add, x) rather than counter.add(x). That keeps the return type exact for both the sync and async clients without a second declaration.

Tests

  • tests/test_events.py: sync, plus async on asyncio and trio. Covers multiplexing, replay, overflow isolation, last-leave teardown, cross-thread close, filters, keepalive activity, reconnect with dedupe and backoff, max_attempts, non-retryable errors, and cancelling a request or a wait.
  • tests/test_rpc.py: encoding, error mapping, invalid output, foreign definitions, and filtered typed events (sync and async).
  • Live tests run against opencode v2.1.2 with isolated XDG dirs.
  • Coverage: _events 96%, _rpc 95%.

Do not merge without review: merging to main publishes a release.

…rpc(Definition))

One SSE connection shared by every subscriber with bounded per-subscriber queues,
server.connected replay, of_type/for_session/where filters, an on_activity
keepalive hook, cancellation under asyncio and trio, and opt-in reconnection with
backoff. RpcDefinition/rpc_method/rpc_event declare plugin RPCs for typed calls,
mapped RpcCallError exceptions and filtered event subscriptions.
@shrijayan
shrijayan force-pushed the feat/shared-events-typed-rpc branch from fd0617d to ff72565 Compare October 22, 2026 10:11
@shrijayan
shrijayan merged commit 6ac9cd0 into main Oct 22, 2026
16 checks passed
@shrijayan
shrijayan deleted the feat/shared-events-typed-rpc branch October 22, 2026 10:17
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