jeevesagent.core.protocols
==========================

.. py:module:: jeevesagent.core.protocols

.. autoapi-nested-parse::

   Protocol definitions for every module boundary.

   These structural types are the contract surface of the harness. Every
   implementation — first-party or third-party — satisfies one of these. The
   loop and the agent only depend on the protocols, never on concrete
   implementations.

   The protocols are intentionally async-only: every method that performs
   I/O is a coroutine, every stream is an :class:`AsyncIterator`, every
   resource is an :class:`AsyncContextManager`.



Classes
-------

.. autoapisummary::

   jeevesagent.core.protocols.Budget
   jeevesagent.core.protocols.Embedder
   jeevesagent.core.protocols.HookHost
   jeevesagent.core.protocols.Memory
   jeevesagent.core.protocols.Model
   jeevesagent.core.protocols.Permissions
   jeevesagent.core.protocols.Runtime
   jeevesagent.core.protocols.RuntimeSession
   jeevesagent.core.protocols.Sandbox
   jeevesagent.core.protocols.Secrets
   jeevesagent.core.protocols.Telemetry
   jeevesagent.core.protocols.ToolHost


Module Contents
---------------

.. py:class:: Budget

   Bases: :py:obj:`Protocol`


   Resource governance — tokens, calls, cost, wall clock.

   ``user_id`` (M9): the agent loop forwards the live
   :class:`~jeevesagent.RunContext`'s user_id into every
   ``allows_step`` and ``consume`` call so multi-tenant budget
   impls can enforce per-user caps. Implementations that don't
   track per-user usage may ignore the kwarg; the framework
   falls back gracefully when the kwarg isn't accepted.


   .. py:method:: allows_step(*, user_id: str | None = None) -> jeevesagent.core.types.BudgetStatus
      :async:



   .. py:method:: consume(*, tokens_in: int, tokens_out: int, cost_usd: float, user_id: str | None = None) -> None
      :async:



.. py:class:: Embedder

   Bases: :py:obj:`Protocol`


   Text-to-vector embedding model used by the memory subsystem.


   .. py:method:: embed(text: str) -> list[float]
      :async:



   .. py:method:: embed_batch(texts: list[str]) -> list[list[float]]
      :async:



   .. py:attribute:: dimensions
      :type:  int


   .. py:attribute:: name
      :type:  str


.. py:class:: HookHost

   Bases: :py:obj:`Protocol`


   Aggregator over user-registered lifecycle callbacks.

   ``user_id`` (M9): same contract as :class:`Permissions` —
   forwarded from the live RunContext so per-user hooks can
   route. Legacy hook impls without the kwarg fall back via
   ``except TypeError`` in the agent loop.


   .. py:method:: on_event(event: jeevesagent.core.types.Event) -> None
      :async:



   .. py:method:: post_tool(call: jeevesagent.core.types.ToolCall, result: jeevesagent.core.types.ToolResult) -> None
      :async:



   .. py:method:: pre_tool(call: jeevesagent.core.types.ToolCall, *, user_id: str | None = None) -> jeevesagent.core.types.PermissionDecision
      :async:



