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
50 changes: 28 additions & 22 deletions src/toad/acp/agent.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
from toad.acp.agent_session import AgentSession
from toad.acp.session_updates import SessionNotificationOwner
from toad.acp.client_session import ClientSessionRequest
from toad.acp.client_files import FileClientRequestOwner
from toad.acp.tool_calls import SessionToolCalls
from toad.acp.agent_configuration import ModelConfigurationSetting, ThinkingConfigurationSetting
from toad.acp.context_measurement import ContextMeasurement
from toad.acp.agent_log import AgentLog
from toad.acp.agent_process import AgentProcess
from toad.acp.agent_controller import AgentController
from toad.acp.transcript_reader import CoordinationTranscriptReader
from toad.acp.permission_controller import PermissionController
from toad.agent_presentation import ACPAgentPresentation
import asyncio
from contextlib import suppress
from dataclasses import replace
Expand All @@ -20,27 +7,46 @@
import rich.repr
from agent_comms.acp_extension import (
CoordinationChangedUpdate,
PromptRequest,
InputStartedUpdate,
PromptRequest,
)
from agent_comms.acp_failure import ACPFailure
from agent_comms.field_codec import FieldCodec
from agent_comms.context_reading import UnmeasuredContext
from agent_comms.field_codec import FieldCodec
from agent_comms.goal_actions import GoalAction
from agent_comms.goal_presentation import GoalSnapshot, IdleGoalExecution
from agent_comms.goals import Goal, GoalDeclaration, NoGoal
from agent_comms.ui_model.observation import ReadOwnerGoal

import toad
from toad import paths
from toad.core import events as core_events
from toad.acp.agent_configuration import (
ModelConfigurationSetting,
ThinkingConfigurationSetting,
)
from toad.acp.agent_controller import AgentController
from toad.acp.agent_log import AgentLog
from toad.acp.agent_process import AgentProcess
from toad.acp.agent_session import AgentSession
from toad.acp.attachment_presentation import CursorPresentation, QueuePresentation
from toad.acp.comms_updates import CommsUpdateConsumer, NoPendingThreadRead, PendingThreadRead, ThreadRead
from toad.acp.client_files import FileClientRequestOwner
from toad.acp.client_session import ClientSessionRequest
from toad.acp.comms_updates import (
CommsUpdateConsumer,
NoPendingThreadRead,
PendingThreadRead,
ThreadRead,
)
from toad.acp.context_measurement import ContextMeasurement
from toad.acp.permission_controller import PermissionController
from toad.acp.projection_attachment import CursorAttachment
from toad.acp.queue_attachment import QueueAttachment
from toad.core.events import UnsupportedResumeAgentFail, AgentReady
from toad.acp.session_updates import SessionNotificationOwner
from toad.acp.tool_calls import SessionToolCalls
from toad.acp.transcript_reader import CoordinationTranscriptReader
from toad.agent import AgentBase
from toad.agent_presentation import ACPAgentPresentation
from toad.agent_schema import AgentDefinition
from toad.core import events as core_events


def generate_datetime_filename(
Expand Down Expand Up @@ -131,7 +137,7 @@ def log(self, line: str) -> None:
def get_info(self) -> str:
return self.definition.name

async def start(self, processes=()) -> None:
async def start(self, processes: tuple[AgentProcess, ...] = ()) -> None:
"""Start the operational agent; attachment resources are acquired separately."""
await self.process.start(self, processes)

Expand Down Expand Up @@ -159,7 +165,7 @@ def session_id(self):
return self.controller.session.session_id

@session_id.setter
def session_id(self, value):
def session_id(self, value) -> None:
self.controller.bind_session(value)

@property
Expand All @@ -170,7 +176,7 @@ def ready(self):
def current_turn(self):
return self.presentation.turns.owner

async def retire_surface(self, surface):
async def retire_surface(self, surface) -> None:
if self.controller.surface.owns(surface):
self.detach_surface(surface)
await self.stop()
Expand Down
29 changes: 16 additions & 13 deletions src/toad/acp/agent_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,30 @@
from dataclasses import dataclass
from typing import ClassVar

from acp.schema import (SessionConfigSelectOption, SessionConfigSelectGroup,
SessionConfigOptionSelect)
from acp.exceptions import RequestError
from acp.schema import (
SessionConfigOptionSelect,
SessionConfigSelectGroup,
SessionConfigSelectOption,
)
from agent_comms.acp_failure import ACPFailure
from agent_comms.declared_family import DeclaredFamily
from agent_comms.mro_dispatch import MroDispatch, handles
from acp.exceptions import RequestError


class SelectChoices(MroDispatch):
"""Flatten only the SDK's decoded external flat/group variants."""
def __init__(self, option: SessionConfigOptionSelect):
def __init__(self, option: SessionConfigOptionSelect) -> None:
self.values: list[SessionConfigSelectOption] = []
for value in option.options:
self.dispatch_sync(value)

@handles(SessionConfigSelectOption)
def choice(self, value):
def choice(self, value) -> None:
self.values.append(value)

@handles(SessionConfigSelectGroup)
def group(self, value):
def group(self, value) -> None:
self.values.extend(value.options)


Expand All @@ -39,7 +42,7 @@ class ConfigurationSetting(DeclaredFamily, affix="ConfigurationSetting"):
def matches(cls, option: SessionConfigOptionSelect) -> bool: ...

@classmethod
def bind(cls, option):
def bind(cls, option: SessionConfigOptionSelect):
if option.current_value not in {value.value for value in SelectChoices(option).values}:
raise ValueError(f"ACP configuration {option.id!r} has no advertised current value")
return cls(option)
Expand Down Expand Up @@ -115,11 +118,11 @@ def matches(cls, option):


class ConfigurationAdvertisements(MroDispatch):
def __init__(self, selections):
def __init__(self, selections: dict[type[ConfigurationSetting], ConfigurationSetting]) -> None:
self.selections = selections

@handles(SessionConfigOptionSelect)
def selection(self, option):
def selection(self, option) -> None:
for kind in ConfigurationSetting.members_with(ConfigurationSetting):
if kind.matches(option):
self.selections[kind] = kind.bind(option)
Expand All @@ -128,11 +131,11 @@ def selection(self, option):

class AgentConfiguration:
"""Single actual advertisement owner; detached surfaces keep this same value."""
def __init__(self, agent):
def __init__(self, agent) -> None:
self.agent = agent
self.reset()

def reset(self):
def reset(self) -> None:
self.selections = {kind: kind() for kind in ConfigurationSetting.members_with(ConfigurationSetting)}

def setting[T: ConfigurationSetting](self, kind: type[T]) -> T:
Expand All @@ -155,7 +158,7 @@ def receive_current_mode(self, value: str) -> None:
self.selections[ModeConfigurationSetting] = self.mode.with_current(value)
self.publish()

def receive(self, options):
def receive(self, options) -> None:
if options is None:
return
# Official external decoder once; consumers use its guaranteed typed fields.
Expand All @@ -166,6 +169,6 @@ def receive(self, options):
self.selections = selections
self.publish()

def publish(self):
def publish(self) -> None:
from toad.core.events import ConfigurationChanged
self.agent.events.publish(ConfigurationChanged())
Loading
Loading