Source code for zett_agent.client

"""Optional creation and event-handling conveniences around the unchanged Agent."""

from collections.abc import AsyncIterator, Mapping, Sequence

from ._agent.lifecycle import final_answer
from ._compat import aclosing_stream
from .agent import Agent, AgentRunConfig
from .dispatcher import AgentEventDispatcher
from .events import AgentEvent
from .extensions.base import AgentExtension
from .extensions.events import CompactionEvent
from .json_types import JsonValue
from .messages import AssistantMessage, UserMessage
from .model import AgentModel, ReasoningEffort
from .resume import ResumePoint
from .sync_runtime import SyncMethodsMixin
from .tools.base import AgentTool


[docs] class AgentClient(SyncMethodsMixin): """Compose an initialized Agent with optional application event callbacks. The public agent attribute exposes the original runtime for advanced APIs, including emit_external_event(). Calling it directly bypasses this client's callbacks. No methods or attributes are injected into the original Agent. Callback failures propagate to the caller and close the underlying stream: unfinished requests are cancelled through the Agent's normal close path. If a callback fails after completion, the request remains completed. Shared handlers must distinguish sessions using event.session_id. Examples: Usage:: agent = await Agent.create(model, config=AgentRunConfig(session_id="existing")) client = AgentClient(agent, event_dispatcher=ConsoleEvents()) reply = await client.run("Continue") """ def __init__(self, agent: Agent, *, event_dispatcher: AgentEventDispatcher | None = None) -> None: self.agent = agent self.event_dispatcher = event_dispatcher
[docs] async def stream( self, message: UserMessage | str, *, config: AgentRunConfig | None = None, model: AgentModel | None = None, reasoning_effort: ReasoningEffort | None = None, parallel_tool_call: bool | None = None, metadata: Mapping[str, JsonValue] | None = None, tags: Mapping[str, JsonValue] | None = None, ) -> AsyncIterator[AgentEvent]: """Dispatch then yield each original event, in order, exactly once. Use aclosing(client.stream(...)) when stopping consumption early. Do not dispatch yielded events manually if a handler is already bound. Args: message: User text or an explicit multimodal UserMessage. config: Optional per-request session and request identity. model: Request-specific model; omitted to use the Agent default. reasoning_effort: Per-request override of the model reasoning level. parallel_tool_call: Per-request override of parallel tool calling. metadata: Request-scoped JSON data supplied to extensions. tags: Request-scoped classifications supplied to extensions. Yields: Original AgentEvent objects after the bound callback completes. Note: Closing early cancels unfinished work. Provider, callback, and cancellation exceptions are not converted into synthetic events. """ stream = self.agent.stream( message, config=config, model=model, reasoning_effort=reasoning_effort, parallel_tool_call=parallel_tool_call, metadata=metadata, tags=tags, ) async with aclosing_stream(self._dispatched(stream)) as events: async for event in events: yield event
async def _dispatched(self, events: AsyncIterator[AgentEvent]) -> AsyncIterator[AgentEvent]: """Hand each event to the bound dispatcher, if there is one, then yield it unchanged.""" async with aclosing_stream(events) as stream: async for event in stream: if self.event_dispatcher is not None: await self.event_dispatcher.dispatch(event) yield event
[docs] async def compact( self, *, config: AgentRunConfig | None = None, model: AgentModel | None = None, metadata: Mapping[str, JsonValue] | None = None, tags: Mapping[str, JsonValue] | None = None, ) -> CompactionEvent | None: """Compact the session's history now, dispatching every event it emits. This is the one-call form of :meth:`Agent.compact`: the pass runs to completion, each event reaches the bound dispatcher in order — so a UI shows the same progress a streamed run shows — and nothing is appended to the conversation or sent to the primary model. Args: config: Session identity; defaults to the initialized configuration. model: Model an extension compacts with when it has none of its own. metadata: Request data visible to extensions and persistence. tags: Request classifications visible to extensions and persistence. Returns: The checkpoint the pass stored, or None when the extensions found nothing worth replacing. Examples: Usage:: stored = await client.compact() """ checkpoint: CompactionEvent | None = None async with aclosing_stream( self.agent.compact(config=config, model=model, metadata=metadata, tags=tags) ) as events: async for event in events: if isinstance(event.compaction, CompactionEvent): checkpoint = event.compaction if self.event_dispatcher is not None: await self.event_dispatcher.dispatch(event) return checkpoint
[docs] async def run( self, message: UserMessage | str, *, config: AgentRunConfig | None = None, model: AgentModel | None = None, reasoning_effort: ReasoningEffort | None = None, parallel_tool_call: bool | None = None, metadata: Mapping[str, JsonValue] | None = None, tags: Mapping[str, JsonValue] | None = None, ) -> AssistantMessage: """Collect a complete stream with callbacks and return its final answer. Args: message: User text or an explicit multimodal UserMessage. config: Per-request identity; omitted to reuse the initialized session. model: Request-specific model; omitted to use the Agent default. reasoning_effort: Override the runtime default for this request only. parallel_tool_call: Override parallel tool calling for this request. metadata: JSON-compatible application data for persistence/extensions. tags: JSON-compatible classifications for persistence/extensions. Returns: The final AssistantMessage after the stream closes successfully. Raises: AgentProtocolError: If no final answer is emitted or the runtime is not initialized. Model and callback failures propagate unchanged. """ return await final_answer( self.stream( message, config=config, model=model, reasoning_effort=reasoning_effort, parallel_tool_call=parallel_tool_call, metadata=metadata, tags=tags, ) )
[docs] async def resume_point( self, *, config: AgentRunConfig | None = None, model: AgentModel | None = None, ) -> ResumePoint | None: """Report where the session was interrupted, or None when there is nothing to resume. See :meth:`Agent.resume_point`. Nothing is written, no model is called, and no event reaches the bound dispatcher. """ return await self.agent.resume_point(config=config, model=model)
[docs] async def stream_resume( self, *, config: AgentRunConfig | None = None, model: AgentModel | None = None, reasoning_effort: ReasoningEffort | None = None, parallel_tool_call: bool | None = None, metadata: Mapping[str, JsonValue] | None = None, tags: Mapping[str, JsonValue] | None = None, ) -> AsyncIterator[AgentEvent]: """Dispatch then yield each event of :meth:`Agent.stream_resume`, in order, exactly once. Use aclosing(client.stream_resume(...)) when stopping consumption early. A session with nothing to resume raises :class:`~zett_agent.exceptions.AgentResumeError`. """ stream = self.agent.stream_resume( config=config, model=model, reasoning_effort=reasoning_effort, parallel_tool_call=parallel_tool_call, metadata=metadata, tags=tags, ) async with aclosing_stream(self._dispatched(stream)) as events: async for event in events: yield event
[docs] async def resume( self, *, config: AgentRunConfig | None = None, model: AgentModel | None = None, reasoning_effort: ReasoningEffort | None = None, parallel_tool_call: bool | None = None, metadata: Mapping[str, JsonValue] | None = None, tags: Mapping[str, JsonValue] | None = None, ) -> AssistantMessage: """Pick an interrupted session up where it stopped, with callbacks, and return the answer. See :meth:`Agent.resume` for what is done in each case and what it raises. """ return await final_answer( self.stream_resume( config=config, model=model, reasoning_effort=reasoning_effort, parallel_tool_call=parallel_tool_call, metadata=metadata, tags=tags, ) )
[docs] async def create_agent( model: AgentModel | None, *, config: AgentRunConfig | None = None, system_prompt: str = "You are a helpful assistant.", tools: Sequence[AgentTool] = (), extensions: Sequence[AgentExtension] | None = None, reasoning_effort: ReasoningEffort = ReasoningEffort.MEDIUM, parallel_tool_call: bool = True, max_iterations: int = 36, max_internal_messages: int = 8, event_dispatcher: AgentEventDispatcher | None = None, ) -> AgentClient: """Create an initialized runtime wrapped in an event-aware AgentClient. Omit config for a new UUIDv7 session, or pass an existing session ID to restore history through your configured persistence extension. All runtime options pass through unchanged; extensions=None keeps Agent defaults. The supplied model and extension resources remain caller-owned. Args: model: Default provider adapter, or None when each request supplies one. config: Stable session identity; omitted to generate a new UUIDv7 ID. system_prompt: Initial instructions rebuilt before each request. tools: Explicit typed tools available to the runtime. extensions: Lifecycle and middleware extensions. None keeps default memory and tool guidance; an explicit sequence replaces those defaults. reasoning_effort: Default reasoning level, overridable per request. parallel_tool_call: Allow supported providers and local tools to use parallel calls. Individual tools may explicitly require serial execution. max_iterations: Maximum model calls for each user or internal message. max_internal_messages: Maximum internal continuations per request. event_dispatcher: Optional callbacks awaited in stream order. Returns: An initialized AgentClient wrapping an ordinary Agent. Raises: ValueError: If runtime limits or extension/tool identities are invalid. Examples: Usage:: from zett_agent.events import AgentEvent from zett_agent.dispatcher import AgentEventDispatcher from zett_agent.client import create_agent class ConsoleEvents(AgentEventDispatcher): async def on_text_delta_event(self, event: AgentEvent) -> None: print(event.delta, end="", flush=True) client = await create_agent(model, event_dispatcher=ConsoleEvents()) reply = await client.run("Hello") await client.run("Continue in the same session") """ agent = await Agent.create( model, config=config, system_prompt=system_prompt, tools=tools, extensions=extensions, reasoning_effort=reasoning_effort, parallel_tool_call=parallel_tool_call, max_iterations=max_iterations, max_internal_messages=max_internal_messages, ) return AgentClient(agent, event_dispatcher=event_dispatcher)