Skip to content
Open
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
30 changes: 23 additions & 7 deletions streaming/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,16 @@ The `events` request body is an `EventStreamRequest`:
"channels": ["messages", "updates", "lifecycle"],
"namespaces": [[]],
"depth": 2,
"since": 123
"since": 123,
"sinceEventId": "evt_abc"
}
```

`since` resumes within the same connection's ring buffer (session `seq`).
`sinceEventId` seeks the durable event tape across reconnects (same id space
as WebSocket `ReconnectParams.lastEventId`). When both are set,
`sinceEventId` takes precedence for durable replay.

Each SSE connection is its own subscription. Closing the connection unsubscribes
from that stream. A client may open multiple event streams for the same thread,
for example one stream for low-latency model tokens and another for state or
Expand Down Expand Up @@ -395,12 +401,22 @@ commands provide explicit reads and time-travel operations.

## Replay and Reconnection

Servers may keep a ring buffer of recent events per thread. Clients use sequence
numbers to recover missed events:

- SSE clients pass `since` in `EventStreamRequest`.
- WebSocket clients call `subscription.reconnect` with `lastEventId` and the
subscriptions they want restored.
Servers may keep a ring buffer of recent events per thread. Clients recover
missed events with two different cursors:

- `seq` / `since` — **connection-scoped**. Each new SSE/WebSocket session
renumbers from the start of that session's ring buffer. Use `since` only to
resume within the same connection after a brief gap.
- `eventId` / `runId` — **durable**. `eventId` is stable across reconnects
(and often anchored to an upstream stream entry). `runId` identifies which
run produced the event so clients can ignore replay from older runs after
hydrate or a deferred first subscribe.

SSE clients pass `since` (session seq) and optionally `sinceEventId` (durable
cursor) in `EventStreamRequest`. WebSocket clients call
`subscription.reconnect` with `lastEventId` and the subscriptions they want
restored. When `sinceEventId` / `lastEventId` is set, the server seeks the
durable tape; session `since` remains for same-connection ring-buffer resume.

The server replays matching buffered events after the requested point and then
switches to live delivery. If the requested event is no longer buffered, servers
Expand Down
2 changes: 1 addition & 1 deletion streaming/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@langchain/protocol",
"version": "0.0.17",
"version": "0.0.19",
"description": "TypeScript bindings for the LangChain agent streaming protocol",
"license": "MIT",
"keywords": [
Expand Down
14 changes: 13 additions & 1 deletion streaming/js/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ export type Event = EventData & Extensible & {
* Monotonic sequence number for ordering
*/
seq?: JsUint;
/**
* Producing run. Durable across connection-local `seq` resets so clients
* can ignore replayed events from older runs after hydrate / reconnect.
*/
run_id?: string;
};

export type ResultData = RunResult | SubscriptionResult | AgentResult | InputResult | StateResult | EmptyResult;
Expand Down Expand Up @@ -457,9 +462,16 @@ export type EventStreamRequest = Extensible & {
*/
depth?: number;
/**
* Replay events after this seq number
* Connection-local seq resume (same session)
*/
since?: JsUint;
/**
* Durable cursor: replay events with eventId strictly after this value,
* then switch to live. Prefer this across reconnects; `since` remains for
* same-connection ring-buffer resume only. Same id space as
* ReconnectParams.lastEventId.
*/
since_event_id?: string;
};

export type SubscriptionCommand = SubscriptionSubscribe | SubscriptionUnsubscribe | SubscriptionReconnect;
Expand Down
10 changes: 9 additions & 1 deletion streaming/protocol.cddl
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,9 @@ Event = {
type: "event",
? eventId: text, ; Unique ID for reconnection (maps to SSE id:)
? seq: js-uint, ; Monotonic sequence number for ordering
; Producing run. Durable across connection-local `seq` resets so clients
; can ignore replayed events from older runs after hydrate / reconnect.
? runId: text,
EventData,
Extensible,
}
Expand Down Expand Up @@ -539,7 +542,12 @@ EventStreamRequest = {
channels: [+ Channel],
? namespaces: [* Namespace], ; Prefix-match these namespace paths
? depth: uint, ; Max depth below namespace prefix
? since: js-uint, ; Replay events after this seq number
? since: js-uint, ; Connection-local seq resume (same session)
; Durable cursor: replay events with eventId strictly after this value,
; then switch to live. Prefer this across reconnects; `since` remains for
; same-connection ring-buffer resume only. Same id space as
; ReconnectParams.lastEventId.
? sinceEventId: text,
Extensible,
}

Expand Down
4 changes: 3 additions & 1 deletion streaming/py/langchain_protocol/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ class _EventFields(TypedDict):
type: Literal["event"]
event_id: NotRequired[str] # Unique ID for reconnection (maps to SSE id:)
seq: NotRequired[JsUint] # Monotonic sequence number for ordering
run_id: NotRequired[str]

class _EventVariant0(_EventFields, LifecycleEvent):
pass
Expand Down Expand Up @@ -387,7 +388,8 @@ class EventStreamRequest(TypedDict):
channels: list[Channel]
namespaces: NotRequired[list[Namespace]] # Prefix-match these namespace paths
depth: NotRequired[int] # Max depth below namespace prefix
since: NotRequired[JsUint] # Replay events after this seq number
since: NotRequired[JsUint] # Connection-local seq resume (same session)
since_event_id: NotRequired[str]

class SubscribeParams(TypedDict):
channels: list[Channel]
Expand Down
2 changes: 1 addition & 1 deletion streaming/py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "langchain-protocol"
description = "Python bindings for the LangChain agent streaming protocol"
license = { text = "MIT" }
readme = "README.md"
version = "0.0.17"
version = "0.0.19"
requires-python = ">=3.10.0,<4.0.0"
classifiers = [
"Development Status :: 3 - Alpha",
Expand Down