Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 14 additions & 7 deletions src/agent_comms/declared_family.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,9 @@ def __new__(
**class_options: Any,
) -> FamilyMeta:
roots = {
ancestor
base._family_root
for base in bases
if isinstance(base, FamilyMeta)
for ancestor in base.__mro__
if ancestor is base._family_root
if isinstance(base, FamilyMeta) and base is not DeclaredFamily
}
if len(roots) > 1:
raise TypeError("A declaration cannot belong to two families.")
Expand Down Expand Up @@ -87,7 +85,11 @@ def __new__(
cls: type[DeclaredFamily] = super().__new__(
mcs, name, bases, namespace, registry_config=config, **class_options
)
cls._family_root = cls if is_root else root
if is_root:
cls._family_root = cls
else:
for family_root in roots:
cls._family_root = family_root
return cast(FamilyMeta, cls)

@staticmethod
Expand Down Expand Up @@ -130,7 +132,7 @@ class DeclaredFamily(ABC, metaclass=FamilyMeta):

declared_name: ClassVar[str]
family_discriminator: ClassVar[str] = "kind"
_family_root: ClassVar[type[DeclaredFamily] | None] = None
_family_root: ClassVar[type[DeclaredFamily]]
_family_affix: ClassVar[str]
__registry__: ClassVar[dict[str, type[DeclaredFamily]]]

Expand Down Expand Up @@ -234,9 +236,14 @@ def members_with(cls, capability: type) -> tuple[type[Self], ...]:
if issubclass(member, cls) and issubclass(member, capability)
)

@classmethod
def family_root(cls) -> type[DeclaredFamily]:
"""The nominal root recorded when this declaration was constructed."""
return cls._family_root

@classmethod
def declared_position(cls) -> int:
root = cast(type[DeclaredFamily], cls._family_root)
root = cls.family_root()
return root.members_with(root).index(cls)