.. py:class:: Memory

   Bases: :py:obj:`Protocol`


   Tiered memory: working blocks, episodic store, semantic graph.


   .. py:method:: append_block(name: str, content: str, *, user_id: str | None = None) -> None
      :async:


      Append to a named block in ``user_id``'s namespace,
      creating it if absent.



   .. py:method:: consolidate() -> None
      :async:


      Background: extract semantic facts from recent episodes.



   .. py:method:: export(*, user_id: str | None = None) -> jeevesagent.core.types.MemoryExport
      :async:


      Full data dump for ``user_id`` — GDPR / data portability.

      Returns every episode, every fact, and the export
      timestamp. Serialise with ``MemoryExport.model_dump_json()``
      for download or downstream processing. Honours the
      ``user_id`` partition: never includes data belonging to a
      different user.



   .. py:method:: forget(*, user_id: str | None = None, session_id: str | None = None, before: datetime.datetime | None = None) -> int
      :async:


      Erase memory for a user — GDPR / "right to be forgotten".

      With ``user_id`` only: erase EVERYTHING (episodes + facts +
      session messages + working blocks) belonging to that user.
      With ``session_id``: erase only that conversation thread for
      that user. With ``before``: erase only data older than the
      timestamp (other args still scope it). All filters AND
      together.

      Returns the total number of records deleted (episodes +
      facts; backends without precise counts may return their
      best estimate).

      ``user_id=None`` erases the anonymous bucket only — same
      partition rule as :meth:`recall`. To erase everything
      across all users, callers must enumerate users and call
      ``forget`` per-user; the framework deliberately makes the
      "delete every user" path explicit so it's not done by
      accident.



   .. py:method:: profile(*, user_id: str | None = None) -> jeevesagent.core.types.MemoryProfile
      :async:


      Summary of what this memory knows about ``user_id``.

      Returns counts (episodes, facts), the most-recent sessions
      the user touched, the last-seen timestamp, and a sample of
      the most-recently-recorded facts. Suitable for rendering a
      "what does the bot know about me?" view to the end user, or
      for an ops dashboard.

      Backends MUST honour ``user_id`` as a hard partition —
      passing one user's id never returns counts derived from
      another user's data.



   .. py:method:: recall(query: str, *, kind: str = 'episodic', limit: int = 5, time_range: tuple[datetime.datetime, datetime.datetime] | None = None, user_id: str | None = None) -> list[jeevesagent.core.types.Episode]
      :async:


      Retrieve episodes (or facts, when ``kind='semantic'``).

      When ``user_id`` is supplied, results are restricted to
      episodes stored with that exact ``user_id`` value. ``None``
      is its own bucket (the "anonymous / single-tenant"
      namespace) — episodes stored with ``user_id=None`` are never
      visible to a query with ``user_id="alice"`` and vice versa.
      Backends MUST honour this filter to preserve the framework's
      multi-tenant safety contract.



   .. py:method:: recall_facts(query: str, *, limit: int = 5, valid_at: datetime.datetime | None = None, user_id: str | None = None) -> list[jeevesagent.core.types.Fact]
      :async:


      Retrieve bi-temporal facts matching ``query``.

      Backends that don't expose a fact store return ``[]``. The agent
      loop calls this directly rather than duck-typing on
      ``memory.facts`` so backends without fact support don't need
      any opt-out mechanism.

      ``user_id`` filters by namespace partition with the same
      semantics as :meth:`recall`: ``None`` is its own bucket and
      does not cross-contaminate with non-None values.



   .. py:method:: remember(episode: jeevesagent.core.types.Episode) -> str
      :async:


      Persist an episode. Returns the episode ID.



   .. py:method:: session_messages(session_id: str, *, user_id: str | None = None, limit: int = 20) -> list[jeevesagent.core.types.Message]
      :async:


      Return the most-recent ``limit`` user/assistant turns from
      the conversation identified by ``session_id``, in order
      (oldest first).

      This is the conversation-continuity primitive — the agent
      loop calls it at the top of every run so that reusing a
      ``session_id`` actually continues the chat (the model sees
      previous turns as real :class:`Message` history) rather than
      starting fresh and relying solely on semantic recall.

      ``user_id`` MUST be respected by backends as a hard
      namespace partition: messages persisted under one
      ``user_id`` are never visible to a query scoped to a
      different one. Backends without persisted message logs
      return ``[]`` — the agent loop falls back to the
      semantic-recall path in that case.



   .. py:method:: update_block(name: str, content: str, *, user_id: str | None = None) -> None
      :async:


      Replace the contents of a named block in ``user_id``'s
      namespace. ``None`` is the anonymous bucket.



   .. py:method:: working(*, user_id: str | None = None) -> list[jeevesagent.core.types.MemoryBlock]
      :async:


      All in-context blocks for ``user_id``. Pinned to every prompt.

      Like every other memory primitive, working blocks are
      user-partitioned: blocks set under one ``user_id`` are
      invisible to a query scoped to a different one. Backends
      MUST honour this — passing alice's user_id never returns
      bob's pinned blocks.



.. py:class:: Model

   Bases: :py:obj:`Protocol`


   LLM provider interface. One adapter per lab (Anthropic, OpenAI, ...).

   The required surface is ``stream(...)`` — every adapter must
   implement it. Adapters MAY additionally override ``complete(...)``
   with a non-streaming (single-shot) call; if not, ``complete``
   falls back to consuming the stream internally and assembling the
   full response, which is correct but slower (per-chunk wire +
   parsing overhead). Architectures use ``complete`` on the
   non-streaming hot path (``agent.run()``) and ``stream`` when a
   consumer is reading from ``agent.stream()``.


   .. py:method:: stream(messages: list[jeevesagent.core.types.Message], *, tools: list[jeevesagent.core.types.ToolDef] | None = None, temperature: float = 1.0, max_tokens: int | None = None) -> collections.abc.AsyncIterator[jeevesagent.core.types.ModelChunk]

      Stream completion chunks. Each chunk is text, tool_call, or finish.



   .. py:attribute:: name
      :type:  str


