Skip to content
6 changes: 3 additions & 3 deletions src/agent_comms/cli_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ def apply_together(cls, ctx: Comms, commands: tuple[Self, ...]) -> tuple[CliResu


@dataclass(frozen=True)
class TargetEdit(Command):
class TargetEdit(Command[Comms, CliResult]):
"""Accepted editor values use the declared field and current bound command."""
declaration: type[CliCommand]
target: TargetSelection
Expand Down Expand Up @@ -746,7 +746,7 @@ class NativeContextInspection(CliResult):
native_source_spans: tuple[NativeSourceSpans, ...]


class CliExecution(Command):
class CliExecution[Answer: CliResult = CliResult](Command[Comms, Answer]):
"""The dispatcher contract, including methods selected through multiple inheritance."""

@abstractmethod
Expand Down Expand Up @@ -801,7 +801,7 @@ class WorktreeArgument:
class CliCommand[Answer: CliResult = CliResult](
DeclaredFamily,
DeclaredOptions,
CliExecution,
CliExecution[Answer],
affix="CliCommand",
discovery_package="agent_comms.tools",
):
Expand Down
2 changes: 1 addition & 1 deletion src/agent_comms/cli_native_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@


@dataclass(frozen=True, kw_only=True)
class NativePackageVerb(DeclaredVerb, Command, affix="Verb"):
class NativePackageVerb(DeclaredVerb, Command[None, None], affix="Verb"):
"""One verb: its fields declare its parser, and apply() does the work."""

@abstractmethod
Expand Down
6 changes: 3 additions & 3 deletions src/agent_comms/command.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
"""Inbound command contract; domain roots reuse DeclaredFamily and FieldCodec."""

from abc import ABC, abstractmethod
from typing import Any, Self
from typing import Self

from pydantic import JsonValue

from .field_codec import FieldCodec


class Command(ABC):
class Command[Context, Result](ABC):
@classmethod
def from_payload(cls, payload: dict[str, JsonValue]) -> Self:
return FieldCodec.decode(cls, payload)

@abstractmethod
def apply(self, ctx: Any) -> Any:
def apply(self, ctx: Context) -> Result:
"""Apply the command using its domain's shared preconditions."""
2 changes: 1 addition & 1 deletion src/agent_comms/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,7 @@ async def mcp(self, request: web.Request) -> web.Response:

@dataclass(frozen=True, kw_only=True)
class ConnectorVerb(
DeclaredVerb, Command,
DeclaredVerb, Command[ConnectorStore, int | str],
affix="Verb", discovery_package=HostService.__module__,
):
"""One connector command: its options are its fields, and its parser comes from them."""
Expand Down
4 changes: 3 additions & 1 deletion src/agent_comms/goal_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ def model_report(self) -> bool:


@dataclass(frozen=True, kw_only=True)
class GoalAction[Result: GoalDeclaration](DeclaredFamily, Command, affix="GoalAction"):
class GoalAction[Result: GoalDeclaration](
DeclaredFamily, Command[GoalActionContext, Result], affix="GoalAction"
):
expect: GoalPrecondition = GoalPrecondition()
preserve_wait: ClassVar[bool] = False
schedules_goal: ClassVar[bool] = False
Expand Down
2 changes: 1 addition & 1 deletion src/agent_comms/queued_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from contextlib import AsyncExitStack, ExitStack, asynccontextmanager
from dataclasses import dataclass, field, replace
from functools import partial
from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar
from typing import TYPE_CHECKING, Generic, Self, TypeVar

from acp import RequestError

Expand Down
18 changes: 9 additions & 9 deletions src/agent_comms/relationships.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ def require_peer(self) -> Thread:
)
return self.second

def publish(self, changed: Collaboration):
def publish(self, changed: Collaboration) -> tuple[RelationshipDocument, Collaboration]:
active = self.active
if changed is active:
return self.document, changed.oriented(self.first)
Expand All @@ -593,19 +593,19 @@ def publish(self, changed: Collaboration):


