Skip to content
Open
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
28 changes: 28 additions & 0 deletions agents/src/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .core import *
from .dynamic_agents import *
from .fibers import *
from .harness import *
from .lifecycle import *
from .observability import *
from .queue import *
Expand Down Expand Up @@ -34,6 +35,7 @@
"AnyToolPart",
"AppendEvent",
"AppendResult",
"Approval",
"CallableMetadata",
"CancelledRun",
"ChatMessageOptions",
Expand Down Expand Up @@ -68,6 +70,11 @@
"FiberStatus",
"Fibers",
"FilePart",
"Harness",
"HarnessEvent",
"HarnessInput",
"HarnessSession",
"HarnessSessions",
"InvalidCronExpressionError",
"JSONValue",
"JobContext",
Expand All @@ -90,12 +97,19 @@
"NonRetryableError",
"Observability",
"ObservabilityEvent",
"OperationEvent",
"OperationNotFoundError",
"OperationResult",
"OperationStatus",
"PendingOperation",
"PendingRun",
"Queue",
"QueueItem",
"ReadonlyConnectionError",
"ReasoningPart",
"Receipt",
"RecentHistoryResult",
"Replay",
"Reschedule",
"RetryOptions",
"RouteAddress",
Expand All @@ -108,10 +122,13 @@
"ScheduleType",
"Scheduler",
"Session",
"SessionBusyError",
"SessionChangeEvent",
"SessionChangeListener",
"SessionMessage",
"SessionNotFoundError",
"SessionRowStat",
"SessionSnapshot",
"Sessions",
"Source",
"SourceDocumentPart",
Expand Down Expand Up @@ -147,24 +164,34 @@
"TaskStep",
"TaskStepAttempt",
"Tasks",
"TextEvent",
"TextPart",
"ToolAnswer",
"ToolApproval",
"ToolApprovalRequestedPart",
"ToolApprovalRespondedPart",
"ToolCallEvent",
"ToolInputAvailablePart",
"ToolInputStreamingPart",
"ToolOutput",
"ToolOutputAvailablePart",
"ToolOutputDeniedPart",
"ToolOutputErrorPart",
"ToolPart",
"ToolRequest",
"ToolResultEvent",
"ToolSpec",
"ToolState",
"Turn",
"TurnResult",
"UIMessage",
"UIMessagePart",
"UnknownPart",
"UpdateEvent",
"WaitingRun",
"WebSocketHandlers",
"WebSockets",
"WhenBusy",
"build_agent_path",
"build_agent_url",
"callable",
Expand All @@ -176,4 +203,5 @@
"route_sub_agent_request",
"subscribe",
"task",
"tool",
)
58 changes: 58 additions & 0 deletions agents/src/agents/harness/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Harnesses: agent loops hosted in a Durable Object (upstream ``agents/harness``)."""

from .errors import OperationNotFoundError, SessionBusyError, SessionNotFoundError
from .harness import Harness
from .session import HarnessSession, HarnessSessions
from .tools import tool
from .types import (
Approval,
HarnessEvent,
HarnessInput,
OperationEvent,
OperationResult,
OperationStatus,
PendingOperation,
Receipt,
Replay,
SessionSnapshot,
TextEvent,
ToolAnswer,
ToolCallEvent,
ToolOutput,
ToolRequest,
ToolResultEvent,
ToolSpec,
Turn,
TurnResult,
WhenBusy,
)

__all__ = (
"Approval",
"Harness",
"HarnessEvent",
"HarnessInput",
"HarnessSession",
"HarnessSessions",
"OperationEvent",
"OperationNotFoundError",
"OperationResult",
"OperationStatus",
"PendingOperation",
"Receipt",
"Replay",
"SessionBusyError",
"SessionNotFoundError",
"SessionSnapshot",
"TextEvent",
"ToolAnswer",
"ToolCallEvent",
"ToolOutput",
"ToolRequest",
"ToolResultEvent",
"ToolSpec",
"Turn",
"TurnResult",
"WhenBusy",
"tool",
)
32 changes: 32 additions & 0 deletions agents/src/agents/harness/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Errors raised by harnesses."""

from ..core.errors import AgentsException

__all__ = ("OperationNotFoundError", "SessionBusyError", "SessionNotFoundError")


class SessionBusyError(AgentsException):
"""A session has open work and the call refuses to queue behind it."""

def __init__(self, session: str) -> None:
super().__init__(f"Harness session {session!r} has an operation open")
self.session = session


class SessionNotFoundError(AgentsException):
"""No session has this id."""

def __init__(self, session: str) -> None:
super().__init__(f"Harness session {session!r} doesn't exist")
self.session = session


class OperationNotFoundError(AgentsException):
"""No operation in the session has this id."""

def __init__(self, session: str, operation_id: str) -> None:
super().__init__(
f"Harness session {session!r} has no operation {operation_id!r}"
)
self.session = session
self.operation_id = operation_id
37 changes: 37 additions & 0 deletions agents/src/agents/harness/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Fanning each session's events out to its watchers."""

import asyncio

from .types import HarnessEvent

__all__ = ("EventHub",)


class EventHub:
"""In-memory watchers per session; events aren't stored."""

__slots__ = ("_watchers",)

def __init__(self) -> None:
self._watchers: dict[str, set[asyncio.Queue[HarnessEvent]]] = {}

def subscribe(self, session: str) -> "asyncio.Queue[HarnessEvent]":
"""Return a queue that receives the session's events from now on."""
queue: asyncio.Queue[HarnessEvent] = asyncio.Queue()
self._watchers.setdefault(session, set()).add(queue)
return queue

def unsubscribe(self, session: str, queue: "asyncio.Queue[HarnessEvent]") -> None:
"""Stop sending the session's events to ``queue``."""
watchers = self._watchers.get(session)
if watchers is None:
return

watchers.discard(queue)
if not watchers:
del self._watchers[session]

def publish(self, session: str, event: HarnessEvent) -> None:
"""Send ``event`` to every watcher of the session."""
for queue in self._watchers.get(session, ()):
queue.put_nowait(event)
Loading
Loading