.. py:class:: Permissions

   Bases: :py:obj:`Protocol`


   Decides whether a tool call is allowed.

   ``user_id`` (M9): the agent loop forwards the live
   :class:`~jeevesagent.RunContext`'s user_id so multi-tenant
   permission impls (e.g. :class:`~jeevesagent.PerUserPermissions`)
   can route to per-user policies. Implementations that don't
   care about the user can ignore the kwarg; the framework's
   fallback ``except TypeError`` covers legacy impls.


   .. py:method:: check(call: jeevesagent.core.types.ToolCall, *, context: collections.abc.Mapping[str, Any], user_id: str | None = None) -> jeevesagent.core.types.PermissionDecision
      :async:



.. py:class:: Runtime

   Bases: :py:obj:`Protocol`


   Durable execution. Wraps every side effect in a journal entry.


   .. py:method:: session(session_id: str) -> contextlib.AbstractAsyncContextManager[RuntimeSession]

      Open or resume a durable session.



   .. py:method:: signal(session_id: str, name: str, payload: Any) -> None
      :async:


      Send an external signal (e.g., human approval) to a session.



   .. py:method:: step(name: str, fn: collections.abc.Callable[Ellipsis, collections.abc.Awaitable[Any]], *args: Any, idempotency_key: str | None = None, **kwargs: Any) -> Any
      :async:


      Execute ``fn`` as a journaled step. Replays cached on resume.



   .. py:method:: stream_step(name: str, fn: collections.abc.Callable[Ellipsis, collections.abc.AsyncIterator[Any]], *args: Any, **kwargs: Any) -> collections.abc.AsyncIterator[Any]

      Execute a streaming step. Replays the aggregate on resume.



   .. py:attribute:: name
      :type:  str


.. py:class:: RuntimeSession

   Bases: :py:obj:`Protocol`


   Handle to an open durable session held by a :class:`Runtime`.


   .. py:method:: deliver(name: str, payload: Any) -> None
      :async:



   .. py:attribute:: id
      :type:  str


.. py:class:: Sandbox

   Bases: :py:obj:`Protocol`


   Isolation layer for tool execution.


   .. py:method:: execute(tool: jeevesagent.core.types.ToolDef, args: collections.abc.Mapping[str, Any]) -> jeevesagent.core.types.ToolResult
      :async:



   .. py:method:: with_filesystem(root: str) -> contextlib.AbstractAsyncContextManager[None]

      Temporary filesystem sandbox for the duration of the context.



.. py:class:: Secrets

   Bases: :py:obj:`Protocol`


   Resolution and redaction of named secrets.

   ``resolve`` / ``store`` are async because most production secrets
   backends (Vault, AWS Secrets Manager, GCP Secret Manager) talk
   over the network. ``lookup_sync`` exists for the
   *constructor-time* path: when the framework needs to wire an
   API key into a model adapter before any event loop is running
   (e.g. ``OpenAIModel(...)`` from inside ``Agent.__init__``).
   Concrete impls returning ``None`` from ``lookup_sync`` for refs
   that can't be resolved synchronously are fine — callers should
   fall back to ``os.environ`` or to the explicit ``api_key=``
   argument.


   .. py:method:: lookup_sync(ref: str) -> str | None

      Synchronous best-effort lookup, for constructor-time
      callers that can't await. Returns ``None`` when the ref
      isn't available synchronously (e.g. the impl needs a
      network round-trip). Default impls in :mod:`jeevesagent.
      security.secrets` cover env-var and in-memory lookups.



   .. py:method:: redact(text: str) -> str


   .. py:method:: resolve(ref: str) -> str
      :async:



   .. py:method:: store(ref: str, value: str) -> None
      :async:



.. py:class:: Telemetry

   Bases: :py:obj:`Protocol`


   OpenTelemetry-compatible tracing/metrics surface.


   .. py:method:: emit_metric(name: str, value: float, **attrs: Any) -> None
      :async:



   .. py:method:: trace(name: str, **attrs: Any) -> contextlib.AbstractAsyncContextManager[jeevesagent.core.types.Span]


.. py:class:: ToolHost

   Bases: :py:obj:`Protocol`


   MCP-aware tool registry. Lazy-loads schemas on demand.


   .. py:method:: call(tool: str, args: collections.abc.Mapping[str, Any], *, call_id: str = '') -> jeevesagent.core.types.ToolResult
      :async:


      Invoke ``tool`` with ``args``. The ``call_id`` is propagated into
      the returned :class:`ToolResult` so the loop can correlate
      results with the originating model-emitted call.



   .. py:method:: list_tools(*, query: str | None = None) -> list[jeevesagent.core.types.ToolDef]
      :async:



   .. py:method:: watch() -> collections.abc.AsyncIterator[jeevesagent.core.types.ToolEvent]

      Notifications when the tool list changes (MCP listChanged).



