Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cfad676
Plan: one declaration per bus read (#3537)
trissim Oct 27, 2026
9c0c6ae
Merge branch 'refactor/one-declaration-per-bus-read' of https://githu…
trissim Oct 27, 2026
c57a747
Declare bus reads once: BusRead family, notifications migrated
trissim Oct 27, 2026
3c23ca2
Merge remote-tracking branch 'origin/main' into work/claude-nra--P872
trissim Oct 27, 2026
36694a5
Merge remote-tracking branch 'origin/main' into work/claude-nra--P872
trissim Oct 28, 2026
b97957a
Merge remote-tracking branch 'origin/main' into work/claude-nra--P872
trissim Oct 28, 2026
1ab89e0
Merge remote-tracking branch 'origin/main' into work/claude-nra--P872
trissim Oct 28, 2026
3d3a357
Type counts are measured each comparison, never stored by content (#3…
trissim Oct 28, 2026
7326056
One goal-history record answers every boundary (#3538)
trissim Oct 28, 2026
36611e2
Input delivery is one owner-live read; MCP no longer answers without …
trissim Oct 28, 2026
c0821f5
Owner-read tests derive their cases and run the delivery read against…
trissim Oct 28, 2026
e607be5
Merge remote-tracking branch 'origin/main' into work/claude-nra--P872
trissim Oct 28, 2026
a4702b1
Owner reads admit only native owners and keep the incarnation they we…
trissim Oct 28, 2026
08263a4
Owner reads are held to their owner's lifetime: real EOF, root, recre…
trissim Oct 28, 2026
5c26262
The runtime envelope owns the owner exchange; the read capability sto…
trissim Oct 28, 2026
9bb8b2e
Owner-read fixtures drive the real async observer and the real deleti…
trissim Oct 28, 2026
8d21db4
A stale owner is refused as identity-changed before its process is as…
trissim Oct 28, 2026
21b0755
Notification reads are root-free operations; the UI envelope carries …
trissim Oct 28, 2026
7bcb1ca
Notification activity holds only projected participants; HistorySourc…
trissim Oct 28, 2026
5bcc78d
Read-state fixture watches native recipients, not the whole team (#3538)
trissim Oct 28, 2026
7b5c78a
Notification reads acquire only their viewer's claims; viewers receiv…
trissim Oct 28, 2026
2e8db82
Controls for viewer-scoped evidence and published-only thread present…
trissim Oct 28, 2026
5126bc6
Presentations construct from their declared fields inline; forced-bat…
trissim Oct 28, 2026
ef1ae76
Forced-batch control lowers the limit only as far as schema validatio…
trissim Oct 28, 2026
ed6ff38
Sent summaries aggregate at their owner; the changed accumulator keep…
trissim Oct 28, 2026
7fff19b
Merge remote-tracking branch 'origin/main' into work/claude-nra--P872
trissim Oct 28, 2026
ffe8bd2
BusRead owns apply once; the reply test reads a read's result from it…
trissim Oct 28, 2026
780b995
Commander detail is one root-free shared read, comms_commander_unit (…
trissim Oct 28, 2026
1c8b7e8
The owner's goal is one owner-live read, comms_owner_goal (#3538)
trissim Oct 28, 2026
d92c063
A transcript page is one root-free shared read, comms_transcript_page…
trissim Oct 28, 2026
d5b7376
MCP answers a commander-detail branch refusal as a failed call; share…
trissim Oct 28, 2026
f675fbe
MCP answers a corrupt transcript as a failed call; tests send tool ar…
trissim Oct 28, 2026
701b587
TranscriptSnapshotUpdate.from_read declares its read and result
trissim Oct 28, 2026
5ed8ac7
debt ratchet: introduced occurrences are the one clean-lines gate; fi…
trissim Oct 28, 2026
908db01
debt ratchet: one lexical binding owner; a missing introduced report …
trissim Oct 28, 2026
1372658
debt ratchet: extracted field types compare by the owner they are bou…
trissim Oct 28, 2026
1ea8ab3
debt ratchet: the held-class edge and Annotated strip read in class s…
trissim Oct 28, 2026
7577385
debt ratchet: ScopeNames decodes bindings; credit is keyed by the nod…
trissim Oct 28, 2026
820a40f
Merge origin/main into one-declaration-per-bus-read
trissim Oct 29, 2026
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
14 changes: 8 additions & 6 deletions src/agent_comms/acp_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from abc import abstractmethod
from dataclasses import dataclass, field, replace
from typing import TYPE_CHECKING, Annotated, Callable, ClassVar
from typing import TYPE_CHECKING, Annotated, Callable, ClassVar, Self


from .acp_failure import (
Expand Down Expand Up @@ -52,8 +52,10 @@
from .reported_turn import ReportedTurn, ReportedTurnState
from .routing import MessageRoute, NoMessageRoute
from .thread_identity import ClaimScoped, ThreadIncarnation
from .tool_result import DeclaredReply
from .transcripts import NativeCommittedPrefix, TranscriptCursor, TranscriptPage, TranscriptReadIdentity
from .tool_result import ToolResult
from .transcripts import (
NativeCommittedPrefix, TranscriptCursor, TranscriptPage, TranscriptRead, TranscriptReadIdentity,
)
from .turn_context import ContextManifest
from .turn_lease import TurnState
from .working_memory_labels import ModelLabel
Expand Down Expand Up @@ -138,7 +140,7 @@ def of(cls, *updates: AgentCommsUpdate) -> UpdateMetadata:


@dataclass(frozen=True)
class GoalSnapshotReply(DeclaredReply, GoalSnapshot):
class GoalSnapshotReply(ToolResult, GoalSnapshot):
"""A goal request's reply: the owner's goal snapshot and its turn state."""

execution: Annotated[GoalExecution, WireName("goalExecution")]
Expand Down Expand Up @@ -698,13 +700,13 @@ class TranscriptDraftUpdate(AgentCommsUpdate):


@dataclass(frozen=True)
class TranscriptSnapshotUpdate(AgentCommsUpdate):
class TranscriptSnapshotUpdate(AgentCommsUpdate, ToolResult):
page: TranscriptPage
identity: TranscriptReadIdentity
draft: DraftRow | None = None

@classmethod
def from_read(cls, read):
def from_read(cls, read: TranscriptRead) -> Self:
page, draft = read.read_snapshot()
return cls(page, read.identity, draft.external_row())

Expand Down
119 changes: 119 additions & 0 deletions src/agent_comms/bus_read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""One declaration per bus read: the operation owns its arguments and its read once.

A ``BusRead`` member is the operation. Each boundary that serves it keeps only what is
its own: the UI its root, request id and refusals (``ReadRequest``), MCP its caller,
exposure and reply (``ToolRequest``). Neither boundary restates the read, and the
reader's authority reaches the operation as a ``BusReader`` the boundary supplies,
never as an argument the caller may choose.
"""
from __future__ import annotations

import asyncio
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING, NoReturn

from .launch_runtime import CallingThreadUnavailable
from .owner_connection import (
OwnerConnection, OwnerReading, OwnerReplyConnection, ResultRuntimeRequest, RuntimeRequestContext,
)
from .presentation import AllOutcomes
from .tool_declaration import ToolDeclaration
from .tool_result import ToolResult

if TYPE_CHECKING:
from .comms import Comms
from .presentation import OutcomeViewer


class BusReader(ABC):
"""Whose authority a read runs under; each boundary supplies its own reader."""

@abstractmethod
def outcome_viewer(self, comms: Comms) -> OutcomeViewer:
"""Whose recipient outcomes this reader may read."""

def subject(self, named: str) -> str:
return named or self.own_thread()

def own_thread(self) -> str:
"""The thread a read targets when its call names none; a reader with no thread has none."""
return self.refuse_thread()

def refuse_thread(self) -> NoReturn:
raise CallingThreadUnavailable("This tool requires a thread caller.")


class OperatorOutcomes:
"""A reader speaking for the owner reads every recipient outcome, as the owner's UI does."""

__slots__ = ()

def outcome_viewer(self, comms: Comms) -> OutcomeViewer:
return AllOutcomes()


class OperatorReader(OperatorOutcomes, BusReader):
"""The owner's UI: it observes the whole bus as the operator."""

__slots__ = ()


class BusRead[Result](ToolDeclaration, ABC):
"""A read of Core's stores, declared once with its arguments; boundaries derive it."""

def apply(self, comms: Comms, caller: BusReader) -> Result:
"""The MCP boundary runs the read under its calling thread's authority."""
return self.read_as(comms, caller)

@abstractmethod
def read_as(self, comms: Comms, reader: BusReader) -> Result:
"""The read under the reader's authority."""


class UiRead[Result](BusRead[Result], ABC):
"""The UI serves this read as the operator; its envelope (root, request id, refusal)
stays on ``ReadRequest``."""

def read(self, comms: Comms) -> Result:
return self.read_as(comms, OperatorReader())


class OwnerLiveRead[Result: ToolResult = ToolResult](BusRead[Result], ABC):
"""A read only the selected thread's live native owner can answer, from its own queue.

MCP and the UI both reach the owner through ``OwnerReadRuntimeRequest``; neither answers
from its own process, and owner loss reaches the caller as the original refusal. The
owner runs ``owned`` against its own registration and session, never against the public
thread selector.
"""

if TYPE_CHECKING:
# The composed ThreadReadTool declares the field; an annotation here would shadow it.
thread: str

def read_as(self, comms: Comms, reader: BusReader) -> Result:
owner = comms.registry.require(reader.subject(self.thread)).incarnation
request = OwnerReadRuntimeRequest(thread=owner.name, operation=self)
return self.decode_reply(asyncio.run(OwnerReplyConnection.answer(comms, owner, request)))

@abstractmethod
async def owned(self, ctx: RuntimeRequestContext) -> Result:
"""The answer computed inside the owner, under its own context."""

@abstractmethod
def decode_reply(self, raw: object) -> Result: ...


@dataclass(frozen=True, kw_only=True)
class OwnerReadRuntimeRequest(OwnerReading[object], ResultRuntimeRequest):
"""The one runtime envelope for every owner-live read: sent once to the held owner, and
answered there by the operation under the owner's context."""

operation: OwnerLiveRead

async def read(self, owner: OwnerConnection) -> object:
return await owner.request(self.declared_name, operation=self.operation)

async def result(self, ctx: RuntimeRequestContext) -> ToolResult:
return await self.operation.owned(ctx)
2 changes: 1 addition & 1 deletion src/agent_comms/commander_hud.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class CommanderOrderDetail:


@dataclass(frozen=True)
class CommanderUnitDetail:
class CommanderUnitDetail(ToolResult):
"""A lazy disclosure; activity uses the existing bounded turn wire projection."""

unit: Annotated[CommanderUnit, DisclosureIdentity()]
Expand Down
6 changes: 5 additions & 1 deletion src/agent_comms/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from .declared_family import DeclaredFamily, UnknownFamilyMember
from .errors import RelationViolationError, ToolRefusedError, UnregisteredThreadError
from .coordination_errors import CoordinationError, Refused
from .work_branches import WorkBranchError
from .field_codec import (
Argument, FieldCodec, GitCommitText, MalformedRecord, PathText, WireName, WireOmitDefault,
RecordMember, boundary_decoder, projected,
Expand Down Expand Up @@ -459,7 +460,10 @@ async def result(self, connector: Connector, caller: ConnectorCaller) -> ToolsCa
try:
reply = await connector.call(tool, self.params.arguments, caller)
return ToolsCallResult((TextContent(json.dumps(reply)),), False, reply)
except (ToolRefusedError, RelationViolationError, CoordinationError) as error:
except (
ToolRefusedError, RelationViolationError, CoordinationError, WorkBranchError,
MalformedRecord, Refused, ConnectionError, TimeoutError,
) as error:
logging.exception("Connector tool %s failed", self.params.name)
return ToolsCallResult.failed(error)

Expand Down
3 changes: 2 additions & 1 deletion src/agent_comms/connector_caller.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from .head_review import HeadReviewOrder, PullRequestHead
from .thread_identity import ThreadIncarnation
from .todos import DirectiveText
from .tool_caller import CommanderForkCaller, NoThreadCaller, OperatorOutcomes, ThreadCaller, ToolCaller
from .bus_read import OperatorOutcomes
from .tool_caller import CommanderForkCaller, NoThreadCaller, ThreadCaller, ToolCaller
from .tool_request import ToolArgumentsRefusedError, ToolRequest


Expand Down
4 changes: 2 additions & 2 deletions src/agent_comms/goal_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from .locked_store import DurableFile
from .private_registry_guard import PrivateRegistryGuard
from .store_files import AtomicFile
from .tool_result import DeclaredReply
from .tool_result import ToolResult
from .reported_turn import ReportedTurnState
from .typed_table import (
Column,
Expand Down Expand Up @@ -170,7 +170,7 @@ def reconcile(


@dataclass(frozen=True)
class RecordedGoalHistory(DeclaredReply):
class RecordedGoalHistory(ToolResult):
"""A thread's goal revisions beside the attempt ledger's failures, which only the ledger writes."""

history: tuple[RecordedGoalRevision, ...]
Expand Down
9 changes: 3 additions & 6 deletions src/agent_comms/goal_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from dataclasses import replace
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING, TypeVar
from typing import TYPE_CHECKING

from agent_comms.field_codec import GoalId
from .agent_activity import AgentActivity
Expand Down Expand Up @@ -72,7 +72,6 @@
from .todos import SendBackVerdict, StrikeReason, Todo, JudgedCall, TodoBatch, TodoCommand, TodoStore

_LOG = logging.getLogger(__name__)
HistoryReply = TypeVar("HistoryReply", bound=RecordedGoalHistory)


class Goals(GoalWaitRecovery):
Expand Down Expand Up @@ -294,12 +293,10 @@ def goal_history(
"""Read durable revisions for one thread incarnation within ``scope``."""
return self.registry.goal_history(name, scope)

def recorded_history(
self, reply: type[HistoryReply], name: str, scope: GoalScope
) -> HistoryReply:
def recorded_history(self, name: str, scope: GoalScope) -> RecordedGoalHistory:
"""Revisions from goal history, execution failures from the attempt ledger."""
owner = self.registry.require(name).incarnation.wire_birth
return reply(
return RecordedGoalHistory(
tuple(entry.recorded() for entry in self.goal_history(name, scope)),
self.open_goal_store().failures(owner, scope),
)
Expand Down
9 changes: 6 additions & 3 deletions src/agent_comms/historical_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import tempfile
from abc import ABC, abstractmethod
from collections import deque
from collections.abc import Callable, Iterator
from collections.abc import Callable, Iterator, Sequence
from contextlib import contextmanager
from dataclasses import dataclass, replace
from functools import partial
Expand All @@ -31,7 +31,9 @@
MessagePageRequest,
PageBounds,
)
from .message_reference import MessageReference
from .messages import CarriesEnvelope, Message
from .notification_evidence import NotificationWindow
from .page_traversal import HistoryCursor, OlderTraversal
from .presentation import MessageNotification, OutcomeViewer
from .private_path import FileObservation, FileRevision, ObservedPath, PrivateDirectoryRole
Expand Down Expand Up @@ -352,8 +354,9 @@ def require(cls, sources: tuple[HistorySource, ...], key: str) -> HistorySource:
source.validate()
return source

def notification_references(self, sources: tuple[HistorySource, ...], references,
viewer: OutcomeViewer):
def notification_references(self, sources: tuple[HistorySource, ...],
references: Sequence[MessageReference],
viewer: OutcomeViewer) -> NotificationWindow:
"""Read this namespace within its archive's current catalogue read, as this viewer
may see it."""

Expand Down
Loading
Loading