mirror of
https://github.com/openai/codex.git
synced 2026-04-30 09:26:44 +00:00
## Summary - add the guardian reviewer flow for `on-request` approvals in command, patch, sandbox-retry, and managed-network approval paths - keep guardian behind `features.guardian_approval` instead of exposing a public `approval_policy = guardian` mode - route ordinary `OnRequest` approvals to the guardian subagent when the feature is enabled, without changing the public approval-mode surface ## Public model - public approval modes stay unchanged - guardian is enabled via `features.guardian_approval` - when that feature is on, `approval_policy = on-request` keeps the same approval boundaries but sends those approval requests to the guardian reviewer instead of the user - `/experimental` only persists the feature flag; it does not rewrite `approval_policy` - CLI and app-server no longer expose a separate `guardian` approval mode in this PR ## Guardian reviewer - the reviewer runs as a normal subagent and reuses the existing subagent/thread machinery - it is locked to a read-only sandbox and `approval_policy = never` - it does not inherit user/project exec-policy rules - it prefers `gpt-5.4` when the current provider exposes it, otherwise falls back to the parent turn's active model - it fail-closes on timeout, startup failure, malformed output, or any other review error - it currently auto-approves only when `risk_score < 80` ## Review context and policy - guardian mirrors `OnRequest` approval semantics rather than introducing a separate approval policy - explicit `require_escalated` requests follow the same approval surface as `OnRequest`; the difference is only who reviews them - managed-network allowlist misses that enter the approval flow are also reviewed by guardian - the review prompt includes bounded recent transcript history plus recent tool call/result evidence - transcript entries and planned-action strings are truncated with explicit `<guardian_truncated ... />` markers so large payloads stay bounded - apply-patch reviews include the full patch content (without duplicating the structured `changes` payload) - the guardian request layout is snapshot-tested using the same model-visible Responses request formatter used elsewhere in core ## Guardian network behavior - the guardian subagent inherits the parent session's managed-network allowlist when one exists, so it can use the same approved network surface while reviewing - exact session-scoped network approvals are copied into the guardian session with protocol/port scope preserved - those copied approvals are now seeded before the guardian's first turn is submitted, so inherited approvals are available during any immediate review-time checks ## Out of scope / follow-ups - the sandbox-permission validation split was pulled into a separate PR and is not part of this diff - a future follow-up can enable `serde_json` preserve-order in `codex-core` and then simplify the guardian action rendering further --------- Co-authored-by: Codex <noreply@openai.com>
600 lines
20 KiB
Rust
600 lines
20 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::config_types::ForcedLoginMethod;
|
|
use codex_protocol::config_types::ReasoningSummary;
|
|
use codex_protocol::config_types::SandboxMode;
|
|
use codex_protocol::config_types::ServiceTier;
|
|
use codex_protocol::config_types::Verbosity;
|
|
use codex_protocol::models::ResponseItem;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use codex_protocol::parse_command::ParsedCommand;
|
|
use codex_protocol::protocol::AskForApproval;
|
|
use codex_protocol::protocol::EventMsg;
|
|
use codex_protocol::protocol::FileChange;
|
|
use codex_protocol::protocol::ReviewDecision;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::TurnAbortReason;
|
|
use codex_protocol::user_input::ByteRange as CoreByteRange;
|
|
use codex_protocol::user_input::TextElement as CoreTextElement;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use ts_rs::TS;
|
|
use uuid::Uuid;
|
|
|
|
// Reuse shared types defined in `common.rs`.
|
|
use crate::protocol::common::AuthMode;
|
|
use crate::protocol::common::GitSha;
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InitializeParams {
|
|
pub client_info: ClientInfo,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub capabilities: Option<InitializeCapabilities>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ClientInfo {
|
|
pub name: String,
|
|
pub title: Option<String>,
|
|
pub version: String,
|
|
}
|
|
|
|
/// Client-declared capabilities negotiated during initialize.
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InitializeCapabilities {
|
|
/// Opt into receiving experimental API methods and fields.
|
|
#[serde(default)]
|
|
pub experimental_api: bool,
|
|
/// Exact notification method names that should be suppressed for this
|
|
/// connection (for example `codex/event/session_configured`).
|
|
#[ts(optional = nullable)]
|
|
pub opt_out_notification_methods: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InitializeResponse {
|
|
pub user_agent: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct NewConversationParams {
|
|
pub model: Option<String>,
|
|
pub model_provider: Option<String>,
|
|
pub profile: Option<String>,
|
|
pub cwd: Option<String>,
|
|
pub approval_policy: Option<AskForApproval>,
|
|
pub sandbox: Option<SandboxMode>,
|
|
pub config: Option<HashMap<String, serde_json::Value>>,
|
|
pub base_instructions: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub developer_instructions: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub compact_prompt: Option<String>,
|
|
pub include_apply_patch_tool: Option<bool>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct NewConversationResponse {
|
|
pub conversation_id: ThreadId,
|
|
pub model: String,
|
|
pub reasoning_effort: Option<ReasoningEffort>,
|
|
pub rollout_path: PathBuf,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ResumeConversationResponse {
|
|
pub conversation_id: ThreadId,
|
|
pub model: String,
|
|
pub initial_messages: Option<Vec<EventMsg>>,
|
|
pub rollout_path: PathBuf,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ForkConversationResponse {
|
|
pub conversation_id: ThreadId,
|
|
pub model: String,
|
|
pub initial_messages: Option<Vec<EventMsg>>,
|
|
pub rollout_path: PathBuf,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(untagged)]
|
|
pub enum GetConversationSummaryParams {
|
|
RolloutPath {
|
|
#[serde(rename = "rolloutPath")]
|
|
rollout_path: PathBuf,
|
|
},
|
|
ThreadId {
|
|
#[serde(rename = "conversationId")]
|
|
conversation_id: ThreadId,
|
|
},
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GetConversationSummaryResponse {
|
|
pub summary: ConversationSummary,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ListConversationsParams {
|
|
pub page_size: Option<usize>,
|
|
pub cursor: Option<String>,
|
|
pub model_providers: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ConversationSummary {
|
|
pub conversation_id: ThreadId,
|
|
pub path: PathBuf,
|
|
pub preview: String,
|
|
pub timestamp: Option<String>,
|
|
pub updated_at: Option<String>,
|
|
pub model_provider: String,
|
|
pub cwd: PathBuf,
|
|
pub cli_version: String,
|
|
pub source: SessionSource,
|
|
pub git_info: Option<ConversationGitInfo>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct ConversationGitInfo {
|
|
pub sha: Option<String>,
|
|
pub branch: Option<String>,
|
|
pub origin_url: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ListConversationsResponse {
|
|
pub items: Vec<ConversationSummary>,
|
|
pub next_cursor: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ResumeConversationParams {
|
|
pub path: Option<PathBuf>,
|
|
pub conversation_id: Option<ThreadId>,
|
|
pub history: Option<Vec<ResponseItem>>,
|
|
pub overrides: Option<NewConversationParams>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ForkConversationParams {
|
|
pub path: Option<PathBuf>,
|
|
pub conversation_id: Option<ThreadId>,
|
|
pub overrides: Option<NewConversationParams>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AddConversationSubscriptionResponse {
|
|
#[schemars(with = "String")]
|
|
pub subscription_id: Uuid,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ArchiveConversationParams {
|
|
pub conversation_id: ThreadId,
|
|
pub rollout_path: PathBuf,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ArchiveConversationResponse {}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RemoveConversationSubscriptionResponse {}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LoginApiKeyParams {
|
|
pub api_key: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LoginApiKeyResponse {}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LoginChatGptResponse {
|
|
#[schemars(with = "String")]
|
|
pub login_id: Uuid,
|
|
pub auth_url: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GitDiffToRemoteResponse {
|
|
pub sha: GitSha,
|
|
pub diff: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ApplyPatchApprovalParams {
|
|
pub conversation_id: ThreadId,
|
|
/// Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent]
|
|
/// and [codex_protocol::protocol::PatchApplyEndEvent].
|
|
pub call_id: String,
|
|
pub file_changes: HashMap<PathBuf, FileChange>,
|
|
/// Optional explanatory reason (e.g. request for extra write access).
|
|
pub reason: Option<String>,
|
|
/// When set, the agent is asking the user to allow writes under this root
|
|
/// for the remainder of the session (unclear if this is honored today).
|
|
pub grant_root: Option<PathBuf>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ApplyPatchApprovalResponse {
|
|
pub decision: ReviewDecision,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecCommandApprovalParams {
|
|
pub conversation_id: ThreadId,
|
|
/// Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent]
|
|
/// and [codex_protocol::protocol::ExecCommandEndEvent].
|
|
pub call_id: String,
|
|
/// Identifier for this specific approval callback.
|
|
pub approval_id: Option<String>,
|
|
pub command: Vec<String>,
|
|
pub cwd: PathBuf,
|
|
pub reason: Option<String>,
|
|
pub parsed_cmd: Vec<ParsedCommand>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
pub struct ExecCommandApprovalResponse {
|
|
pub decision: ReviewDecision,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CancelLoginChatGptParams {
|
|
#[schemars(with = "String")]
|
|
pub login_id: Uuid,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GitDiffToRemoteParams {
|
|
pub cwd: PathBuf,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CancelLoginChatGptResponse {}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LogoutChatGptParams {}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LogoutChatGptResponse {}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GetAuthStatusParams {
|
|
pub include_token: Option<bool>,
|
|
pub refresh_token: Option<bool>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecOneOffCommandParams {
|
|
pub command: Vec<String>,
|
|
pub timeout_ms: Option<u64>,
|
|
pub cwd: Option<PathBuf>,
|
|
pub sandbox_policy: Option<SandboxPolicy>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ExecOneOffCommandResponse {
|
|
pub exit_code: i32,
|
|
pub stdout: String,
|
|
pub stderr: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GetAuthStatusResponse {
|
|
pub auth_method: Option<AuthMode>,
|
|
pub auth_token: Option<String>,
|
|
pub requires_openai_auth: Option<bool>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GetUserAgentResponse {
|
|
pub user_agent: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct UserInfoResponse {
|
|
pub alleged_user_email: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GetUserSavedConfigResponse {
|
|
pub config: UserSavedConfig,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SetDefaultModelParams {
|
|
pub model: Option<String>,
|
|
pub reasoning_effort: Option<ReasoningEffort>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SetDefaultModelResponse {}
|
|
|
|
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct UserSavedConfig {
|
|
pub approval_policy: Option<AskForApproval>,
|
|
pub sandbox_mode: Option<SandboxMode>,
|
|
pub sandbox_settings: Option<SandboxSettings>,
|
|
pub forced_chatgpt_workspace_id: Option<String>,
|
|
pub forced_login_method: Option<ForcedLoginMethod>,
|
|
pub model: Option<String>,
|
|
pub model_reasoning_effort: Option<ReasoningEffort>,
|
|
pub model_reasoning_summary: Option<ReasoningSummary>,
|
|
pub model_verbosity: Option<Verbosity>,
|
|
pub tools: Option<Tools>,
|
|
pub profile: Option<String>,
|
|
pub profiles: HashMap<String, Profile>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Profile {
|
|
pub model: Option<String>,
|
|
pub model_provider: Option<String>,
|
|
pub approval_policy: Option<AskForApproval>,
|
|
pub model_reasoning_effort: Option<ReasoningEffort>,
|
|
pub model_reasoning_summary: Option<ReasoningSummary>,
|
|
pub model_verbosity: Option<Verbosity>,
|
|
pub chatgpt_base_url: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Tools {
|
|
pub web_search: Option<bool>,
|
|
pub view_image: Option<bool>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SandboxSettings {
|
|
#[serde(default)]
|
|
pub writable_roots: Vec<AbsolutePathBuf>,
|
|
pub network_access: Option<bool>,
|
|
pub exclude_tmpdir_env_var: Option<bool>,
|
|
pub exclude_slash_tmp: Option<bool>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SendUserMessageParams {
|
|
pub conversation_id: ThreadId,
|
|
pub items: Vec<InputItem>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SendUserTurnParams {
|
|
pub conversation_id: ThreadId,
|
|
pub items: Vec<InputItem>,
|
|
pub cwd: PathBuf,
|
|
pub approval_policy: AskForApproval,
|
|
pub sandbox_policy: SandboxPolicy,
|
|
pub model: String,
|
|
#[serde(
|
|
default,
|
|
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
|
serialize_with = "super::serde_helpers::serialize_double_option",
|
|
skip_serializing_if = "Option::is_none"
|
|
)]
|
|
pub service_tier: Option<Option<ServiceTier>>,
|
|
pub effort: Option<ReasoningEffort>,
|
|
pub summary: ReasoningSummary,
|
|
/// Optional JSON Schema used to constrain the final assistant message for
|
|
/// this turn.
|
|
pub output_schema: Option<serde_json::Value>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SendUserTurnResponse {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::PathBuf;
|
|
|
|
#[test]
|
|
fn send_user_turn_params_preserve_explicit_null_service_tier() {
|
|
let params = SendUserTurnParams {
|
|
conversation_id: ThreadId::new(),
|
|
items: vec![],
|
|
cwd: PathBuf::from("/tmp"),
|
|
approval_policy: AskForApproval::Never,
|
|
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
|
model: "gpt-4.1".to_string(),
|
|
service_tier: Some(None),
|
|
effort: None,
|
|
summary: ReasoningSummary::Auto,
|
|
output_schema: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_value(¶ms).expect("params should serialize");
|
|
assert_eq!(
|
|
serialized.get("serviceTier"),
|
|
Some(&serde_json::Value::Null)
|
|
);
|
|
|
|
let roundtrip: SendUserTurnParams =
|
|
serde_json::from_value(serialized).expect("params should deserialize");
|
|
assert_eq!(roundtrip.service_tier, Some(None));
|
|
|
|
let without_override = SendUserTurnParams {
|
|
conversation_id: ThreadId::new(),
|
|
items: vec![],
|
|
cwd: PathBuf::from("/tmp"),
|
|
approval_policy: AskForApproval::Never,
|
|
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
|
model: "gpt-4.1".to_string(),
|
|
service_tier: None,
|
|
effort: None,
|
|
summary: ReasoningSummary::Auto,
|
|
output_schema: None,
|
|
};
|
|
let serialized_without_override =
|
|
serde_json::to_value(&without_override).expect("params should serialize");
|
|
assert_eq!(serialized_without_override.get("serviceTier"), None);
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InterruptConversationParams {
|
|
pub conversation_id: ThreadId,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct InterruptConversationResponse {
|
|
pub abort_reason: TurnAbortReason,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SendUserMessageResponse {}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AddConversationListenerParams {
|
|
pub conversation_id: ThreadId,
|
|
#[serde(default)]
|
|
pub experimental_raw_events: bool,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RemoveConversationListenerParams {
|
|
#[schemars(with = "String")]
|
|
pub subscription_id: Uuid,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[serde(tag = "type", content = "data")]
|
|
pub enum InputItem {
|
|
Text {
|
|
text: String,
|
|
/// UI-defined spans within `text` used to render or persist special elements.
|
|
#[serde(default)]
|
|
text_elements: Vec<V1TextElement>,
|
|
},
|
|
Image {
|
|
image_url: String,
|
|
},
|
|
LocalImage {
|
|
path: PathBuf,
|
|
},
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(rename = "ByteRange")]
|
|
pub struct V1ByteRange {
|
|
/// Start byte offset (inclusive) within the UTF-8 text buffer.
|
|
pub start: usize,
|
|
/// End byte offset (exclusive) within the UTF-8 text buffer.
|
|
pub end: usize,
|
|
}
|
|
|
|
impl From<CoreByteRange> for V1ByteRange {
|
|
fn from(value: CoreByteRange) -> Self {
|
|
Self {
|
|
start: value.start,
|
|
end: value.end,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<V1ByteRange> for CoreByteRange {
|
|
fn from(value: V1ByteRange) -> Self {
|
|
Self {
|
|
start: value.start,
|
|
end: value.end,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(rename = "TextElement")]
|
|
pub struct V1TextElement {
|
|
/// Byte range in the parent `text` buffer that this element occupies.
|
|
pub byte_range: V1ByteRange,
|
|
/// Optional human-readable placeholder for the element, displayed in the UI.
|
|
pub placeholder: Option<String>,
|
|
}
|
|
|
|
impl From<CoreTextElement> for V1TextElement {
|
|
fn from(value: CoreTextElement) -> Self {
|
|
Self {
|
|
byte_range: value.byte_range.into(),
|
|
placeholder: value._placeholder_for_conversion_only().map(str::to_string),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<V1TextElement> for CoreTextElement {
|
|
fn from(value: V1TextElement) -> Self {
|
|
Self::new(value.byte_range.into(), value.placeholder)
|
|
}
|
|
}
|
|
|
|
impl InputItem {
|
|
pub fn text_char_count(&self) -> usize {
|
|
match self {
|
|
InputItem::Text { text, .. } => text.chars().count(),
|
|
InputItem::Image { .. } | InputItem::LocalImage { .. } => 0,
|
|
}
|
|
}
|
|
}
|