Skip to main content

codex_runtime/runtime/api/
models.rs

1use std::time::Duration;
2
3use serde_json::Value;
4use thiserror::Error;
5use tokio::sync::broadcast::Receiver as BroadcastReceiver;
6use tokio::time::Instant;
7
8use crate::plugin::{BlockReason, HookPhase};
9use crate::runtime::core::Runtime;
10use crate::runtime::errors::{RpcError, RuntimeError};
11use crate::runtime::events::{
12    AgentMessageDeltaNotification, Envelope, TurnCancelledNotification, TurnCompletedNotification,
13    TurnFailedNotification, TurnInterruptedNotification,
14};
15use crate::runtime::hooks::RuntimeHookConfig;
16use crate::runtime::turn_lifecycle::LaggedTurnTerminal;
17use crate::runtime::turn_output::TurnStreamCollector;
18
19use super::{
20    flow::HookExecutionState, turn_error::PromptTurnErrorSignal, ApprovalPolicy, PromptAttachment,
21    ReasoningEffort, SandboxPolicy, ThreadId, TurnId, DEFAULT_REASONING_EFFORT,
22};
23
24#[derive(Clone, Debug, PartialEq)]
25pub struct PromptRunParams {
26    pub cwd: String,
27    pub prompt: String,
28    pub model: Option<String>,
29    pub effort: Option<ReasoningEffort>,
30    pub approval_policy: ApprovalPolicy,
31    pub sandbox_policy: SandboxPolicy,
32    /// Explicit opt-in gate for privileged sandbox usage (SEC-004).
33    /// Default stays false to preserve safe-by-default posture.
34    pub privileged_escalation_approved: bool,
35    pub attachments: Vec<PromptAttachment>,
36    pub timeout: Duration,
37    pub output_schema: Option<Value>,
38}
39
40impl PromptRunParams {
41    /// Create prompt-run params with safe defaults.
42    /// Allocation: two String allocations for cwd/prompt. Complexity: O(n), n = input lengths.
43    pub fn new(cwd: impl Into<String>, prompt: impl Into<String>) -> Self {
44        Self {
45            cwd: cwd.into(),
46            prompt: prompt.into(),
47            model: None,
48            effort: Some(DEFAULT_REASONING_EFFORT),
49            approval_policy: ApprovalPolicy::Never,
50            sandbox_policy: SandboxPolicy::Preset(super::SandboxPreset::ReadOnly),
51            privileged_escalation_approved: false,
52            attachments: Vec::new(),
53            timeout: Duration::from_secs(120),
54            output_schema: None,
55        }
56    }
57
58    /// Set explicit model override.
59    /// Allocation: one String. Complexity: O(model length).
60    pub fn with_model(mut self, model: impl Into<String>) -> Self {
61        self.model = Some(model.into());
62        self
63    }
64
65    /// Set explicit reasoning effort.
66    /// Allocation: none. Complexity: O(1).
67    pub fn with_effort(mut self, effort: ReasoningEffort) -> Self {
68        self.effort = Some(effort);
69        self
70    }
71
72    /// Set approval policy override.
73    /// Allocation: none. Complexity: O(1).
74    pub fn with_approval_policy(mut self, approval_policy: ApprovalPolicy) -> Self {
75        self.approval_policy = approval_policy;
76        self
77    }
78
79    /// Set sandbox policy override.
80    /// Allocation: depends on sandbox payload move/clone at callsite. Complexity: O(1).
81    pub fn with_sandbox_policy(mut self, sandbox_policy: SandboxPolicy) -> Self {
82        self.sandbox_policy = sandbox_policy;
83        self
84    }
85
86    /// Explicitly approve privileged sandbox escalation for this run.
87    /// Callers are expected to set approval policy + scope alongside this flag.
88    pub fn allow_privileged_escalation(mut self) -> Self {
89        self.privileged_escalation_approved = true;
90        self
91    }
92
93    /// Set prompt timeout.
94    /// Allocation: none. Complexity: O(1).
95    pub fn with_timeout(mut self, timeout: Duration) -> Self {
96        self.timeout = timeout;
97        self
98    }
99
100    /// Set one optional JSON Schema for the final assistant message.
101    pub fn with_output_schema(mut self, output_schema: Value) -> Self {
102        self.output_schema = Some(output_schema);
103        self
104    }
105
106    /// Add one generic attachment.
107    /// Allocation: amortized O(1) push. Complexity: O(1).
108    pub fn with_attachment(mut self, attachment: PromptAttachment) -> Self {
109        self.attachments.push(attachment);
110        self
111    }
112
113    /// Add one `@path` attachment.
114    /// Allocation: one String. Complexity: O(path length).
115    pub fn attach_path(self, path: impl Into<String>) -> Self {
116        self.with_attachment(PromptAttachment::AtPath {
117            path: path.into(),
118            placeholder: None,
119        })
120    }
121
122    /// Add one `@path` attachment with placeholder.
123    /// Allocation: two Strings. Complexity: O(path + placeholder length).
124    pub fn attach_path_with_placeholder(
125        self,
126        path: impl Into<String>,
127        placeholder: impl Into<String>,
128    ) -> Self {
129        self.with_attachment(PromptAttachment::AtPath {
130            path: path.into(),
131            placeholder: Some(placeholder.into()),
132        })
133    }
134
135    /// Add one remote image attachment.
136    /// Allocation: one String. Complexity: O(url length).
137    pub fn attach_image_url(self, url: impl Into<String>) -> Self {
138        self.with_attachment(PromptAttachment::ImageUrl { url: url.into() })
139    }
140
141    /// Add one local image attachment.
142    /// Allocation: one String. Complexity: O(path length).
143    pub fn attach_local_image(self, path: impl Into<String>) -> Self {
144        self.with_attachment(PromptAttachment::LocalImage { path: path.into() })
145    }
146
147    /// Add one skill attachment.
148    /// Allocation: two Strings. Complexity: O(name + path length).
149    pub fn attach_skill(self, name: impl Into<String>, path: impl Into<String>) -> Self {
150        self.with_attachment(PromptAttachment::Skill {
151            name: name.into(),
152            path: path.into(),
153        })
154    }
155}
156
157#[derive(Clone, Debug, PartialEq, Eq)]
158pub struct PromptRunResult {
159    pub thread_id: ThreadId,
160    pub turn_id: TurnId,
161    pub assistant_text: String,
162}
163
164#[derive(Clone, Debug, PartialEq)]
165pub enum PromptRunStreamEvent {
166    AgentMessageDelta(AgentMessageDeltaNotification),
167    TurnCompleted(TurnCompletedNotification),
168    TurnFailed(TurnFailedNotification),
169    TurnInterrupted(TurnInterruptedNotification),
170    TurnCancelled(TurnCancelledNotification),
171}
172
173pub(crate) struct PromptRunStreamState {
174    pub(crate) last_turn_error: Option<PromptTurnErrorSignal>,
175    pub(crate) lagged_terminal: Option<LaggedTurnTerminal>,
176    pub(crate) final_result: Option<Result<PromptRunResult, PromptRunError>>,
177}
178
179pub(crate) struct PromptStreamCleanupState {
180    pub(crate) run_cwd: String,
181    pub(crate) run_model: Option<String>,
182    pub(crate) scoped_hooks: Option<RuntimeHookConfig>,
183    pub(crate) hook_state: Option<HookExecutionState>,
184    pub(crate) cleaned_up: bool,
185}
186
187pub struct PromptRunStream {
188    pub(crate) runtime: Runtime,
189    pub(crate) thread_id: ThreadId,
190    pub(crate) turn_id: TurnId,
191    pub(crate) live_rx: BroadcastReceiver<Envelope>,
192    pub(crate) stream: TurnStreamCollector,
193    pub(crate) state: PromptRunStreamState,
194    pub(crate) deadline: Instant,
195    pub(crate) timeout: Duration,
196    pub(crate) cleanup: PromptStreamCleanupState,
197}
198
199#[derive(Clone, Copy, Debug, PartialEq, Eq)]
200pub enum PromptTurnTerminalState {
201    Failed,
202    CompletedWithoutAssistantText,
203}
204
205#[derive(Clone, Debug, PartialEq, Eq)]
206pub struct PromptTurnFailure {
207    pub terminal_state: PromptTurnTerminalState,
208    pub source_method: String,
209    pub code: Option<i64>,
210    pub message: String,
211}
212
213impl std::fmt::Display for PromptTurnFailure {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        let terminal = match self.terminal_state {
216            PromptTurnTerminalState::Failed => "failed",
217            PromptTurnTerminalState::CompletedWithoutAssistantText => {
218                "completed_without_assistant_text"
219            }
220        };
221        if let Some(code) = self.code {
222            write!(
223                f,
224                "terminal={terminal} source_method={} code={code} message={}",
225                self.source_method, self.message
226            )
227        } else {
228            write!(
229                f,
230                "terminal={terminal} source_method={} message={}",
231                self.source_method, self.message
232            )
233        }
234    }
235}
236
237#[derive(Clone, Debug, Error, PartialEq, Eq)]
238pub enum PromptRunError {
239    #[error("rpc error: {0}")]
240    Rpc(#[from] RpcError),
241    #[error("runtime error: {0}")]
242    Runtime(#[from] RuntimeError),
243    #[error("turn failed: {0}")]
244    TurnFailedWithContext(PromptTurnFailure),
245    #[error("turn failed")]
246    TurnFailed,
247    #[error("turn interrupted")]
248    TurnInterrupted,
249    #[error("turn timed out after {0:?}")]
250    Timeout(Duration),
251    #[error("turn completed without assistant text: {0}")]
252    TurnCompletedWithoutAssistantText(PromptTurnFailure),
253    #[error("assistant text is empty")]
254    EmptyAssistantText,
255    #[error("attachment not found: {0}")]
256    AttachmentNotFound(String),
257    /// A pre-hook explicitly blocked execution before any RPC was sent.
258    #[error("blocked by hook '{hook_name}' at {phase:?}: {message}")]
259    BlockedByHook {
260        hook_name: String,
261        phase: HookPhase,
262        message: String,
263    },
264}
265
266impl PromptRunError {
267    /// Convert a raw [`BlockReason`] into [`PromptRunError::BlockedByHook`].
268    /// Allocation: clones two Strings.
269    pub(crate) fn from_block(r: BlockReason) -> Self {
270        Self::BlockedByHook {
271            hook_name: r.hook_name,
272            phase: r.phase,
273            message: r.message,
274        }
275    }
276}