pub struct RunTurnOptions {Show 18 fields
pub approval_decisions: Vec<ApprovalDecision>,
pub max_steps: Option<usize>,
pub stream_tx: Option<Sender<TurnStreamEvent>>,
pub native_search_allowed: bool,
pub session_approved_tools: HashMap<String, CapabilitySet>,
pub unattended: bool,
pub is_delegated_worker: bool,
pub escape_hatch: bool,
pub escalate_sandbox_denials: bool,
pub untrusted_context_seed: bool,
pub dispatch_recorder: Option<Arc<dyn DispatchRecorder>>,
pub clock: Option<Arc<dyn Clock + Send + Sync>>,
pub cache_hint: CacheHint,
pub delegate_descriptors: Vec<DelegateDescriptor>,
pub delegate_max_fanout: Option<u32>,
pub delegate_turn_budget: Option<u32>,
pub question_answers: Vec<VerifiedAnswer>,
pub turn_start_unix_ms: Option<u64>,
}Expand description
Options for a single run_turn invocation.
A small builder-style struct rather than a long parameter list — keeps the
hot-path call sites readable (RunTurnOptions::default()) and gives the
HITL-resume path a typed slot for occurrence-ordered decisions without
adding another positional argument every existing caller would have to
thread through.
Fields§
§approval_decisions: Vec<ApprovalDecision>Verified, occurrence-ordered decisions for exact tool calls.
Used by the control plane → harness resume cycle: the control plane replays the conversation’s event log, collects every verified response not yet consumed by a matching tool result, and passes the ordered entries here so each dangling call consumes one decision.
A different tool or argument tuple cannot inherit a decision. Identical tuples remain separate entries and are paired oldest-first.
max_steps: Option<usize>Per-agent override of the provider↔tool round-trip cap (#801). None
falls through to POLYCHROME_AGENT_MAX_STEPS (per-deployment), then the
crate’s fixed default of 8 — which is tight for the shipped coding-tool
family; a caller that knows this turn’s agent needs a larger (or
smaller) budget sets it here rather than every deployment being stuck
on one global default.
stream_tx: Option<Sender<TurnStreamEvent>>When set, the turn loop forwards each TurnStreamEvent (text delta,
tool start) as it arrives, so a caller can stream partial output
mid-turn (the harness forwards these over its bidi stream → control
plane → Slack chat.appendStream). None keeps the buffered path:
the full TurnResult is always returned regardless.
Bounded (#251): forwarding is an awaited Sender::send, so a slow
consumer on the other end (an idle Slack client, a stalled control
plane) applies real backpressure all the way back through
polyc_llm::turn::collect_turn_observed to the provider stream poll
loop, instead of letting turn-stream events accumulate in memory
without limit.
native_search_allowed: boolWhether this turn’s resolved agent is SCOPED to the provider’s native
web-search-grounding primitive (issue #1226) — i.e. its
builtinTools names polyc_capability::NATIVE_SEARCH_GROUNDING
(re-exported as polyc_tools::web::NATIVE_SEARCH_GROUNDING for that
crate’s callers).
true does not mean grounding is on for every step: the per-step gate
(see the answering loop, which is the only caller that ever sets
CompletionRequest::web_search) additionally requires
polyc_capability::Capability::ArbitraryEgress to survive this
step’s taint state before actually turning the request flag on — the
same required ⊆ granted comparison every other tool call goes
through, applied once per step since there is no per-call tool_use
for this provider-native primitive to intercept. The summarizer and
classifier build their own requests and never consult this at all.
session_approved_tools: HashMap<String, CapabilitySet>Session-scoped approvals (“approve & don’t ask again”), already
filtered to THIS turn’s caller by the control plane (the per-user
scope): tool name → the capability set the signed grant covered at
approval time (#595). A gated call to one of these tools
auto-executes WITHOUT pausing — REGARDLESS of its arguments — but only
when the grant’s covered set includes every capability the call is
currently missing AND ToolExecutor::cacheable_approval returns
true for the tool (the authoritative idempotency gate: a
non-idempotent tool can never be session-approved even if a stale
entry is present).
Scoped per-tool (not per-exact-args) because “don’t ask again” means “stop prompting me for this tool”; a model rarely repeats an identical call, so binding to exact args would make the approval near-useless. The covered-capability key keeps one convenience approval from silently widening: if the tool’s required set later grows, the old grant does not cover the new capability and the gate asks again.
Unlike Self::approval_decisions these are NOT drained on execution.
unattended: boolWhether this turn runs unattended — a scheduled routine firing with no human present (#623). The control plane sets it only for that path (an explicit wire flag, never inferred from the conversation-id shape here).
When true, a gated call the capability decision would escalate does not pause
with a PendingApproval — there is no one to answer it and ADR 0003
forbids park-and-resume on this path. It resolves fail-closed to a
denial-with-reason: the model receives a legible tool-result error (so it
can finish the turn without the tool), the call surfaces on
TurnResult::unattended_denials for the control plane to record as a
durable audit event, and the turn runs to a normal end.
Default false ⇒ every attended turn is byte-for-byte unchanged: an
escalation still pauses with a PendingApproval exactly as today.
is_delegated_worker: boolWhether this turn IS a delegated worker’s own nested turn
(run_delegate_call), as opposed to a top-level or orchestrator
turn. __delegate_to already caps delegation depth at one by never
resolving delegate_descriptors for a nested call, but __handoff_to
has no equivalent depth cap of its own: it’s advertised
unconditionally by run_turn/run_turn_with and matched by tool
NAME regardless of advertisement. Without this flag a worker that
calls (or hallucinates calling) __handoff_to would suspend its own
nested turn with a pending_handoff the delegate machinery has no way
to surface — the orphaned request silently degrades into
run_delegate_call’s "worker produced no answer" (ForcedCompletion
also skips a turn with a pending handoff). When true, the reserved
spec is never advertised AND a matching tool call is never treated as
a handoff — it resolves through the ordinary unknown-tool path
instead, exactly like any other unadvertised name.
Default false ⇒ every non-delegated turn is byte-for-byte unchanged.
escape_hatch: boolEnables the fuzzy-match escape hatch (#582, invariant 9): when the
model calls a tool name that was NOT advertised this turn, the loop
builds a retrieval query from the call itself (the name split into
words plus the argument text — the model’s own expression of the
capability it needs), asks ToolExecutor::recover_unadvertised for
the closest not-yet-advertised tools, and — at most ONCE per turn —
appends the matches to the advertised set so the model can re-issue
the call against a real tool. The failed call resolves to a synthetic
result naming the newly available tools; every firing is logged as a
false-negative retrieval miss. A second unadvertised call in the same
turn (same or different name) gets the ordinary unknown-tool result.
Default false ⇒ byte-for-byte today’s behavior: an unadvertised call
resolves however the executor answers it (typically an unknown-tool
error result). The harness sets this from the wire retrieval config’s
escape_hatch knob, resolved control-plane-side.
escalate_sandbox_denials: boolEnable the graduated-approval sandbox-denial ESCALATION (#301): when
true, a call ToolExecutor::sandbox_would_deny flags is routed
through the approval gate (pauses with a PendingApproval) instead of
being executed and returning the sandbox’s flat denial to the model. The
control plane sets this from the resolved per-persona approval policy.
Default false, so existing callers are unaffected: a sandbox-denied
call runs and surfaces its own error exactly as before.
untrusted_context_seed: boolDurable seed for the untrusted-content-in-context taint state,
computed by the control plane over the conversation’s FULL durable event
log (any quarantined_content-tagged event) and OR-ed into the agent’s
structural in-memory check (untrusted_content_in_context). Taint is
the provenance input to grant derivation: while it holds, the granted
set loses arbitrary egress and external mutation.
The structural check only sees untrusted content that is still a live
LlmContent::ToolResult in the projected transcript. History compaction
folds older tool results into a single System summary message — erasing
the ToolResult the check keys on — and a non-principal participant’s
chat text is never a ToolResult at all. In both cases the durable log
still carries the quarantined provenance, so the control plane reads it
there and passes the verdict in here. true keeps the taint state live
even when the transcript looks clean; the containment escalation then
still fires.
Default false: a conversation with no durable untrusted provenance (and
no multi-party input) is unaffected, so a first egress on a genuinely
clean context still runs unattended.
dispatch_recorder: Option<Arc<dyn DispatchRecorder>>Signs + records dispatch mutations (#67, #539/#540) before they apply.
When None (the default), pre_dispatch Modify/InjectContext and
post_dispatch redactions are NOT applied — the proposed call runs and
the raw result stands — so a policy mutation is inert unless a signer is
wired. When present, each mutation is recorded first and applied only on
success (fail-closed).
clock: Option<Arc<dyn Clock + Send + Sync>>The turn’s clock and jitter source (#656). When None (the default) the
turn wires retry::RealClock — real wall time for jitter entropy and a
real timer for the retry backoff — so production behaves exactly as
before. A test supplies a virtual clock with a fixed jitter seed so the
retry backoff (the turn loop’s only non-determinism) replays identically
and can be stepped without a wall-clock wait.
cache_hint: CacheHintProvider prompt-caching hint for this turn (#629).
When CacheHint::StablePrefix, each step’s CompletionRequest marks
the stable prefix — the system text plus the tool-spec set built once per
turn (#628) — as cacheable, so a provider that supports prompt caching
skips re-processing it on every step (the biggest latency lever on a
multi-step turn). A provider without caching ignores it. Default
CacheHint::None ⇒ no caching, so auxiliary calls that build their own
options are unaffected. The control plane sets it from its turn-boundary
config snapshot, so the knob lands at a turn boundary, never as a compiled
constant.
delegate_descriptors: Vec<DelegateDescriptor>This turn’s resolved __delegate_to targets (#870), one entry per
live can_delegate_to entry the bound Agent declares — each a
complete, self-contained worker configuration the control plane
resolved at dispatch. run_turn_with advertises the reserved
delegate::DELEGATE_TOOL_NAME tool ONLY when this is non-empty; a
call to it is resolved by find_delegate_descriptor and dispatched
as a nested, context-isolated run_turn_with call that joins the SAME
batch’s ordinary tool futures (contrast HandoffRequest, which
short-circuits the batch). Default empty ⇒ byte-for-byte identical to
a turn with no delegation targets: no tool is advertised, so a model
that never sees the name can’t emit it.
delegate_max_fanout: Option<u32>Fan-out width cap for this turn (#874): the maximum number of
__delegate_to calls allowed in a SINGLE batch/step — resolved
control-plane-side from the bound agent’s Agent.delegateMaxFanout
(see polyc_control_plane::delegate::resolve_delegate_max_fanout).
None ⇒ this crate’s own DEFAULT_DELEGATE_MAX_FANOUT, clamped
to DELEGATE_MAX_FANOUT_CEILING regardless of source — a caller
that resolves a wire value ALREADY clamps it, but this crate clamps
again defensively so a directly-constructed RunTurnOptions (a
test, or a future caller) can’t accidentally exceed the ceiling
either. A __delegate_to call beyond the cap, counted within the
SAME batch in source order, resolves to a structured error result —
it is never queued, never silently dropped, and never counts as an
executed delegation for forensic/usage purposes (no
DelegateRecord is produced for it).
delegate_turn_budget: Option<u32>Turn-scoped total delegate-call budget (#874): the maximum number
of __delegate_to calls this turn may dispatch ACROSS ALL its
batches/steps — not just one batch. Bounds a pathological
re-decompose-every-step loop from spawning unbounded workers over a
long-running turn, complementing Self::delegate_max_fanout’s
per-batch bound. None ⇒ DEFAULT_DELEGATE_TURN_BUDGET, clamped
to DELEGATE_TURN_BUDGET_CEILING. A call beyond the turn budget
resolves to a structured error exactly like an over-fan-out call.
question_answers: Vec<VerifiedAnswer>Verified, signed answers to ask_question questions this conversation
gathered since the turn paused (#1660) — the question-pause SIBLING
of Self::approval_decisions, not a reuse of it. Populated by the
harness from control-plane-verified question_response events on the
turn input.
The RUNTIME identity is the occurrence (turn_id, call_id, index)
(#2523): step::QuestionResumePrePass matches an entry against a
dangling call only when all three are equal, so an answer for one
occurrence can never resolve a later call that merely reused the
provider id. The call’s question_args_json is bound one layer down,
in the SIGNATURE: polyc_turn_runner::verify_question_answers
reconstructs the signed canonical from the forwarded args, so an
altered args value fails verification and the entry never reaches this
field. Carrying the args into question::VerifiedAnswer as well
would add a field the runtime match does not read; the occurrence names
exactly one call, and that call’s own arguments are re-parsed from the
transcript.
Consumed by step::QuestionResumePrePass: a dangling ask_question
tool_use in the resumed transcript resolves once every question in
its call has a matching entry here; any question still missing
re-pauses the turn exactly as a fresh call would. Default empty ⇒
byte-for-byte identical to a turn with no pending questions.
turn_start_unix_ms: Option<u64>This turn’s frozen dispatch clock (#1323), in Unix milliseconds:
the SAME value the control plane freezes once per dispatch, renders
as the top-level turn_start_block system message, and records as
ModelCallRecord.captured_clock_unix_ms. run_delegate_call renders
it into a worker’s own turn-start system message so a delegated
worker learns the turn’s start instant exactly like the top-level
turn does, instead of improvising one against its training-data era.
Never read from a fresh clock on this path: replay determinism
(INV-11) requires the worker’s rendered prompt to reproduce
byte-identically, which a second, independently-timed read could not
guarantee. None means no turn-start stamp is rendered for any
worker this turn delegates to (the caller didn’t resolve one, or the
instant was underivable) — a worker told nothing is safer than one
told a wrong time, mirroring turn_start_block’s own rule.
Trait Implementations§
Source§impl Clone for RunTurnOptions
impl Clone for RunTurnOptions
Source§fn clone(&self) -> RunTurnOptions
fn clone(&self) -> RunTurnOptions
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for RunTurnOptions
impl Debug for RunTurnOptions
Source§impl Default for RunTurnOptions
impl Default for RunTurnOptions
Source§fn default() -> RunTurnOptions
fn default() -> RunTurnOptions
Auto Trait Implementations§
impl !RefUnwindSafe for RunTurnOptions
impl !UnwindSafe for RunTurnOptions
impl Freeze for RunTurnOptions
impl Send for RunTurnOptions
impl Sync for RunTurnOptions
impl Unpin for RunTurnOptions
impl UnsafeUnpin for RunTurnOptions
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more