Xuehao Xu f0cb67b223
feat(extensions): expose incremental run evidence reader (#5405)
* feat(extensions): expose incremental run evidence reader

* fix(extensions): address run evidence review feedback

* docs(extensions): clarify run deletion reconciliation

* docs(migrations): align current head documentation

* fix(extensions): isolate run evidence event reads

* test: avoid pinning run change migration to latest head

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-16 21:18:05 +08:00

94 lines
2.9 KiB
Python

"""Stable, read-only run evidence contracts for extension services."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Protocol
class InvalidRunEvidenceCursor(ValueError):
"""The cursor is malformed, unsupported, or belongs to another scope."""
@dataclass(frozen=True)
class RunStatusView:
"""Authoritative lifecycle projection for one visible run."""
thread_id: str = ""
run_id: str = ""
status: str = ""
created_at: str = ""
updated_at: str = ""
error: str | None = None
stop_reason: str | None = None
@dataclass(frozen=True)
class RunEventView:
"""Persisted evidence envelope; ``seq`` is monotonic within its thread.
Reader-returned content and metadata are detached from host storage.
Fields are frozen, but nested payload containers may be modified locally.
"""
thread_id: str = ""
run_id: str = ""
seq: int = 0
event_type: str = ""
category: str = ""
content: Any = None
metadata: dict[str, Any] = field(default_factory=dict)
created_at: str = ""
@dataclass(frozen=True)
class RunPage:
"""Changed runs plus the opaque cursor represented by this page."""
items: tuple[RunStatusView, ...] = ()
next_cursor: str | None = None
has_more: bool = False
@dataclass(frozen=True)
class RunEventPage:
"""One forward page from a known run's persisted event stream."""
items: tuple[RunEventView, ...] = ()
next_after_seq: int | None = None
has_more: bool = False
class RunEvidenceReader(Protocol):
"""Host-bound evidence reader; implementations never expose writes."""
async def list_changed_runs(self, *, cursor: str | None, limit: int) -> RunPage:
"""Return visible runs changed after ``cursor`` in stable order.
Callers persist ``next_cursor`` only after their own output is durable.
Reusing the input cursor is valid and may replay items. An empty page
means caught up; unsupported hosts omit the reader instead. Deletions
do not produce tombstones in this feed. Consumers reconciling a run
they already know must treat ``get_run_status(...) is None`` as absent.
"""
raise NotImplementedError("the host does not provide changed-run discovery")
async def list_run_events(
self,
*,
thread_id: str,
run_id: str,
after_seq: int | None,
limit: int,
) -> RunEventPage:
"""Return events with thread-scoped ``seq > after_seq``.
A missing or invisible run returns an empty page, preventing identity
probing across the reader's host-bound scope.
"""
raise NotImplementedError("the host does not provide run-event reading")
async def get_run_status(self, *, thread_id: str, run_id: str) -> RunStatusView | None:
"""Return authoritative status, or ``None`` when not visible."""
raise NotImplementedError("the host does not provide run-status reading")