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
4 changes: 2 additions & 2 deletions src/toad/acp/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
PromptRequest,
)
from agent_comms.context_reading import UnmeasuredContext
from agent_comms.nominal.field_codec import FieldCodec
from agent_comms.nominal.field_codec import FieldCodec, GoalId
from agent_comms.goal_actions import GoalAction
from agent_comms.goal_presentation import GoalSnapshot, IdleGoalExecution
from agent_comms.goals import Goal, GoalDeclaration, NoGoal
Expand Down Expand Up @@ -245,7 +245,7 @@ async def get_goal_snapshot(self) -> GoalSnapshot:
consumer.dispatch_sync(fact)
return snapshot

async def get_goal_history(self, goal_id: str) -> RecordedGoalHistory:
async def get_goal_history(self, goal_id: GoalId) -> RecordedGoalHistory:
result = await self.controller.request_owner("goal_history", goal_id=goal_id)
return FieldCodec.decode(RecordedGoalHistory, result)

Expand Down
22 changes: 15 additions & 7 deletions src/toad/agent_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from abc import ABC, abstractmethod
from functools import cached_property
from pathlib import Path
from typing import TYPE_CHECKING
from typing import ClassVar, TYPE_CHECKING

from agent_comms.thread_presentation import ThreadPresentation
from agent_comms.errors import GoalRefused
from agent_comms.goal_history import RecordedGoalHistory
from textual.content import Content

Expand All @@ -19,6 +20,8 @@

if TYPE_CHECKING:
from agent_comms.goal_actions import GoalAction
from agent_comms.goals import Goal, GoalDeclaration
from agent_comms.nominal.field_codec import GoalId
from agent_comms.session_load import ExistingSessionLoadPlan

