diff --git a/src/toad/agents.py b/src/toad/agents.py index e0be31f3e..a96c235fd 100644 --- a/src/toad/agents.py +++ b/src/toad/agents.py @@ -8,6 +8,13 @@ class AgentReadError(Exception): """Problem reading the agents.""" +class AgentNotFound(AgentReadError): + """The requested agent is absent from the available declarations.""" + + def __init__(self, identity: str) -> None: + super().__init__(f"Agent not found: {identity}") + + async def read_agents() -> dict[str, AgentDefinition]: """Read agent information from data/agents diff --git a/src/toad/session_navigation.py b/src/toad/session_navigation.py index 06d1be6c0..e6dafa812 100644 --- a/src/toad/session_navigation.py +++ b/src/toad/session_navigation.py @@ -22,6 +22,7 @@ from agent_comms.thread_identity import ThreadIncarnation from toad.core import events as core_events +from toad.agents import AgentNotFound from toad.core.events import CoreEventStream from toad.navigation_preparation import CommsNavigationRequest, ThreadNavigationRequest from toad.session_entry import ( @@ -30,7 +31,7 @@ PreviewSessionEntry, SessionEntry, ) -from toad.session_scope import AgentAttached, AgentBinding, AgentSession, SessionScope +from toad.session_scope import AgentAttached, AgentBinding, AgentSession, ProjectSessionOrigin, SessionScope from toad.session_tracker import CommsViewKey, OpenTab, SessionDetails from toad.tab_identity import SessionTabIdentity, TabIdentity, TabTargetGone from toad.tab_identity.kinds import AcpTab, HistoryTab, MailboxTab, PreviewTab @@ -158,7 +159,7 @@ def prepare(): original=(prepared.root, prepared.thread.incarnation), ) - async def history_entry(self, identity: HistoryTab, source: SessionScope) -> HistorySessionEntry: + async def history_entry(self, identity: HistoryTab, source: ProjectSessionOrigin) -> HistorySessionEntry: def participants(): service = self.app.coordination_access.observation.observed().service if service.root != Path(identity.root): @@ -301,11 +302,14 @@ async def launch( project: Checkout, initial_prompt: str | None = None, ) -> None: - entry = await self.launch_entry( - agent_identity, agent_session_id=agent_session_id, - session_pk=session_pk, project=project, initial_prompt=initial_prompt, - ) - if entry is not None: + try: + entry = await self.launch_entry( + agent_identity, agent_session_id=agent_session_id, + session_pk=session_pk, project=project, initial_prompt=initial_prompt, + ) + except AgentNotFound as error: + self.app.notify(str(error), title="Launch agent", severity="warning", markup=False) + else: await self.open(entry) async def launch_entry( @@ -316,7 +320,7 @@ async def launch_entry( session_pk: int | None = None, project: Checkout, initial_prompt: str | None = None, - ) -> SessionEntry | None: + ) -> SessionEntry: from toad.agents import read_agents from toad.screens.main import MainScreen @@ -334,7 +338,7 @@ async def launch_entry( None, ) if existing is not None: - return self.get(existing) + return self.entries[existing][existing] agent, title = None, None if session_pk is not None: saved = await database.session_get(session_pk) @@ -344,9 +348,8 @@ async def launch_entry( agents = await read_agents() try: agent = agents[agent_identity] - except KeyError: - app.notify("Agent not found", title="Launch agent", severity="warning", markup=False) - return + except KeyError as error: + raise AgentNotFound(agent_identity) from error tab = ( AcpTab(session_pk) if session_pk is not None else await self.saved(agent, project, title=title or "New Session", diff --git a/src/toad/tab_identity/__init__.py b/src/toad/tab_identity/__init__.py index 2c32e770c..5437f7cec 100644 --- a/src/toad/tab_identity/__init__.py +++ b/src/toad/tab_identity/__init__.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from toad.session_entry import SessionEntry from toad.session_navigation import SessionNavigation - from toad.session_scope import SessionScope + from toad.session_scope import ProjectSessionOrigin class TabTargetGone(Exception): @@ -42,9 +42,9 @@ async def entry(self, navigation: SessionNavigation) -> SessionEntry: ... class SessionTabIdentity(TabIdentity): """An agent or mailbox tab can own history tabs.""" - async def history_source(self, navigation: SessionNavigation) -> SessionScope: + async def history_source(self, navigation: SessionNavigation) -> ProjectSessionOrigin: """An ACP session must establish its actual identity before routing history.""" entry = await self.restore(navigation) await navigation.app.select_session(entry.mode) await navigation.app.workspace_sessions.require(entry.mode).wait_content_ready() - return navigation.app.workspace_sessions.require(entry.mode) + return navigation.app.workspace_sessions.require(entry.mode).origin.require() diff --git a/src/toad/tab_identity/history_origin.py b/src/toad/tab_identity/history_origin.py index 48a1a4b6f..e849b7e64 100644 --- a/src/toad/tab_identity/history_origin.py +++ b/src/toad/tab_identity/history_origin.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from toad.session_navigation import SessionNavigation - from toad.session_scope import SessionScope + from toad.session_scope import ProjectSessionOrigin class HistoryOrigin(DeclaredFamily, affix="HistoryOrigin"): @@ -22,7 +22,7 @@ class HistoryOrigin(DeclaredFamily, affix="HistoryOrigin"): def mode_name(self) -> str: ... @abstractmethod - async def restore(self, navigation: SessionNavigation) -> SessionScope: ... + async def restore(self, navigation: SessionNavigation) -> ProjectSessionOrigin: ... @dataclass(frozen=True) @@ -31,8 +31,8 @@ class WelcomeHistoryOrigin(HistoryOrigin): def mode_name(self) -> str: return WELCOME - async def restore(self, navigation: SessionNavigation) -> SessionScope: - source = await navigation.app.prepare_session(WELCOME) + async def restore(self, navigation: SessionNavigation) -> ProjectSessionOrigin: + source = (await navigation.app.prepare_session(WELCOME)).origin.require() await source.prepare_history_origin() return source @@ -45,5 +45,5 @@ class SessionHistoryOrigin(HistoryOrigin): def mode_name(self) -> str: return self.identity.mode_name - async def restore(self, navigation: SessionNavigation) -> SessionScope: + async def restore(self, navigation: SessionNavigation) -> ProjectSessionOrigin: return await self.identity.history_source(navigation) diff --git a/src/toad/tab_identity/kinds.py b/src/toad/tab_identity/kinds.py index 191f29e0b..1c85db27e 100644 --- a/src/toad/tab_identity/kinds.py +++ b/src/toad/tab_identity/kinds.py @@ -9,6 +9,7 @@ from agent_comms.thread_identity import ThreadIncarnation from toad.conversation_kind import ConversationKind +from toad.agents import AgentNotFound from toad.session_entry import PreviewSessionEntry from toad.tab_identity import SessionTabIdentity, TabIdentity, TabTargetGone from toad.tab_identity.history_origin import HistoryOrigin @@ -27,15 +28,15 @@ async def entry(self, navigation): project = Checkout.checked(saved.meta_json.cwd.require_present().path) except ValueError as error: raise TabTargetGone(self, f"Saved ACP session {self.key}: {error}") from error - entry = await navigation.launch_entry( - saved.agent_identity, - agent_session_id=saved.agent_session_id, - session_pk=self.key, - project=project, - ) - if entry is None: - raise TabTargetGone(self, f"Saved ACP agent {saved.agent_identity} is unavailable") - return entry + try: + return await navigation.launch_entry( + saved.agent_identity, + agent_session_id=saved.agent_session_id, + session_pk=self.key, + project=project, + ) + except AgentNotFound as error: + raise TabTargetGone(self, str(error)) from error @dataclass(frozen=True) @@ -50,7 +51,7 @@ async def history_source(self, navigation): # Mailbox metadata already carries the original thread identity. Its # history needs the owner shell, not an ACP connection or painted body. entry = await self.restore(navigation) - return await navigation.app.prepare_session(entry.mode) + return (await navigation.app.prepare_session(entry.mode)).origin.require() @dataclass(frozen=True)