Skip to main content

codex_runtime/runtime/api/
models.rs

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