use std::collections::HashMap;
use std::ffi::OsStr;
use std::fmt;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use crate::product::git_utils::GhostCommit;
use crate::product::mcp_types::CallToolResult;
use crate::product::mcp_types::RequestId;
use crate::product::mcp_types::Resource as McpResource;
use crate::product::mcp_types::ResourceTemplate as McpResourceTemplate;
use crate::product::mcp_types::Tool as McpTool;
use crate::product::protocol::ThreadId;
use crate::product::protocol::approvals::ElicitationRequestEvent;
use crate::product::protocol::config_types::Identity;
use crate::product::protocol::config_types::IdentityKind;
use crate::product::protocol::config_types::Personality;
use crate::product::protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use crate::product::protocol::config_types::WindowsSandboxLevel;
use crate::product::protocol::custom_prompts::CustomPrompt;
use crate::product::protocol::dynamic_tools::DynamicToolCallRequest;
use crate::product::protocol::dynamic_tools::DynamicToolResponse;
use crate::product::protocol::dynamic_tools::DynamicToolSpec;
use crate::product::protocol::items::TurnItem;
use crate::product::protocol::memory_citation::MemoryCitation;
use crate::product::protocol::message_history::HistoryEntry;
use crate::product::protocol::models::BaseInstructions;
use crate::product::protocol::models::ContentItem;
use crate::product::protocol::models::TranscriptItem;
use crate::product::protocol::models::WebSearchAction;
use crate::product::protocol::num_format::format_with_separators;
use crate::product::protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use crate::product::protocol::parse_command::ParsedCommand;
use crate::product::protocol::plan_tool::UpdatePlanArgs;
use crate::product::protocol::request_user_input::RequestUserInputResponse;
use crate::product::protocol::user_input::UserInput;
use crate::product::protocol::workflow::WorkflowRolloutItem;
use crate::product::protocol::workflow::WorkflowUpdateEvent;
use crate::product::utils_absolute_path::AbsolutePathBuf;
pub use lha_llm::types::TokenUsage;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
use serde_with::serde_as;
use strum_macros::Display;
use tracing::error;
use ts_rs::TS;
pub use crate::product::protocol::approvals::ApplyPatchApprovalRequestEvent;
pub use crate::product::protocol::approvals::ElicitationAction;
pub use crate::product::protocol::approvals::ExecApprovalRequestEvent;
pub use crate::product::protocol::approvals::ExecPolicyAmendment;
pub use crate::product::protocol::request_user_input::RequestUserInputEvent;
pub const USER_INSTRUCTIONS_OPEN_TAG: &str = "<user_instructions>";
pub const USER_INSTRUCTIONS_CLOSE_TAG: &str = "</user_instructions>";
pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = "<environment_context>";
pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = "</environment_context>";
pub const IDENTITY_OPEN_TAG: &str = "<identity>";
pub const IDENTITY_CLOSE_TAG: &str = "</identity>";
pub const USER_MESSAGE_BEGIN: &str = "## My request for LHA:";
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct Submission {
pub id: String,
pub op: Op,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
pub struct McpServerRefreshConfig {
pub mcp_servers: Value,
pub mcp_oauth_credentials_store_mode: Value,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant)]
#[non_exhaustive]
pub enum Op {
Interrupt,
CleanBackgroundTerminals,
UserInput {
items: Vec<UserInput>,
#[serde(skip_serializing_if = "Option::is_none")]
final_output_json_schema: Option<Value>,
},
UserTurn {
items: Vec<UserInput>,
cwd: PathBuf,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
model: String,
#[serde(skip_serializing_if = "Option::is_none")]
effort: Option<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
final_output_json_schema: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
identity: Option<Identity>,
#[serde(skip_serializing_if = "Option::is_none")]
personality: Option<Personality>,
#[serde(skip_serializing_if = "Option::is_none")]
tui_buddy: Option<BuddyTurnSnapshot>,
},
OverrideTurnContext {
#[serde(skip_serializing_if = "Option::is_none")]
cwd: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
approval_policy: Option<AskForApproval>,
#[serde(skip_serializing_if = "Option::is_none")]
sandbox_policy: Option<SandboxPolicy>,
#[serde(skip_serializing_if = "Option::is_none")]
windows_sandbox_level: Option<WindowsSandboxLevel>,
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
effort: Option<Option<ReasoningEffortConfig>>,
#[serde(skip_serializing_if = "Option::is_none")]
summary: Option<ReasoningSummaryConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
identity: Option<Identity>,
#[serde(skip_serializing_if = "Option::is_none")]
personality: Option<Personality>,
},
ExecApproval {
id: String,
decision: ReviewDecision,
},
PatchApproval {
id: String,
decision: ReviewDecision,
},
ResolveElicitation {
server_name: String,
request_id: RequestId,
decision: ElicitationAction,
},
#[serde(rename = "user_input_answer", alias = "request_user_input_response")]
UserInputAnswer {
id: String,
response: RequestUserInputResponse,
},
DynamicToolResponse {
id: String,
response: DynamicToolResponse,
},
AddToHistory {
text: String,
},
GetHistoryEntryRequest { offset: usize, log_id: u64 },
ListMcpTools {
#[serde(default, skip_serializing_if = "Option::is_none")]
request_id: Option<u64>,
},
RefreshMcpServers { config: McpServerRefreshConfig },
ListCustomPrompts,
ListSkills {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
cwds: Vec<PathBuf>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
force_reload: bool,
},
Compact,
SetThreadName { name: String },
Undo,
ThreadRollback { num_turns: u32 },
Review { review_request: ReviewRequest },
Shutdown,
RunUserShellCommand {
command: String,
},
ListModels,
ThreadGoalGet,
ThreadGoalSetObjective {
objective: String,
mode: ThreadGoalSetMode,
},
ThreadGoalSetStatus { status: ThreadGoalStatus },
ThreadGoalClear,
ThreadGoalStartFromProposedPlan { plan_text: String },
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub struct BuddyTurnSnapshot {
pub enabled: bool,
pub muted: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub species: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub eye: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hat: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rarity: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shiny: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub personality: Option<String>,
pub observer_enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observer_model: Option<String>,
pub observer_max_reaction_chars: usize,
}
#[derive(
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Display,
JsonSchema,
TS,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum AskForApproval {
#[serde(rename = "untrusted")]
#[strum(serialize = "untrusted")]
UnlessTrusted,
OnFailure,
#[default]
OnRequest,
Never,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum NetworkAccess {
#[default]
Restricted,
Enabled,
}
impl NetworkAccess {
pub fn is_enabled(self) -> bool {
matches!(self, NetworkAccess::Enabled)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)]
#[strum(serialize_all = "kebab-case")]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum SandboxPolicy {
#[serde(rename = "danger-full-access")]
DangerFullAccess,
#[serde(rename = "read-only")]
ReadOnly,
#[serde(rename = "external-sandbox")]
ExternalSandbox {
#[serde(default)]
network_access: NetworkAccess,
},
#[serde(rename = "workspace-write")]
WorkspaceWrite {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
writable_roots: Vec<AbsolutePathBuf>,
#[serde(default)]
network_access: bool,
#[serde(default)]
exclude_tmpdir_env_var: bool,
#[serde(default)]
exclude_slash_tmp: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
pub struct WritableRoot {
pub root: AbsolutePathBuf,
pub read_only_subpaths: Vec<AbsolutePathBuf>,
}
impl WritableRoot {
pub fn is_path_writable(&self, path: &Path) -> bool {
if !path.starts_with(&self.root) {
return false;
}
for subpath in &self.read_only_subpaths {
if path.starts_with(subpath) {
return false;
}
}
true
}
}
impl FromStr for SandboxPolicy {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
impl SandboxPolicy {
pub fn new_read_only_policy() -> Self {
SandboxPolicy::ReadOnly
}
pub fn new_workspace_write_policy() -> Self {
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}
}
pub fn has_full_disk_read_access(&self) -> bool {
true
}
pub fn has_full_disk_write_access(&self) -> bool {
match self {
SandboxPolicy::DangerFullAccess => true,
SandboxPolicy::ExternalSandbox { .. } => true,
SandboxPolicy::ReadOnly => false,
SandboxPolicy::WorkspaceWrite { .. } => false,
}
}
pub fn has_full_network_access(&self) -> bool {
match self {
SandboxPolicy::DangerFullAccess => true,
SandboxPolicy::ExternalSandbox { network_access } => network_access.is_enabled(),
SandboxPolicy::ReadOnly => false,
SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
}
}
pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
match self {
SandboxPolicy::DangerFullAccess => Vec::new(),
SandboxPolicy::ExternalSandbox { .. } => Vec::new(),
SandboxPolicy::ReadOnly => Vec::new(),
SandboxPolicy::WorkspaceWrite {
writable_roots,
exclude_tmpdir_env_var,
exclude_slash_tmp,
network_access: _,
} => {
let mut roots: Vec<AbsolutePathBuf> = writable_roots.clone();
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd);
match cwd_absolute {
Ok(cwd) => {
roots.push(cwd);
}
Err(e) => {
error!(
"Ignoring invalid cwd {:?} for sandbox writable root: {}",
cwd, e
);
}
}
if cfg!(unix) && !exclude_slash_tmp {
#[allow(clippy::expect_used)]
let slash_tmp =
AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute");
if slash_tmp.as_path().is_dir() {
roots.push(slash_tmp);
}
}
if !exclude_tmpdir_env_var
&& let Some(tmpdir) = std::env::var_os("TMPDIR")
&& !tmpdir.is_empty()
{
match AbsolutePathBuf::from_absolute_path(PathBuf::from(&tmpdir)) {
Ok(tmpdir_path) => {
roots.push(tmpdir_path);
}
Err(e) => {
error!(
"Ignoring invalid TMPDIR value {tmpdir:?} for sandbox writable root: {e}",
);
}
}
}
roots
.into_iter()
.map(|writable_root| {
let mut subpaths: Vec<AbsolutePathBuf> = Vec::new();
#[allow(clippy::expect_used)]
let top_level_git = writable_root
.join(".git")
.expect(".git is a valid relative path");
let top_level_git_is_file = top_level_git.as_path().is_file();
let top_level_git_is_dir = top_level_git.as_path().is_dir();
if top_level_git_is_dir || top_level_git_is_file {
if top_level_git_is_file
&& is_git_pointer_file(&top_level_git)
&& let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
&& !subpaths
.iter()
.any(|subpath| subpath.as_path() == gitdir.as_path())
{
subpaths.push(gitdir);
}
subpaths.push(top_level_git);
}
#[allow(clippy::expect_used)]
let top_level_lha = writable_root
.join(".lha")
.expect(".lha is a valid relative path");
if top_level_lha.as_path().is_dir() {
subpaths.push(top_level_lha);
}
WritableRoot {
root: writable_root,
read_only_subpaths: subpaths,
}
})
.collect()
}
}
}
}
fn is_git_pointer_file(path: &AbsolutePathBuf) -> bool {
path.as_path().is_file() && path.as_path().file_name() == Some(OsStr::new(".git"))
}
fn resolve_gitdir_from_file(dot_git: &AbsolutePathBuf) -> Option<AbsolutePathBuf> {
let contents = match std::fs::read_to_string(dot_git.as_path()) {
Ok(contents) => contents,
Err(err) => {
error!(
"Failed to read {path} for gitdir pointer: {err}",
path = dot_git.as_path().display()
);
return None;
}
};
let trimmed = contents.trim();
let (_, gitdir_raw) = match trimmed.split_once(':') {
Some(parts) => parts,
None => {
error!(
"Expected {path} to contain a gitdir pointer, but it did not match `gitdir: <path>`.",
path = dot_git.as_path().display()
);
return None;
}
};
let gitdir_raw = gitdir_raw.trim();
if gitdir_raw.is_empty() {
error!(
"Expected {path} to contain a gitdir pointer, but it was empty.",
path = dot_git.as_path().display()
);
return None;
}
let base = match dot_git.as_path().parent() {
Some(base) => base,
None => {
error!(
"Unable to resolve parent directory for {path}.",
path = dot_git.as_path().display()
);
return None;
}
};
let gitdir_path = match AbsolutePathBuf::resolve_path_against_base(gitdir_raw, base) {
Ok(path) => path,
Err(err) => {
error!(
"Failed to resolve gitdir path {gitdir_raw} from {path}: {err}",
path = dot_git.as_path().display()
);
return None;
}
};
if !gitdir_path.as_path().exists() {
error!(
"Resolved gitdir path {path} does not exist.",
path = gitdir_path.as_path().display()
);
return None;
}
Some(gitdir_path)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Event {
pub id: String,
pub msg: EventMsg,
}
#[derive(Debug, Clone, Deserialize, Serialize, Display, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type")]
#[strum(serialize_all = "snake_case")]
pub enum EventMsg {
Error(ErrorEvent),
Warning(WarningEvent),
ContextCompacted(ContextCompactedEvent),
ThreadRolledBack(ThreadRolledBackEvent),
#[serde(rename = "task_started", alias = "turn_started")]
TurnStarted(TurnStartedEvent),
#[serde(rename = "task_complete", alias = "turn_complete")]
TurnComplete(TurnCompleteEvent),
TokenCount(TokenCountEvent),
InputSlimming(InputSlimmingEvent),
BuddyReaction(BuddyReactionEvent),
AgentMessage(AgentMessageEvent),
UserMessage(UserMessageEvent),
AgentMessageDelta(AgentMessageDeltaEvent),
AgentReasoning(AgentReasoningEvent),
AgentReasoningDelta(AgentReasoningDeltaEvent),
AgentReasoningRawContent(AgentReasoningRawContentEvent),
AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent),
AgentReasoningSectionBreak(AgentReasoningSectionBreakEvent),
SessionConfigured(SessionConfiguredEvent),
ThreadNameUpdated(ThreadNameUpdatedEvent),
ThreadGoalUpdated(ThreadGoalUpdatedEvent),
ThreadGoalCleared(ThreadGoalClearedEvent),
ThreadGoalSnapshot(ThreadGoalSnapshotEvent),
ThreadGoalReplaceConfirmationRequired(ThreadGoalReplaceConfirmationRequiredEvent),
McpStartupUpdate(McpStartupUpdateEvent),
McpStartupComplete(McpStartupCompleteEvent),
McpToolCallBegin(McpToolCallBeginEvent),
McpToolCallEnd(McpToolCallEndEvent),
WebSearchBegin(WebSearchBeginEvent),
WebSearchEnd(WebSearchEndEvent),
ExecCommandBegin(ExecCommandBeginEvent),
ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
TerminalInteraction(TerminalInteractionEvent),
ExecCommandEnd(ExecCommandEndEvent),
ViewImageToolCall(ViewImageToolCallEvent),
ExecApprovalRequest(ExecApprovalRequestEvent),
RequestUserInput(RequestUserInputEvent),
DynamicToolCallRequest(DynamicToolCallRequest),
WorkflowUpdate(WorkflowUpdateEvent),
ElicitationRequest(ElicitationRequestEvent),
ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
DeprecationNotice(DeprecationNoticeEvent),
BackgroundEvent(BackgroundEventEvent),
UndoStarted(UndoStartedEvent),
UndoCompleted(UndoCompletedEvent),
StreamError(StreamErrorEvent),
PatchApplyBegin(PatchApplyBeginEvent),
PatchApplyEnd(PatchApplyEndEvent),
TurnDiff(TurnDiffEvent),
GetHistoryEntryResponse(GetHistoryEntryResponseEvent),
McpListToolsResponse(McpListToolsResponseEvent),
ListCustomPromptsResponse(ListCustomPromptsResponseEvent),
ListSkillsResponse(ListSkillsResponseEvent),
SkillsUpdateAvailable,
AgentJobStatus(AgentJobStatusEvent),
PlanUpdate(UpdatePlanArgs),
TurnAborted(TurnAbortedEvent),
ShutdownComplete,
EnteredReviewMode(ReviewRequest),
ExitedReviewMode(ExitedReviewModeEvent),
#[serde(rename = "raw_transcript_item")]
RawTranscriptItem(RawTranscriptItemEvent),
ItemStarted(ItemStartedEvent),
ItemCompleted(ItemCompletedEvent),
AgentMessageContentDelta(AgentMessageContentDeltaEvent),
PlanDelta(PlanDeltaEvent),
ReasoningContentDelta(ReasoningContentDeltaEvent),
ReasoningRawContentDelta(ReasoningRawContentDeltaEvent),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum CodexErrorInfo {
ContextWindowExceeded,
UsageLimitExceeded,
ModelCap {
model: String,
reset_after_seconds: Option<u64>,
},
HttpConnectionFailed {
http_status_code: Option<u16>,
},
ResponseStreamConnectionFailed {
http_status_code: Option<u16>,
},
InternalServerError,
Unauthorized,
BadRequest,
SandboxError,
ResponseStreamDisconnected {
http_status_code: Option<u16>,
},
ResponseTooManyFailedAttempts {
http_status_code: Option<u16>,
},
ThreadRollbackFailed,
Other,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
pub struct RawTranscriptItemEvent {
pub item: TranscriptItem,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
pub struct ItemStartedEvent {
pub thread_id: ThreadId,
pub turn_id: String,
pub item: TurnItem,
}
impl HasLegacyEvent for ItemStartedEvent {
fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
match &self.item {
TurnItem::WebSearch(item) => vec![EventMsg::WebSearchBegin(WebSearchBeginEvent {
call_id: item.id.clone(),
})],
_ => Vec::new(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
pub struct ItemCompletedEvent {
pub thread_id: ThreadId,
pub turn_id: String,
pub item: TurnItem,
}
pub trait HasLegacyEvent {
fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg>;
}
impl HasLegacyEvent for ItemCompletedEvent {
fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
self.item.as_legacy_events(show_raw_agent_reasoning)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
pub struct AgentMessageContentDeltaEvent {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
pub delta: String,
}
impl HasLegacyEvent for AgentMessageContentDeltaEvent {
fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
vec![EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
delta: self.delta.clone(),
})]
}
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
pub struct PlanDeltaEvent {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
pub delta: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
pub struct ReasoningContentDeltaEvent {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
pub delta: String,
#[serde(default)]
pub summary_index: i64,
}
impl HasLegacyEvent for ReasoningContentDeltaEvent {
fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
vec![EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent {
delta: self.delta.clone(),
})]
}
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
pub struct ReasoningRawContentDeltaEvent {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
pub delta: String,
#[serde(default)]
pub content_index: i64,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, TS, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum AgentJobKind {
Explorer,
Reviewer,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, TS, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum AgentJobDisplayStatus {
Running,
Completed,
Failed,
Cancelled,
TimedOut,
NotFound,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS, JsonSchema)]
pub struct AgentJobStatusEvent {
pub job_id: String,
pub agent_type: AgentJobKind,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub status: AgentJobDisplayStatus,
#[ts(optional)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl HasLegacyEvent for ReasoningRawContentDeltaEvent {
fn as_legacy_events(&self, _: bool) -> Vec<EventMsg> {
vec![EventMsg::AgentReasoningRawContentDelta(
AgentReasoningRawContentDeltaEvent {
delta: self.delta.clone(),
},
)]
}
}
impl HasLegacyEvent for EventMsg {
fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
match self {
EventMsg::ItemStarted(event) => event.as_legacy_events(show_raw_agent_reasoning),
EventMsg::ItemCompleted(event) => event.as_legacy_events(show_raw_agent_reasoning),
EventMsg::AgentMessageContentDelta(event) => {
event.as_legacy_events(show_raw_agent_reasoning)
}
EventMsg::ReasoningContentDelta(event) => {
event.as_legacy_events(show_raw_agent_reasoning)
}
EventMsg::ReasoningRawContentDelta(event) => {
event.as_legacy_events(show_raw_agent_reasoning)
}
_ => Vec::new(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ExitedReviewModeEvent {
pub review_output: Option<ReviewOutputEvent>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ErrorEvent {
pub message: String,
#[serde(default)]
pub codex_error_info: Option<CodexErrorInfo>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct WarningEvent {
pub message: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ContextCompactedEvent;
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct TurnCompleteEvent {
pub last_agent_message: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct TurnStartedEvent {
pub model_context_window: Option<i64>,
#[serde(default)]
pub identity_kind: IdentityKind,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct TokenUsageInfo {
pub total_token_usage: TokenUsage,
pub last_token_usage: TokenUsage,
#[ts(type = "number | null")]
pub model_context_window: Option<i64>,
}
impl TokenUsageInfo {
pub fn new_or_append(
info: &Option<TokenUsageInfo>,
last: &Option<TokenUsage>,
model_context_window: Option<i64>,
) -> Option<Self> {
if info.is_none() && last.is_none() {
return None;
}
let mut info = match info {
Some(info) => info.clone(),
None => Self {
total_token_usage: TokenUsage::default(),
last_token_usage: TokenUsage::default(),
model_context_window,
},
};
if let Some(model_context_window) = model_context_window {
info.model_context_window = Some(model_context_window);
}
if let Some(last) = last {
info.append_last_usage(last);
}
Some(info)
}
pub fn append_last_usage(&mut self, last: &TokenUsage) {
self.total_token_usage.add_assign(last);
self.last_token_usage = last.clone();
}
pub fn fill_to_context_window(&mut self, context_window: i64) {
let previous_total = self.total_token_usage.total_tokens;
let delta = (context_window - previous_total).max(0);
self.model_context_window = Some(context_window);
self.total_token_usage = TokenUsage {
total_tokens: context_window,
..TokenUsage::default()
};
self.last_token_usage = TokenUsage {
total_tokens: delta,
..TokenUsage::default()
};
}
pub fn full_context_window(context_window: i64) -> Self {
let mut info = Self {
total_token_usage: TokenUsage::default(),
last_token_usage: TokenUsage::default(),
model_context_window: Some(context_window),
};
info.fill_to_context_window(context_window);
info
}
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct TokenCountEvent {
pub info: Option<TokenUsageInfo>,
}
#[derive(
Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, Hash, JsonSchema, TS,
)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "protocol/")]
pub enum InputSlimmingScope {
#[default]
HistoricalToolOutputs,
LiveZoneToolOutputs,
}
impl InputSlimmingScope {
pub fn as_str(self) -> &'static str {
match self {
Self::HistoricalToolOutputs => "historical_tool_outputs",
Self::LiveZoneToolOutputs => "live_zone_tool_outputs",
}
}
pub fn short_label(self) -> &'static str {
match self {
Self::HistoricalToolOutputs => "hist",
Self::LiveZoneToolOutputs => "live",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct InputSlimmingEvent {
#[serde(default)]
pub scope: InputSlimmingScope,
pub last: InputSlimmingTokenStats,
pub total: InputSlimmingTokenStats,
}
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct InputSlimmingTokenStats {
pub tokens_before: i64,
pub tokens_after: i64,
pub tokens_saved: i64,
pub replacements: i64,
}
impl InputSlimmingTokenStats {
pub fn add_assign(&mut self, other: &Self) {
self.tokens_before = self.tokens_before.saturating_add(other.tokens_before);
self.tokens_after = self.tokens_after.saturating_add(other.tokens_after);
self.tokens_saved = self.tokens_saved.saturating_add(other.tokens_saved);
self.replacements = self.replacements.saturating_add(other.replacements);
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "protocol/")]
pub enum ThreadGoalStatus {
Active,
Paused,
Blocked,
UsageLimited,
BudgetLimited,
Complete,
}
pub const MAX_THREAD_GOAL_OBJECTIVE_CHARS: usize = 4_000;
pub fn validate_thread_goal_objective(value: &str) -> Result<(), String> {
if value.is_empty() {
return Err("goal objective must not be empty".to_string());
}
if value.chars().count() > MAX_THREAD_GOAL_OBJECTIVE_CHARS {
return Err(format!(
"goal objective must be at most {MAX_THREAD_GOAL_OBJECTIVE_CHARS} characters"
));
}
Ok(())
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "protocol/")]
pub struct ThreadGoal {
pub thread_id: ThreadId,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub goal_id: String,
pub objective: String,
pub status: ThreadGoalStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub token_budget: Option<i64>,
pub tokens_used: i64,
pub time_used_seconds: i64,
pub created_at: i64,
pub updated_at: i64,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ThreadGoalSetMode {
ConfirmIfExists,
ReplaceExisting {
expected_goal_id: String,
},
UpdateExisting {
expected_goal_id: String,
status: ThreadGoalStatus,
token_budget: Option<i64>,
},
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "protocol/")]
pub struct ThreadGoalUpdatedEvent {
pub thread_id: ThreadId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub turn_id: Option<String>,
pub goal: ThreadGoal,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "protocol/")]
pub struct ThreadGoalClearedEvent {
pub thread_id: ThreadId,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "protocol/")]
pub struct ThreadGoalSnapshotEvent {
pub thread_id: ThreadId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub goal: Option<ThreadGoal>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "protocol/")]
pub struct ThreadGoalReplaceConfirmationRequiredEvent {
pub thread_id: ThreadId,
pub existing_goal: ThreadGoal,
pub objective: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct FinalOutput {
pub token_usage: TokenUsage,
}
impl From<TokenUsage> for FinalOutput {
fn from(token_usage: TokenUsage) -> Self {
Self { token_usage }
}
}
impl fmt::Display for FinalOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let token_usage = &self.token_usage;
write!(
f,
"Token usage: total={} input={}{} output={}{}",
format_with_separators(token_usage.blended_total()),
format_with_separators(token_usage.non_cached_input()),
if token_usage.cached_input() > 0 {
format!(
" (+ {} cached)",
format_with_separators(token_usage.cached_input())
)
} else {
String::new()
},
format_with_separators(token_usage.output_tokens),
if token_usage.reasoning_output_tokens > 0 {
format!(
" (reasoning {})",
format_with_separators(token_usage.reasoning_output_tokens)
)
} else {
String::new()
}
)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct BuddyReactionEvent {
pub text: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct AgentMessageEvent {
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub memory_citation: Option<MemoryCitation>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct UserMessageEvent {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub images: Option<Vec<String>>,
#[serde(default)]
pub local_images: Vec<std::path::PathBuf>,
#[serde(default)]
pub text_elements: Vec<crate::product::protocol::user_input::TextElement>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct AgentMessageDeltaEvent {
pub delta: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct AgentReasoningEvent {
pub text: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct AgentReasoningRawContentEvent {
pub text: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct AgentReasoningRawContentDeltaEvent {
pub delta: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct AgentReasoningSectionBreakEvent {
#[serde(default)]
pub item_id: String,
#[serde(default)]
pub summary_index: i64,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct AgentReasoningDeltaEvent {
pub delta: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
pub struct McpInvocation {
pub server: String,
pub tool: String,
pub arguments: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
pub struct McpToolCallBeginEvent {
pub call_id: String,
pub invocation: McpInvocation,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
pub struct McpToolCallEndEvent {
pub call_id: String,
pub invocation: McpInvocation,
#[ts(type = "string")]
pub duration: Duration,
pub result: Result<CallToolResult, String>,
}
impl McpToolCallEndEvent {
pub fn is_success(&self) -> bool {
match &self.result {
Ok(result) => !result.is_error.unwrap_or(false),
Err(_) => false,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct WebSearchBeginEvent {
pub call_id: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct WebSearchEndEvent {
pub call_id: String,
pub query: String,
pub action: WebSearchAction,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ConversationPathResponseEvent {
pub conversation_id: ThreadId,
pub path: PathBuf,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ResumedHistory {
pub conversation_id: ThreadId,
pub history: Vec<RolloutItem>,
pub rollout_path: PathBuf,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub enum InitialHistory {
New,
Resumed(ResumedHistory),
Forked(Vec<RolloutItem>),
}
impl InitialHistory {
pub fn forked_from_id(&self) -> Option<ThreadId> {
match self {
InitialHistory::New => None,
InitialHistory::Resumed(resumed) => {
resumed.history.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => meta_line.meta.forked_from_id,
_ => None,
})
}
InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.id),
_ => None,
}),
}
}
pub fn session_cwd(&self) -> Option<PathBuf> {
match self {
InitialHistory::New => None,
InitialHistory::Resumed(resumed) => session_cwd_from_items(&resumed.history),
InitialHistory::Forked(items) => session_cwd_from_items(items),
}
}
pub fn get_rollout_items(&self) -> Vec<RolloutItem> {
match self {
InitialHistory::New => Vec::new(),
InitialHistory::Resumed(resumed) => resumed.history.clone(),
InitialHistory::Forked(items) => items.clone(),
}
}
pub fn get_event_msgs(&self) -> Option<Vec<EventMsg>> {
match self {
InitialHistory::New => None,
InitialHistory::Resumed(resumed) => Some(
resumed
.history
.iter()
.filter_map(|ri| match ri {
RolloutItem::EventMsg(ev) => Some(ev.clone()),
_ => None,
})
.collect(),
),
InitialHistory::Forked(items) => Some(
items
.iter()
.filter_map(|ri| match ri {
RolloutItem::EventMsg(ev) => Some(ev.clone()),
_ => None,
})
.collect(),
),
}
}
pub fn get_base_instructions(&self) -> Option<BaseInstructions> {
match self {
InitialHistory::New => None,
InitialHistory::Resumed(resumed) => {
resumed.history.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => meta_line.meta.base_instructions.clone(),
_ => None,
})
}
InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => meta_line.meta.base_instructions.clone(),
_ => None,
}),
}
}
pub fn get_dynamic_tools(&self) -> Option<Vec<DynamicToolSpec>> {
match self {
InitialHistory::New => None,
InitialHistory::Resumed(resumed) => {
resumed.history.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(),
_ => None,
})
}
InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => meta_line.meta.dynamic_tools.clone(),
_ => None,
}),
}
}
}
fn session_cwd_from_items(items: &[RolloutItem]) -> Option<PathBuf> {
items.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.cwd.clone()),
_ => None,
})
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
#[serde(rename_all = "lowercase")]
#[ts(rename_all = "lowercase")]
pub enum SessionSource {
Cli,
#[default]
VSCode,
Exec,
Mcp,
Agent,
#[serde(other)]
Unknown,
}
impl fmt::Display for SessionSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SessionSource::Cli => f.write_str("cli"),
SessionSource::VSCode => f.write_str("vscode"),
SessionSource::Exec => f.write_str("exec"),
SessionSource::Mcp => f.write_str("mcp"),
SessionSource::Agent => f.write_str("agent"),
SessionSource::Unknown => f.write_str("unknown"),
}
}
}
impl SessionSource {
pub fn is_non_root_agent(&self) -> bool {
matches!(self, SessionSource::Agent)
}
}
pub const MEMORY_MODE_ENABLED: &str = "enabled";
pub const MEMORY_MODE_DISABLED: &str = "disabled";
pub const MEMORY_MODE_POLLUTED: &str = "polluted";
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
pub struct SessionMeta {
pub id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")]
pub forked_from_id: Option<ThreadId>,
pub timestamp: String,
pub cwd: PathBuf,
pub originator: String,
pub cli_version: String,
pub rollout_schema_version: u32,
#[serde(default)]
pub source: SessionSource,
pub model_provider: Option<String>,
pub base_instructions: Option<BaseInstructions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_mode: Option<String>,
}
impl Default for SessionMeta {
fn default() -> Self {
SessionMeta {
id: ThreadId::default(),
forked_from_id: None,
timestamp: String::new(),
cwd: PathBuf::new(),
originator: String::new(),
cli_version: String::new(),
rollout_schema_version: default_rollout_schema_version(),
source: SessionSource::default(),
model_provider: None,
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
pub struct SessionMetaLine {
#[serde(flatten)]
pub meta: SessionMeta,
#[serde(skip_serializing_if = "Option::is_none")]
pub git: Option<GitInfo>,
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
pub enum RolloutItem {
SessionMeta(SessionMetaLine),
TranscriptItem(TranscriptItem),
GhostSnapshot(GhostSnapshotRecord),
Compacted(CompactedItem),
TurnContext(TurnContextItem),
InputSlimmingStoredInput(InputSlimmingStoredInputItem),
Workflow(WorkflowRolloutItem),
EventMsg(EventMsg),
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS, PartialEq, Eq)]
pub struct InputSlimmingStoredInputItem {
pub hash: String,
pub original: String,
pub metadata: InputSlimmingStoredInputMetadata,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS, PartialEq, Eq)]
pub struct InputSlimmingStoredInputMetadata {
#[serde(default)]
pub scope: InputSlimmingScope,
pub strategy: String,
pub tool_name: String,
pub original_tokens: usize,
pub compressed_tokens: usize,
pub created_turn_id: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum GhostSnapshotStatus {
Pending,
Captured { ghost_commit: GhostCommit },
Consumed,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS, PartialEq, Eq)]
pub struct GhostSnapshotRecord {
pub turn_id: String,
pub status: GhostSnapshotStatus,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
pub struct CompactedItem {
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub replacement_history: Option<Vec<TranscriptItem>>,
#[serde(default)]
pub replacement_history_omits_initial_context: bool,
}
impl From<CompactedItem> for TranscriptItem {
fn from(value: CompactedItem) -> Self {
TranscriptItem::Message {
id: None,
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
text: value.message,
}],
end_turn: None,
}
}
}
pub const ROLLOUT_SCHEMA_VERSION_V3: u32 = 3;
pub const ROLLOUT_SCHEMA_VERSION_V4: u32 = 4;
pub const ROLLOUT_SCHEMA_VERSION_V5: u32 = 5;
pub fn current_rollout_schema_version() -> u32 {
ROLLOUT_SCHEMA_VERSION_V5
}
fn default_rollout_schema_version() -> u32 {
current_rollout_schema_version()
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
pub struct TurnContextItem {
pub cwd: PathBuf,
pub approval_policy: AskForApproval,
pub sandbox_policy: SandboxPolicy,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub personality: Option<Personality>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identity: Option<Identity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<ReasoningEffortConfig>,
pub summary: ReasoningSummaryConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub developer_instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub final_output_json_schema: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation_policy: Option<TruncationPolicy>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(tag = "mode", content = "limit", rename_all = "snake_case")]
pub enum TruncationPolicy {
Bytes(usize),
Tokens(usize),
}
#[derive(Serialize, Deserialize, Clone, JsonSchema)]
pub struct RolloutLine {
pub timestamp: String,
#[serde(flatten)]
pub item: RolloutItem,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
pub struct GitInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub commit_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repository_url: Option<String>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum ReviewDelivery {
Inline,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
pub enum ReviewTarget {
UncommittedChanges,
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
BaseBranch { branch: String },
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
Commit {
sha: String,
title: Option<String>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
Custom { instructions: String },
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
pub struct ReviewRequest {
pub target: ReviewTarget,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub user_facing_hint: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
pub struct ReviewOutputEvent {
pub findings: Vec<ReviewFinding>,
pub overall_correctness: String,
pub overall_explanation: String,
pub overall_confidence_score: f32,
}
impl Default for ReviewOutputEvent {
fn default() -> Self {
Self {
findings: Vec::new(),
overall_correctness: String::default(),
overall_explanation: String::default(),
overall_confidence_score: 0.0,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
pub struct ReviewFinding {
pub title: String,
pub body: String,
pub confidence_score: f32,
pub priority: i32,
pub code_location: ReviewCodeLocation,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
pub struct ReviewCodeLocation {
pub absolute_file_path: PathBuf,
pub line_range: ReviewLineRange,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
pub struct ReviewLineRange {
pub start: u32,
pub end: u32,
}
#[derive(
Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, Default,
)]
#[serde(rename_all = "snake_case")]
pub enum ExecCommandSource {
#[default]
Agent,
UserShell,
UnifiedExecStartup,
UnifiedExecInteraction,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ExecCommandBeginEvent {
pub call_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub process_id: Option<String>,
pub turn_id: String,
pub command: Vec<String>,
pub cwd: PathBuf,
pub parsed_cmd: Vec<ParsedCommand>,
#[serde(default)]
pub source: ExecCommandSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub interaction_input: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ExecCommandEndEvent {
pub call_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub process_id: Option<String>,
pub turn_id: String,
pub command: Vec<String>,
pub cwd: PathBuf,
pub parsed_cmd: Vec<ParsedCommand>,
#[serde(default)]
pub source: ExecCommandSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub interaction_input: Option<String>,
pub stdout: String,
pub stderr: String,
#[serde(default)]
pub aggregated_output: String,
pub exit_code: i32,
#[ts(type = "string")]
pub duration: Duration,
pub formatted_output: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ViewImageToolCallEvent {
pub call_id: String,
pub path: PathBuf,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum ExecOutputStream {
Stdout,
Stderr,
}
#[serde_as]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
pub struct ExecCommandOutputDeltaEvent {
pub call_id: String,
pub stream: ExecOutputStream,
#[serde_as(as = "serde_with::base64::Base64")]
#[schemars(with = "String")]
#[ts(type = "string")]
pub chunk: Vec<u8>,
}
#[serde_as]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
pub struct TerminalInteractionEvent {
pub call_id: String,
pub process_id: String,
pub stdin: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct BackgroundEventEvent {
pub message: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct DeprecationNoticeEvent {
pub summary: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct UndoStartedEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct UndoCompletedEvent {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ThreadRolledBackEvent {
pub num_turns: u32,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct StreamErrorEvent {
pub message: String,
#[serde(default)]
pub codex_error_info: Option<CodexErrorInfo>,
#[serde(default)]
pub additional_details: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct StreamInfoEvent {
pub message: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct PatchApplyBeginEvent {
pub call_id: String,
#[serde(default)]
pub turn_id: String,
pub auto_approved: bool,
pub changes: HashMap<PathBuf, FileChange>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct PatchApplyEndEvent {
pub call_id: String,
#[serde(default)]
pub turn_id: String,
pub stdout: String,
pub stderr: String,
pub success: bool,
#[serde(default)]
pub changes: HashMap<PathBuf, FileChange>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct TurnDiffEvent {
pub unified_diff: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct GetHistoryEntryResponseEvent {
pub offset: usize,
pub log_id: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub entry: Option<HistoryEntry>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct McpListToolsResponseEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub request_id: Option<u64>,
pub tools: std::collections::HashMap<String, McpTool>,
pub resources: std::collections::HashMap<String, Vec<McpResource>>,
pub resource_templates: std::collections::HashMap<String, Vec<McpResourceTemplate>>,
pub auth_statuses: std::collections::HashMap<String, McpAuthStatus>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct McpStartupUpdateEvent {
pub server: String,
pub status: McpStartupStatus,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case", tag = "state")]
#[ts(rename_all = "snake_case", tag = "state")]
pub enum McpStartupStatus {
Starting,
Ready,
Failed { error: String },
Cancelled,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, Default)]
pub struct McpStartupCompleteEvent {
pub ready: Vec<String>,
pub failed: Vec<McpStartupFailure>,
pub cancelled: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct McpStartupFailure {
pub server: String,
pub error: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum McpAuthStatus {
Unsupported,
NotLoggedIn,
BearerToken,
OAuth,
}
impl fmt::Display for McpAuthStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
McpAuthStatus::Unsupported => "Unsupported",
McpAuthStatus::NotLoggedIn => "Not logged in",
McpAuthStatus::BearerToken => "Bearer token",
McpAuthStatus::OAuth => "OAuth",
};
f.write_str(text)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ListCustomPromptsResponseEvent {
pub custom_prompts: Vec<CustomPrompt>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ListSkillsResponseEvent {
pub skills: Vec<SkillsListEntry>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum SkillScope {
User,
Repo,
System,
Admin,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct SkillMetadata {
pub name: String,
pub description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub short_description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub interface: Option<SkillInterface>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub dependencies: Option<SkillDependencies>,
pub path: PathBuf,
pub scope: SkillScope,
pub enabled: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
pub struct SkillInterface {
#[ts(optional)]
pub display_name: Option<String>,
#[ts(optional)]
pub short_description: Option<String>,
#[ts(optional)]
pub icon_small: Option<PathBuf>,
#[ts(optional)]
pub icon_large: Option<PathBuf>,
#[ts(optional)]
pub brand_color: Option<String>,
#[ts(optional)]
pub default_prompt: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
pub struct SkillDependencies {
pub tools: Vec<SkillToolDependency>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq, Eq)]
pub struct SkillToolDependency {
#[serde(rename = "type")]
#[ts(rename = "type")]
pub r#type: String,
pub value: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub transport: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub command: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub url: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct SkillErrorInfo {
pub path: PathBuf,
pub message: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct SkillsListEntry {
pub cwd: PathBuf,
pub skills: Vec<SkillMetadata>,
pub errors: Vec<SkillErrorInfo>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct SessionConfiguredEvent {
pub session_id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")]
pub forked_from_id: Option<ThreadId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub thread_name: Option<String>,
pub model: String,
pub identity_kind: IdentityKind,
pub model_provider_id: String,
pub approval_policy: AskForApproval,
pub sandbox_policy: SandboxPolicy,
pub cwd: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<ReasoningEffortConfig>,
pub history_log_id: u64,
pub history_entry_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_messages: Option<Vec<EventMsg>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rollout_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ThreadNameUpdatedEvent {
pub thread_id: ThreadId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub thread_name: Option<String>,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum ReviewDecision {
Approved,
ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: ExecPolicyAmendment,
},
ApprovedForSession,
#[default]
Denied,
Abort,
}
impl ReviewDecision {
pub fn to_opaque_string(&self) -> &'static str {
match self {
ReviewDecision::Approved => "approved",
ReviewDecision::ApprovedExecpolicyAmendment { .. } => "approved_with_amendment",
ReviewDecision::ApprovedForSession => "approved_for_session",
ReviewDecision::Denied => "denied",
ReviewDecision::Abort => "abort",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type")]
pub enum FileChange {
Add {
content: String,
},
Delete {
content: String,
},
Update {
unified_diff: String,
move_path: Option<PathBuf>,
},
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct Chunk {
pub orig_index: u32,
pub deleted_lines: Vec<String>,
pub inserted_lines: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct TurnAbortedEvent {
pub reason: TurnAbortReason,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum TurnAbortReason {
Interrupted,
Replaced,
ReviewEnded,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::product::protocol::items::UserMessageItem;
use crate::product::protocol::items::WebSearchItem;
use anyhow::Result;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::NamedTempFile;
#[test]
fn current_rollout_schema_version_is_v5() {
assert_eq!(current_rollout_schema_version(), ROLLOUT_SCHEMA_VERSION_V5);
assert_eq!(current_rollout_schema_version(), 5);
}
#[test]
fn session_meta_requires_explicit_rollout_schema_version() {
let value = json!({
"id": ThreadId::new(),
"timestamp": "2025-01-01T00:00:00Z",
"cwd": "/tmp",
"originator": "lha",
"cli_version": "0.0.0",
"model_provider": null,
"base_instructions": null
});
assert!(serde_json::from_value::<SessionMetaLine>(value).is_err());
}
#[test]
fn session_meta_serializes_rollout_schema_version() -> Result<()> {
let value = serde_json::to_value(SessionMeta::default())?;
assert_eq!(
value.get("rollout_schema_version"),
Some(&json!(ROLLOUT_SCHEMA_VERSION_V5))
);
Ok(())
}
#[test]
fn thread_goal_objective_validation_enforces_bounds() {
assert!(validate_thread_goal_objective("").is_err());
assert!(
validate_thread_goal_objective(&"a".repeat(MAX_THREAD_GOAL_OBJECTIVE_CHARS)).is_ok()
);
assert!(
validate_thread_goal_objective(&"a".repeat(MAX_THREAD_GOAL_OBJECTIVE_CHARS + 1))
.is_err()
);
}
#[test]
fn external_sandbox_reports_full_access_flags() {
let restricted = SandboxPolicy::ExternalSandbox {
network_access: NetworkAccess::Restricted,
};
assert!(restricted.has_full_disk_write_access());
assert!(!restricted.has_full_network_access());
let enabled = SandboxPolicy::ExternalSandbox {
network_access: NetworkAccess::Enabled,
};
assert!(enabled.has_full_disk_write_access());
assert!(enabled.has_full_network_access());
}
#[test]
fn list_mcp_tools_deserializes_without_request_id() -> Result<()> {
let value = json!({
"type": "list_mcp_tools"
});
assert_eq!(
serde_json::from_value::<Op>(value)?,
Op::ListMcpTools { request_id: None }
);
Ok(())
}
#[test]
fn list_mcp_tools_round_trips_request_id() -> Result<()> {
let op = Op::ListMcpTools {
request_id: Some(42),
};
assert_eq!(
serde_json::to_value(&op)?,
json!({
"type": "list_mcp_tools",
"request_id": 42
})
);
assert_eq!(
serde_json::from_value::<Op>(serde_json::to_value(&op)?)?,
op
);
Ok(())
}
#[test]
fn item_started_event_from_web_search_emits_begin_event() {
let event = ItemStartedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-1".into(),
item: TurnItem::WebSearch(WebSearchItem {
id: "search-1".into(),
query: "find docs".into(),
action: WebSearchAction::Search {
query: Some("find docs".into()),
queries: None,
},
}),
};
let legacy_events = event.as_legacy_events(false);
assert_eq!(legacy_events.len(), 1);
match &legacy_events[0] {
EventMsg::WebSearchBegin(event) => assert_eq!(event.call_id, "search-1"),
_ => panic!("expected WebSearchBegin event"),
}
}
#[test]
fn item_started_event_from_non_web_search_emits_no_legacy_events() {
let event = ItemStartedEvent {
thread_id: ThreadId::new(),
turn_id: "turn-1".into(),
item: TurnItem::UserMessage(UserMessageItem::new(&[])),
};
assert!(event.as_legacy_events(false).is_empty());
}
#[test]
fn user_input_serialization_omits_final_output_json_schema_when_none() -> Result<()> {
let op = Op::UserInput {
items: Vec::new(),
final_output_json_schema: None,
};
let json_op = serde_json::to_value(op)?;
assert_eq!(json_op, json!({ "type": "user_input", "items": [] }));
Ok(())
}
#[test]
fn user_input_deserializes_without_final_output_json_schema_field() -> Result<()> {
let op: Op = serde_json::from_value(json!({ "type": "user_input", "items": [] }))?;
assert_eq!(
op,
Op::UserInput {
items: Vec::new(),
final_output_json_schema: None,
}
);
Ok(())
}
#[test]
fn user_input_serialization_includes_final_output_json_schema_when_some() -> Result<()> {
let schema = json!({
"type": "object",
"properties": {
"answer": { "type": "string" }
},
"required": ["answer"],
"additionalProperties": false
});
let op = Op::UserInput {
items: Vec::new(),
final_output_json_schema: Some(schema.clone()),
};
let json_op = serde_json::to_value(op)?;
assert_eq!(
json_op,
json!({
"type": "user_input",
"items": [],
"final_output_json_schema": schema,
})
);
Ok(())
}
#[test]
fn user_input_text_serializes_empty_text_elements() -> Result<()> {
let input = UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
};
let json_input = serde_json::to_value(input)?;
assert_eq!(
json_input,
json!({
"type": "text",
"text": "hello",
"text_elements": [],
})
);
Ok(())
}
#[test]
fn user_message_event_serializes_empty_metadata_vectors() -> Result<()> {
let event = UserMessageEvent {
message: "hello".to_string(),
images: None,
local_images: Vec::new(),
text_elements: Vec::new(),
};
let json_event = serde_json::to_value(event)?;
assert_eq!(
json_event,
json!({
"message": "hello",
"local_images": [],
"text_elements": [],
})
);
Ok(())
}
#[test]
fn serialize_event() -> Result<()> {
let conversation_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
let rollout_file = NamedTempFile::new()?;
let event = Event {
id: "1234".to_string(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: conversation_id,
forked_from_id: None,
thread_name: None,
model: "codex-mini-latest".to_string(),
identity_kind: IdentityKind::Nobody,
model_provider_id: "openai".to_string(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::ReadOnly,
cwd: PathBuf::from("/home/user/project"),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
history_entry_count: 0,
initial_messages: None,
rollout_path: Some(rollout_file.path().to_path_buf()),
}),
};
let expected = json!({
"id": "1234",
"msg": {
"type": "session_configured",
"session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
"model": "codex-mini-latest",
"identity_kind": "nobody",
"model_provider_id": "openai",
"approval_policy": "never",
"sandbox_policy": {
"type": "read-only"
},
"cwd": "/home/user/project",
"reasoning_effort": "medium",
"history_log_id": 0,
"history_entry_count": 0,
"rollout_path": format!("{}", rollout_file.path().display()),
}
});
assert_eq!(expected, serde_json::to_value(&event)?);
Ok(())
}
#[test]
fn vec_u8_as_base64_serialization_and_deserialization() -> Result<()> {
let event = ExecCommandOutputDeltaEvent {
call_id: "call21".to_string(),
stream: ExecOutputStream::Stdout,
chunk: vec![1, 2, 3, 4, 5],
};
let serialized = serde_json::to_string(&event)?;
assert_eq!(
r#"{"call_id":"call21","stream":"stdout","chunk":"AQIDBAU="}"#,
serialized,
);
let deserialized: ExecCommandOutputDeltaEvent = serde_json::from_str(&serialized)?;
assert_eq!(deserialized, event);
Ok(())
}
#[test]
fn serialize_mcp_startup_update_event() -> Result<()> {
let event = Event {
id: "init".to_string(),
msg: EventMsg::McpStartupUpdate(McpStartupUpdateEvent {
server: "srv".to_string(),
status: McpStartupStatus::Failed {
error: "boom".to_string(),
},
}),
};
let value = serde_json::to_value(&event)?;
assert_eq!(value["msg"]["type"], "mcp_startup_update");
assert_eq!(value["msg"]["server"], "srv");
assert_eq!(value["msg"]["status"]["state"], "failed");
assert_eq!(value["msg"]["status"]["error"], "boom");
Ok(())
}
#[test]
fn serialize_mcp_startup_complete_event() -> Result<()> {
let event = Event {
id: "init".to_string(),
msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
ready: vec!["a".to_string()],
failed: vec![McpStartupFailure {
server: "b".to_string(),
error: "bad".to_string(),
}],
cancelled: vec!["c".to_string()],
}),
};
let value = serde_json::to_value(&event)?;
assert_eq!(value["msg"]["type"], "mcp_startup_complete");
assert_eq!(value["msg"]["ready"][0], "a");
assert_eq!(value["msg"]["failed"][0]["server"], "b");
assert_eq!(value["msg"]["failed"][0]["error"], "bad");
assert_eq!(value["msg"]["cancelled"][0], "c");
Ok(())
}
#[test]
fn context_window_percent_uses_real_remaining_capacity() {
let usage = TokenUsage {
total_tokens: 10_600,
..TokenUsage::default()
};
assert_eq!(usage.percent_of_context_window_remaining(30_400), 65);
}
#[test]
fn context_window_percent_floors_and_clamps() {
let low_usage = TokenUsage {
total_tokens: 1,
..TokenUsage::default()
};
assert_eq!(low_usage.percent_of_context_window_remaining(100), 99);
let overfull_usage = TokenUsage {
total_tokens: 250,
..TokenUsage::default()
};
assert_eq!(overfull_usage.percent_of_context_window_remaining(100), 0);
assert_eq!(overfull_usage.percent_of_context_window_remaining(0), 0);
}
#[test]
fn token_usage_info_updates_context_window_when_appending_usage() {
let initial = Some(TokenUsageInfo {
total_token_usage: TokenUsage {
total_tokens: 10,
..TokenUsage::default()
},
last_token_usage: TokenUsage {
total_tokens: 10,
..TokenUsage::default()
},
model_context_window: Some(30_400),
});
let next = Some(TokenUsage {
total_tokens: 5,
..TokenUsage::default()
});
let info =
TokenUsageInfo::new_or_append(&initial, &next, Some(60_800)).expect("token usage info");
assert_eq!(
info,
TokenUsageInfo {
total_token_usage: TokenUsage {
total_tokens: 15,
..TokenUsage::default()
},
last_token_usage: TokenUsage {
total_tokens: 5,
..TokenUsage::default()
},
model_context_window: Some(60_800),
}
);
}
}