Hermes streaming cursors, deadlines, and cancellation
Use this reference when a Hermes-backed Action must survive a delivery disconnect, deadline, or cancellation request. It defines the native AIP action-event position and the evidence available after interruption.
The AIP cursor resumes delivery. It does not restart provider execution, guarantee exactly-once consumption, or prove that cancellation stopped a model, tool, message, memory, or network effect.
Position types
These positions are not interchangeable.
| Position | Example | Owner | Valid use |
|---|---|---|---|
| AIP action-event cursor | aev_cursor_42_17 |
AIP runtime | Resume the same Action event route |
| Legacy event-only cursor | evt_cursor_42 |
AIP runtime | Accepted input without a retained chunk position |
| AIP chunk sequence | 17 |
AIP lifecycle store | Order chunks inside one Action |
| Native SSE event ID | AIP event ID or chunk:17 |
aipd renderer |
Identify one delivered SSE frame, not route position |
Hermes SSE id |
Provider-defined or absent | Hermes endpoint | Interpret an upstream frame only |
Responses sequence_number |
Provider integer | Hermes Responses API | Order provider Responses events |
| Structured-run event sequence | Provider integer | Hermes run store | Page provider run events through run_events |
Persist the cursor emitted by the aip.stream.cursor frame. Keep provider IDs
and sequences with their payloads, but never submit them as an AIP route cursor.
Native routes
The action-scoped routes are:
GET /aip/v1/actions/{action_id}/events
GET /aip/v1/actions/{action_id}/chunks
POST /aip/v1/actions/{action_id}/cancel
The events route accepts these query parameters:
| Parameter | Type | Default | Behavior |
|---|---|---|---|
cursor |
String | Start of retained view | Opaque action-event continuation |
limit |
Integer | 100 |
Clamped to 1..1000 across events and chunks |
kind or kinds |
String list | All event kinds | Filters stored AIP events, not included chunks |
include_chunks |
Boolean | false |
Adds chunks after the selected events |
follow |
Boolean | false |
Returns native SSE and polls until terminal |
The chunks route fixes the event-kind filter to aip.stream.chunk and enables
chunk inclusion. Both routes use the same combined cursor implementation.
Reading requires visibility to the Action and its tenant. Cancellation is a separate authorized lifecycle write.
Cursor grammar
The current runtime emits:
aev_cursor_<event_offset>_<last_chunk_sequence|none>
For example:
aev_cursor_42_17
aev_cursor_42_none
The route also accepts the legacy event-only form:
evt_cursor_<event_offset>
Treat all forms as opaque. The event offset is an implementation position in the retained event store, not the number of matching Action events returned to the caller. Filtering can advance it across records that are not returned.
A legacy cursor carries no chunk position. With include_chunks=true, the
runtime can return retained chunks from the beginning. Prefer the combined
cursor once the route has emitted one.
Malformed prefixes, missing components, non-numeric event offsets, and invalid
chunk sequences return query.invalid_cursor. An event position outside the
retained replay window returns stream.cursor_expired.
Page selection
One action-event read proceeds in this order:
- parse the event and chunk positions;
- read stored events for the Action using the event position and kind filters;
- subtract the returned event count from the page limit;
- when requested, append chunks whose AIP sequence is greater than the chunk position until the remaining row capacity is used;
- return the advanced combined cursor and current Action terminal flag.
The default page has at most 100 combined records. The caller’s requested limit is clamped to 1,000. Stored event selection also stops before exceeding a 4 MiB event-page bound.
The JSON response has separate arrays:
{
"action_id": "act_0123456789abcdef0123456789abcdef",
"events": [],
"chunks": [],
"cursor": "aev_cursor_42_17",
"terminal": false
}
Events are returned before chunks. The arrays are not merged into one
chronological sequence. Within chunks, order by the AIP sequence; within
events, retain the store order. Use domain identifiers to correlate an event
and chunk rather than comparing their array positions.
The kind filter applies only to events. Setting
kind=aip.stream.chunk does not replace include_chunks=true when the caller
needs lifecycle chunks.
Native SSE frames
With follow=true, aipd renders repeated action-event pages as SSE.
| SSE event name | SSE id |
Data | Resume meaning |
|---|---|---|---|
| Stored AIP event kind | AIP event ID | { "event": ... } |
One record only; not a route cursor |
aip.stream.chunk |
chunk:<sequence> |
{ "chunk": ... } |
One chunk only; not a route cursor |
aip.stream.cursor |
Combined AIP cursor | { "next_cursor": ... } |
Persist this value for route resume |
aip.stream.terminal |
Combined AIP cursor | Action ID and terminal flag | Delivery reached a terminal Action view |
aip.error |
No resumable ID | Typed protocol error | Stop and apply the error decision |
The follow implementation polls every 500 milliseconds when no frame is
ready. The outer SSE connection sends a keep-alive comment every 15 seconds.
Neither value is an Action SLA or a provider heartbeat.
Follow stops after the Action view is terminal or after a protocol error. The terminal frame means the lifecycle is terminal; read status or result for its authoritative outcome and receipts.
Last-Event-ID handling
The HTTP binding selects the query cursor first. Only when it is absent does
it read the Last-Event-ID header.
This precedence lets a client override an automatically retained SSE event ID
with its last verified combined cursor. That matters because a connection can
break after an AIP event ID or chunk:<sequence> frame but before the following
aip.stream.cursor frame.
Safe persistence rules:
- accept a cursor only from
aip.stream.cursor.data.next_cursoror the terminal frame’s combined ID; - commit application processing before committing that cursor;
- reconnect to the same Action route and authorization context;
- send the committed value as the
cursorquery parameter; - tolerate a repeated event or chunk and reject conflicting duplicate data.
Do not persist the latest arbitrary SSE id. Do not construct a combined
cursor from an AIP event ID and chunk sequence.
Cursor error decisions
| Error or observation | Meaning | Decision |
|---|---|---|
query.invalid_cursor |
The supplied value is not a valid route cursor | Restore the last committed combined cursor; do not guess a position |
stream.cursor_expired |
The event position is outside retained replay | Read Action status/result, record the delivery gap, and reconcile required effects |
| Empty non-terminal page | No new matching record is currently retained | Continue bounded polling or follow; do not submit a replacement Action |
| Repeated AIP event ID | Delivery replayed a stored event | De-duplicate by event ID and compare content |
| Repeated chunk sequence | Delivery replayed a chunk | Accept only identical content for that Action and sequence |
| Conflicting chunk sequence | Retained evidence is inconsistent | Stop consumption and investigate storage/publication integrity |
| Terminal frame without expected provider evidence | AIP lifecycle settled before the client saw all expected provider detail | Read the result and preserve the evidence gap |
Cursor expiry is a delivery-evidence gap. It is not evidence that the Action did not run.
Deadline boundaries
The connector publishes these timeout classes:
| Capability family | Published timeout | Connector request boundary |
|---|---|---|
chat_stream, responses_stream, run_events, session_chat_stream |
300,000 ms | Trusted deadline or cancellation can stop earlier; default client permits up to 900,000 ms |
operator |
900,000 ms | Operator deadline, policy timeout, and provider state can settle earlier |
| Non-streaming Hermes operation | 60,000 ms | Some operation-specific clients use a shorter or longer request timeout |
Published values are contract declarations, not observed latency or availability. A trusted execution deadline is carried separately from the connector client’s HTTP timeout. Reaching either boundary does not roll back work already accepted by Hermes or an external tool.
Native Action cancellation
POST /aip/v1/actions/{action_id}/cancel accepts an optional JSON reason:
{
"reason": "The requester no longer needs this operation"
}
The runtime performs these lifecycle actions:
- authorize a write against the target Action;
- if the Action is currently active, wake its trusted cancellation token and invoke the active connector cancellation callback;
- mark the Action lifecycle and queued record cancelled;
- append
aip.action.cancelled; - return an AIP
ActionResultwith statuscancelledand{ "cancelled": true }output.
The connector callback is active-only. Cancelling an inactive record can change the AIP lifecycle without calling Hermes. The returned result is an AIP acknowledgement, not provider terminal evidence.
Closing a follow connection performs none of these steps.
Hermes cancellation effects
| Active Action family | Connector/provider effect | Required reconciliation |
|---|---|---|
| Chat or Responses stream | Wakes the connector token and drops the active provider stream; Hermes attempts to interrupt its task after disconnect | Read original AIP result; inspect session, stored response, tools, and delivery effects |
| Session chat stream | Drops the active session stream cooperatively | Read the transcript and external effects; do not infer rollback |
Structured run_events read |
Stops the observation Action, not necessarily the bound Hermes run | Read run_status; invoke separately governed run_stop when provider interruption is required |
| Active operator source or resume | Records cancellation intent, maps resume to source, attempts provider stop, and reconciles within its grace | Inspect operator binding and provider state; native cancel output omits the provider classification |
Operator already at requires_human |
No active connector callback exists | Use a governed deny resume for the prompt or separately approved run_stop for full interruption |
Provider completion can win a race with cancellation. Conversely, the AIP lifecycle can report cancelled while provider state remains unknown. Preserve both observations and do not replace one with the other.
run_stop is a provider mutation with its own capability contract, approval,
Action ID, and idempotency key. It is not an alias for native Action cancel.
Security and evidence
Action-event reads and cancellation use the transport-authenticated principal and Action ownership or tenant context. Do not move a cursor to another tenant, principal, Action, route, or deployment.
Event and chunk payloads can contain prompts, model output, reasoning, tool arguments, results, personal data, and provider identifiers. Encrypt retained cursors with their application record, but redact or minimize the payloads stored beside them.
For an interrupted operation, retain:
- deployment and endpoint identity;
- capability and Action ID;
- last committed combined cursor;
- last accepted AIP chunk sequence and event IDs;
- cancellation reason and timestamp when present;
- final AIP status/result and receipts;
- separately verified Hermes and external-system state.
This evidence is source-aligned. It does not establish live cancellation, durable retention, provider availability, or production qualification.