Backplane.AgentRuntime.ConversationAdapter behaviour (backplane_agent_runtime v1.10.52)

Copy Markdown

Host boundaries for Backplane.AgentRuntime.Conversation.

stream/2 returns a lazy enumerable of normalized event maps (structs are accepted). Types: response_started, content_text_delta, content_thinking_delta, tool_call_started, tool_call_arguments_delta, tool_call_completed, usage_updated, response_completed, response_failed. A completed tool call has %{id: binary, name: binary, arguments: map}; response_completed carries the authoritative assistant message. The first terminal ends enumeration.

provider_idle_timeout opts into a finite idle timer renewed after each valid normalized event. The default provider timer bounds the entire effect. Tools and hooks retain their total effect limit. Normalized enumeration cannot see raw SSE comments or transport events omitted by the adapter; the transport owns raw stream observation timeouts.

Optional Goal accounting consumes cumulative usage per attempt. Normalized usage_updated and response_completed events carry nonnegative integer input_tokens, cached_tokens, output_tokens, and reasoning_tokens, or nil/:unknown for unavailable metrics. Reasoning is already included in output. The runtime stamps report identity from the trusted run/incarnation and attempt, commits usage before acknowledging enumeration, and deduplicates terminal repeats. Budgeted Goals require a host-issued binding with authoritative usage capability; unknown charge closes automatic admission. Provider idle observation pauses while bounded local journal/Goal storage acknowledges an event. Root and storage deadlines remain enforced.

response_failed may carry a normalized error map with class: :usage_limit for a proven provider usage-limit rejection. Other terminal provider errors block automatic Goal continuation. Hosts own error classification; the runtime does not infer quota failures from message text. An assistant message may carry phase: :commentary, :final, or :final_answer; absent phase is final for Goal empty-turn accounting.

A host may call its existing provider facade here: no HTTP/SSE parser belongs in this runtime. Requests contain messages, turn_id, run_id, incarnation, step_id, attempt_id, catalog_revision, and canonical tools shaped as %{name: binary, description: binary, parameters: map}. Context is trusted, ephemeral and never persisted. Provider stream/2 and direct/nested prepare_tool/2 contexts contain catalog_snapshot, the complete native catalog pinned to that request or invocation. Its revision matches the request/invocation's catalog_revision; registry, authority, provider tools and native metadata are retained, including admitted registry-only tools. Previously captured contexts remain pinned after publication. Runtime-owned snapshots override the same field in provider_context and never enter provider requests, history, events or public JSON. The field grants no publication or dispatch callbacks to these adapters.

Optional hooks run in bounded workers: prompt/2 before appending each user message, stop/2 after a response with no tools and no consumed steering. Stop may add a user message to continue (the host owns stop-hook recursion policy). prepare_tool/2 runs before final schema/grant checks, approval and the invocation commit. It receives the pinned invocation and trusted context with informational input_kind: :function | :custom; it can allow unchanged input, return replacement arguments/raw custom input, or reject. Only input changes are accepted; descriptor, backend, catalog and authority remain pinned. The hook runs once per invocation, including supported nested calls. It must not dispatch effects. Post-tool hooks belong in the registered backend. A backend gets backend_context.interact.(request) for correlated waits, resolving to {:ok, value}. Only the trusted host should expose resolve/3 to its UI after authenticating the responder. Interaction answers never grant tool authority.

Optional tool_effect_timeout/2 inspects the final validated and approved invocation once, before intent commit. Return :default for effect_timeout, {:ok, milliseconds} for a finite tool budget in 1..2_147_483_647, or a structured {:error, Error.t()} to reject admission. Its context retains the pinned catalog_snapshot, identity and input_kind but has no runtime dispatch, interaction, feedback or publication callbacks. Preparation, approval and selection share the remaining admission allowance. Low-level Execution.run/5 and other effect/idle/commit/cleanup defaults are unchanged.

Direct and nested backends receive runtime-owned effect_deadline.() returning {:ok, %{selected_timeout_ms: integer, deadline_ms: integer | nil, remaining_ms: integer, paused: boolean}}. The effective tool budget is clipped by the finite root and enclosing effect, and starts only after intent ACK. Repeated reads share diminishing active time. Human waits freeze remaining time with a nil deadline; resume shifts the monotonic expiry without renewing time. Completion, cancellation, timeout and owner loss fence retained getters with {:error, %Error{class: :resource_conflict}}. Absolute deadlines are live-VM monotonic values, potentially negative, and must never be persisted.

Direct and nested preparation/backend contexts expose append_feedback.(%{id: stable_id, text: text, source: source, stage: stage}). All four values are nonempty UTF-8 strings: id/source/stage are at most 256 bytes, text at most 64 KiB. Atom or string field names are accepted; extra fields and ambiguous aliases are rejected. The callback returns {:ok, %{id: id, correlation: runtime_identity, revision: committed_revision, store_mode: :durable | :ephemeral}} only after the canonical Store checkpoint and a producer-liveness recheck. Ephemeral acceptance is not durable.

Feedback belongs to the immutable invocation attempt assigned before preparation; input rewrites and approval edits preserve that identity. A receipt does not attest to argument bytes, dispatch or tool success. Denied attempts retain accepted feedback. Exact retries from the same live invocation return the original receipt; changing payload/provenance/invocation under the same ID conflicts. Completed/cancelled producers cannot retry.

At the next natural provider boundary, accepted feedback is checkpointed once as user-role messages, after the complete assistant-tool/result batch. Content starts with [Host feedback]; host_feedback metadata has string keys for ID, source, stage, correlation, accepted revision and Store mode. It grants no system authority and never starts an extra turn. Adapters should preserve this canonical content and may carry metadata as their transport supports.

Interaction requests must be JSON-safe maps. Atom keys/enums are accepted; their public representation uses strings. Opaque values, invalid UTF-8, ambiguous normalized keys, nesting beyond 32 and encoded values over 256 KiB are rejected before admission. Root interaction/resolve semantics retain the original atom-shaped request and answer for existing adapters.

Summary

Callbacks

prepare_tool(map, map)

(optional)
@callback prepare_tool(map(), map()) ::
  :allow
  | {:allow, map() | binary()}
  | {:deny, term()}
  | {:error, Backplane.AgentRuntime.Error.t()}

prompt(map, map)

(optional)
@callback prompt(map(), map()) :: {:ok, map()} | {:error, term()}

stop(list, map)

(optional)
@callback stop([map()], map()) :: :stop | {:continue, map()} | {:error, term()}

stream(map, map)

@callback stream(map(), map()) :: Enumerable.t()

tool_effect_timeout(map, map)

(optional)
@callback tool_effect_timeout(map(), map()) ::
  :default | {:ok, pos_integer()} | {:error, Backplane.AgentRuntime.Error.t()}