agent_engine/runtime/types.rs
1use serde::Deserialize;
2
3// Stream event types now live in agent-core so rpc_dispatch can use them
4// without an upward dependency on the engine layer.
5pub use agent_core::{AgentEvent, LlmEvent, SessionEvent, StreamEvent};
6
7/// Per-turn signals shared by the host and the engine.
8///
9/// Engine → host: whether the turn reached its NORMAL end — the final
10/// response came back un-cancelled and asked for no tools (nothing left to
11/// do). Set before the engine publishes that turn's final `MessageHistory`.
12/// `Done` cannot say this: several cancelled paths also end `Ok` + `Done`.
13/// A host that cancels a turn reads it once the stream is drained: a turn
14/// that finished just before the cancel reached it (Esc a moment too late)
15/// is complete, not interrupted.
16///
17/// Host → engine: WHY the host cancelled the turn, noted before it cancels
18/// the token, so a canceled `tool_result` says so ("Canceled by user" only
19/// when it was the user). Nothing noted = the turn's default cause: the
20/// user (the historical wording), or what the host chose at turn start
21/// (`with_default_cause`: a driver turn whose token something else cancels,
22/// e.g. the grant's deadline, was cut by the driver ending).
23#[derive(Debug, Clone, Default)]
24pub struct TurnCompletion {
25 completed: std::sync::Arc<std::sync::atomic::AtomicBool>,
26 /// `0` = unset, else `CancelCause as u8`.
27 cancel_cause: std::sync::Arc<std::sync::atomic::AtomicU8>,
28 /// Used when nothing was noted; `None` = the user.
29 default_cause: Option<CancelCause>,
30}
31
32/// Why a host cancelled a turn, as far as a canceled tool result says.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[repr(u8)]
35pub enum CancelCause {
36 User = 1,
37 CostCap = 2,
38 Restart = 3,
39 Host = 4,
40 Driver = 5,
41}
42
43impl CancelCause {
44 /// Leading words of a canceled `tool_result`. Every variant starts with
45 /// "Canceled" (the phase-4 bound tests key on it); the user's is the
46 /// historical, byte-identical "Canceled by user".
47 pub fn phrase(self) -> &'static str {
48 match self {
49 CancelCause::User => "Canceled by user",
50 CancelCause::CostCap => "Canceled (session cost cap reached)",
51 CancelCause::Restart => "Canceled (Synaps restarted)",
52 CancelCause::Host => "Canceled (session stopped by the host)",
53 CancelCause::Driver => "Canceled (session driver revoked)",
54 }
55 }
56
57 fn from_u8(v: u8) -> Option<Self> {
58 Some(match v {
59 1 => CancelCause::User,
60 2 => CancelCause::CostCap,
61 3 => CancelCause::Restart,
62 4 => CancelCause::Host,
63 5 => CancelCause::Driver,
64 _ => return None,
65 })
66 }
67}
68
69impl TurnCompletion {
70 pub fn new() -> Self {
71 Self::default()
72 }
73
74 /// A turn whose un-noted cancels are `cause`'s (see the type docs).
75 pub fn with_default_cause(cause: CancelCause) -> Self {
76 Self {
77 default_cause: Some(cause),
78 ..Self::default()
79 }
80 }
81
82 pub fn completed(&self) -> bool {
83 self.completed.load(std::sync::atomic::Ordering::Acquire)
84 }
85
86 pub(crate) fn mark_completed(&self) {
87 self.completed
88 .store(true, std::sync::atomic::Ordering::Release);
89 }
90
91 /// Note why the host is cancelling this turn. First cause wins: a
92 /// teardown that revokes a driver and then cancels keeps its own cause.
93 pub fn note_cancel_cause(&self, cause: CancelCause) {
94 let _ = self.cancel_cause.compare_exchange(
95 0,
96 cause as u8,
97 std::sync::atomic::Ordering::AcqRel,
98 std::sync::atomic::Ordering::Acquire,
99 );
100 }
101
102 /// Leading words of this turn's canceled tool results.
103 pub(crate) fn cancel_phrase(&self) -> &'static str {
104 CancelCause::from_u8(self.cancel_cause.load(std::sync::atomic::Ordering::Acquire))
105 .or(self.default_cause)
106 .unwrap_or(CancelCause::User)
107 .phrase()
108 }
109}
110
111/// Shared mutable auth state. Lives behind `Arc<RwLock<_>>` so the spawned
112/// streaming task and the parent Runtime always see the same (freshest) token.
113#[derive(Clone)]
114pub(super) struct AuthState {
115 pub(super) auth_token: String,
116 pub(super) auth_type: String,
117 pub(super) refresh_token: Option<String>,
118 pub(super) token_expires: Option<u64>,
119 /// Local source only: mtime of auth.json observed just before the token
120 /// in `auth_token` was loaded. A different on-disk mtime means the file
121 /// was rotated externally (`synaps login`, cron, sync) and the fast path
122 /// must reload. `None` disables the check (remote / never loaded).
123 pub(super) auth_file_mtime: Option<std::time::SystemTime>,
124 /// Which credential `auth_token` was vended for:
125 /// `"<local | remote:<endpoint>#<principal fingerprint>>|<storage_key>"`
126 /// (see `runtime/auth.rs::anthropic_binding`). The principal fingerprint
127 /// is an opaque SHA-256 prefix of the machine token — never the token
128 /// itself, never an access token. `None` = unknown/legacy (api_key
129 /// harnesses, scrubbed). The pre-stream refresh refuses to serve a
130 /// cached token whose binding no longer matches the selected account on
131 /// the current source, so a config/source/principal switch can never
132 /// silently keep the previous account's token.
133 pub(super) bound_credential: Option<String>,
134}
135
136/// Redacting Debug — never print the access or refresh token.
137impl std::fmt::Debug for AuthState {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 f.debug_struct("AuthState")
140 .field(
141 "auth_token",
142 &if self.auth_token.is_empty() {
143 "<empty>"
144 } else {
145 "<redacted>"
146 },
147 )
148 .field("auth_type", &self.auth_type)
149 .field(
150 "refresh_token",
151 &self.refresh_token.as_ref().map(|_| "<redacted>"),
152 )
153 .field("token_expires", &self.token_expires)
154 .field("auth_file_mtime", &self.auth_file_mtime)
155 .field("bound_credential", &self.bound_credential)
156 .finish()
157 }
158}
159
160#[allow(dead_code)]
161#[derive(Debug, Deserialize)]
162pub(super) struct AnthropicAuth {
163 #[serde(rename = "type")]
164 pub(super) auth_type: String,
165 pub(super) refresh: Option<String>,
166 pub(super) access: Option<String>,
167 pub(super) expires: Option<u64>,
168 pub(super) key: Option<String>,
169}