@dataclass(frozen=True)
class RelationshipEdit(Command, DeclaredFamily, affix="RelationshipEdit"):
class RelationshipEdit[Result](
Command[RelationshipEditContext, tuple[RelationshipDocument, Result]],
DeclaredFamily, affix="RelationshipEdit",
):
note: str = ""

def __post_init__(self):
if len(self.note) > 2000:
raise ValueError("Collaboration notes are limited to 2000 characters")

@abstractmethod
def apply(self, ctx: RelationshipEditContext): ...


class RetainingRelationshipEdit(RelationshipEdit):
def apply(self, ctx: RelationshipEditContext):
class RetainingRelationshipEdit(RelationshipEdit[Collaboration]):
def apply(self, ctx: RelationshipEditContext) -> tuple[RelationshipDocument, Collaboration]:
peer = ctx.require_peer()
return ctx.publish(self.change(ctx, peer))

Expand All @@ -631,8 +631,8 @@ def change(self, ctx: RelationshipEditContext, peer: Thread) -> Collaboration:
return active.updated(self.note, time.time())


class RemoveRelationshipEdit(RelationshipEdit):
def apply(self, ctx: RelationshipEditContext):
class RemoveRelationshipEdit(RelationshipEdit[None]):
def apply(self, ctx: RelationshipEditContext) -> tuple[RelationshipDocument, None]:
existing = ctx.existing
if existing is None:
return ctx.document, None
Expand Down
6 changes: 4 additions & 2 deletions src/agent_comms/runtime_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import asyncio
import json
from abc import abstractmethod
from collections.abc import AsyncIterator
from collections.abc import AsyncIterator, Awaitable
from contextlib import asynccontextmanager
from dataclasses import dataclass
from functools import partial
Expand Down Expand Up @@ -67,7 +67,9 @@ async def send(self, line: OwnerLine) -> None:


@dataclass(frozen=True, kw_only=True)
class RuntimeRequest(DeclaredFamily, Command, affix="RuntimeRequest"):
class RuntimeRequest(
DeclaredFamily, Command[RuntimeRequestContext, Awaitable[None]], affix="RuntimeRequest"
):
thread: str

@classmethod
Expand Down
2 changes: 1 addition & 1 deletion src/agent_comms/session_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import os
from functools import partial
from pathlib import Path
from typing import Any, cast
from typing import cast

from acp import RequestError
from acp.schema import (
Expand Down
6 changes: 5 additions & 1 deletion src/agent_comms/todos.py
Original file line number Diff line number Diff line change
Expand Up @@ -2739,8 +2739,12 @@ def closed(cls, current: Todo, order: ProblemOrder) -> Todo:
revision=current.revision + 1)


# Todo stores these commands, so it is declared after them; the alias reads it lazily.
type CommandedTodo = Todo


@dataclass(frozen=True)
class TodoCommand(Command, DeclaredFamily, affix="TodoOperation"):
class TodoCommand(Command[CommandedTodo, CommandedTodo], DeclaredFamily, affix="TodoOperation"):
expected_revision: int

def __post_init__(self) -> None:
Expand Down
7 changes: 4 additions & 3 deletions tools/debt_ratchet.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,9 +419,10 @@ def sites(tree: ast.AST, filename: str) -> Iterator[ast.AST]:

@staticmethod
def occurrences(node: ast.AST) -> int:
metadata = typing.cast(
types.MemberDescriptorType, typing.cast(typing.Any, dataclasses.Field).metadata
).__name__
descriptor: types.MemberDescriptorType = inspect.getattr_static(
dataclasses.Field, "metadata"
)
metadata = descriptor.__name__
return sum(
typing.cast(int, MetadataKeys().visit(keyword.value))
for keyword in typing.cast(ast.Call, node).keywords
Expand Down
Loading