代码提交

This commit is contained in:
董宏凯 committed 2026-08-15 12:51:58 +08:00
1 parent 500dab29ab
commit 70da7c5649
59 files changed
+12108 -148

No files matched your search

@@ -0,0 +1,157 @@
"""Deep Research event protocol.
Every observable thing that happens during a research job — a search query fired,
a source added, a report chunk written, a model invocation billed — becomes an
event. Events are **persisted first** (to ``deep_research_events``) then pushed
to the SSE stream; this ordering lets a client reconnect with ``?after=<seq>``
and replay anything missed (§15.3).
This module defines:
- :data:`EventType` — the closed set of event ``type`` strings (§15.2).
- :data:`ResearchPhase` — the phase enum (§14.3).
- :class:`DeepResearchEvent` — the envelope persisted + serialised over SSE.
- :class:`EventSink` — the adapter protocol runners call.
The concrete :class:`PersistingEventSink` (adapter) lives in
``adapters/event_sink.py`` and bridges to the event-store repository.
"""
from __future__ import annotations
from datetime import UTC, datetime
from typing import Any, Literal, Protocol, runtime_checkable
from pydantic import BaseModel, Field
# ── phase enum (§14.3) ───────────────────────────────────────────────────────
ResearchPhase = Literal[
"initializing",
"planning",
"collecting",
"curating",
"compressing",
"deepening",
"writing",
"reviewing",
"illustrating",
"exporting",
"done",
]
ALL_PHASES: tuple[ResearchPhase, ...] = (
"initializing",
"planning",
"collecting",
"curating",
"compressing",
"deepening",
"writing",
"reviewing",
"illustrating",
"exporting",
"done",
)
# ── event type enum (§15.2) ──────────────────────────────────────────────────
#
# Kept as a Literal union (not an enum.IntEnum) so the persisted string and the
# SSE JSON ``type`` field are identical and stable across versions.
EventType = Literal[
"job_status",
"phase_changed",
"plan_created",
"queries_planned",
"query_started",
"query_completed",
"source_added",
"sources_curated",
"context_compressed",
"deep_branch_started",
"deep_branch_completed",
"report_reset",
"report_chunk",
"report_completed",
"artifact_created",
"image_generation_started",
"image_generated",
"awaiting_input",
"model_usage",
"warning",
"error",
"heartbeat",
]
class DeepResearchEvent(BaseModel):
"""A single observable event in a research job.
``seq`` is assigned by the event store (monotonic per job); callers leave it
``0`` when emitting and read it back from the persisted row.
"""
seq: int = 0
session_id: str = ""
job_id: str = ""
type: EventType
phase: ResearchPhase = "initializing"
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
payload: dict[str, Any] = Field(default_factory=dict)
model_config = {"extra": "allow"}
def to_sse_dict(self) -> dict[str, Any]:
"""Serialise for the ``data:`` line of the SSE stream (§15.1)."""
return {
"seq": self.seq,
"sessionId": self.session_id,
"jobId": self.job_id,
"type": self.type,
"phase": self.phase,
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
"payload": self.payload,
}
@runtime_checkable
class EventSink(Protocol):
"""Adapter protocol for emitting research events.
Implementations persist the event (assigning ``seq``) before returning, so a
crash after ``emit()`` never loses an already-observed event.
"""
async def emit(
self,
type: EventType, # noqa: A002 — intentional arg name for readability
*,
phase: ResearchPhase = "initializing",
payload: dict[str, Any] | None = None,
) -> DeepResearchEvent:
"""Persist + return an event. ``session_id``/``job_id`` are bound by the sink."""
...
async def publish_live(
self,
type: str, # noqa: A002 - intentionally outside the durable EventType union
*,
phase: ResearchPhase = "initializing",
payload: dict[str, Any] | None = None,
) -> None:
"""Publish a best-effort, non-persisted frame.
Provider text deltas use this path. Bounded ``report_chunk`` events
remain the durable recovery checkpoints.
"""
...
__all__ = [
"ALL_PHASES",
"DeepResearchEvent",
"EventSink",
"EventType",
"ResearchPhase",
]