diff --git a/src/agent_comms/cursor_owner.py b/src/agent_comms/cursor_owner.py index 8123aa581d..877914495b 100644 --- a/src/agent_comms/cursor_owner.py +++ b/src/agent_comms/cursor_owner.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING from agent_comms.native_runtime_input import NativeRuntimeTable + from .acp_extension import CursorObservation, EmptyCursorObservation, VerifiedCursorObservation from .agent_backend import InputId from .coordination_database import CoordinationStore @@ -23,8 +24,12 @@ from .thread_identity import GenerationCounter if TYPE_CHECKING: + from sqlite3 import Connection + + from .coordination_tables.participants import OwnerGenerations from .historical_native_inputs import HistoricalNativeInput from .native_entries import NativeEvidenceScope + from .proven_source_coverage import CoverageInput, NoInjectedInput, ProvenSourceCoverage @dataclass(frozen=True) @@ -48,7 +53,7 @@ async def read_async(cls, bus: MessageBus, *, wire_root_id: str, owner_name: str ) @classmethod - def _read_owned(cls, bus, root_id, owner_name, source_reads, store) -> CursorState: + def _read_owned(cls, bus: MessageBus, root_id: str, owner_name: str, source_reads: NativeEvidenceScope, store: Coordination) -> CursorState: with store.session.boundary(bus) as boundary: reason = "Current native cursor has no matching live owner" bus._registry.store.private_guard_unlocked().require_root(root_id, StaleFence(reason)) @@ -96,14 +101,14 @@ async def refresh_async(cls, bus: MessageBus, *, wire_root_id: str, owner_name: ) @classmethod - def _refresh_owned(cls, bus, root_id, owner_name, source_reads, store) -> CursorState: + def _refresh_owned(cls, bus: MessageBus, root_id: str, owner_name: str, source_reads: NativeEvidenceScope, store: Coordination) -> CursorState: thread, generation = bus._registry.live_owner_with_claim(owner_name) person = store.participants.get(thread.incarnation.require_recorded().lookup) owner = cls(thread=thread, claimed_generation=generation, wire_root_id=root_id, generation=person.owner_identity.generation) return owner._advance_owned(bus, source_reads, store) - def _advance_owned(self, bus, source_reads, store) -> CursorState: + def _advance_owned(self, bus: MessageBus, source_reads: NativeEvidenceScope, store: Coordination) -> CursorState: with source_reads.borrow(): sources = SourceCoverage(bus, store, wire_root_id=self.wire_root_id, recipient_lookup=self.lookup) @@ -126,7 +131,7 @@ def _advance_owned(self, bus, source_reads, store) -> CursorState: return state @property - def participant_identity(self): + def participant_identity(self) -> OwnerGenerations: return ParticipantOwner(self.thread, self.generation).coordinator_identity(self.lookup) @property @@ -157,19 +162,19 @@ def state(self, db: sqlite3.Connection) -> CursorState: self.require_recorded_input(db, row) return RecordedCursor(row) - def _matches_input(self, row, reference) -> bool: + def _matches_input(self, row: NativeRuntimeInput | None, reference: NativeInputReference) -> bool: return (row is not None and row.owner_identity == self.participant_identity and row.sent_owner_claimed_generation.matches(self.claimed_generation) and row.reference == reference) - def matches_prefix(self, db, evidence) -> bool: + def matches_prefix(self, db: Connection, evidence: tuple[HistoricalNativeInput, ...]) -> bool: return all(self._matches_input(NativeRuntimeInput.one(db, input_id=item.input_id), item.reference) for item in evidence) - def require_recorded_input(self, db, cursor) -> None: + def require_recorded_input(self, db: Connection, cursor: CurrentNativeCursor) -> None: cursor.reference.require_recorded_input(self, db) - def require_coverage(self, db, cursor, coverage) -> None: + def require_coverage(self, db: Connection, cursor: CurrentNativeCursor, coverage: ProvenSourceCoverage) -> None: if cursor.owner_identity != self.participant_identity: raise IdentityConflict("current native cursor owner identity differs") if cursor.covered_seq > coverage.covered_seq: @@ -178,7 +183,7 @@ def require_coverage(self, db, cursor, coverage) -> None: raise IdentityConflict("current cursor borrows historical owner source proof") self.require_recorded_input(db, cursor) - def require_recorded_reference(self, db, reference: NativeInputReference) -> None: + def require_recorded_reference(self, db: Connection, reference: NativeInputReference) -> None: row = NativeRuntimeInput.one(db, input_id=reference.input_id) if row is None or not row.sent_owner_claimed_generation.matches(self.claimed_generation): raise IdentityConflict("current cursor input claim_scope differs") @@ -212,9 +217,9 @@ def cursor(self) -> CurrentNativeCursor | None: ... def injected_seq(self) -> int: ... @abstractmethod - def without_input(self, proof, owner, coverage): ... + def without_input(self, proof: NoInjectedInput, owner: CursorOwner, coverage: ProvenSourceCoverage) -> CurrentNativeCursor: ... - def require_progress(self, owner, coverage, injected) -> None: + def require_progress(self, owner: CursorOwner, coverage: ProvenSourceCoverage, injected: int) -> None: if coverage.covered_seq < self.covered_seq: raise IdentityConflict("current cursor would regress") @@ -231,24 +236,24 @@ def require_input_progress(self, proof: HistoricalNativeInput) -> None: ... def claim_mismatch(self, injected_seq: int) -> bool: ... @abstractmethod - def publish(self, db, owner, coverage, proof) -> CursorState: ... + def publish(self, db: Connection, owner: CursorOwner, coverage: ProvenSourceCoverage, proof: CoverageInput) -> CursorState: ... @abstractmethod - def require_coverage(self, owner, db, coverage, proof) -> None: ... + def require_coverage(self, owner: CursorOwner, db: Connection, coverage: ProvenSourceCoverage, proof: CoverageInput) -> None: ... @dataclass(frozen=True) class UnstartedCursor(CursorState): @property - def covered_seq(self): + def covered_seq(self) -> int: return 0 @property - def cursor(self): + def cursor(self) -> CurrentNativeCursor | None: return None @property - def injected_seq(self): + def injected_seq(self) -> int: return 0 # No injected native input; cursor presence is this state's type. def proves_input(self, input_id: str) -> bool: @@ -260,20 +265,20 @@ def require_input_progress(self, proof: HistoricalNativeInput) -> None: def claim_mismatch(self, injected_seq: int) -> bool: return False - def without_input(self, proof, owner, coverage): + def without_input(self, proof: NoInjectedInput, owner: CursorOwner, coverage: ProvenSourceCoverage) -> CurrentNativeCursor: return proof.build(owner, coverage.covered_seq) - def observation(self): + def observation(self) -> CursorObservation: return EmptyCursorObservation() - def publish(self, db, owner, coverage, proof): + def publish(self, db: Connection, owner: CursorOwner, coverage: ProvenSourceCoverage, proof: CoverageInput) -> CursorState: if coverage.covered_seq <= self.covered_seq: return self # A blocked first source has not established a cursor. cursor = proof.candidate(self, owner, coverage) cursor.insert(db) return RecordedCursor(cursor) - def require_coverage(self, owner, db, coverage, proof): + def require_coverage(self, owner: CursorOwner, db: Connection, coverage: ProvenSourceCoverage, proof: CoverageInput) -> None: pass # No durable cursor exists to assert coverage. @@ -282,27 +287,27 @@ class RecordedCursor(CursorState): row: CurrentNativeCursor @property - def covered_seq(self): + def covered_seq(self) -> int: return self.row.covered_seq @property - def cursor(self): + def cursor(self) -> CurrentNativeCursor | None: return self.row @property - def injected_seq(self): + def injected_seq(self) -> int: return self.row.injected_seq - def without_input(self, proof, owner, coverage): + def without_input(self, proof: NoInjectedInput, owner: CursorOwner, coverage: ProvenSourceCoverage) -> CurrentNativeCursor: return replace(self.row, covered_seq=coverage.covered_seq) - def observation(self): + def observation(self) -> CursorObservation: return VerifiedCursorObservation(self.row) def proves_input(self, input_id: str) -> bool: return self.row.input_id.holds(input_id) - def require_progress(self, owner, coverage, injected): + def require_progress(self, owner: CursorOwner, coverage: ProvenSourceCoverage, injected: int) -> None: super().require_progress(owner, coverage, injected) if (self.row.owner_identity != owner.participant_identity or injected < self.row.injected_seq): @@ -317,7 +322,7 @@ def claim_mismatch(self, injected_seq: int) -> bool: return False raise IdentityConflict("current cursor input claim_scope differs") - def publish(self, db, owner, coverage, proof): + def publish(self, db: Connection, owner: CursorOwner, coverage: ProvenSourceCoverage, proof: CoverageInput) -> CursorState: cursor = proof.candidate(self, owner, coverage) if cursor.covered_seq == self.row.covered_seq and cursor.injected_seq == self.row.injected_seq: return self @@ -334,6 +339,6 @@ def publish(self, db, owner, coverage, proof): raise StaleFence("current cursor monotonic update lost its fence") return RecordedCursor(cursor) - def require_coverage(self, owner, db, coverage, proof): + def require_coverage(self, owner: CursorOwner, db: Connection, coverage: ProvenSourceCoverage, proof: CoverageInput) -> None: owner.require_coverage(db, self.row, coverage) proof.require_reference(owner, self.row) diff --git a/src/agent_comms/native_child.py b/src/agent_comms/native_child.py index 6941f68c02..60364cede0 100644 --- a/src/agent_comms/native_child.py +++ b/src/agent_comms/native_child.py @@ -4,27 +4,35 @@ import asyncio from abc import ABC, abstractmethod -from collections.abc import Callable, Collection, Coroutine -from contextlib import AsyncExitStack, ExitStack, asynccontextmanager +from collections.abc import AsyncIterator, Awaitable, Callable, Collection, Coroutine, Mapping +from contextlib import AbstractContextManager, AsyncExitStack, ExitStack, asynccontextmanager from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING, ClassVar, NoReturn -from .pi_events import PiEvent - from .child_process import AttachedChild, ChildOutcome, join_retirement from .declared_family import DeclaredFamily from .launch_runtime import INHERITED_ENVIRONMENT from .native_attestation import NativeAttestation, PendingAttestation -from .native_pi import NativePiRpcLaunch from .native_failures import NativePiUnavailable, NativeRequestFailure +from .native_pi import NativePiRpcLaunch from .native_session_reopen import NativeSessionIdentity +from .pi_events import PiEvent from .pi_rpc import PiRpcChannel from .private_path import PrivateSocketRole from .session_revision import SessionRevision if TYPE_CHECKING: - from .turn_context import Provenance + from typing import Self + + from .diagnostics import PublicationMeasurements + from .native_package import VerifiedNativePackage + from .participant_identity import ParticipantIdentity + from .pi_commands import ExtensionUiResponse, PiCommand + from .pi_native_backend import PiNativeBackend + from .pi_payloads import PiResponseData, StateData + from .threads import Thread + from .turn_context import Provenance, TurnContext class RetirementState(DeclaredFamily): @@ -137,7 +145,7 @@ class PiChild: forwarding: InputForwarder = field(default_factory=NoInputForwarder, init=False, repr=False) _close_job: asyncio.Task[RetirementState] | None = field(default=None, init=False, repr=False) - def send(self, command) -> asyncio.Future: + def send(self, command: PiCommand) -> asyncio.Future: future = self.reader.track(command) self.proc.send(self.reader.command_bytes(command)) return future @@ -145,21 +153,32 @@ def send(self, command) -> asyncio.Future: async def drain(self) -> None: await self.proc.drain() - async def send_fenced(self, command, boundary, *, measurements) -> None: + async def send_fenced( + self, command: PiCommand, boundary: Callable[[], AbstractContextManager[None]], + *, measurements: PublicationMeasurements, + ) -> None: self.reader.track(command) await self.proc.send_fenced(self.reader.command_bytes(command), boundary, measurements=measurements) - async def reply_ui(self, response) -> None: + async def reply_ui(self, response: ExtensionUiResponse) -> None: self.send(response) await asyncio.wait_for(self.drain(), timeout=2) @classmethod - async def start_command(cls, argv, *, cwd, env=INHERITED_ENVIRONMENT, pass_fds=(), - event_type=PiEvent, **state): + async def start_command( + cls, + argv: tuple[str, ...], + *, + cwd: str, + env: Mapping[str, str] = INHERITED_ENVIRONMENT, + pass_fds: tuple[int, ...] = (), + event_type: type[PiEvent] = PiEvent, + **state, + ) -> Self: proc = await AttachedChild.start(argv, cwd=cwd, env=env, pass_fds=pass_fds) return cls(proc, PiRpcChannel(proc.stdout, event_type=event_type), **state) - async def request(self, command): + async def request(self, command: PiCommand) -> PiResponseData: if self.retired: raise RuntimeError('Pi child has retired') async with self.failures(): @@ -241,7 +260,7 @@ async def _shutdown(self, *, graceful: bool) -> RetirementState: return Retired(outcome) @asynccontextmanager - async def failures(self): + async def failures(self) -> AsyncIterator[None]: """Retain original bounded child output before transporting its failure.""" try: yield @@ -270,7 +289,9 @@ def forget_context(self) -> None: self.coordination_revision = () @classmethod - async def start(cls, key, attestation): + async def start( + cls, key: tuple[NativePiRpcLaunch, tuple[int, int]], attestation: NativeAttestation, + ) -> Self: launch, _ = key with ExitStack() as resources: environment = dict(launch.env) @@ -290,22 +311,25 @@ def available(self) -> bool: ... retained = False - def managed_launch(self, command, arguments, *, package, root, owner, directory, worktree, environment, session_file): + def managed_launch(self, command: str, arguments: tuple[str, ...], *, package: VerifiedNativePackage, root: Path, owner: ParticipantIdentity, directory: Path, worktree: Path, environment: dict[str, str] | None, session_file: str | None) -> NativePiRpcLaunch: return NativePiRpcLaunch.managed( command, arguments, package=package, root=root, owner=owner, directory=directory, worktree=worktree, environment=environment, session_file=session_file, ) - def reuse(self, key) -> PiSessionChild | None: + def reuse(self, key: tuple[NativePiRpcLaunch, tuple[int, int]]) -> PiSessionChild | None: return None - async def context_for_input(self, thread, context): + async def context_for_input(self, thread: Thread, context: TurnContext) -> TurnContext: return context def idle(self) -> RetainedNative: raise NativePiUnavailable("Selected idle Pi child is unavailable or stale") - async def inspect(self, persistent, prepare, request): + async def inspect( + self, persistent: PiNativeBackend, prepare: Callable[[], Awaitable[StateData]], + request: PiCommand, + ) -> PiResponseData: """Prepare once, then inspect through the acquired idle capability.""" await prepare() return await persistent.child_state.idle().inspect_acquired(persistent, request) @@ -332,7 +356,7 @@ class ChildNative(NativeChildState): @abstractmethod def _shutdown_successor(self) -> NativeChildState: ... - def retire(self, successor=None): + def retire(self, successor: NativeChildState | None = None) -> NativeChildState: following = self._shutdown_successor() if successor is None else successor return RetiringNative(self.child, following) @@ -352,10 +376,13 @@ def _shutdown_successor(self) -> NativeChildState: class RetiringNative(SuccessorNative): available = False - async def inspect(self, persistent, prepare, request): + async def inspect( + self, persistent: PiNativeBackend, prepare: Callable[[], Awaitable[StateData]], + request: PiCommand, + ) -> PiResponseData: raise NativePiUnavailable("Native context requires an acquired native child") - def retire(self, successor=None): + def retire(self, successor: NativeChildState | None = None) -> NativeChildState: if successor is not None: self.successor = successor return self @@ -368,7 +395,10 @@ def settled(self) -> NativeChildState: @dataclass class BorrowedNative(SuccessorNative): - async def inspect(self, persistent, prepare, request): + async def inspect( + self, persistent: PiNativeBackend, prepare: Callable[[], Awaitable[StateData]], + request: PiCommand, + ) -> PiResponseData: # The active TurnSession owns receive/correlation. Borrow its original # pending response instead of starting a competing reader. async with request.pending_response(self.child) as response: @@ -385,12 +415,12 @@ def current(self) -> bool: launch = self.child.key[0] return self.child.proc.alive() and self.revision.current(launch.session.files(launch.root)) - def idle(self): + def idle(self) -> RetainedNative: if not self.current: return super().idle() return self - async def inspect_acquired(self, persistent, request): + async def inspect_acquired(self, persistent: PiNativeBackend, request: PiCommand) -> PiResponseData: async with persistent.lock: current = persistent.child_state.idle() data = await current.child.request(request) @@ -403,13 +433,13 @@ async def inspect_acquired(self, persistent, request): current.child.forget_context() return data - def reuse(self, key): + def reuse(self, key: tuple[NativePiRpcLaunch, tuple[int, int]]) -> PiSessionChild | None: if self.child.key == key and self.current: self.child.attestation = PendingAttestation(self.identity) return self.child return None - def managed_launch(self, command, arguments, *, package, root, owner, directory, worktree, environment, session_file): + def managed_launch(self, command: str, arguments: tuple[str, ...], *, package: VerifiedNativePackage, root: Path, owner: ParticipantIdentity, directory: Path, worktree: Path, environment: dict[str, str] | None, session_file: str | None) -> NativePiRpcLaunch: if self.current and self.child.key[0].package == package: original, authentication = self.child.key candidate = original.retained_managed(command, arguments, @@ -426,7 +456,7 @@ def managed_launch(self, command, arguments, *, package, root, owner, directory, def _shutdown_successor(self) -> NativeChildState: return EmptyNative() - async def context_for_input(self, thread, context): + async def context_for_input(self, thread: Thread, context: TurnContext) -> TurnContext: if thread.project_resume_due or self.child.key[0].cwd != Path(thread.worktree): self.child.forget_context() if not self.current or self.identity.session_file != thread.runtime.read_key: diff --git a/src/agent_comms/native_input_record.py b/src/agent_comms/native_input_record.py index 85993a98d1..793d6c8114 100644 --- a/src/agent_comms/native_input_record.py +++ b/src/agent_comms/native_input_record.py @@ -9,8 +9,7 @@ import sqlite3 from abc import ABC, abstractmethod from dataclasses import dataclass, field -from typing import Annotated, ClassVar, Literal, TYPE_CHECKING - +from typing import TYPE_CHECKING, Annotated, ClassVar, Literal from .coordination_contracts import validate_execution_id from .coordination_errors import IdentityConflict, UnrecordedNativeContext @@ -28,8 +27,19 @@ from .typed_table import Column, IntegerStorage, SqlStorage, TextStorage if TYPE_CHECKING: + from sqlite3 import Connection + from agent_comms.coordination_tables.executions import ExecutionRecord + from .assignment_states import AssignmentState + from .coordination_tables.assignments import WakeAssignment + from .cursor_owner import CursorOwner + from .historical_native_inputs import HistoricalNativeInput + from .message_reference import MessageReference + from .native_entries import MessageEntry, RecordedEntry, TranscriptProjection + from .native_runtime_input import NativeRuntimeInput + from .transcript_events import TranscriptEvent + @dataclass(frozen=True, slots=True) class NativeInputCommit: @@ -158,7 +168,9 @@ def sql_constraint(cls, column: str) -> str: names = tuple(member.declared_name for member in cls.members_with(cls)) return TextStorage.constraints(column, Literal[names])[0] - def transcript_events(self, entry, projection, user): + def transcript_events( + self, entry: RecordedEntry, projection: TranscriptProjection, user: MessageEntry, + ) -> list[TranscriptEvent]: """Selected prompts consume original wire sources, never human authorship. This display is derived inside the original read snapshot. It is not an @@ -175,18 +187,18 @@ def transcript_events(self, entry, projection, user): def require_attempt(self) -> FullNativeExecution: raise IdentityConflict("Native triage input has no execution attempt") - def published_replies(self, db, owner_lookup): + def published_replies(self, db: sqlite3.Connection, owner_lookup: str) -> tuple[MessageReference, ...]: """TRIAGE has no execution publication; FULL owns that relation.""" return () - def matches_execution(self, execution, owner_lookup: str) -> bool: + def matches_execution(self, execution: ExecutionRecord, owner_lookup: str) -> bool: return False def retried_in(self, db: sqlite3.Connection) -> bool: """TRIAGE has no attempts, so a later attempt never replaces it.""" return False - def proves_full_source(self, proof) -> bool: + def proves_full_source(self, proof: HistoricalNativeInput) -> bool: return False @classmethod @@ -194,7 +206,7 @@ def proves_full_source(self, proof) -> bool: def source_membership_sql(cls) -> str: """Select the assignments included in this stage's input.""" - def source_assignment_ids(self, db, input_id) -> tuple[str, ...]: + def source_assignment_ids(self, db: Connection, input_id: str) -> tuple[str, ...]: from .coordination_tables.assignments import WakeAssignment columns = ",".join(f"c.{column}" for column in WakeAssignment.columns()) @@ -206,11 +218,13 @@ def source_assignment_ids(self, db, input_id) -> tuple[str, ...]: return tuple(original.assignment_id for original in originals) @abstractmethod - def record_sources(self, db, input_id, assignments) -> None: + def record_sources(self, db: Connection, input_id: str, assignments: tuple[WakeAssignment, ...]) -> None: """Record only membership which is not already owned by execution claims.""" @abstractmethod - def historical_proof(self, record, *, lifecycle, **source): + def historical_proof( + self, record: NativeRuntimeInput, *, lifecycle: AssignmentState, **source, + ) -> HistoricalNativeInput | None: """Acquire the declared recorded-proof member from the original SQL row.""" raise NotImplementedError @@ -221,7 +235,7 @@ def from_columns(cls, execution_id: NativeColumn, attempt_ordinal: NativeColumn) raise NotImplementedError @abstractmethod - def binding_fields(self): + def binding_fields(self) -> dict[str, NativeColumn]: """Publish this member through the unchanged durable SQL contract.""" raise NotImplementedError @@ -229,7 +243,9 @@ def binding_fields(self): @dataclass(frozen=True) class TriageNativeExecution(NativeInputExecution): - def transcript_events(self, entry, projection, user): + def transcript_events( + self, entry: RecordedEntry, projection: TranscriptProjection, user: MessageEntry, + ) -> list[TranscriptEvent]: return [event.as_context() for event in super().transcript_events(entry, projection, user)] @@ -240,7 +256,7 @@ def source_membership_sql(cls) -> str: return (f"SELECT s.input_id, member.value AS assignment_id, CAST(member.key AS INTEGER) AS ordinal " f"FROM {TriageNativeSources.declared_name} s, json_each(s.assignment_ids) member") - def record_sources(self, db, input_id, assignments) -> None: + def record_sources(self, db: Connection, input_id: str, assignments: tuple[WakeAssignment, ...]) -> None: from .triage_native_sources import TriageNativeSources TriageNativeSources(input_id, tuple(row.assignment_id for row in assignments)).insert(db) @@ -251,10 +267,12 @@ def from_columns(cls, execution_id: NativeColumn, attempt_ordinal: NativeColumn) attempt_ordinal.require_unrecorded() return cls() - def binding_fields(self): + def binding_fields(self) -> dict[str, NativeColumn]: return {"execution_id": UnrecordedNativeColumn(), "attempt_ordinal": UnrecordedNativeColumn()} - def historical_proof(self, record, *, lifecycle, **source): + def historical_proof( + self, record: NativeRuntimeInput, *, lifecycle: AssignmentState, **source, + ) -> HistoricalNativeInput | None: return record.verdict.historical_proof(record, lifecycle, self, **source) @@ -274,7 +292,7 @@ def source_membership_sql(cls) -> str: f"JOIN {ExecutionAssignmentLink.declared_name} c ON c.execution_id=n.execution_id " f"WHERE n.stage='{cls.declared_name}'") - def record_sources(self, db, input_id, assignments) -> None: + def record_sources(self, db: Connection, input_id: str, assignments: tuple[WakeAssignment, ...]) -> None: if self.source_assignment_ids(db, input_id) != tuple(row.assignment_id for row in assignments): raise IdentityConflict("Full native input differs from its original execution membership") @@ -291,14 +309,14 @@ def from_columns(cls, execution_id: NativeColumn, attempt_ordinal: NativeColumn) "attempt_ordinal": FieldCodec.encode(attempt_ordinal), }) - def binding_fields(self): + def binding_fields(self) -> dict[str, NativeColumn]: return {"execution_id": TextNativeColumn(self.execution_id), "attempt_ordinal": NumberNativeColumn(self.attempt_ordinal)} def require_attempt(self) -> FullNativeExecution: return self - def matches_execution(self, execution, owner_lookup: str) -> bool: + def matches_execution(self, execution: ExecutionRecord, owner_lookup: str) -> bool: return ( execution.execution_id == self.execution_id and execution.owner_lookup == owner_lookup @@ -314,7 +332,7 @@ def retried_in(self, db: sqlite3.Connection) -> bool: """A transient provider failure leaves this input behind a later attempt.""" return self._execution(db).lifecycle.current_attempt_ordinal != self.attempt_ordinal - def published_replies(self, db, owner_lookup): + def published_replies(self, db: sqlite3.Connection, owner_lookup: str) -> tuple[MessageReference, ...]: from .coordination_tables.responses import ResponseObligation execution = self._execution(db) if not self.matches_execution(execution, owner_lookup): @@ -331,13 +349,15 @@ def published_replies(self, db, owner_lookup): ) ) - def historical_proof(self, record, *, lifecycle, **source): + def historical_proof( + self, record: NativeRuntimeInput, *, lifecycle: AssignmentState, **source, + ) -> HistoricalNativeInput | None: from .historical_native_inputs import FullHistoricalNativeInput record.verdict.require_undecided() return FullHistoricalNativeInput(execution=self, **source) - def proves_full_source(self, proof) -> bool: + def proves_full_source(self, proof: HistoricalNativeInput) -> bool: return proof.expected_prompt_equality_established @@ -358,7 +378,7 @@ class NativeContextReference(DeclaredFamily, affix="Reference"): def require_recorded(self) -> NativeInputReference: raise UnrecordedNativeContext("Native input has no recorded context") - def require_recorded_input(self, owner, db) -> None: + def require_recorded_input(self, owner: CursorOwner, db: sqlite3.Connection) -> None: """An unrecorded context grants no committed-input identity.""" @@ -382,7 +402,7 @@ def require_recorded(self) -> NativeInputReference: return self @classmethod - def acquire(cls, row) -> NativeInputReference: + def acquire(cls, row: NativeInputContext) -> NativeInputReference: """Decode the complete original SQL reference, never a partial projection.""" try: result = FieldCodec.construct(cls, dict( @@ -400,7 +420,7 @@ def acquire(cls, row) -> NativeInputReference: except MalformedRecord as error: raise IdentityConflict("Native recorded context reference is incomplete") from error - def require_recorded_input(self, owner, db) -> None: + def require_recorded_input(self, owner: CursorOwner, db: sqlite3.Connection) -> None: owner.require_recorded_reference(db, self) diff --git a/src/agent_comms/notification_assignment.py b/src/agent_comms/notification_assignment.py index 6ee86e03d8..686a1710be 100644 --- a/src/agent_comms/notification_assignment.py +++ b/src/agent_comms/notification_assignment.py @@ -7,7 +7,7 @@ from collections.abc import Iterable, Iterator, Sequence from dataclasses import dataclass, replace from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, NoReturn from .audience_manifest import FrozenRecipient from .coordination_schema import CoordinatorTable @@ -26,10 +26,14 @@ from .typed_table import BooleanStorage, TypedRow if TYPE_CHECKING: - from .private_registry_guard import RegistryPrivacy + from collections.abc import Mapping + from typing import Self + from .agent_activity import RecipientActivity from .bus_publication import Delivered + from .coordination_database import InvalidStore, MissingStore from .presentation import MessageNotification + from .private_registry_guard import RegistryPrivacy from .wake_policy import RecipientDecision @@ -47,7 +51,7 @@ def progress(self, *, pending_at_ms: int, queued: bool) -> AssignmentProgress: . class NotShownContextEvidence(AssignmentContextEvidence): - def progress(self, *, pending_at_ms, queued): + def progress(self, *, pending_at_ms: int, queued: bool) -> AssignmentProgress: progress = QueuedProgress if queued else SentProgress return progress(pending_at_ms) @@ -58,13 +62,13 @@ class ShownContextEvidence(AssignmentContextEvidence): committed_at_ms: int - def progress(self, *, pending_at_ms, queued): + def progress(self, *, pending_at_ms: int, queued: bool) -> AssignmentProgress: return ShownToModelProgress(self.committed_at_ms) @dataclass(frozen=True) class AssignmentResponse(DeclaredFamily, affix="Response"): - def recorded(self, message) -> AssignmentResponse: + def recorded(self, message: Delivered) -> AssignmentResponse: return self @classmethod @@ -92,15 +96,15 @@ def delivered_notification( class UnansweredResponse(AssignmentResponse): - def recorded(self, message): + def recorded(self, message: Delivered) -> AssignmentResponse: return AnsweredResponse( message.reference, int(message.timestamp * 1000) ) - def completed_notification(self, progress, notification): + def completed_notification(self, progress: AssignmentProgress, notification: MessageNotification) -> MessageNotification: return progress.read_notification(notification) - def delivered_notification(self, notification, delivered_at_ms): + def delivered_notification(self, notification: MessageNotification, delivered_at_ms: int) -> MessageNotification: return replace( notification, state=ShownToModelProgress(delivered_at_ms), @@ -113,12 +117,12 @@ class AnsweredResponse(AssignmentResponse): reference: MessageReference received_at_ms: int - def completed_notification(self, progress, notification): + def completed_notification(self, progress: AssignmentProgress, notification: MessageNotification) -> MessageNotification: return progress.answered_notification( notification, self.reference, self.received_at_ms ) - def delivered_notification(self, notification, delivered_at_ms): + def delivered_notification(self, notification: MessageNotification, delivered_at_ms: int) -> MessageNotification: return replace( notification, state=AnsweredProgress(ShownToModelProgress(delivered_at_ms)), @@ -134,16 +138,16 @@ class AssignmentActivity: progress: AssignmentProgress response: AssignmentResponse - def pending_notification(self, notification, queued_at_ms): + def pending_notification(self, notification: MessageNotification, queued_at_ms: int) -> MessageNotification: return self.progress.selected_notification(notification, busy=False) - def completed_notification(self, notification): + def completed_notification(self, notification: MessageNotification) -> MessageNotification: return self.response.completed_notification(self.progress, notification) - def delivered_notification(self, notification, delivered_at_ms): + def delivered_notification(self, notification: MessageNotification, delivered_at_ms: int) -> MessageNotification: return self.response.delivered_notification(notification, delivered_at_ms) - def recorded_response(self, message): + def recorded_response(self, message: Delivered) -> Self: return replace(self, response=self.response.recorded(message)) @@ -168,8 +172,8 @@ def column_projection(cls) -> dict[str, str]: } @classmethod - def _decode_row(cls, row, positions) -> AssignmentActivityEvidence: - def column(name): + def _decode_row(cls, row, positions: Mapping[str, int]) -> AssignmentActivityEvidence: + def column(name: str): return row[positions[name]] return cls( @@ -206,7 +210,7 @@ class UnrecordedNotificationSource(NotificationSource): recipient: FrozenRecipient decision: RecipientDecision - def project(self, observation: RecipientActivity, sender_lookup: str): + def project(self, observation: RecipientActivity, sender_lookup: str) -> MessageNotification: return self.decision.initial_notification( self.recipient, sender_lookup, observation=observation) @@ -344,11 +348,15 @@ def read( return result @classmethod - def missing_store(cls, store, sources, privacy): + def missing_store( + cls, store: MissingStore, sources: Sequence[Delivered], privacy: RegistryPrivacy, + ) -> NotificationAssignmentRead: return privacy.missing_notification_assignments(sources) @classmethod - def invalid_store(cls, store, sources, privacy): + def invalid_store( + cls, store: InvalidStore, sources: Sequence[Delivered], privacy: RegistryPrivacy, + ) -> NoReturn: raise ValueError(f"Channel notification status unavailable: {store.failure_reason}") @property diff --git a/src/agent_comms/proven_source_coverage.py b/src/agent_comms/proven_source_coverage.py index 2301929c5a..e19d02113f 100644 --- a/src/agent_comms/proven_source_coverage.py +++ b/src/agent_comms/proven_source_coverage.py @@ -17,14 +17,14 @@ from agent_comms.coordination_errors import IdentityConflict from agent_comms.coordinator import Coordination from agent_comms.lifecycle_capability import Active +from agent_comms.native_runtime_input import NativeRuntimeTable from .agent_backend import InputId from .audience_manifest import FrozenRecipient -from .cohort_schema import ClaimBatchAcknowledgements, CohortTable -from agent_comms.native_runtime_input import NativeRuntimeTable from .bus import Bus from .bus_publication import Delivered from .bus_source_page import CoveragePage +from .cohort_schema import ClaimBatchAcknowledgements, CohortTable from .coordination_cohort import AcceptedCohort, _claim_record from .coordination_session import NativeBoundary from .field_codec import FieldCodec, Id128Text @@ -72,10 +72,12 @@ class CoverageInput(ABC): """The new suffix either injects one native reference or preserves the old one.""" @abstractmethod - def injected_for(self, state) -> int: ... + def injected_for(self, state: CursorState) -> int: ... @abstractmethod - def candidate(self, state, owner, coverage): ... + def candidate( + self, state: CursorState, owner: CursorOwner, coverage: ProvenSourceCoverage, + ) -> CurrentNativeCursor: ... @abstractmethod def allows(self, owner: CursorOwner, db: sqlite3.Connection, state: CursorState) -> bool: ... @@ -83,7 +85,7 @@ def allows(self, owner: CursorOwner, db: sqlite3.Connection, state: CursorState) @abstractmethod def require_reference(self, owner: CursorOwner, cursor: CurrentNativeCursor) -> None: ... - def build(self, owner, covered_seq): + def build(self, owner: CursorOwner, covered_seq: int) -> CurrentNativeCursor: return CurrentNativeCursor( wire_root_id=owner.wire_root_id, recipient_lookup=owner.lookup, owner_generation=owner.generation, @@ -97,14 +99,16 @@ def input_fields(self) -> dict: ... @dataclass(frozen=True) class NoInjectedInput(CoverageInput): - def input_fields(self): + def input_fields(self) -> dict: return dict(injected_seq=0, input_id=UnrecordedNativeColumn(), stage=UnrecordedNativeColumn(), session_id=UnrecordedNativeColumn(), request_generation=UnrecordedNativeColumn()) - def injected_for(self, state): + def injected_for(self, state: CursorState) -> int: return state.injected_seq - def candidate(self, state, owner, coverage): + def candidate( + self, state: CursorState, owner: CursorOwner, coverage: ProvenSourceCoverage, + ) -> CurrentNativeCursor: return state.without_input(self, owner, coverage) def allows(self, owner: CursorOwner, db: sqlite3.Connection, state: CursorState) -> bool: @@ -118,7 +122,7 @@ def require_reference(self, owner: CursorOwner, cursor: CurrentNativeCursor) -> class InjectedInput(CoverageInput): original: HistoricalNativeInput - def input_fields(self): + def input_fields(self) -> dict: original = self.original return dict( injected_seq=original.source_seq, input_id=TextNativeColumn(original.input_id), @@ -126,10 +130,12 @@ def input_fields(self): session_id=TextNativeColumn(original.context.session_id), request_generation=NumberNativeColumn(original.context.request_generation), ) - def injected_for(self, state): + def injected_for(self, state: CursorState) -> int: return self.original.source_seq - def candidate(self, state, owner, coverage): + def candidate( + self, state: CursorState, owner: CursorOwner, coverage: ProvenSourceCoverage, + ) -> CurrentNativeCursor: return self.build(owner, coverage.covered_seq) def allows(self, owner: CursorOwner, db: sqlite3.Connection, state: CursorState) -> bool: diff --git a/src/agent_comms/selected_triage.py b/src/agent_comms/selected_triage.py index 2eb19a4917..518e4271ca 100644 --- a/src/agent_comms/selected_triage.py +++ b/src/agent_comms/selected_triage.py @@ -6,7 +6,7 @@ from abc import abstractmethod from dataclasses import dataclass from functools import partial -from typing import ClassVar, Literal +from typing import TYPE_CHECKING, ClassVar, Literal from .assignment_states import FailedAssignment, IgnoredAssignment, TriagePendingAssignment from .coordination_tables.assignments import WakeAssignment @@ -21,6 +21,21 @@ ) from .typed_table import TextStorage +if TYPE_CHECKING: + from sqlite3 import Connection + + from .assignment_states import AssignmentState + from .coordinated_runtime import SelectedExecution + from .historical_native_inputs import HistoricalNativeInput, TriageHistoricalNativeInput + from .native_input_record import TriageNativeExecution + from .native_pi import NativeContextProof + from .native_runtime_input import NativeRuntimeInput + from .private_send_claim import PrivateSendClaim + from .private_send_stage import TriageNativeSend + from .selected_participant import SelectedParticipant + from .selected_result import CoordinatedTurn + from .selected_session import SelectedSession + class InvalidTriageDecision(ValueError): """A model result violates its contract; participant identity is unchanged.""" @@ -54,22 +69,22 @@ def recognizes(cls, text: str) -> bool: @classmethod @abstractmethod - def proves_source(cls, evidence) -> bool: ... + def proves_source(cls, evidence: tuple[HistoricalNativeInput, ...]) -> bool: ... @abstractmethod - def settle(self, store, db, current: WakeAssignment) -> None: ... + def settle(self, store: Coordination, db: Connection, current: WakeAssignment) -> None: ... @abstractmethod - async def continue_turn(self, participant, session, input_id, execution, settled): ... + async def continue_turn(self, participant: SelectedParticipant, session: SelectedSession, input_id: str, execution: SelectedExecution, settled: tuple[WakeAssignment, ...]) -> CoordinatedTurn: ... @dataclass(frozen=True) class IgnoreSelectedTriage(SelectedTriage, declared_name="IGNORE"): @classmethod - def proves_source(cls, evidence) -> bool: + def proves_source(cls, evidence: tuple[HistoricalNativeInput, ...]) -> bool: return True - def settle(self, store, db, current): + def settle(self, store: Coordination, db: Connection, current: WakeAssignment) -> None: # Both declared edges stay in the native proof's one transaction. Never # publish a retryable TRIAGE_PENDING state after the input has been sent. now = store.session.now(current.updated_at_ms) @@ -90,7 +105,7 @@ def settle(self, store, db, current): updated_at_ms=store.session.now(now), ) - async def continue_turn(self, participant, session, input_id, execution, settled): + async def continue_turn(self, participant: SelectedParticipant, session: SelectedSession, input_id: str, execution: SelectedExecution, settled: tuple[WakeAssignment, ...]) -> CoordinatedTurn: from .selected_result import CoordinatedTurn await Coordination.run_worker(participant.consume_reply_wait) @@ -100,14 +115,14 @@ async def continue_turn(self, participant, session, input_id, execution, settled @dataclass(frozen=True) class FullSelectedTriage(SelectedTriage, declared_name="FULL"): @classmethod - def proves_source(cls, evidence) -> bool: + def proves_source(cls, evidence: tuple[HistoricalNativeInput, ...]) -> bool: return any(proof.execution.proves_full_source(proof) for proof in evidence) - def settle(self, store, db, current): + def settle(self, store: Coordination, db: Connection, current: WakeAssignment) -> None: # Deferred until the execution owner atomically engages this exact claim. pass - async def continue_turn(self, participant, session, input_id, execution, settled): + async def continue_turn(self, participant: SelectedParticipant, session: SelectedSession, input_id: str, execution: SelectedExecution, settled: tuple[WakeAssignment, ...]) -> CoordinatedTurn: from .coordination_tables.executions import WireOrigin await participant.require_current() @@ -131,21 +146,21 @@ def acquire(cls, text: str) -> SelectedTriageOutcome: return RejectedTriageOutcome(error) @abstractmethod - def settle(self, participant, stage, claim_scope, context) -> tuple[WakeAssignment, ...]: ... + def settle(self, participant: SelectedParticipant, stage: TriageNativeSend, claim_scope: PrivateSendClaim, context: NativeContextProof) -> tuple[WakeAssignment, ...]: ... @abstractmethod - async def continue_turn(self, participant, session, input_id, execution, settled): ... + async def continue_turn(self, participant: SelectedParticipant, session: SelectedSession, input_id: str, execution: SelectedExecution, settled: tuple[WakeAssignment, ...]) -> CoordinatedTurn | None: ... @dataclass(frozen=True) class DecidedTriageOutcome(SelectedTriageOutcome): decision: SelectedTriage - def settle(self, participant, stage, claim_scope, context): + def settle(self, participant: SelectedParticipant, stage: TriageNativeSend, claim_scope: PrivateSendClaim, context: NativeContextProof) -> tuple[WakeAssignment, ...]: return stage.commit(participant.store, participant.identity, claim_scope.input_id, claim_scope.token_digest, context, self.decision) - async def continue_turn(self, participant, session, input_id, execution, settled): + async def continue_turn(self, participant: SelectedParticipant, session: SelectedSession, input_id: str, execution: SelectedExecution, settled: tuple[WakeAssignment, ...]) -> CoordinatedTurn | None: return await self.decision.continue_turn(participant, session, input_id, execution, settled) @@ -153,11 +168,11 @@ async def continue_turn(self, participant, session, input_id, execution, settled class RejectedTriageOutcome(SelectedTriageOutcome): error: InvalidTriageDecision - def settle(self, participant, stage, claim_scope, context): + def settle(self, participant: SelectedParticipant, stage: TriageNativeSend, claim_scope: PrivateSendClaim, context: NativeContextProof) -> tuple[WakeAssignment, ...]: return stage.fail_decision(participant.store, participant.identity, claim_scope.input_id, claim_scope.token_digest, context) - async def continue_turn(self, participant, session, input_id, execution, settled): + async def continue_turn(self, participant: SelectedParticipant, session: SelectedSession, input_id: str, execution: SelectedExecution, settled: tuple[WakeAssignment, ...]) -> CoordinatedTurn | None: from .selected_result import CoordinatedTurn, publish_native_failure sent = await Coordination.run_worker(partial( @@ -177,7 +192,7 @@ class TriageDecisionRecord(DeclaredFamily, JsonShapeFamily, affix="TriageDecisio decided: ClassVar[bool] = True @abstractmethod - def historical_proof(self, record, lifecycle, execution, **source): ... + def historical_proof(self, record: NativeRuntimeInput, lifecycle: AssignmentState, execution: TriageNativeExecution, **source) -> TriageHistoricalNativeInput | None: ... def require_undecided(self) -> None: raise MalformedRecord("A full native input records no triage verdict") @@ -188,10 +203,10 @@ class TriageDecisionText(TriageDecisionRecord, JsonShapeMember): value: str @classmethod - def from_json_value(cls, value): + def from_json_value(cls, value: str) -> DecidedTriageRecord: return DecidedTriageRecord(RecordedTriageText.from_text(value)) - def historical_proof(self, record, lifecycle, execution, **source): + def historical_proof(self, record: NativeRuntimeInput, lifecycle: AssignmentState, execution: TriageNativeExecution, **source) -> TriageHistoricalNativeInput | None: raise TypeError("Triage decision text must be decoded at ingress") @@ -199,10 +214,10 @@ def historical_proof(self, record, lifecycle, execution, **source): class DecidedTriageRecord(TriageDecisionRecord): decision: type[SelectedTriage] - def to_wire(self): + def to_wire(self) -> str: return RecordedTriageText.encode(self.decision) - def historical_proof(self, record, lifecycle, execution, **source): + def historical_proof(self, record: NativeRuntimeInput, lifecycle: AssignmentState, execution: TriageNativeExecution, **source) -> TriageHistoricalNativeInput | None: from .historical_native_inputs import TriageHistoricalNativeInput return TriageHistoricalNativeInput(execution=execution, decision=self.decision, **source) @@ -216,7 +231,7 @@ class AbsentTriageDecisionRecord(TriageDecisionRecord, JsonShapeMember): def require_undecided(self) -> None: pass - def historical_proof(self, record, lifecycle, execution, **source): + def historical_proof(self, record: NativeRuntimeInput, lifecycle: AssignmentState, execution: TriageNativeExecution, **source) -> TriageHistoricalNativeInput | None: return lifecycle.rejected_triage_history(execution, **source) @@ -224,7 +239,7 @@ class RecordedTriageText(TextRepresentation[type[SelectedTriage]]): """The original native SQL decision uses lowercase declared triage names.""" @classmethod - def encode(cls, value): + def encode(cls, value) -> str: return FieldCodec.encode(value, type[SelectedTriage]).lower() @classmethod @@ -235,7 +250,7 @@ def from_text(cls, value: str) -> type[SelectedTriage]: return decision @classmethod - def schema(cls): + def schema(cls) -> dict[str, str | list[str]]: return { "type": "string", "enum": [member.declared_name.lower() for member in SelectedTriage.members_with(SelectedTriage)], diff --git a/src/agent_comms/transcripts.py b/src/agent_comms/transcripts.py index 3929d0b0ce..487fce13f3 100644 --- a/src/agent_comms/transcripts.py +++ b/src/agent_comms/transcripts.py @@ -4,22 +4,22 @@ import logging from abc import abstractmethod -from collections.abc import Awaitable, Sequence +from collections.abc import Awaitable, Iterator, Sequence from contextlib import contextmanager from dataclasses import dataclass, field, fields, replace from functools import partial from pathlib import Path -from typing import Annotated, Callable, ClassVar - +from typing import TYPE_CHECKING, Annotated, Callable, ClassVar, NoReturn from .agent_backend import RuntimeLocation +from .bus import Bus from .bus_publication import Delivered from .coordination_database import CoordinationStore from .coordination_errors import CoordinationReadUnavailable, StaleRevision, TranscriptChanged from .declared_family import DeclaredFamily from .errors import RelationViolationError from .external_transcript import ExternalTranscript -from .field_codec import FieldCodec, projected, ContentExclude, WireName +from .field_codec import ContentExclude, FieldCodec, WireName, projected from .historical_views import HistorySource from .message_bus import MessageBus from .message_reference import MessageReference @@ -48,7 +48,16 @@ ) from .turn_digests import ThreadTurns, TurnBound, TurnDigestRead from .turn_lease import TurnLeaseFence -from .bus import Bus + +if TYPE_CHECKING: + from sqlite3 import Connection + + from .coordination_database import InvalidStore, MissingStore + from .registry_document import RegistrySnapshot + from .registry_provenance import RegistryProvenance + from .thread_identity import ThreadIncarnation + from .thread_text import ThreadParent, ThreadText + from .view_unread import TranscriptReadState _LOG = logging.getLogger(__name__) @@ -73,7 +82,7 @@ def at_sequence(self, sequence: int) -> TranscriptCursor: def with_frontier(self, frontier: WireFrontier) -> TranscriptCursor: ... @abstractmethod - def mark_read(self, reads, viewer, runtime: RuntimeLocation) -> None: ... + def mark_read(self, reads: TranscriptReadState, viewer: str, runtime: RuntimeLocation) -> None: ... def contains(self, other: TranscriptCursor) -> bool: return other.within(self) @@ -144,7 +153,7 @@ def at_offset(self, offset: int) -> TranscriptCursor: def with_frontier(self, frontier: WireFrontier) -> TranscriptCursor: return self - def mark_read(self, reads: object, viewer: object, runtime: RuntimeLocation) -> None: + def mark_read(self, reads: TranscriptReadState, viewer: str, runtime: RuntimeLocation) -> None: """Nothing has been drawn, so there is nothing to acknowledge.""" @@ -157,13 +166,13 @@ class NoNativeTranscriptCursor(TranscriptCursor): wire: WireFrontier = field(default_factory=NoWireFrontier) @property - def session_file(self): + def session_file(self) -> str | None: return self.source.read_key def with_frontier(self, frontier: WireFrontier) -> TranscriptCursor: return replace(self, wire=frontier) - def mark_read(self, reads, viewer, runtime: RuntimeLocation) -> None: + def mark_read(self, reads: TranscriptReadState, viewer: str, runtime: RuntimeLocation) -> None: """There is no painted native transcript to acknowledge.""" @@ -181,7 +190,7 @@ def with_prefix(self, prefix: TranscriptCursor) -> TranscriptCursor: def with_frontier(self, frontier: WireFrontier) -> TranscriptCursor: return frontier.cursor(self.session_file, self.offset) - def mark_read(self, reads, viewer, runtime: RuntimeLocation) -> None: + def mark_read(self, reads: TranscriptReadState, viewer: str, runtime: RuntimeLocation) -> None: if self.session_file != runtime.read_key: raise TranscriptChanged() for key in runtime.read_keys(): @@ -247,7 +256,7 @@ class TranscriptReadIdentity(DeclaredFamily, affix="TranscriptReadIdentity"): through: TranscriptCursor | None @projected(view="content", name="thread") - def content_thread(self): + def content_thread(self) -> ThreadIncarnation: return self.thread.incarnation @property @@ -256,20 +265,20 @@ def historical_source(self) -> str | None: @classmethod @abstractmethod - def resolve_source(cls, owner, name, source_runtime, historical_source) -> PageSource: + def resolve_source(cls, owner: Transcripts, name: str, source_runtime: str | None, historical_source: str | None) -> PageSource: """Acquire this member's namespace, stores and identity constructor.""" @abstractmethod - def namespace(self, owner): + def namespace(self, owner: Transcripts) -> RegistryProvenance: """Reacquire this exact source without substituting a live namespace.""" def publish_turn(self, publish: Callable[[str, Thread], None]) -> None: """Recorded evidence cannot settle a turn or permit new input.""" - def recapture(self, owner) -> TranscriptReadIdentity: + def recapture(self, owner: Transcripts) -> TranscriptReadIdentity: return owner.recapture_page_identity(self) - def require_binding(self, owner, name) -> None: + def require_binding(self, owner: Transcripts, name: str) -> None: registry = self.namespace(owner) requested = registry.threads.get(registry.canonical_name(name)) if requested is None: @@ -278,7 +287,7 @@ def require_binding(self, owner, name) -> None: raise StaleRevision("Transcript witness belongs to another original thread") @projected(view="content", name="bus") - def content_bus(self): + def content_bus(self) -> RecordedBusIdentity: # Appends beyond this page's assigned messages do not change its content. # Replacing the bus file does. return self.bus_identity @@ -318,11 +327,11 @@ class LiveTranscriptReadIdentity(TranscriptReadIdentity): thread: Annotated[Thread, ContentExclude()] @projected(view="content", name="thread") - def content_thread(self): + def content_thread(self) -> tuple[ThreadIncarnation, ThreadParent, ThreadText]: return self.thread.incarnation, self.thread.parent, self.thread.task @classmethod - def resolve_source(cls, owner, name, source_runtime, historical_source): + def resolve_source(cls, owner: Transcripts, name: str, source_runtime: str | None, historical_source: str | None) -> PageSource: registry = owner.registry.snapshot() thread, runtime, inherited = owner._thread_transcript_source( name, source_runtime, registry=registry, @@ -332,7 +341,7 @@ def resolve_source(cls, owner, name, source_runtime, historical_source): owner.root, owner.bus.bus, owner.routes, records, cls, LiveAssignedTranscriptSource.for_thread(owner.root, thread, owner.bus.bus)) - def namespace(self, owner): + def namespace(self, owner: Transcripts) -> RegistryProvenance: return owner.registry.snapshot() def publish_turn(self, publish: Callable[[str, Thread], None]) -> None: @@ -348,13 +357,13 @@ def historical_source(self) -> str: return self.source.key @projected(view="content", name="source") - def content_source(self): + def content_source(self) -> tuple[str, str, RecordedBusIdentity, RecordedBusIdentity, int, FileObservation]: return (self.source.key, self.source.original_root, self.source.bus_identity, self.source.snapshot_bus_identity, self.source.snapshot_bus_revision, self.source.snapshot_registry_revision) @classmethod - def resolve_source(cls, owner, name, source_runtime, historical_source): + def resolve_source(cls, owner: Transcripts, name: str, source_runtime: str | None, historical_source: str | None) -> PageSource: source = HistorySource.require(owner.bus.history.sources(), historical_source) thread = source.provenance.require(name) runtime = thread.runtime @@ -368,13 +377,13 @@ def resolve_source(cls, owner, name, source_runtime, historical_source): partial(cls, source=source), AssignedTranscriptSource.for_recorded(root, thread, bus, records)) - def namespace(self, owner): + def namespace(self, owner: Transcripts) -> RegistryProvenance: current = HistorySource.require(owner.bus.history.sources(), self.source.key) if current != self.source: raise StaleRevision("Transcript witness belongs to another recorded source") return current.provenance - def recapture(self, owner) -> TranscriptReadIdentity: + def recapture(self, owner: Transcripts) -> TranscriptReadIdentity: self.namespace(owner) return TranscriptReadIdentity.recapture(self, owner) @@ -392,21 +401,21 @@ class TranscriptRead: @classmethod @contextmanager - def publication_source(cls, root): + def publication_source(cls, root: Path) -> Iterator[Connection | None]: privacy = RegistryPrivacy.capture(root) with CoordinationStore.observed(CoordinationStore.path_in(root)).observing(cls, privacy) as db: yield db @classmethod - def capture(cls, db, privacy: RegistryPrivacy): + def capture(cls, db: Connection, privacy: RegistryPrivacy) -> Connection | None: return privacy.publication_connection(db) @classmethod - def missing_store(cls, store, privacy: RegistryPrivacy): + def missing_store(cls, store: MissingStore, privacy: RegistryPrivacy) -> None: return None @classmethod - def invalid_store(cls, store, privacy: RegistryPrivacy): + def invalid_store(cls, store: InvalidStore, privacy: RegistryPrivacy) -> NoReturn: raise RelationViolationError("Native reply source has an invalid coordinator") def current(self) -> bool: @@ -588,7 +597,7 @@ def external(self, name: str) -> ExternalTranscript: page = self._page_source(name, None, None) return page.runtime.external_records(page.root, page.thread.incarnation) - def _page_source(self, name, through, historical_source): + def _page_source(self, name: str, through: TranscriptCursor | None, historical_source: str | None) -> PageSource: member = (LiveTranscriptReadIdentity if historical_source is None else RecordedTranscriptReadIdentity) return member.resolve_source( @@ -596,7 +605,7 @@ def _page_source(self, name, through, historical_source): ) def _thread_transcript_source( - self, name: str, source_runtime: str | None = None, *, registry + self, name: str, source_runtime: str | None = None, *, registry: RegistrySnapshot ) -> tuple[Thread, RuntimeLocation, bool]: """Resolve a temporary inherited view without changing runtime ownership.""" thread = registry.require(name)