Runtime integration and evaluation
Source:vignettes/runtime-integration.Rmd
runtime-integration.RmdDeputy owns permissions, budgets, hooks, run identity, checkpoints, context policy, and delegation. ellmer owns provider requests, schema transport, tool execution, model parameters, and provider tracing. The integration requires released ellmer 0.5.0 or later.
Explicit fallback Chats
The caller chooses every destination by supplying configured
fallback_chats. Deputy clones these templates, preserves
their credentials, base URL, model parameters, and request callbacks,
and installs the Agent’s conversation, system prompt, governed tools,
and observers. Templates must have no history or tools. Fallback neither
discovers a provider nor silently selects a model.
agent <- Agent$new(
ellmer::chat("openai/gpt-5.6-luna"),
fallback_chats = list(ellmer::chat("openai/gpt-5.6-terra")),
permissions = permissions_readonly(),
usage_limits = UsageLimits(max_requests = 5)
)
result <- agent$run_sync("Summarize the supplied evidence.")Fallback is eligible for a connection failure or HTTP 408, 429, 500,
502, 503, or 504 before any response content or requested tool. Upstream
serializers check content/tool compatibility with the selected provider;
Deputy does not drop content or rewrite provider-specific tools to make
a fallback succeed. Authentication errors, application callbacks, and
validation failures are terminal. A partially streamed response or
completed tool effect locks the run to its current Chat; Deputy
preserves the partial result and signals the failure. It never restarts
a completed tool loop. The selected Chat remains active; subsequent runs
can advance to later configured templates, without revisiting earlier
ones. Automatic compaction belongs to the active run.
SessionStart and UserPromptSubmit precede
PreCompact, and PostCompact follows an
accepted summary. Summary failures still produce an inspectable
last_run() with terminal hooks and usage. Between tool
rounds, compaction runs at the next model-request boundary, after all
tool results settle.
If summary failure, cancellation, or a limit stops that boundary, settled tool results remain in the conversation and survive save/load. Resuming sends those results once, without repeating the tool or inventing an assistant response. Host changes to the turns or system prompt while a summary is in flight reject the stale replacement and preserve the host’s edits.
Use a separate policy to authorize summary recovery destinations:
policy <- ContextPolicy(
summary_fallback_chats = list(ellmer::chat("openai/gpt-5.6-luna"))
)
agent <- Agent$new(
ellmer::chat("openai/gpt-5.6-terra"),
context_policy = policy,
fallback_chats = list(ellmer::chat("openai/gpt-5.6-sol"))
)The primary summary uses an isolated clone of the active Chat. Only a
transient model transport failure can advance to
summary_fallback_chats, after ellmer’s own retries. Summary
Chats have no tools, inherited callbacks, or unrelated history.
Selecting one does not change the task provider. Internal summaries are
never streamed as task output, and a successful summary does not prevent
a later task fallback before task output or effects occur.
Every summary dispatch shares the run’s request/token/cost budget,
including failed dispatches. Unknown cost stays unknown and stops a
cost-limited run. ContextPolicy() and compaction outcomes
are read-only S7 values. Construct another policy to change
configuration; Agent policy getters return snapshots with cloned summary
templates. Chats inside a standalone policy remain reference objects.
compact() and last_compaction() return
DeputyCompaction values, whose attempt records preserve
original provider conditions.
policy <- ContextPolicy(max_tokens = 16000, fallback = "text")
settings <- S7::props(policy)
settings$max_tokens <- 24000L
revised_policy <- do.call(ContextPolicy, settings)
revised_policy$max_tokens
outcome <- DeputyCompaction("custom", FALSE, 4, 2, summary = "Earlier work")
outcome$turns_compactedFor JSON reports, project the outcome and its nested
AgentUsage values to plain lists, and select condition
classes or other intended diagnostics. S7::props() alone
does not recursively convert nested values or remove sensitive provider
evidence. Session save/load retains conversation and summary evidence;
it keeps the receiving Agent’s context policy.
ContextPolicy(fallback = "text") permits deterministic
degraded recovery when summary generation fails and the run can still
continue; it is opt-in and emits a Notification with code
compact_fallback. Cancellation leaves the active context
unchanged. A completed summary stays installed if its usage exhausts the
budget before task execution.
agent$last_compaction() reports the method, run ID,
usage, and destination attempts. Request and fallback events use
phase = "compaction" for summary work. Manual
agent$compact() keeps its synchronous, active-Chat-only
contract. agent$get_turns() and agent$turns()
retain the complete selected conversation across compaction.
agent$get_context_turns() returns only the smaller model
context. The model receives that context and its installed summary;
retaining the display transcript does not send old turns back to the
provider.
The retained prefix lives in memory and grows with the selected
conversation. The host still owns durable history storage, conversation
identity, and branches. set_turns() selects a replacement
conversation, clears the old prefix and summary, and uses the supplied
turns as context. Automatic compaction can reduce that restored context
before the next request. A host needing retrieval can expose authorized
bounded reads separately; transcript text grants no authority.
save_session() uses snapshot schema 3 to preserve both
views without executable tool references. load_session()
restores them under the receiving Agent’s permissions. Older development
snapshot schemas are rejected; restore existing native conversation
history through its host, then create a new snapshot.
LeadAgent’s fallback policy applies to the lead. Child definitions inherit the selected provider and do not implicitly gain additional destinations.
request_start, request_end,
request_error, and fallback events retain
model selection and original conditions with run IDs. These local events
may contain provider error details. request_error requires
a recognized httr2 HTTP/transport condition and evidence that the Chat
reached model dispatch. Streaming requests use ellmer’s recorded partial
turn to distinguish dispatch from request callbacks. Ambiguous failures
do not select another provider. Application callback and validation
failures retain any observed request_end and a separate
run_error with the original condition. Failed dispatches
count toward max_requests. Reported token totals include
only observed usage; unknown cost is NA, and a cost cap
prevents further requests when that cost cannot be enforced.
ellmer configures httr2 with ellmer_max_tries (default 3
total attempts) and ellmer_timeout_s (default 300 seconds).
Configure these once at application startup if needed. Deputy does not
change process-wide options during runs or add another retry loop. HTTP
retries belong to the upstream transport: our real streaming fixture
verifies three HTTP attempts are still one governed model dispatch. The
structured async path uses httr2 1.3.0’s req_perform_promise(),
which explicitly does not perform retries. Our fixture confirms one HTTP
attempt for this path; callers must not assume identical retry behavior.
Deputy does not estimate hidden HTTP attempts from model-request
callbacks or promise immediate cancellation during an upstream backoff
or in-flight structured request.
Tracing governance
Configure an otel provider before loading ellmer and
Deputy. Existing ellmer spans describe invocation, model calls, HTTP
work, and actual tool execution. Deputy adds a deputy.run
parent span and governance events, using deputy.run.id,
deputy.agent.id, parent/delegation IDs, and ellmer’s
gen_ai.conversation.id mapped to the Deputy session ID.
Async delegated runs retain their trace parent and run correlation.
# In a fresh R session, this records locally without configuring an exporter.
record <- otelsdk::with_otel_record({
library(deputy)
agent <- Agent$new(ellmer::chat("openai/gpt-5.6-luna"))
agent$run_sync("Summarize this supplied text.")
})
record$value$run_id
record$tracesA policy denial records a permission event and does not pretend a
tool ran. Deputy’s trace adapter exports a small allowlist of
identifiers and decisions; it omits prompts, tool inputs/results,
validation feedback, conditions, paths, and product run context. Custom
stop text is represented as custom. This allowlist stays in
place even when upstream content capture is enabled.
ellmer’s message capture is separately opt-in via
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true,
set before loading ellmer. Deputy does not override it or create a
second exporter. The caller’s upstream tracer may record endpoint URLs
and tool descriptions; configure those without secrets and apply the
exporter’s own privacy policy as appropriate.
An external evaluation runner
The runnable 10-evaluation.R example executes a fixed
two-row dataset, applies an application-owned exact-answer rule, and
reports case/run/session IDs, completion status, requests, reported
tokens, cost, duration, and error class.
run_context$evaluation$case_id links local evidence to the
case. The same run ID appears on the trace, without enabling message
capture.
source(system.file("examples", "standalone", "10-evaluation.R", package = "deputy"))
evaluationReplace that dataset and scoring rule in your external runner. Deputy
provides AgentResult, usage, stop reasons, events, and
correlated traces; it does not own a dataset format, scoring framework,
exporter, or experiment database.