from toad.conversation_submission import (
Expand Down Expand Up @@ -97,6 +100,17 @@ def accepts_failed_input(self, message: CoreEventMessage) -> bool: ...
@abstractmethod
def matches_agent(self, agent) -> bool: ...

goal_control_refusal: ClassVar[str] = "Persistent goals require an agent-comms session"

async def get_goal_history(self, goal_id: GoalId) -> RecordedGoalHistory:
return RecordedGoalHistory.empty()

async def edit_goal(self, goal: Goal, text: str) -> Goal:
raise GoalRefused(self.goal_control_refusal)

async def update_goal(self, action: type[GoalAction], text: str = "") -> GoalDeclaration:
raise GoalRefused(self.goal_control_refusal)


class NoAgentConnection(AgentConnection):
"""A real saved view before it acquires any native connection."""
Expand Down Expand Up @@ -178,12 +192,6 @@ def begin_submission(self, owner: ConversationSubmissions, submission: AgentInpu
def accepts_failed_input(self, message: CoreEventMessage) -> bool:
return False

async def get_goal_history(self, goal_id: str) -> RecordedGoalHistory:
return RecordedGoalHistory.empty()

async def update_goal(self, action: type[GoalAction], text: str):
raise ValueError("Persistent goals require an agent-comms session")

def cancel_allowed(self, turn: TurnOwner) -> bool:
return False

Expand Down
40 changes: 20 additions & 20 deletions src/toad/goal_interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@
from __future__ import annotations

from abc import abstractmethod
from collections.abc import Iterator
from collections.abc import Awaitable, Iterator
from contextlib import contextmanager
from typing import ClassVar, TYPE_CHECKING
from weakref import ref
from weakref import ReferenceType, ref

from agent_comms.coordination_errors import Refused
from agent_comms.errors import GoalRefused
from agent_comms.nominal.declared_family import DeclaredFamily
from agent_comms.goal_actions import ClearGoalAction, GoalAction
from agent_comms.goal_presentation import ControlledExecution, GoalExecution
from agent_comms.goal_presentation import GoalExecution
from agent_comms.goal_history import RecordedGoalHistory
from toad.agent_binding import AgentConnection

if TYPE_CHECKING:
from toad.screens.modal import ModalScreen
from toad.widgets.conversation import Conversation


Expand Down Expand Up @@ -76,30 +77,26 @@ async def apply(cls, session):


class ToggleInteraction(GoalInteraction):
label = "Pause"
label = GoalExecution.toggle_label

@classmethod
def visible(cls, session):
return session.display.can_control and isinstance(session.execution, ControlledExecution)
def visible(cls, session: GoalSession) -> bool:
return session.display.can_control and session.execution.can_toggle()

@classmethod
def caption(cls, session, collapsed):
def caption(cls, session: GoalSession, collapsed: bool) -> str:
return session.execution.toggle_label if cls.visible(session) else cls.label

@classmethod
async def apply(cls, session):
execution = session.execution
if isinstance(execution, ControlledExecution):
await session.change(execution.toggle)
else:
session.view.feedback.flash(session.display.heading(execution))
async def apply(cls, session: GoalSession) -> None:
await session.execution.apply_toggle(session.change, session.unavailable)


class EditInteraction(GoalInteraction):
label = "Edit"

@classmethod
async def apply(cls, session):
async def apply(cls, session: GoalSession) -> None:
from toad.screens.goal_edit import GoalEdit
from toad.widgets.goal_text import goal_mention_candidates
view = session.view
Expand Down Expand Up @@ -129,10 +126,10 @@ class GoalSession:

def __init__(self, view: Conversation):
self.view = view
self._modal = None
self._modal: ReferenceType[ModalScreen[None]] | None = None

@property
def modal(self):
def modal(self) -> ModalScreen[None] | None:
return self._modal() if self._modal is not None else None

@property
Expand All @@ -143,13 +140,13 @@ def display(self):
def execution(self) -> GoalExecution:
return self.view.goal_execution

def owns(self, agent) -> bool:
def owns(self, agent: AgentConnection) -> bool:
view = self.view
if not view.is_attached:
return False
return view.goal_controls is self and view.agent_binding is agent

def present(self, modal) -> None:
def present(self, modal: ModalScreen[None]) -> None:
self._modal = ref(modal)
self.view.app.push_screen(modal)

Expand All @@ -162,11 +159,14 @@ async def activate(self, action: type[GoalInteraction]) -> None:
if not self.owns(agent):
return
if not action.enabled(self):
view.feedback.flash(view.goal_display.heading(view.goal_execution))
self.unavailable()
return
with self.reporting_refusals(agent):
await action.apply(self)

def unavailable(self) -> None:
self.view.feedback.flash(self.display.heading(self.execution))

@contextmanager
def reporting_refusals(self, agent: AgentConnection) -> Iterator[None]:
"""Buttons and slash controls report once, only to their original source."""
Expand All @@ -189,7 +189,7 @@ async def change(self, action: type[GoalAction], text: str = "") -> None:
if self.owns(agent):
view.prompt.focus()

async def write(self, agent, operation):
async def write[Result](self, agent: AgentConnection, operation: Awaitable[Result]) -> Result:
"""Every write settles by reading the owner, including rejected writes."""
try:
return await operation
Expand Down
4 changes: 2 additions & 2 deletions src/toad/screens/goal_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from toad.widgets.channel_prompt import DraftPrompt


class GoalEdit(CoreEventReceiver, DismissibleScreen[str | None]):
class GoalEdit(CoreEventReceiver, DismissibleScreen[None]):
AUTO_FOCUS = "ChannelTextArea"
DEFAULT_CSS = """
GoalEdit { align: center middle; background: $background 40%; }
Expand Down Expand Up @@ -83,7 +83,7 @@ async def save(self, text: str) -> None:
except Refused as error:
self.query_one("#goal-edit-error", Static).update(str(error))
else:
self.dismiss(text.strip())
self.dismiss()
finally:
self.saving = False
save_button.disabled = False
Expand Down
Loading