Skip to main content

RunTurnOptions

Struct RunTurnOptions 

Source
pub struct RunTurnOptions {
Show 18 fields pub approved_call_ids: HashSet<(String, String, String)>, pub approved_overrides: HashMap<(String, String, String), ApprovalOverride>, pub denied_call_ids: HashSet<(String, String, String)>, pub max_steps: Option<usize>, pub stream_tx: Option<Sender<TurnStreamEvent>>, pub native_search_allowed: bool, pub session_approved_tools: HashMap<String, CapabilitySet>, pub remembered_grants: HashMap<String, RememberedGrant>, pub unattended: 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>,
}
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 the approved-call-ids set without adding a third positional HashSet argument every existing caller would have to thread through.

Fields§

§approved_call_ids: HashSet<(String, String, String)>

Provider-assigned tool-call ids the caller has previously gathered signed HITL approvals for. When ToolExecutor::needs_approval returns true for a tool call, the loop checks this set: if the call’s id is present, the tool executes as normal; if absent, the loop pauses with a fresh PendingApproval as today.

Used by the control plane → harness resume cycle: the control plane replays the conversation’s event log, collects every verified approval_response that isn’t yet answered by a matching tool_result message in the transcript, and passes the set here so the harness re-drives the function-calling loop with the previously-paused tools executed.

Each entry is the signed (request_id, tool_name, args_json) tuple — the approval is bound to that exact call (#141), so a re-emitted same-id call with different args/tool does NOT inherit the approval (it re-pauses).

§approved_overrides: HashMap<(String, String, String), ApprovalOverride>

Per approved call, the approver’s in-flight EDIT to apply on execution (#67): the arguments to run in place of the model’s proposal. Keyed by the same signed (request_id, tool_name, args_json) identity as Self::approved_call_ids, where the tuple’s args_json is the model’s PROPOSED args (the identity), and the ApprovalOverride carries the approver’s replacement. A call approved without an edit has no entry here — resolve_approved_call then runs the proposed args unchanged, so the common approve path is untouched.

§denied_call_ids: HashSet<(String, String, String)>

Verified signed HITL denials as (request_id, tool_name, args_json) tuples (a verified approval_response with approved == false).

A denial must RESOLVE the call, not leave it pending: when ToolExecutor::needs_approval returns true for a call whose (id, name, args) tuple is in this set, the loop emits a synthetic denial tool_result (carrying {"approved":false,"error":"denied by human approver"}) WITHOUT executing the tool and WITHOUT re-pausing. As with approvals the denial is bound to the exact call — the same id with different args is a new request, not an inherited denial.

A call needing approval that is in neither Self::approved_call_ids nor this set still pends as before.

§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: bool

Whether 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 grant 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::approved_call_ids these are NOT drained on execution.

§remembered_grants: HashMap<String, RememberedGrant>

Passkey-signed remembered grants for this turn’s caller, keyed by tool name → the capability set a verified grant covers (#594). Unlike Self::session_approved_tools (the interactive “don’t ask again” path, which satisfies the disposition AFTER the gate escalates), a remembered grant feeds the capability decision itself: it becomes the per-call policy’s polyc_capability::GrantPolicy::taint_resilient set for its covered tool, so polyc_capability::decide allows a tainted egress/mutation the grant covers WITHOUT ever escalating — the human authorized the exact tainted shape at the enrollment ceremony. One decision path, no override, no leg-clearing flag.

Populated by the harness from the control-plane-verified grants on the turn input (each grant’s principal matched this turn’s caller, its signed coverage matched the current template coverage, and it survived revocation/suspension). Default empty ⇒ byte-for-byte identical to a turn with no grants: the per-call gate then builds polyc_capability::GrantPolicy::default and every path is unchanged.

Each value is a RememberedGrant carrying both the covered set and the opaque audit identity (grant_ref, coverage hash) the harness verified, so a GrantReplayClear the loop records stamps its identity from birth.

§unattended: bool

Whether this turn runs unattended — a trigger-originated firing of an enrollment conversation 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 (no live grant covers it, a coverage break, or an off-shape call) 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. The next scheduled firing is the retry.

Default false ⇒ every attended turn is byte-for-byte unchanged: an escalation still pauses with a PendingApproval exactly as today.

§escape_hatch: bool

Enables 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: bool

Enable 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: bool

Durable 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: CacheHint

Provider 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. NoneDEFAULT_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.

Trait Implementations§

Source§

impl Clone for RunTurnOptions

Source§

fn clone(&self) -> RunTurnOptions

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for RunTurnOptions

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for RunTurnOptions

Source§

fn default() -> RunTurnOptions

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more