pub enum TurnEvent {
ContextCompacted {
reason: CompactionReason,
summarized_messages: u32,
summary_preview: String,
},
TextDelta(String),
ToolStarted {
name: String,
},
ApprovalPending {
turn_id: String,
request_id: String,
tool_name: String,
title: String,
args_json: String,
reason: String,
resolve_token: String,
preview: Option<ApprovalPreview>,
},
QuestionPending {
turn_id: String,
call_id: String,
index: u32,
header: String,
question: String,
options: Vec<QuestionOptionPrompt>,
args_json: String,
answer_token: String,
already_surfaced: bool,
},
HandoffStarted {
child_agent_id: String,
reason: String,
},
InviteDelivery {
target_user_id: String,
code: String,
inviter_display: String,
},
WalletLinkPrompt {
link_url: Option<String>,
renewal: bool,
requested: bool,
},
WalletUpdatePrompt {
update_url: String,
new_limit: String,
},
WalletRevokePrompt {
revoke_url: String,
},
TurnFailed {
kind: TurnFailureKind,
message: String,
},
Done,
}Expand description
Incremental event emitted while a turn streams from the control plane.
Unlike AgentDialer::run_turn, which folds the whole turn into one
string, the streaming API surfaces each meaningful step as it arrives so a
live surface (e.g. Slack chat.appendStream) can update in place.
Variants§
ContextCompacted
The turn’s context was auto-compacted before it ran. Emitted ONCE, as
the first event of the turn (before any TurnEvent::TextDelta), so a
live surface can render a brief notice rather than letting earlier work
silently vanish from the model’s view. The full transcript is retained
server-side; only this turn’s prompt was compacted.
Fields
reason: CompactionReasonWhether the context was summarized or truncated.
TextDelta(String)
Incremental assistant answer text (model/assistant role).
ToolStarted
A tool call has started, named for a user-visible “thinking step”.
ApprovalPending
The turn paused before executing a tool that requires human approval.
Emitted (one per pending call) from the terminal AgentEnd just before
TurnEvent::Done. The caller submits a decision via
ApprovalService.Respond (the THIN path) and re-drives the turn.
Fields
tool_name: StringThe tool/function name awaiting approval. Raw machine identifier; the field of record for trust/audit.
title: StringHuman display label (MCP-style title) for the tool, for rendering
in the approval prompt. May be empty; the surface then derives one
from tool_name.
reason: StringWhy this call is gated, when the pause is an OVERRIDE of a call that would not otherwise need approval. Empty for an ordinary gated call; non-empty only for the lethal-trifecta / Rule-of-Two containment override — rendered on the approval card so the approver sees that untrusted content is in context and this is an outbound call.
resolve_token: StringShort-lived signed capability (#787), freshly minted for THIS
card and scoped to turn_id + request_id + the conversation it
belongs to.
Opaque to the caller: carry it back unmodified to
ApprovalDialer::respond. ApprovalService.Respond rejects a
decision whose token is missing, expired, or bound to a different
request or conversation.
preview: Option<ApprovalPreview>Computed-preview enrichment (#1496), set only for a
routine_create call whose arguments parsed against the compiled
spec shape — None for every other tool. Built by the control
plane from the compiled spec, never from the model’s narration.
Render it with approval_preview_text, never a hand-written
per-surface rendering.
QuestionPending
A question the model asked via ask_question is awaiting an answer
(#1660). Emitted (one per pending question) from the terminal
AgentEnd just before TurnEvent::Done — the question-pause
SIBLING of TurnEvent::ApprovalPending above, not a reuse of it:
ask_question is a decision the user makes, not a danger/permission
gate, so it has its own event-log pair and its own signed-answer
canonical (polyc_crypto::question). The caller submits a decision
via QuestionService.Respond (the THIN path, mirroring
ApprovalService.Respond) and re-drives the turn.
Fields
turn_id: StringTurn that emitted this question occurrence. A provider re-mints a
tool-call id across turns, so the turn is what separates two
questions that share a (call_id, index) (#2523).
call_id: StringThe ask_question call id this question came from. Several
pending questions can share the same call_id (one call can ask
up to three questions at once) — identity is
(turn_id, call_id, index).
options: Vec<QuestionOptionPrompt>2-4 mutually exclusive options to offer.
args_json: StringThe raw ask_question call’s full arguments JSON (every question
in the call, not just this one) — the audit binding a signed
answer must match against.
answer_token: StringShort-lived signed capability, freshly minted for THIS card and
scoped to turn_id + call_id + index + the conversation it
belongs to. Opaque to the caller: carry it back unmodified to
QuestionDialer::respond. QuestionService.Respond rejects a
decision whose token is missing, expired, or bound to a
different occurrence or conversation.
already_surfaced: boolWhether this still-unanswered question was already surfaced once
(a later turn’s boundary marker sits after its
question_request in the event log while it stayed unanswered).
false: render the full interactive card; true: render a
compact reminder instead (#1970). Derived by the control plane
at replay time — never edge-cached. Unlike
TurnEvent::ApprovalPending (which never carries this bit —
approvals only ride it through PendingApproval’s recovery
path), THIS streamed event is itself the vehicle for
re-surfacing a question on every redrive, so it copies the
control plane’s already-computed value straight through rather
than always reporting false.
HandoffStarted
The turn suspended to delegate to a sub-agent: the model invoked the
reserved __handoff_to primitive. Surfaced (once) from the terminal
AgentEnd.handoff just before TurnEvent::Done. The child runs in an
independent conversation. The parent receives no child result. An edge
can render “delegating…” instead of going silent.
Fields
InviteDelivery
The control plane minted an admin invite this turn for the edge to
deliver privately (agent-evaluable admin invite, #698). Surfaced from
the terminal AgentEnd just before TurnEvent::Done. The edge opens
the target’s direct message and delivers the code there — and ONLY
there. The code reached neither the agent nor a channel; the edge fails
closed (telling the admin, delivering nothing) if it can’t reach the
target privately.
Fields
target_user_id: StringThe target’s provider-native user id (from the mention markup). The edge opens THIS person’s direct message.
WalletLinkPrompt
A paid tool call this turn needed a USABLE linked wallet (#519;
renewal branch #2122). Surfaced from the terminal AgentEnd just
before TurnEvent::Done. Carries only the URL and the renewal bit —
no copy — so every edge renders the identical “Link a
wallet”/“Renew access” card text through the shared polyc_proto
helpers instead of hand-writing its own wording.
Fields
link_url: Option<String>The deployment’s wallet-link URL, when known. None = point the
reader at an admin instead of rendering a dangling button.
renewal: boolTrue when the caller HAD a linked wallet whose delegation is no longer usable (expired, revoked, or otherwise broken) — false when they never linked one at all. Selects the “renew” card copy over the first-time “set one up” copy.
requested: boolTrue when the caller directly asked to link (or replace) a
wallet — the wallet_link tool called on its own, never the
paid_fetch/web_fetch payment-interrupt path renewal covers.
Selects the “here’s a secure link” card copy over both the
first-time and renewal payment-interrupt wording.
WalletUpdatePrompt
wallet_update_limit minted a fresh TIP-1011 in-place
spending-limit-update ceremony this turn (issue #1041/#1159).
Surfaced from the terminal AgentEnd just before
TurnEvent::Done. Carries only the URL and the requested cap — no
copy — so every edge renders the identical “Update spending cap”
card text through the shared polyc_proto helpers instead of
hand-writing its own wording.
Fields
WalletRevokePrompt
This turn’s unlink_self (wallet target) call minted a TIP-1011
hard-revoke ceremony for the just-unlinked delegated key (issue
#1042/#1156). Surfaced from the terminal AgentEnd just before
TurnEvent::Done. Carries only the URL — no copy — so every edge
renders the identical “Revoke access” card text through the shared
polyc_proto helpers instead of hand-writing its own wording.
Fields
TurnFailed
The turn failed durably instead of completing (#756). Surfaced from
the terminal AgentEnd.failure just before
TurnEvent::Done — a structured, durable fact the control plane
persisted, not merely the Connect RPC status a dial error would carry.
AgentEnd.failure exists specifically so “external surfaces (Slack,
the cockpit) can react” (see its wire doc comment); before this
variant existed, no edge ever read it, and a durably-failed turn with
no other content (no messages, no pending approvals, no handoff, no
invites) surfaced as silence — every edge saw Done with nothing to
show and posted nothing.
Fields
kind: TurnFailureKindProvider-agnostic failure classification (mirrors the wire
TurnFailureKind 1:1; TURN_FAILURE_KIND_UNSPECIFIED maps to
TurnFailureKind::Other — a definite failure with an unknown
reason is still a definite failure).
message: StringHuman-readable diagnostic text (the underlying provider/tool
error). Log-only — an edge’s user-facing wording comes from its
own shared failure-notice helper keyed on
TurnFailureKind::is_retryable, not this string.
Done
Terminal event: the turn has ended and no further events follow.
Trait Implementations§
impl Eq for TurnEvent
Source§impl From<PendingApprovalPrompt> for TurnEvent
TurnEvent::ApprovalPending carries the exact same fields as
PendingApprovalPrompt — reuse that conversion rather than a second
hand-written field list that could drift from it.
impl From<PendingApprovalPrompt> for TurnEvent
TurnEvent::ApprovalPending carries the exact same fields as
PendingApprovalPrompt — reuse that conversion rather than a second
hand-written field list that could drift from it.
Source§fn from(p: PendingApprovalPrompt) -> Self
fn from(p: PendingApprovalPrompt) -> Self
Source§impl From<PendingQuestionPrompt> for TurnEvent
TurnEvent::QuestionPending carries the exact same fields as
PendingQuestionPrompt — reuse that conversion rather than a second
hand-written field list that could drift from it.
impl From<PendingQuestionPrompt> for TurnEvent
TurnEvent::QuestionPending carries the exact same fields as
PendingQuestionPrompt — reuse that conversion rather than a second
hand-written field list that could drift from it.
Source§fn from(p: PendingQuestionPrompt) -> Self
fn from(p: PendingQuestionPrompt) -> Self
impl StructuralPartialEq for TurnEvent
Auto Trait Implementations§
impl Freeze for TurnEvent
impl RefUnwindSafe for TurnEvent
impl Send for TurnEvent
impl Sync for TurnEvent
impl Unpin for TurnEvent
impl UnsafeUnpin for TurnEvent
impl UnwindSafe for TurnEvent
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<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<T> FutureExt for T
impl<T> FutureExt for T
Source§fn with_context(self, otel_cx: Context) -> WithContext<Self> ⓘ
fn with_context(self, otel_cx: Context) -> WithContext<Self> ⓘ
Source§fn with_current_context(self) -> WithContext<Self> ⓘ
fn with_current_context(self) -> WithContext<Self> ⓘ
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 moreSource§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request