Skip to main content

agent_sdk_foundation/
types.rs

1//! Core types for the agent SDK.
2//!
3//! This module contains the fundamental types used throughout the SDK:
4//!
5//! - [`ThreadId`]: Unique identifier for conversation threads
6//! - [`AgentConfig`]: Configuration for the agent loop
7//! - [`TokenUsage`]: Token consumption statistics
8//! - [`ToolResult`]: Result returned from tool execution
9//! - [`ToolTier`]: Permission tiers for tools
10//! - [`AgentRunState`]: Outcome of running the agent loop (looping mode)
11//! - [`TurnOutcome`]: Outcome of running a single turn (single-turn mode)
12//! - [`TurnSummary`]: Structured server-facing outcome metadata
13//! - [`AgentInput`]: Input to start or resume an agent run
14//! - [`AgentContinuation`]: Opaque state for resuming after confirmation
15//! - [`AgentState`]: Checkpointable agent state
16
17use crate::audit::AuditProvenance;
18use crate::llm::{ContentBlock, ContentSource};
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21use time::OffsetDateTime;
22use uuid::Uuid;
23
24/// Unique identifier for a conversation thread
25#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub struct ThreadId(pub String);
27
28impl ThreadId {
29    #[must_use]
30    pub fn new() -> Self {
31        Self(Uuid::new_v4().to_string())
32    }
33
34    #[must_use]
35    pub fn from_string(s: impl Into<String>) -> Self {
36        Self(s.into())
37    }
38}
39
40impl Default for ThreadId {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl std::fmt::Display for ThreadId {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        write!(f, "{}", self.0)
49    }
50}
51
52/// Configuration for the agent loop
53#[derive(Clone, Debug)]
54pub struct AgentConfig {
55    /// Maximum number of turns (LLM round-trips) before stopping
56    pub max_turns: Option<usize>,
57    /// Maximum tokens per response.
58    ///
59    /// If `None`, the SDK uses the provider/model-specific default.
60    pub max_tokens: Option<u32>,
61    /// System prompt for the agent
62    pub system_prompt: String,
63    /// Model identifier
64    pub model: String,
65    /// Retry configuration for transient errors
66    pub retry: RetryConfig,
67    /// Enable streaming responses from the LLM.
68    ///
69    /// When `true`, emits `TextDelta` and `ThinkingDelta` events as text arrives
70    /// in real-time. When `false` (default), waits for the complete response
71    /// before emitting `Text` and `Thinking` events.
72    pub streaming: bool,
73    /// Optional per-tool execution timeout in milliseconds.
74    ///
75    /// When set, the agent loop races each tool's `execute()` future
76    /// against this budget at the SDK boundary (mirroring
77    /// `SubagentConfig::timeout_ms`). A tool that exceeds the budget is
78    /// stopped and reported with a synthetic timeout `ToolResult`, keeping
79    /// the `tool_use` / `tool_result` history balanced even for
80    /// non-cooperative tools. `None` (default) disables the boundary
81    /// timeout entirely.
82    pub tool_timeout_ms: Option<u64>,
83}
84
85impl Default for AgentConfig {
86    fn default() -> Self {
87        Self {
88            max_turns: None,
89            max_tokens: None,
90            system_prompt: String::new(),
91            model: String::from("claude-sonnet-4-5-20250929"),
92            retry: RetryConfig::default(),
93            streaming: false,
94            tool_timeout_ms: None,
95        }
96    }
97}
98
99/// Configuration for retry behavior on transient errors.
100#[derive(Clone, Debug)]
101pub struct RetryConfig {
102    /// Maximum number of retry attempts
103    pub max_retries: u32,
104    /// Base delay in milliseconds for exponential backoff
105    pub base_delay_ms: u64,
106    /// Maximum delay cap in milliseconds
107    pub max_delay_ms: u64,
108}
109
110impl Default for RetryConfig {
111    fn default() -> Self {
112        Self {
113            max_retries: 5,
114            base_delay_ms: 1000,
115            max_delay_ms: 120_000,
116        }
117    }
118}
119
120impl RetryConfig {
121    /// Create a retry config with no retries (for testing)
122    #[must_use]
123    pub const fn no_retry() -> Self {
124        Self {
125            max_retries: 0,
126            base_delay_ms: 0,
127            max_delay_ms: 0,
128        }
129    }
130
131    /// Create a retry config with fast retries (for testing)
132    #[must_use]
133    pub const fn fast() -> Self {
134        Self {
135            max_retries: 5,
136            base_delay_ms: 10,
137            max_delay_ms: 100,
138        }
139    }
140}
141
142/// Token usage statistics
143#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
144pub struct TokenUsage {
145    pub input_tokens: u32,
146    pub output_tokens: u32,
147    #[serde(default)]
148    pub cached_input_tokens: u32,
149    #[serde(default)]
150    pub cache_creation_input_tokens: u32,
151}
152
153impl TokenUsage {
154    pub const fn add(&mut self, other: &Self) {
155        self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
156        self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
157        self.cached_input_tokens = self
158            .cached_input_tokens
159            .saturating_add(other.cached_input_tokens);
160        self.cache_creation_input_tokens = self
161            .cache_creation_input_tokens
162            .saturating_add(other.cache_creation_input_tokens);
163    }
164}
165
166/// Result of a tool execution
167#[derive(Clone, Debug, Serialize, Deserialize)]
168pub struct ToolResult {
169    /// Whether the tool execution succeeded
170    pub success: bool,
171    /// Output content (displayed to user and fed back to LLM)
172    pub output: String,
173    /// Optional structured data
174    pub data: Option<serde_json::Value>,
175    /// Optional documents (PDFs, images) to pass back to the LLM as native content blocks.
176    /// The agent appends these as `ContentBlock::Document` / `ContentBlock::Image` blocks
177    /// in the same user message as the tool result, so the model can read them directly.
178    #[serde(default, skip_serializing_if = "Vec::is_empty")]
179    pub documents: Vec<ContentSource>,
180    /// Duration of the tool execution in milliseconds
181    pub duration_ms: Option<u64>,
182}
183
184impl ToolResult {
185    #[must_use]
186    pub fn success(output: impl Into<String>) -> Self {
187        Self {
188            success: true,
189            output: output.into(),
190            data: None,
191            documents: Vec::new(),
192            duration_ms: None,
193        }
194    }
195
196    #[must_use]
197    pub fn success_with_data(output: impl Into<String>, data: serde_json::Value) -> Self {
198        Self {
199            success: true,
200            output: output.into(),
201            data: Some(data),
202            documents: Vec::new(),
203            duration_ms: None,
204        }
205    }
206
207    #[must_use]
208    pub fn error(message: impl Into<String>) -> Self {
209        Self {
210            success: false,
211            output: message.into(),
212            data: None,
213            documents: Vec::new(),
214            duration_ms: None,
215        }
216    }
217
218    #[must_use]
219    pub const fn with_duration(mut self, duration_ms: u64) -> Self {
220        self.duration_ms = Some(duration_ms);
221        self
222    }
223
224    /// Attach documents (PDFs, images) to be sent back to the LLM as native content blocks.
225    ///
226    /// Use this when a tool produces a binary document that the model should read directly,
227    /// e.g. a decrypted PDF that Anthropic can parse natively via its document API.
228    ///
229    /// # Example
230    /// ```rust,ignore
231    /// use agent_sdk::{ToolResult, ContentSource};
232    ///
233    /// Ok(ToolResult::success("PDF decrypted.").with_documents(vec![
234    ///     ContentSource::new("application/pdf", base64_data),
235    /// ]))
236    /// ```
237    #[must_use]
238    pub fn with_documents(mut self, documents: Vec<ContentSource>) -> Self {
239        self.documents = documents;
240        self
241    }
242}
243
244/// Permission tier for tools
245#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
246pub enum ToolTier {
247    /// Read-only, always allowed (e.g., `get_balance`)
248    Observe,
249    /// Requires confirmation before execution.
250    /// The application determines the confirmation type (normal, PIN, biometric).
251    Confirm,
252}
253
254/// Snapshot of agent state for checkpointing
255#[derive(Clone, Debug, Serialize, Deserialize)]
256pub struct AgentState {
257    pub thread_id: ThreadId,
258    pub turn_count: usize,
259    pub total_usage: TokenUsage,
260    pub metadata: HashMap<String, serde_json::Value>,
261    #[serde(with = "time::serde::rfc3339")]
262    pub created_at: OffsetDateTime,
263}
264
265impl AgentState {
266    #[must_use]
267    pub fn new(thread_id: ThreadId) -> Self {
268        Self {
269            thread_id,
270            turn_count: 0,
271            total_usage: TokenUsage::default(),
272            metadata: HashMap::new(),
273            created_at: OffsetDateTime::now_utc(),
274        }
275    }
276}
277
278/// Error from the agent loop.
279#[derive(Debug, Clone)]
280pub struct AgentError {
281    /// Error message
282    pub message: String,
283    /// Whether the error is potentially recoverable
284    pub recoverable: bool,
285}
286
287impl AgentError {
288    #[must_use]
289    pub fn new(message: impl Into<String>, recoverable: bool) -> Self {
290        Self {
291            message: message.into(),
292            recoverable,
293        }
294    }
295}
296
297impl std::fmt::Display for AgentError {
298    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299        write!(f, "{}", self.message)
300    }
301}
302
303impl std::error::Error for AgentError {}
304
305/// Outcome of running the agent loop.
306#[derive(Debug)]
307#[non_exhaustive]
308pub enum AgentRunState {
309    /// Agent completed successfully.
310    Done {
311        total_turns: u32,
312        total_usage: TokenUsage,
313    },
314
315    /// Agent was refused by the model (safety/policy).
316    Refusal {
317        total_turns: u32,
318        total_usage: TokenUsage,
319    },
320
321    /// Agent encountered an error.
322    Error(AgentError),
323
324    /// Agent is awaiting confirmation for a tool call.
325    /// The application should present this to the user and call resume.
326    AwaitingConfirmation {
327        /// ID of the pending tool call (from LLM)
328        tool_call_id: String,
329        /// Tool name string (for LLM protocol)
330        tool_name: String,
331        /// Human-readable display name
332        display_name: String,
333        /// Tool input parameters
334        input: serde_json::Value,
335        /// Description of what confirmation is needed
336        description: String,
337        /// Versioned continuation envelope for resuming.
338        continuation: Box<ContinuationEnvelope>,
339    },
340
341    /// Agent run was cancelled via a cancellation token.
342    Cancelled {
343        total_turns: u32,
344        total_usage: TokenUsage,
345    },
346}
347
348/// Information about a pending tool call that was extracted from the LLM response.
349#[derive(Clone, Debug, Serialize, Deserialize)]
350pub struct PendingToolCallInfo {
351    /// Unique ID for this tool call (from LLM)
352    pub id: String,
353    /// Tool name string (for LLM protocol)
354    pub name: String,
355    /// Human-readable display name
356    pub display_name: String,
357    /// Permission tier of the tool, captured at the moment the LLM
358    /// requested the call.
359    ///
360    /// Persisted on the continuation so that authoritative audit records
361    /// on the externalized tool-runtime path can attribute the correct
362    /// tier even though the registry is no longer reachable at resume
363    /// time. Defaults to [`ToolTier::Confirm`] (the strictest default)
364    /// when deserialized from a continuation that predates this field.
365    #[serde(default = "default_pending_tier")]
366    pub tier: ToolTier,
367    /// Tool input parameters as requested by the LLM.
368    pub input: serde_json::Value,
369    /// Effective input after SDK preparation (e.g. listen-context enrichment).
370    ///
371    /// For most tools this equals `input`.  The server persists this for
372    /// execution while `input` stays as the audit trail.
373    #[serde(default)]
374    pub effective_input: serde_json::Value,
375    /// Optional context for tools that prepare asynchronously and execute later.
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub listen_context: Option<ListenExecutionContext>,
378}
379
380/// Default tier used when deserializing a continuation that predates
381/// the `tier` field — the strictest default so legacy continuations
382/// surface as confirm-tier rather than silently observe-tier.
383const fn default_pending_tier() -> ToolTier {
384    ToolTier::Confirm
385}
386
387// ── Structured policy input ──────────────────────────────────────────
388
389/// Structured input passed to the `pre_tool_use` hook for policy
390/// evaluation.
391///
392/// Bundles every datum that a server-side policy engine needs to make an
393/// allow / block / confirm decision, replacing the earlier loose
394/// `(tool_name, input, tier)` triple.
395///
396/// The `AgentHooks` trait itself lives in `agent-sdk-tools` to avoid a
397/// dependency cycle; this struct is the stable contract they share.
398#[derive(Clone, Debug)]
399pub struct ToolInvocation {
400    /// Unique ID for this tool call (from LLM).
401    pub tool_call_id: String,
402    /// Tool name string (for LLM protocol).
403    pub tool_name: String,
404    /// Human-readable display name.
405    pub display_name: String,
406    /// Permission tier of the tool.
407    pub tier: ToolTier,
408    /// Input parameters as requested by the LLM (the audit trail).
409    pub requested_input: serde_json::Value,
410    /// Input after SDK preparation — may differ from `requested_input`
411    /// for listen-tools that enrich input during the ready phase.
412    pub effective_input: serde_json::Value,
413    /// Optional listen-execution context, present when the tool uses
414    /// the listen/execute pattern.
415    pub listen_context: Option<ListenExecutionContext>,
416}
417
418/// Context captured for listen/execute tools while awaiting confirmation.
419#[derive(Clone, Debug, Serialize, Deserialize)]
420pub struct ListenExecutionContext {
421    /// Opaque operation identifier used to execute/cancel.
422    pub operation_id: String,
423    /// Revision used for optimistic concurrency checks.
424    pub revision: u64,
425    /// Snapshot shown to the user during confirmation.
426    pub snapshot: serde_json::Value,
427    /// Optional expiration timestamp (RFC3339).
428    #[serde(
429        default,
430        skip_serializing_if = "Option::is_none",
431        with = "time::serde::rfc3339::option"
432    )]
433    pub expires_at: Option<OffsetDateTime>,
434}
435
436/// Continuation state that allows resuming the agent loop.
437///
438/// This contains all the internal state needed to continue execution
439/// after receiving a confirmation decision. Pass this back when resuming.
440///
441/// # Turn-summary fields
442///
443/// `response_id` and `stop_reason` capture the **turn-closing** LLM call
444/// that produced [`AgentContinuation::pending_tool_calls`] before the
445/// pause. They are carried across the pause boundary so the
446/// [`TurnSummary`] emitted on the resume path can report the same LLM
447/// metadata as the pre-pause summary for the same turn.
448///
449/// Both are `Option` and default to `None` for forward compatibility
450/// with continuations persisted before these fields existed.
451#[derive(Clone, Debug, Serialize, Deserialize)]
452pub struct AgentContinuation {
453    /// Thread ID (used for validation on resume)
454    pub thread_id: ThreadId,
455    /// Current turn number
456    pub turn: usize,
457    /// Total token usage so far
458    pub total_usage: TokenUsage,
459    /// Token usage for this specific turn (from the LLM call that generated tool calls)
460    pub turn_usage: TokenUsage,
461    /// All pending tool calls from this turn
462    pub pending_tool_calls: Vec<PendingToolCallInfo>,
463    /// Index of the tool call awaiting confirmation
464    pub awaiting_index: usize,
465    /// Tool results already collected (for tools before the awaiting one)
466    pub completed_results: Vec<(String, ToolResult)>,
467    /// Agent state snapshot
468    pub state: AgentState,
469    /// Provider response ID from the LLM call that produced this turn's
470    /// pending tool calls.
471    ///
472    /// `None` for continuations persisted before this field was added,
473    /// or when the provider did not return an ID.
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub response_id: Option<String>,
476    /// Stop reason from the LLM call that produced this turn's pending
477    /// tool calls.
478    ///
479    /// `None` for continuations persisted before this field was added.
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub stop_reason: Option<crate::llm::StopReason>,
482    /// Full content blocks from the LLM response that produced this
483    /// turn's pending tool calls (text, thinking, and tool-use blocks).
484    ///
485    /// When the LLM emits text before tool calls (e.g. "I will run
486    /// that." followed by a `tool_use` block), those text blocks must be
487    /// preserved so Phase 5 can reconstruct the complete assistant
488    /// message in the conversation history.
489    ///
490    /// Empty for continuations persisted before this field was added.
491    #[serde(default, skip_serializing_if = "Vec::is_empty")]
492    pub response_content: Vec<crate::llm::ContentBlock>,
493}
494
495// ── Versioned continuation envelope ──────────────────────────────────
496
497/// Current envelope version.
498pub const CONTINUATION_VERSION: u32 = 1;
499
500/// Versioned wrapper around [`AgentContinuation`].
501///
502/// This is the **public durable boundary** for server persistence.
503/// Servers serialise this envelope (not the raw `AgentContinuation`)
504/// so future SDK versions can evolve the inner payload while keeping
505/// a stable wire format.
506///
507/// Unknown versions are rejected at resume time, giving servers a
508/// clear upgrade signal instead of silent data corruption.
509#[derive(Clone, Debug, Serialize, Deserialize)]
510pub struct ContinuationEnvelope {
511    /// Schema version — currently [`CONTINUATION_VERSION`].
512    pub version: u32,
513    /// The continuation payload.
514    pub payload: AgentContinuation,
515}
516
517impl ContinuationEnvelope {
518    /// Wrap a continuation in the current version envelope.
519    #[must_use]
520    pub const fn wrap(payload: AgentContinuation) -> Self {
521        Self {
522            version: CONTINUATION_VERSION,
523            payload,
524        }
525    }
526
527    /// Validate the envelope version, returning the inner continuation
528    /// or an error if the version is unknown.
529    ///
530    /// # Errors
531    ///
532    /// Returns an error string if `version` does not match
533    /// [`CONTINUATION_VERSION`].
534    pub fn unwrap_validated(self) -> Result<AgentContinuation, String> {
535        if self.version != CONTINUATION_VERSION {
536            return Err(format!(
537                "Unsupported continuation version {}: expected {}",
538                self.version, CONTINUATION_VERSION,
539            ));
540        }
541        Ok(self.payload)
542    }
543}
544
545/// A tool result provided by the external runtime for a specific tool call.
546///
547/// This is the durable handoff payload: a root worker serialises these
548/// alongside the [`AgentContinuation`] and provides them on resume via
549/// [`AgentInput::SubmitToolResults`].
550#[derive(Clone, Debug, Serialize, Deserialize)]
551pub struct ExternalToolResult {
552    /// The tool call ID this result corresponds to (must match a
553    /// [`PendingToolCallInfo::id`] from the original
554    /// [`TurnOutcome::PendingToolCalls`]).
555    pub tool_call_id: String,
556    /// The execution result.
557    pub result: ToolResult,
558}
559
560/// Input to start or resume an agent run.
561#[derive(Debug)]
562pub enum AgentInput {
563    /// Start a new conversation with user text.
564    Text(String),
565
566    /// Start a new conversation with rich content (text, images, documents).
567    Message(Vec<ContentBlock>),
568
569    /// Resume after a confirmation decision.
570    Resume {
571        /// The versioned continuation envelope from `AwaitingConfirmation`.
572        continuation: Box<ContinuationEnvelope>,
573        /// ID of the tool call being confirmed/rejected.
574        tool_call_id: String,
575        /// Whether the user confirmed the action.
576        confirmed: bool,
577        /// Optional reason if rejected.
578        rejection_reason: Option<String>,
579    },
580
581    /// Resume after external tool execution.
582    ///
583    /// Use this after [`TurnOutcome::PendingToolCalls`] when
584    /// [`ToolRuntime::External`] is set.  The caller must provide a result
585    /// for **every** pending tool call listed in the continuation.
586    ///
587    /// The SDK validates the continuation envelope version, appends the
588    /// tool results to the message store, and continues to the next LLM turn.
589    SubmitToolResults {
590        /// The versioned continuation from [`TurnOutcome::PendingToolCalls`].
591        continuation: Box<ContinuationEnvelope>,
592        /// One result per pending tool call.  The order does not matter,
593        /// but every `tool_call_id` from the continuation must be covered.
594        results: Vec<ExternalToolResult>,
595    },
596
597    /// Continue to the next turn (for single-turn mode).
598    ///
599    /// Use this after `TurnOutcome::NeedsMoreTurns` to execute the next turn.
600    /// The message history already contains tool results from the previous turn.
601    Continue,
602}
603
604/// Result of tool execution - may indicate async operation in progress.
605#[derive(Clone, Debug, Serialize, Deserialize)]
606pub enum ToolOutcome {
607    /// Tool completed synchronously with success
608    Success(ToolResult),
609
610    /// Tool completed synchronously with failure
611    Failed(ToolResult),
612
613    /// Tool started an async operation - must stream status to completion
614    InProgress {
615        /// Identifier for the operation (to query status)
616        operation_id: String,
617        /// Initial message for the user
618        message: String,
619    },
620}
621
622impl ToolOutcome {
623    #[must_use]
624    pub fn success(output: impl Into<String>) -> Self {
625        Self::Success(ToolResult::success(output))
626    }
627
628    #[must_use]
629    pub fn failed(message: impl Into<String>) -> Self {
630        Self::Failed(ToolResult::error(message))
631    }
632
633    #[must_use]
634    pub fn in_progress(operation_id: impl Into<String>, message: impl Into<String>) -> Self {
635        Self::InProgress {
636            operation_id: operation_id.into(),
637            message: message.into(),
638        }
639    }
640
641    /// Returns true if operation is still in progress
642    #[must_use]
643    pub const fn is_in_progress(&self) -> bool {
644        matches!(self, Self::InProgress { .. })
645    }
646}
647
648// ============================================================================
649// Tool Execution Idempotency Types
650// ============================================================================
651
652/// Status of a tool execution for idempotency tracking.
653#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
654pub enum ExecutionStatus {
655    /// Execution started but not yet completed
656    InFlight,
657    /// Execution completed (success or failure)
658    Completed,
659}
660
661/// Record of a tool execution for idempotency.
662///
663/// This struct tracks tool executions to prevent duplicate execution when
664/// the agent loop retries after a failure. The write-ahead pattern ensures
665/// that execution intent is recorded BEFORE calling the tool, and updated
666/// with results AFTER completion.
667#[derive(Clone, Debug, Serialize, Deserialize)]
668pub struct ToolExecution {
669    /// The tool call ID from the LLM (unique per invocation)
670    pub tool_call_id: String,
671    /// Thread this execution belongs to
672    pub thread_id: ThreadId,
673    /// Tool name
674    pub tool_name: String,
675    /// Display name
676    pub display_name: String,
677    /// Input parameters (for verification)
678    pub input: serde_json::Value,
679    /// Current status
680    pub status: ExecutionStatus,
681    /// Result if completed
682    pub result: Option<ToolResult>,
683    /// For async tools: the operation ID returned by `execute()`
684    pub operation_id: Option<String>,
685    /// Timestamp when execution started
686    #[serde(with = "time::serde::rfc3339")]
687    pub started_at: OffsetDateTime,
688    /// Timestamp when execution completed
689    #[serde(with = "time::serde::rfc3339::option")]
690    pub completed_at: Option<OffsetDateTime>,
691}
692
693impl ToolExecution {
694    /// Create a new in-flight execution record.
695    #[must_use]
696    pub fn new_in_flight(
697        tool_call_id: impl Into<String>,
698        thread_id: ThreadId,
699        tool_name: impl Into<String>,
700        display_name: impl Into<String>,
701        input: serde_json::Value,
702        started_at: OffsetDateTime,
703    ) -> Self {
704        Self {
705            tool_call_id: tool_call_id.into(),
706            thread_id,
707            tool_name: tool_name.into(),
708            display_name: display_name.into(),
709            input,
710            status: ExecutionStatus::InFlight,
711            result: None,
712            operation_id: None,
713            started_at,
714            completed_at: None,
715        }
716    }
717
718    /// Mark this execution as completed with a result.
719    pub fn complete(&mut self, result: ToolResult) {
720        self.status = ExecutionStatus::Completed;
721        self.result = Some(result);
722        self.completed_at = Some(OffsetDateTime::now_utc());
723    }
724
725    /// Set the operation ID for async tool tracking.
726    pub fn set_operation_id(&mut self, operation_id: impl Into<String>) {
727        self.operation_id = Some(operation_id.into());
728    }
729
730    /// Returns true if this execution is still in flight.
731    #[must_use]
732    pub fn is_in_flight(&self) -> bool {
733        self.status == ExecutionStatus::InFlight
734    }
735
736    /// Returns true if this execution has completed.
737    #[must_use]
738    pub fn is_completed(&self) -> bool {
739        self.status == ExecutionStatus::Completed
740    }
741}
742
743/// Outcome of running a single turn.
744///
745/// This is returned by `run_turn` to indicate what happened and what to do next.
746///
747/// # Server-facing contract
748///
749/// Every terminal variant (everything except [`TurnOutcome::Error`]) carries
750/// a [`TurnSummary`] with the provider/model/stop-reason/response-id/usage
751/// provenance that later server phases need to durably persist. Matching by
752/// field name continues to work because the legacy variant fields are
753/// preserved alongside the new `summary` field.
754#[derive(Debug)]
755pub enum TurnOutcome {
756    /// Turn completed successfully, but more turns are needed.
757    ///
758    /// Tools were executed and their results are stored in the message history.
759    /// Call `run_turn` again with `AgentInput::Continue` to proceed.
760    NeedsMoreTurns {
761        /// The turn number that just completed
762        turn: usize,
763        /// Token usage for this turn
764        turn_usage: TokenUsage,
765        /// Cumulative token usage so far
766        total_usage: TokenUsage,
767        /// Structured server-facing outcome metadata.
768        summary: TurnSummary,
769    },
770
771    /// Agent completed successfully (no more tool calls).
772    Done {
773        /// Total turns executed
774        total_turns: u32,
775        /// Cumulative token usage
776        total_usage: TokenUsage,
777        /// Structured server-facing outcome metadata.
778        summary: TurnSummary,
779    },
780
781    /// A tool requires user confirmation.
782    ///
783    /// Present this to the user and call `run_turn` with `AgentInput::Resume`
784    /// to continue.
785    AwaitingConfirmation {
786        /// ID of the pending tool call (from LLM)
787        tool_call_id: String,
788        /// Tool name string (for LLM protocol)
789        tool_name: String,
790        /// Human-readable display name
791        display_name: String,
792        /// Tool input parameters
793        input: serde_json::Value,
794        /// Description of what confirmation is needed
795        description: String,
796        /// Versioned continuation envelope for resuming.
797        continuation: Box<ContinuationEnvelope>,
798        /// Structured server-facing outcome metadata.
799        summary: TurnSummary,
800    },
801
802    /// Model refused the request (safety/policy).
803    Refusal {
804        /// Total turns executed
805        total_turns: u32,
806        /// Cumulative token usage
807        total_usage: TokenUsage,
808        /// Structured server-facing outcome metadata.
809        summary: TurnSummary,
810    },
811
812    /// The turn was cancelled via a cancellation token.
813    Cancelled {
814        /// Total turns executed before cancellation
815        total_turns: u32,
816        /// Cumulative token usage
817        total_usage: TokenUsage,
818        /// Structured server-facing outcome metadata.
819        summary: TurnSummary,
820    },
821
822    /// An error occurred.
823    ///
824    /// No [`TurnSummary`] is attached because the error may have occurred
825    /// before the turn produced any durable LLM provenance.
826    Error(AgentError),
827
828    /// Tool calls are ready for external execution.
829    ///
830    /// Only returned when [`ToolRuntime::External`] is set in [`TurnOptions`].
831    /// The caller is responsible for executing the tool calls and resuming
832    /// with [`AgentInput::SubmitToolResults`], providing one
833    /// [`ExternalToolResult`] for each pending tool call.
834    ///
835    /// The `continuation` must be passed back unmodified — it carries the
836    /// turn identity, token usage, and agent state needed to validate and
837    /// apply the results.
838    PendingToolCalls {
839        /// The turn number that produced these tool calls
840        turn: usize,
841        /// Token usage for this turn's LLM call
842        turn_usage: TokenUsage,
843        /// Cumulative token usage so far
844        total_usage: TokenUsage,
845        /// Tool calls to execute externally
846        tool_calls: Vec<PendingToolCallInfo>,
847        /// Versioned continuation envelope for resuming after external tool execution.
848        continuation: Box<ContinuationEnvelope>,
849        /// Structured server-facing outcome metadata.
850        summary: TurnSummary,
851    },
852}
853
854impl TurnOutcome {
855    /// Returns the attached [`TurnSummary`], if the variant carries one.
856    ///
857    /// Present on every variant except [`TurnOutcome::Error`].
858    #[must_use]
859    pub const fn summary(&self) -> Option<&TurnSummary> {
860        match self {
861            Self::NeedsMoreTurns { summary, .. }
862            | Self::Done { summary, .. }
863            | Self::AwaitingConfirmation { summary, .. }
864            | Self::Refusal { summary, .. }
865            | Self::Cancelled { summary, .. }
866            | Self::PendingToolCalls { summary, .. } => Some(summary),
867            Self::Error(_) => None,
868        }
869    }
870}
871
872// ── Turn summary ─────────────────────────────────────────────────────
873
874/// Structured server-facing outcome metadata for a single turn.
875///
876/// Captures everything the server needs to durably persist about a
877/// turn's LLM-level provenance: thread/turn identity, provider and model
878/// identifiers, response ID and stop reason from the turn-closing LLM
879/// call, token usage, tool-call count, wall-clock duration, and the
880/// [`TurnOptions`] the caller requested.
881///
882/// # Why this exists
883///
884/// The original [`TurnOutcome`] only exposed token counts and turn
885/// numbers. Later server phases need:
886///
887/// - **Provider / model** — to correlate rows across provider rotations
888///   and to route audit streams by provider.
889/// - **Response ID** — to join durable turn rows against the raw
890///   provider response stored externally (observability pipelines,
891///   replay, support escalations).
892/// - **Stop reason** — to branch on `end_turn` vs `tool_use` vs
893///   `refusal` without re-parsing message history.
894/// - **Tool-call count** — to bill tool execution and detect runaway
895///   turns without walking the tool registry.
896/// - **Duration** — to feed SLO dashboards and auto-tune retry budgets.
897/// - **Tool runtime / strict durability flags** — to record which
898///   execution profile was in effect, so later replay can reconstruct
899///   the same decisions.
900///
901/// # Serialization
902///
903/// `TurnSummary` is fully serializable. Servers are expected to persist
904/// it alongside (or inside) their turn rows. Duration is exposed as
905/// `duration_ms` (milliseconds) to avoid a serde dance around
906/// [`std::time::Duration`].
907///
908/// # Authoritative vs convenience
909///
910/// Fields in `TurnSummary` are **authoritative** for server execution:
911/// they are produced by the same code path that writes the durable
912/// event store and are guaranteed to be consistent with the events the
913/// server observed on the wire. Convenience accessors on [`TurnOutcome`]
914/// (e.g. the legacy `input_tokens` / `output_tokens` fields on `Done`)
915/// are kept only so local callers do not have to break; new code should
916/// read from `summary` instead.
917#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
918pub struct TurnSummary {
919    /// Thread this turn belongs to.
920    ///
921    /// Duplicated from the call site so the summary is self-describing
922    /// when persisted alone (for durable audit rows).
923    pub thread_id: ThreadId,
924    /// Turn number that produced this outcome (1-indexed).
925    pub turn: usize,
926    /// Total number of turns executed in this run so far.
927    ///
928    /// For mid-run outcomes like `NeedsMoreTurns` / `PendingToolCalls`
929    /// this equals `turn`. For terminal outcomes (`Done`, `Refusal`,
930    /// `Cancelled`) it reflects the final total.
931    pub total_turns: u32,
932    /// Token usage for the LLM call(s) that produced this turn.
933    pub turn_usage: TokenUsage,
934    /// Cumulative token usage across every turn in this run so far.
935    pub total_usage: TokenUsage,
936    /// Provider / model provenance captured from the turn-closing
937    /// LLM call — identical shape to [`AuditProvenance`] so durable
938    /// audit rows stay consistent with turn rows.
939    pub provenance: AuditProvenance,
940    /// Provider response ID from the turn-closing LLM call.
941    ///
942    /// `None` when the provider did not return an ID or the turn
943    /// terminated before the LLM responded (e.g. cancelled before the
944    /// first call).
945    pub response_id: Option<String>,
946    /// Stop reason reported by the turn-closing LLM call.
947    ///
948    /// `None` when no response was produced for this turn (e.g. the
949    /// turn was cancelled before the LLM replied, or the turn was
950    /// resumed purely from external tool results without calling the
951    /// LLM again).
952    pub stop_reason: Option<crate::llm::StopReason>,
953    /// Number of tool calls the LLM requested in this turn.
954    ///
955    /// Zero for pure text turns.
956    pub tool_call_count: usize,
957    /// Wall-clock duration of this turn, in milliseconds.
958    ///
959    /// Measured from the start of `run_turn` to the moment the outcome
960    /// is returned. Clamped to `u64::MAX` on the unlikely overflow.
961    pub duration_ms: u64,
962    /// The [`ToolRuntime`] selected for this turn.
963    pub tool_runtime: ToolRuntime,
964    /// Whether strict durability was requested for this turn.
965    pub strict_durability: bool,
966}
967
968impl TurnSummary {
969    /// Construct an empty summary for a thread / provider / model.
970    ///
971    /// Used by the runtime as a starting point; it then updates
972    /// specific fields as the turn progresses. Tests and downstream
973    /// consumers should generally pattern-match on the outcome and
974    /// read fields from the populated summary rather than construct
975    /// one from scratch.
976    #[must_use]
977    pub fn new(
978        thread_id: ThreadId,
979        turn: usize,
980        provenance: AuditProvenance,
981        options: &TurnOptions,
982    ) -> Self {
983        Self {
984            thread_id,
985            turn,
986            total_turns: 0,
987            turn_usage: TokenUsage::default(),
988            total_usage: TokenUsage::default(),
989            provenance,
990            response_id: None,
991            stop_reason: None,
992            tool_call_count: 0,
993            duration_ms: 0,
994            tool_runtime: options.tool_runtime.clone(),
995            strict_durability: options.strict_durability,
996        }
997    }
998}
999
1000// ── Execution options ────────────────────────────────────────────────
1001
1002/// How tool calls should be handled during a turn.
1003#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1004#[serde(rename_all = "snake_case")]
1005pub enum ToolRuntime {
1006    /// Tools are executed inline by the SDK (the default local-agent behavior).
1007    #[default]
1008    Inline,
1009    /// Tool calls are returned to the caller for external execution.
1010    ///
1011    /// When set, `run_turn` yields [`TurnOutcome::PendingToolCalls`] instead
1012    /// of executing tools itself. The server is responsible for running
1013    /// tools and calling `run_turn` again.
1014    External,
1015}
1016
1017/// Options that control how a single `run_turn` invocation behaves.
1018///
1019/// The default is suitable for local/CLI usage (inline tools, no extra
1020/// durability). Server mode should set `tool_runtime: External` and
1021/// `strict_durability: true`.
1022#[derive(Debug, Clone, Default)]
1023pub struct TurnOptions {
1024    /// How tool calls should be handled.
1025    pub tool_runtime: ToolRuntime,
1026    /// When true, state is checkpointed at every critical boundary
1027    /// (before LLM call, after LLM response, after tool execution).
1028    /// Provides crash-safe server semantics at the cost of extra writes.
1029    pub strict_durability: bool,
1030}
1031
1032// ── RunOptions ───────────────────────────────────────────────────────
1033
1034/// Per-run trace metadata applied to every span emitted by the agent
1035/// loop.
1036///
1037/// Passed to [`run_with_options`](#method.run_with_options) /
1038/// [`run_turn_with_options`](#method.run_turn_with_options) /
1039/// [`run_persistent_with_options`](#method.run_persistent_with_options)
1040/// so a consumer can configure session / user / Langfuse trace
1041/// metadata once and have it land on every emitted span — without
1042/// writing manual span code or pre-installing baggage on the `OTel`
1043/// context.
1044///
1045/// The SDK applies the contents of `RunOptions` at the root
1046/// `invoke_agent` span:
1047///
1048/// * `session_id` / `user_id` — copied to W3C baggage so Langfuse
1049///   `session.id` / `user.id` filters fire on every child span (the
1050///   baggage propagation path lives in `agent_sdk::observability::baggage`).
1051/// * `trace_name` — set as `langfuse.trace.name`.
1052/// * `trace_tags` — set as `langfuse.trace.tags`.
1053/// * `trace_metadata` — each entry stamped under `langfuse.trace.metadata.<key>`.
1054/// * `release` — set as `langfuse.release`.
1055/// * `environment` — set as `langfuse.environment`.
1056/// * `trace_text_max_chars` — overrides the default ceiling
1057///   (`agent_sdk::observability::langfuse::DEFAULT_TRACE_TEXT_MAX_CHARS`)
1058///   for `langfuse.trace.input` / `langfuse.trace.output`.
1059///
1060/// The SDK also computes `langfuse.trace.input` from the supplied
1061/// [`AgentInput`] (after PII redaction) and
1062/// streams `langfuse.trace.output` as the agent emits text, tool, and
1063/// error events.
1064///
1065/// `RunOptions` is `Clone + Debug + Default`; it carries only display
1066/// strings and opaque metadata values (no secrets) so the standard
1067/// `Debug` derive is safe to expose in error contexts.
1068///
1069/// # Example
1070///
1071/// ```no_run
1072/// use agent_sdk_foundation::types::RunOptions;
1073/// use serde_json::json;
1074///
1075/// let opts = RunOptions {
1076///     session_id: Some("thread-42".to_string()),
1077///     user_id: Some("user-7".to_string()),
1078///     trace_name: Some("myapp.assistant.mobile".to_string()),
1079///     trace_tags: vec!["mobile.android".to_string()],
1080///     trace_metadata: json!({"version": "1.2.3"})
1081///         .as_object()
1082///         .cloned()
1083///         .unwrap_or_default(),
1084///     ..Default::default()
1085/// };
1086/// # let _ = opts;
1087/// ```
1088#[derive(Clone, Debug, Default)]
1089pub struct RunOptions {
1090    /// Langfuse `session.id` / W3C `session.id` baggage entry.
1091    pub session_id: Option<String>,
1092    /// Langfuse `user.id` / W3C `user.id` baggage entry.
1093    pub user_id: Option<String>,
1094    /// Display name of the trace in the Langfuse UI.
1095    pub trace_name: Option<String>,
1096    /// Free-form labels attached to the trace.
1097    pub trace_tags: Vec<String>,
1098    /// Trace-level metadata stamped as `langfuse.trace.metadata.<key>`.
1099    pub trace_metadata: serde_json::Map<String, serde_json::Value>,
1100    /// Release identifier for the trace's build.
1101    pub release: Option<String>,
1102    /// Langfuse environment slug (`prod`, `staging`, …).
1103    pub environment: Option<String>,
1104    /// Override the default character ceiling for trace-level free-text
1105    /// attributes. `None` falls back to
1106    /// `agent_sdk::observability::langfuse::DEFAULT_TRACE_TEXT_MAX_CHARS`.
1107    pub trace_text_max_chars: Option<usize>,
1108}
1109
1110#[cfg(test)]
1111mod tests {
1112    use super::*;
1113    use crate::llm::StopReason;
1114
1115    fn sample_summary() -> TurnSummary {
1116        TurnSummary {
1117            thread_id: ThreadId::from_string("t-summary"),
1118            turn: 2,
1119            total_turns: 2,
1120            turn_usage: TokenUsage {
1121                input_tokens: 100,
1122                output_tokens: 50,
1123                ..Default::default()
1124            },
1125            total_usage: TokenUsage {
1126                input_tokens: 200,
1127                output_tokens: 75,
1128                ..Default::default()
1129            },
1130            provenance: AuditProvenance::new("anthropic", "claude-sonnet-4-5-20250929"),
1131            response_id: Some("resp_123".into()),
1132            stop_reason: Some(StopReason::ToolUse),
1133            tool_call_count: 3,
1134            duration_ms: 1_234,
1135            tool_runtime: ToolRuntime::External,
1136            strict_durability: true,
1137        }
1138    }
1139
1140    #[test]
1141    fn turn_summary_round_trips_through_json() {
1142        let original = sample_summary();
1143        let json = serde_json::to_string(&original).expect("serialize");
1144        let recovered: TurnSummary = serde_json::from_str(&json).expect("deserialize");
1145        assert_eq!(recovered, original);
1146    }
1147
1148    #[test]
1149    fn turn_summary_json_has_expected_keys() {
1150        let summary = sample_summary();
1151        let value = serde_json::to_value(&summary).unwrap();
1152
1153        // The wire format is the durable server contract — assert
1154        // every field is present so accidental renames break this
1155        // test rather than silently corrupting persisted rows.
1156        for key in [
1157            "thread_id",
1158            "turn",
1159            "total_turns",
1160            "turn_usage",
1161            "total_usage",
1162            "provenance",
1163            "response_id",
1164            "stop_reason",
1165            "tool_call_count",
1166            "duration_ms",
1167            "tool_runtime",
1168            "strict_durability",
1169        ] {
1170            assert!(value.get(key).is_some(), "missing key {key}");
1171        }
1172
1173        // Snake-case tool-runtime variant is stable for server rows.
1174        assert_eq!(value["tool_runtime"], serde_json::json!("external"));
1175        // Snake-case stop-reason variant matches the provider wire format.
1176        assert_eq!(value["stop_reason"], serde_json::json!("tool_use"));
1177    }
1178
1179    #[test]
1180    fn turn_outcome_summary_accessor_works_for_every_variant() {
1181        let summary = sample_summary();
1182
1183        let outcomes = vec![
1184            TurnOutcome::NeedsMoreTurns {
1185                turn: 1,
1186                turn_usage: TokenUsage::default(),
1187                total_usage: TokenUsage::default(),
1188                summary: summary.clone(),
1189            },
1190            TurnOutcome::Done {
1191                total_turns: 1,
1192                total_usage: TokenUsage::default(),
1193                summary: summary.clone(),
1194            },
1195            TurnOutcome::Refusal {
1196                total_turns: 1,
1197                total_usage: TokenUsage::default(),
1198                summary: summary.clone(),
1199            },
1200            TurnOutcome::Cancelled {
1201                total_turns: 1,
1202                total_usage: TokenUsage::default(),
1203                summary: summary.clone(),
1204            },
1205        ];
1206
1207        for outcome in &outcomes {
1208            let got = outcome.summary().expect("summary must be present");
1209            assert_eq!(got, &summary);
1210        }
1211
1212        // Error variant has no summary.
1213        let error_outcome =
1214            TurnOutcome::Error(AgentError::new("boom", /* recoverable */ false));
1215        assert!(error_outcome.summary().is_none());
1216    }
1217
1218    #[test]
1219    fn empty_turn_summary_new_captures_options_and_provenance() {
1220        let opts = TurnOptions {
1221            tool_runtime: ToolRuntime::External,
1222            strict_durability: true,
1223        };
1224        let provenance = AuditProvenance::new("openai", "gpt-5");
1225        let summary =
1226            TurnSummary::new(ThreadId::from_string("t-new"), 7, provenance.clone(), &opts);
1227
1228        assert_eq!(summary.thread_id, ThreadId::from_string("t-new"));
1229        assert_eq!(summary.turn, 7);
1230        assert_eq!(summary.total_turns, 0);
1231        assert_eq!(summary.provenance, provenance);
1232        assert_eq!(summary.tool_runtime, ToolRuntime::External);
1233        assert!(summary.strict_durability);
1234        assert!(summary.response_id.is_none());
1235        assert!(summary.stop_reason.is_none());
1236        assert_eq!(summary.tool_call_count, 0);
1237        assert_eq!(summary.duration_ms, 0);
1238    }
1239
1240    #[test]
1241    fn stop_reason_as_str_matches_serde_representation() {
1242        // The durable stop_reason discriminant used in TurnSummary and
1243        // audit rows must match the serde wire format exactly.
1244        let cases = [
1245            (StopReason::EndTurn, "end_turn"),
1246            (StopReason::ToolUse, "tool_use"),
1247            (StopReason::MaxTokens, "max_tokens"),
1248            (StopReason::StopSequence, "stop_sequence"),
1249            (StopReason::Refusal, "refusal"),
1250            (
1251                StopReason::ModelContextWindowExceeded,
1252                "model_context_window_exceeded",
1253            ),
1254        ];
1255        for (variant, expected) in cases {
1256            assert_eq!(variant.as_str(), expected);
1257            let json = serde_json::to_value(variant).unwrap();
1258            assert_eq!(json, serde_json::json!(expected));
1259        }
1260    }
1261
1262    fn sample_continuation() -> AgentContinuation {
1263        let thread = ThreadId::from_string("t-continuation");
1264        AgentContinuation {
1265            thread_id: thread.clone(),
1266            turn: 4,
1267            total_usage: TokenUsage {
1268                input_tokens: 200,
1269                output_tokens: 80,
1270                ..Default::default()
1271            },
1272            turn_usage: TokenUsage {
1273                input_tokens: 50,
1274                output_tokens: 40,
1275                ..Default::default()
1276            },
1277            pending_tool_calls: vec![PendingToolCallInfo {
1278                id: "call_1".into(),
1279                name: "echo".into(),
1280                display_name: "Echo".into(),
1281                tier: ToolTier::Confirm,
1282                input: serde_json::json!({"message": "hi"}),
1283                effective_input: serde_json::json!({"message": "hi"}),
1284                listen_context: None,
1285            }],
1286            awaiting_index: 0,
1287            completed_results: Vec::new(),
1288            state: AgentState::new(thread),
1289            response_id: Some("resp_7914".into()),
1290            stop_reason: Some(StopReason::ToolUse),
1291            response_content: Vec::new(),
1292        }
1293    }
1294
1295    #[test]
1296    fn agent_continuation_round_trips_llm_metadata() {
1297        // `response_id` and `stop_reason` travel through
1298        // durable persistence so the resume-side `TurnSummary` reports
1299        // the same LLM metadata as the pre-pause summary for the same
1300        // turn. Guard the wire format so future renames break here
1301        // rather than silently dropping the fields.
1302        let original = sample_continuation();
1303        let json = serde_json::to_string(&original).expect("serialize");
1304
1305        let value: serde_json::Value = serde_json::from_str(&json).expect("to value");
1306        assert_eq!(value["response_id"], serde_json::json!("resp_7914"));
1307        assert_eq!(value["stop_reason"], serde_json::json!("tool_use"));
1308
1309        let recovered: AgentContinuation = serde_json::from_str(&json).expect("deserialize");
1310        assert_eq!(recovered.response_id.as_deref(), Some("resp_7914"));
1311        assert_eq!(recovered.stop_reason, Some(StopReason::ToolUse));
1312    }
1313
1314    #[test]
1315    fn agent_continuation_deserializes_legacy_payload_without_llm_metadata() {
1316        // Servers that persisted continuations before this contract
1317        // landed don't have `response_id` / `stop_reason` fields on
1318        // disk. Those
1319        // payloads must still deserialise so running servers do not
1320        // break on SDK upgrade — the fields default to `None`.
1321        let thread = ThreadId::from_string("t-legacy");
1322        let legacy_json = serde_json::json!({
1323            "thread_id": thread,
1324            "turn": 1,
1325            "total_usage": { "input_tokens": 10, "output_tokens": 5 },
1326            "turn_usage": { "input_tokens": 10, "output_tokens": 5 },
1327            "pending_tool_calls": [],
1328            "awaiting_index": 0,
1329            "completed_results": [],
1330            "state": AgentState::new(thread.clone()),
1331        });
1332
1333        let recovered: AgentContinuation =
1334            serde_json::from_value(legacy_json).expect("legacy payload deserialises");
1335        assert_eq!(recovered.thread_id, thread);
1336        assert_eq!(recovered.turn, 1);
1337        assert!(
1338            recovered.response_id.is_none(),
1339            "legacy payloads default to None",
1340        );
1341        assert!(
1342            recovered.stop_reason.is_none(),
1343            "legacy payloads default to None",
1344        );
1345    }
1346
1347    #[test]
1348    fn agent_continuation_omits_llm_metadata_when_none() {
1349        // `response_id` / `stop_reason` are `skip_serializing_if = None`
1350        // so that payloads where the provider did not return IDs stay
1351        // compact and look identical to the legacy wire format. This
1352        // protects any downstream consumer that matches exact keys.
1353        let thread = ThreadId::from_string("t-omit");
1354        let cont = AgentContinuation {
1355            thread_id: thread.clone(),
1356            turn: 1,
1357            total_usage: TokenUsage::default(),
1358            turn_usage: TokenUsage::default(),
1359            pending_tool_calls: Vec::new(),
1360            awaiting_index: 0,
1361            completed_results: Vec::new(),
1362            state: AgentState::new(thread),
1363            response_id: None,
1364            stop_reason: None,
1365            response_content: Vec::new(),
1366        };
1367        let value = serde_json::to_value(&cont).unwrap();
1368        assert!(value.get("response_id").is_none());
1369        assert!(value.get("stop_reason").is_none());
1370        assert!(value.get("response_content").is_none());
1371    }
1372}