Runtime
Runtime is the execution-time environment available during one agent or graph run. Use it to pass request-specific dependencies into tools, middleware, and graph nodes without hardcoding globals.
Common runtime data includes:
| Runtime data | Use for |
|---|---|
context
| Per-run values such as user ID, tenant, permissions, locale, feature flags, or request configuration. |
store
| Long-term memory shared across runs or threads. |
stream_writer
| Custom application events emitted during Event Streaming. |
execution
| Current graph, node, step, task, thread, checkpoint, and run metadata. |
server_info
| Optional deployment/auth metadata supplied by your own service boundary. |
config
|
Graph config, including checkpoint configurable values such as thread_id.
|
Agent Context
Declare a context schema on a module-defined agent when the run requires specific context fields:
defmodule MyApp.SupportAgent do
use BeamWeaver.Agent
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
tools do
tool MyApp.FetchPreferencesTool
end
context_schema do
field :user_id, :string, required: true
field :user_name, :string
end
end
Pass context when invoking the agent:
alias BeamWeaver.Core.Message
MyApp.SupportAgent.invoke(
%{messages: [Message.user("What email style should I use?")]},
context: %{user_id: "user-123", user_name: "John Smith"}
)
Runtime-built agents use the same option:
{:ok, agent} =
BeamWeaver.Agent.build(
name: "support_agent",
model: BeamWeaver.Models.init_chat_model!("openai:gpt-5.4"),
tools: [MyApp.FetchPreferencesTool],
context_schema: %{
user_id: %{type: :string, required: true}
}
)
BeamWeaver.Agent.invoke(
agent,
%{messages: [Message.user("What's my account status?")]},
context: %{user_id: "user-123"}
)
Inside Tools
Tools access runtime data through explicit injected arguments. This keeps the model-visible schema clean while still giving the handler access to context, store, config, checkpointer, state, or a tool runtime struct.
alias BeamWeaver.Core.Tool
fetch_preferences =
Tool.from_function!(
name: "fetch_email_preferences",
description: "Fetch email preferences for the current user.",
input_schema: %{
"type" => "object",
"properties" => %{
"context" => %{"type" => "object"},
"store" => %{"type" => "object"}
},
"required" => ["context", "store"]
},
injected: [context: :context, store: :store],
handler: fn input, _opts ->
context = input[:context] || input["context"] || %{}
store = input[:store] || input["store"]
user_id = context[:user_id] || context["user_id"]
case BeamWeaver.Memory.get(store, ["users"], user_id) do
{:ok, %{value: %{"preferences" => preferences}}} ->
preferences
_other ->
"The user prefers brief and polite email."
end
end
)
Use %BeamWeaver.Core.ToolRuntime{} when a tool needs several runtime fields or needs to emit streamed progress:
streaming_tool =
Tool.from_function!(
name: "fetch_records",
description: "Fetch records and stream progress.",
input_schema: %{
"type" => "object",
"properties" => %{
"query" => %{"type" => "string"},
"tool_runtime" => %{"type" => "object"}
},
"required" => ["query", "tool_runtime"]
},
injected: [tool_runtime: :tool_runtime],
handler: fn input, _opts ->
runtime = input[:tool_runtime] || input["tool_runtime"]
BeamWeaver.Core.ToolRuntime.emit_output_delta(runtime, %{phase: :started})
BeamWeaver.Core.ToolRuntime.emit_output_delta(runtime, %{phase: :finished})
"Fetched records for #{input["query"] || input[:query]}"
end
)
Inside Middleware
Node-style middleware hooks receive %BeamWeaver.Graph.Runtime{} as the second or third argument:
defmodule MyApp.RequestLogger do
@behaviour BeamWeaver.Agent.Middleware
require Logger
def name(_middleware), do: :request_logger
def before_model(state, runtime) do
user_id = get_in(runtime.context || %{}, [:user_id])
thread_id = get_in(runtime.execution || %{}, [:thread_id])
messages = Map.get(state, :messages, Map.get(state, "messages", []))
Logger.info("user=#{user_id} thread=#{thread_id} messages=#{length(messages)}")
nil
end
end
Wrap-style middleware reads runtime through the request struct:
defmodule MyApp.DynamicUserPrompt do
@behaviour BeamWeaver.Agent.Middleware
alias BeamWeaver.Agent.ModelRequest
def name(_middleware), do: :dynamic_user_prompt
def wrap_model_call(%ModelRequest{} = request, handler) do
user_name = get_in(request.runtime.context || %{}, [:user_name]) || "there"
prompt = "You are helpful. Address the user as #{user_name}."
request
|> ModelRequest.override(system_prompt: prompt)
|> handler.()
end
end
Middleware can also declare context it requires:
def context_schema(_middleware) do
%{user_id: %{type: :string, required: true}}
end
Store
runtime.store is the configured long-term memory store. Configure it on the agent or pass it at build time:
defmodule MyApp.MemoryAgent do
use BeamWeaver.Agent
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
tools do
tool MyApp.FetchPreferencesTool
end
store BeamWeaver.Memory.ETS.new()
end
Use BeamWeaver.Memory.ETS for local/test storage and BeamWeaver.Memory.Ecto for durable Postgres-backed storage. Checkpointing is separate: stores are long-term memory, while checkpointers persist graph state for a thread.
Stream Writer
runtime.stream_writer emits application-specific stream events from inside graph nodes or middleware. Prefer BeamWeaver.Agent.stream_events/3 or BeamWeaver.Graph.Compiled.stream_events/3 to consume them as typed envelopes.
defmodule MyApp.RetrievalProgress do
@behaviour BeamWeaver.Agent.Middleware
def name(_middleware), do: :retrieval_progress
def before_model(_state, runtime) do
runtime.stream_writer.(%{phase: :retrieval, progress: 0.25})
runtime.stream_writer.(%{phase: :reranking, progress: 0.75})
nil
end
end
Tool progress should use BeamWeaver.Core.ToolRuntime.emit_output_delta/2 so the event is associated with the current tool call.
Execution Info
Runtime execution metadata identifies the current graph task:
def before_model(_state, runtime) do
info = runtime.execution || %{}
IO.inspect(%{
graph: info[:graph],
node: info[:node],
step: info[:step],
task_id: info[:task_id],
thread_id: info[:thread_id],
run_id: info[:run_id]
})
nil
end
Inside tools, the same value is available as tool_runtime.execution_info.
handler = fn input, _opts ->
runtime = input[:tool_runtime] || input["tool_runtime"]
runtime.execution_info[:thread_id]
end
thread_id comes from graph config and scopes checkpoints:
config = %{"configurable" => %{"thread_id" => "support-thread-1"}}
MyApp.SupportAgent.invoke(input, config: config, context: %{user_id: "user-123"})
Server Info
server_info is optional deployment metadata. BeamWeaver can hydrate it from configurable values, or your application can pass equivalent metadata through the graph boundary.
config = %{
"configurable" => %{
"thread_id" => "support-thread-1",
"assistant_id" => "support",
"graph_id" => "support-v1",
"langgraph_auth_user" => %{
"identity" => "user-123",
"display_name" => "John Smith",
"permissions" => ["support:read"]
}
}
}
Middleware can read it:
def before_model(_state, runtime) do
case runtime.server_info do
%{user: %{identity: user_id}} -> IO.inspect(user_id, label: "authenticated user")
_other -> :ok
end
nil
end
Low-Level Runtime Agents
Most applications should use use BeamWeaver.Agent, BeamWeaver.Agent.build/1, or BeamWeaver.Graph. The lower-level BeamWeaver.Runtime.Agent API remains available for supervised work orchestration outside the agent DSL.
Start a runtime agent process:
{:ok, agent} = BeamWeaver.Runtime.Agent.start_child(id: "agent-1")
Runtime options added at process startup are:
| Option | Meaning | Default |
|---|---|---|
:owner
| Optional process whose exit stops the runtime agent and all active work. |
nil
|
:cancel_grace_ms
| Time allowed for cooperative cancellation before forced termination. |
100
|
:subscriber_queue_limit
| Maximum queued messages before a slow subscriber is removed. |
1_000
|
start_model_call/4 and start_tool_call/5 also accept per-work options:
| Option | Meaning | Default |
|---|---|---|
:timeout
|
Maximum work duration in milliseconds, or :infinity.
|
5_000
|
:dispatch_hook
|
Optional BeamWeaver.DispatchHook struct called before each attempt.
|
nil
|
:dispatch_context
| Application-owned context passed to the dispatch hook. |
nil
|
:max_retries
| Additional tool attempts after recoverable runtime errors. |
0
|
Subscribe to runtime events:
:ok = BeamWeaver.Runtime.Agent.subscribe(agent)
Start model work:
{:ok, work} =
BeamWeaver.Runtime.Agent.start_model_call(agent, input, fn input, emit ->
emit.({:delta, "hello"})
{:ok, {:final, input}}
end)
Subscribers receive process-runtime messages:
{:beam_weaver_agent, agent_id, {:stream, work_id, chunk}}
{:beam_weaver_agent, agent_id, {:completed, work_id, result}}
{:beam_weaver_agent, agent_id, {:failed, work_id, error}}
{:beam_weaver_agent, agent_id, {:cancelled, work_id, error}}
Tool work can retry recoverable crashes:
BeamWeaver.Runtime.Agent.start_tool_call(agent, "lookup", input, fun, max_retries: 1)
Cancellation is cooperative first. Work can poll BeamWeaver.Runtime.Agent.cancellation/0 at safe boundaries and return {:cancelled, reason}. If it does not acknowledge the signal within :cancel_grace_ms, the runtime terminates it and reports a failed cancellation timeout instead of inventing a successful cancellation.
Applications may pass a BeamWeaver.DispatchHook struct through :dispatch_hook; its before_dispatch/3 callback runs immediately before each model or tool attempt. The hook is deliberately policy-neutral: the application owns the request context and any permit meaning. Subscriber delivery is bounded by :subscriber_queue_limit; a subscriber that cannot keep up is removed.
Every work item gets a trace run ID through the returned work struct. Use BeamWeaver.Tracing.get_run/1 or BeamWeaver.Tracing.get_tree/1 to inspect trace state.