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
7 changes: 7 additions & 0 deletions src/toad/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 15 additions & 12 deletions src/toad/session_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand All @@ -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

Expand All @@ -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)
Expand All @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions src/toad/tab_identity/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Durable tab identities; Textual names are a projection of the wire identity."""

from __future__ import annotations
Expand All @@ -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):
Expand Down Expand Up @@ -42,9 +42,9 @@
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()
10 changes: 5 additions & 5 deletions src/toad/tab_identity/history_origin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand All @@ -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)
Expand All @@ -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

Expand All @@ -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)
21 changes: 11 additions & 10 deletions src/toad/tab_identity/kinds.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Each tab kind carries the typed key and the operation that reopens it."""

from dataclasses import dataclass
Expand All @@ -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
Expand All @@ -27,15 +28,15 @@
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)
Expand All @@ -50,7 +51,7 @@
# 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)
Expand Down
Loading