Expand Down
29 changes: 21 additions & 8 deletions src/agent_comms/typed_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import os
import re
import sqlite3
import sys
import types
from abc import ABC, abstractmethod
from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence
Expand Down Expand Up @@ -1961,7 +1960,11 @@ def update(


class ProducedTable(ProducedRow, TypedTable, declared_name=None):
"""A named table whose producing family owns one source commitment."""
"""A named table whose producing family owns one source commitment.

The first install after this change rebuilds bus projections once because the digest
format changed.
"""

@classmethod
def producer_digest(cls) -> str:
Expand All @@ -1974,13 +1977,23 @@ def producer_digest(cls) -> str:
@cache
def _producer_digest(cls) -> str:
producers = cls.producer_classes()
modules = {
cls.__module__,
*(row.__module__ for row in cls.members_with(cls)),
*(owner.__module__ for producer in producers
for owner in producer.__mro__ if owner is not object),
owners: set[type[DeclaredFamily]] = {
owner
for row in (cls, *cls.members_with(cls))
for owner in row.__mro__
if issubclass(owner, cls)
}
source = "\0".join(getsource(sys.modules[name]) for name in sorted(modules))
for producer in producers:
root = producer.family_root()
owners.update(
owner
for owner in producer.__mro__
if issubclass(owner, cls) or issubclass(owner, root)
)
source = "\0".join(
f"{owner.__module__}.{owner.__qualname__}\0{getsource(owner)}"
for owner in sorted(owners, key=lambda owner: (owner.__module__, owner.__qualname__))
)
return hashlib.sha256(source.encode()).hexdigest()


Expand Down
2 changes: 1 addition & 1 deletion stack/pi-native.sha256
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ d64e8c05c7932d377034820b1db0df45612afe64885c0765f8f348a12e1bc955 dist/core/skil
0391543cf703a93a0163b37b3ccebe53e0f12bff6f6162b86d56fb017fe49011 dist/core/system-prompt.js
# agent-comms-native-stock-v1 @earendil-works/pi-coding-agent@0.85.1
# agent-comms-native-tree-v1 17edf8badd73bafccf592b651b5a9c0f71b2b05b89db470d1042d3ccb544a0b9
# agent-comms-native-inputs-v1 a420ec4c8414f1e9b5355eeaf303c1c1f798d47c876d5612bf2512daf445294b
# agent-comms-native-inputs-v1 e57296bd11e2b731df3c744633772054a8568de2a96dc2012623aa83a54c8cda
4 changes: 2 additions & 2 deletions stack/test-native-global-extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import time
import tempfile

from agent_comms.comms import wire
from agent_comms.field_codec import FieldCodec
from agent_comms.storage_rules import StorageRoots
from agent_comms.native_package import NativeImportsManifest, VerifiedNativePackage
Expand Down Expand Up @@ -62,10 +63,9 @@ def main(fixture: Path):
}
environment.update(RestartEnvironment(native_config=agent, agent_directory=agent).encode_native())
from agent_comms.child_process import ProcessIdentity
from agent_comms.comms import Comms
from agent_comms.threads import Thread

comms = Comms(root / "wire")
comms = wire(root / "wire")
thread = Thread("native-startup-fixture", frozenset(), str(project),
storage_roots=StorageRoots(root / "scratch"),
process_identity=ProcessIdentity.capture(os.getpid()))
Expand Down
113 changes: 113 additions & 0 deletions tests/test_bus.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import os
import shutil
import signal
import sqlite3
import subprocess
import sys
from contextlib import closing, contextmanager
from dataclasses import replace
from pathlib import Path

import pytest

Expand Down Expand Up @@ -48,6 +52,115 @@
from bus_record_fixture import publication_processes, record_from


@pytest.fixture
def projection_source(tmp_path: Path) -> Path:
checkout = tmp_path / "producer-source"
source = checkout / "src" / "agent_comms"
original = Path(__file__).resolve().parents[1] / "src" / "agent_comms"
shutil.copytree(original, source, ignore=shutil.ignore_patterns("__pycache__", "skills"))
(source / "skills").symlink_to(original / "skills", target_is_directory=True)
subprocess.run(("git", "init", "-q", str(checkout)), check=True, capture_output=True)
subprocess.run(
(
"git",
"-C",
str(checkout),
"-c",
"user.name=Digest fixture",
"-c",
"user.email=digest@example.invalid",
"-c",
"commit.gpgsign=false",
"-c",
"core.hooksPath=/dev/null",
"commit",
"--allow-empty",
"-qm",
"Source fixture",
),
check=True,
capture_output=True,
)
return source


def projection_source_digest(source: Path) -> str:
return subprocess.run(
(
sys.executable,
"-I",
"-c",
"import sys; sys.path.insert(0, sys.argv[1]); "
"from agent_comms.messages import BusProjectionTables; "
"print(BusProjectionTables._producer_digest())",
str(source.parent),
),
check=True,
capture_output=True,
text=True,
timeout=60,
).stdout.strip()


@pytest.mark.parametrize(
("module", "old", "new"),
(
(
"typed_table.py",
"Durable files compare their stored DDL text",
"An unrelated infrastructure docstring: durable files compare their stored DDL text",
),
(
"field_codec.py",
"class TimestampText(",
"def unrelated_projection_probe():\n return 17\n\n\nclass TimestampText(",
),
),
)
def test_projection_digest_ignores_infrastructure_source(
projection_source: Path,
module: str,
old: str,
new: str,
) -> None:
before = projection_source_digest(projection_source)
path = projection_source / module
original = path.read_text()
assert original.count(old) == 1
path.write_text(original.replace(old, new))
assert projection_source_digest(projection_source) == before


@pytest.mark.parametrize(
("module", "old", "new"),
(
(
"bus_publication.py",
"yield SentSource(self.audience.sender_lookup, self.seq)",
"yield SentSource(self.audience.sender_lookup, self.seq + 1)",
),
("bus_activity_index.py", '=max("{name}",excluded.', '=min("{name}",excluded.'),
(
"messages.py",
"for record in BusCapability.history(db):",
"for record in reversed(tuple(BusCapability.history(db))):",
),
),
)
def test_projection_digest_tracks_producing_source(
projection_source: Path,
module: str,
old: str,
new: str,
) -> None:
before = projection_source_digest(projection_source)
path = projection_source / module
original = path.read_text()
assert original.count(old) == 1
path.write_text(original.replace(old, new))
assert projection_source_digest(projection_source) != before


def test_bus_attachment_and_failed_write_do_not_install_a_schema(tmp_path):
bus = Bus(Bus.path_in(tmp_path / "not-installed"))
assert not bus.path.parent.exists()
Expand Down
Loading