mirror of
https://github.com/openai/codex.git
synced 2026-11-07 16:35:32 +00:00
current time reminders impl for system clock (varlatency 2/n) (#28824)
Stacked on #28822. ## Summary - add a host-injectable current-time provider with a built-in system implementation - record UTC developer reminders in history immediately before due model requests - keep cadence state per session and force a refresh after compaction This does NOT include the app server client <-> server clock logic. This PR is only for the reminder message & system clock that will be used in prod. ## Testing - `just test -p codex-core varlatency_` - `just clippy -p codex-core -p codex-app-server -p codex-mcp-server -p codex-thread-manager-sample` - `just fmt`
This commit is contained in:
1 parent
01a2df2947
commit
752ed90d78
25 files changed
+516
-27
No files matched your search
@@ -250,6 +250,7 @@ mod tests {
|
|||||||
Some(state_db.clone()),
|
Some(state_db.clone()),
|
||||||
"11111111-1111-4111-8111-111111111111".to_string(),
|
"11111111-1111-4111-8111-111111111111".to_string(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
thread_manager.start_thread(good_config).await?;
|
thread_manager.start_thread(good_config).await?;
|
||||||
|
|||||||
@@ -379,6 +379,7 @@ impl MessageProcessor {
|
|||||||
outgoing.clone(),
|
outgoing.clone(),
|
||||||
thread_state_manager.clone(),
|
thread_state_manager.clone(),
|
||||||
)),
|
)),
|
||||||
|
/*external_time_provider*/ None,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let models_manager = thread_manager.get_models_manager();
|
let models_manager = thread_manager.get_models_manager();
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
|||||||
analytics_events_client: Some(parent_session.services.analytics_events_client.clone()),
|
analytics_events_client: Some(parent_session.services.analytics_events_client.clone()),
|
||||||
thread_store: Arc::clone(&parent_session.services.thread_store),
|
thread_store: Arc::clone(&parent_session.services.thread_store),
|
||||||
attestation_provider: parent_session.services.attestation_provider.clone(),
|
attestation_provider: parent_session.services.attestation_provider.clone(),
|
||||||
|
external_time_provider: Some(Arc::clone(&parent_session.services.time_provider)),
|
||||||
inherited_multi_agent_version: Some(MultiAgentVersion::Disabled),
|
inherited_multi_agent_version: Some(MultiAgentVersion::Disabled),
|
||||||
}))
|
}))
|
||||||
.or_cancel(&cancel_token)
|
.or_cancel(&cancel_token)
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use chrono::DateTime;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
use super::ContextualUserFragment;
|
||||||
|
|
||||||
|
pub(crate) struct CurrentTimeReminder {
|
||||||
|
current_time: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CurrentTimeReminder {
|
||||||
|
pub(crate) fn new(current_time: DateTime<Utc>) -> Self {
|
||||||
|
Self { current_time }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextualUserFragment for CurrentTimeReminder {
|
||||||
|
fn role(&self) -> &'static str {
|
||||||
|
"developer"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn markers(&self) -> (&'static str, &'static str) {
|
||||||
|
Self::type_markers()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_markers() -> (&'static str, &'static str) {
|
||||||
|
("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"It is {}.",
|
||||||
|
self.current_time.format("%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ mod available_plugins_instructions;
|
|||||||
mod available_skills_instructions;
|
mod available_skills_instructions;
|
||||||
mod collaboration_mode_instructions;
|
mod collaboration_mode_instructions;
|
||||||
mod contextual_user_message;
|
mod contextual_user_message;
|
||||||
|
mod current_time_reminder;
|
||||||
mod environment_context;
|
mod environment_context;
|
||||||
mod guardian_followup_review_reminder;
|
mod guardian_followup_review_reminder;
|
||||||
mod hook_additional_context;
|
mod hook_additional_context;
|
||||||
@@ -44,6 +45,7 @@ pub(crate) use codex_core_skills::SkillInstructions;
|
|||||||
pub(crate) use collaboration_mode_instructions::CollaborationModeInstructions;
|
pub(crate) use collaboration_mode_instructions::CollaborationModeInstructions;
|
||||||
pub(crate) use contextual_user_message::is_contextual_user_fragment;
|
pub(crate) use contextual_user_message::is_contextual_user_fragment;
|
||||||
pub(crate) use contextual_user_message::parse_visible_hook_prompt_message;
|
pub(crate) use contextual_user_message::parse_visible_hook_prompt_message;
|
||||||
|
pub(crate) use current_time_reminder::CurrentTimeReminder;
|
||||||
pub(crate) use environment_context::EnvironmentContext;
|
pub(crate) use environment_context::EnvironmentContext;
|
||||||
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
|
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
|
||||||
pub(crate) use hook_additional_context::HookAdditionalContext;
|
pub(crate) use hook_additional_context::HookAdditionalContext;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use chrono::DateTime;
|
||||||
|
use chrono::Utc;
|
||||||
|
use codex_features::CurrentTimeSource;
|
||||||
|
use codex_protocol::ThreadId;
|
||||||
|
|
||||||
|
use crate::config::CurrentTimeReminderConfig;
|
||||||
|
|
||||||
|
pub type TimeFuture<'a> = Pin<Box<dyn Future<Output = Result<DateTime<Utc>>> + Send + 'a>>;
|
||||||
|
|
||||||
|
/// Host integration boundary for obtaining the current time.
|
||||||
|
pub trait TimeProvider: Send + Sync {
|
||||||
|
fn current_time(&self, thread_id: ThreadId) -> TimeFuture<'_>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SystemTimeProvider;
|
||||||
|
|
||||||
|
impl TimeProvider for SystemTimeProvider {
|
||||||
|
fn current_time(&self, _thread_id: ThreadId) -> TimeFuture<'_> {
|
||||||
|
Box::pin(async { Ok(Utc::now()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_time_provider(
|
||||||
|
config: Option<&CurrentTimeReminderConfig>,
|
||||||
|
external_provider: Option<Arc<dyn TimeProvider>>,
|
||||||
|
) -> Result<Arc<dyn TimeProvider>> {
|
||||||
|
match config.map(|config| config.clock_source).unwrap_or_default() {
|
||||||
|
CurrentTimeSource::System => Ok(Arc::new(SystemTimeProvider)),
|
||||||
|
CurrentTimeSource::External => external_provider.ok_or_else(|| {
|
||||||
|
anyhow!(
|
||||||
|
"features.current_time_reminder.clock_source is external, but no external current-time provider is available"
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ pub mod config;
|
|||||||
pub mod connectors;
|
pub mod connectors;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
mod context_manager;
|
mod context_manager;
|
||||||
|
mod current_time;
|
||||||
mod environment_selection;
|
mod environment_selection;
|
||||||
pub mod exec;
|
pub mod exec;
|
||||||
pub mod exec_env;
|
pub mod exec_env;
|
||||||
@@ -186,6 +187,8 @@ pub use client_common::ResponseEvent;
|
|||||||
pub use client_common::ResponseStream;
|
pub use client_common::ResponseStream;
|
||||||
pub use codex_prompts::REVIEW_PROMPT;
|
pub use codex_prompts::REVIEW_PROMPT;
|
||||||
pub use compact::content_items_to_text;
|
pub use compact::content_items_to_text;
|
||||||
|
pub use current_time::TimeFuture;
|
||||||
|
pub use current_time::TimeProvider;
|
||||||
pub use event_mapping::parse_turn_item;
|
pub use event_mapping::parse_turn_item;
|
||||||
pub use exec_policy::ExecPolicyError;
|
pub use exec_policy::ExecPolicyError;
|
||||||
pub use exec_policy::check_execpolicy_for_warnings;
|
pub use exec_policy::check_execpolicy_for_warnings;
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ pub async fn build_prompt_input(
|
|||||||
state_db.clone(),
|
state_db.clone(),
|
||||||
installation_id,
|
installation_id,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
let thread = thread_manager.start_thread(config).await?;
|
let thread = thread_manager.start_thread(config).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use crate::context::NetworkRuleSaved;
|
|||||||
use crate::context::PermissionsInstructions;
|
use crate::context::PermissionsInstructions;
|
||||||
use crate::context::PersonalitySpecInstructions;
|
use crate::context::PersonalitySpecInstructions;
|
||||||
use crate::context::RecommendedPluginsInstructions;
|
use crate::context::RecommendedPluginsInstructions;
|
||||||
|
use crate::current_time::TimeProvider;
|
||||||
use crate::default_skill_metadata_budget;
|
use crate::default_skill_metadata_budget;
|
||||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||||
use crate::exec_policy::ExecPolicyManager;
|
use crate::exec_policy::ExecPolicyManager;
|
||||||
@@ -215,6 +216,7 @@ mod rollout_budget;
|
|||||||
mod rollout_reconstruction;
|
mod rollout_reconstruction;
|
||||||
#[allow(clippy::module_inception)]
|
#[allow(clippy::module_inception)]
|
||||||
pub(crate) mod session;
|
pub(crate) mod session;
|
||||||
|
pub(crate) mod time_reminder;
|
||||||
mod token_budget;
|
mod token_budget;
|
||||||
pub(crate) mod turn;
|
pub(crate) mod turn;
|
||||||
pub(crate) mod turn_context;
|
pub(crate) mod turn_context;
|
||||||
@@ -439,6 +441,7 @@ pub(crate) struct CodexSpawnArgs {
|
|||||||
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
|
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
|
||||||
pub(crate) thread_store: Arc<dyn ThreadStore>,
|
pub(crate) thread_store: Arc<dyn ThreadStore>,
|
||||||
pub(crate) attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
pub(crate) attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||||||
|
pub(crate) external_time_provider: Option<Arc<dyn TimeProvider>>,
|
||||||
pub(crate) inherited_multi_agent_version: Option<MultiAgentVersion>,
|
pub(crate) inherited_multi_agent_version: Option<MultiAgentVersion>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,6 +525,7 @@ impl Codex {
|
|||||||
analytics_events_client,
|
analytics_events_client,
|
||||||
thread_store,
|
thread_store,
|
||||||
attestation_provider,
|
attestation_provider,
|
||||||
|
external_time_provider,
|
||||||
inherited_multi_agent_version,
|
inherited_multi_agent_version,
|
||||||
} = args;
|
} = args;
|
||||||
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||||
@@ -669,6 +673,7 @@ impl Codex {
|
|||||||
thread_store,
|
thread_store,
|
||||||
parent_rollout_thread_trace,
|
parent_rollout_thread_trace,
|
||||||
attestation_provider,
|
attestation_provider,
|
||||||
|
external_time_provider,
|
||||||
multi_agent_version,
|
multi_agent_version,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -492,6 +492,7 @@ impl Session {
|
|||||||
thread_store: Arc<dyn ThreadStore>,
|
thread_store: Arc<dyn ThreadStore>,
|
||||||
parent_rollout_thread_trace: ThreadTraceContext,
|
parent_rollout_thread_trace: ThreadTraceContext,
|
||||||
attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||||||
|
external_time_provider: Option<Arc<dyn TimeProvider>>,
|
||||||
multi_agent_version: Option<MultiAgentVersion>,
|
multi_agent_version: Option<MultiAgentVersion>,
|
||||||
) -> anyhow::Result<Arc<Self>> {
|
) -> anyhow::Result<Arc<Self>> {
|
||||||
debug!(
|
debug!(
|
||||||
@@ -516,6 +517,10 @@ impl Session {
|
|||||||
}
|
}
|
||||||
InitialHistory::Resumed(resumed_history) => resumed_history.conversation_id,
|
InitialHistory::Resumed(resumed_history) => resumed_history.conversation_id,
|
||||||
};
|
};
|
||||||
|
let time_provider = crate::current_time::resolve_time_provider(
|
||||||
|
config.current_time_reminder.as_ref(),
|
||||||
|
external_time_provider,
|
||||||
|
)?;
|
||||||
let mcp_thread_init = thread_extension_init.clone();
|
let mcp_thread_init = thread_extension_init.clone();
|
||||||
let thread_extension_data = codex_extension_api::ExtensionData::new_with_init(
|
let thread_extension_data = codex_extension_api::ExtensionData::new_with_init(
|
||||||
thread_id.to_string(),
|
thread_id.to_string(),
|
||||||
@@ -1022,6 +1027,7 @@ impl Session {
|
|||||||
live_thread: live_thread_init.as_ref().cloned(),
|
live_thread: live_thread_init.as_ref().cloned(),
|
||||||
thread_store: Arc::clone(&thread_store),
|
thread_store: Arc::clone(&thread_store),
|
||||||
attestation_provider: attestation_provider.clone(),
|
attestation_provider: attestation_provider.clone(),
|
||||||
|
time_provider,
|
||||||
model_client: ModelClient::new(
|
model_client: ModelClient::new(
|
||||||
Some(Arc::clone(&auth_manager)),
|
Some(Arc::clone(&auth_manager)),
|
||||||
thread_id,
|
thread_id,
|
||||||
|
|||||||
@@ -4867,6 +4867,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
|||||||
)),
|
)),
|
||||||
codex_rollout_trace::ThreadTraceContext::disabled(),
|
codex_rollout_trace::ThreadTraceContext::disabled(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
Some(config.multi_agent_version_from_features()),
|
Some(config.multi_agent_version_from_features()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -5032,6 +5033,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
|||||||
/*state_db*/ None,
|
/*state_db*/ None,
|
||||||
)),
|
)),
|
||||||
attestation_provider: None,
|
attestation_provider: None,
|
||||||
|
time_provider: Arc::new(crate::current_time::SystemTimeProvider),
|
||||||
model_client: ModelClient::new(
|
model_client: ModelClient::new(
|
||||||
Some(auth_manager.clone()),
|
Some(auth_manager.clone()),
|
||||||
thread_id,
|
thread_id,
|
||||||
@@ -5216,6 +5218,7 @@ async fn make_session_with_config_and_rx(
|
|||||||
)),
|
)),
|
||||||
codex_rollout_trace::ThreadTraceContext::disabled(),
|
codex_rollout_trace::ThreadTraceContext::disabled(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
Some(config.multi_agent_version_from_features()),
|
Some(config.multi_agent_version_from_features()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -5328,6 +5331,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
|||||||
)),
|
)),
|
||||||
codex_rollout_trace::ThreadTraceContext::disabled(),
|
codex_rollout_trace::ThreadTraceContext::disabled(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
Some(config.multi_agent_version_from_features()),
|
Some(config.multi_agent_version_from_features()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -7078,6 +7082,7 @@ where
|
|||||||
state_db,
|
state_db,
|
||||||
)),
|
)),
|
||||||
attestation_provider: None,
|
attestation_provider: None,
|
||||||
|
time_provider: Arc::new(crate::current_time::SystemTimeProvider),
|
||||||
model_client: ModelClient::new(
|
model_client: ModelClient::new(
|
||||||
Some(Arc::clone(&auth_manager)),
|
Some(Arc::clone(&auth_manager)),
|
||||||
thread_id,
|
thread_id,
|
||||||
|
|||||||
@@ -736,6 +736,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
|||||||
analytics_events_client: None,
|
analytics_events_client: None,
|
||||||
thread_store,
|
thread_store,
|
||||||
attestation_provider: None,
|
attestation_provider: None,
|
||||||
|
external_time_provider: None,
|
||||||
inherited_multi_agent_version: None,
|
inherited_multi_agent_version: None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
use codex_protocol::error::CodexErr;
|
||||||
|
use codex_protocol::error::Result as CodexResult;
|
||||||
|
|
||||||
|
use super::session::Session;
|
||||||
|
use super::turn_context::TurnContext;
|
||||||
|
use crate::context::ContextualUserFragment;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct CurrentTimeReminderState {
|
||||||
|
model_requests_since_delivery: u64,
|
||||||
|
last_window_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CurrentTimeReminderState {
|
||||||
|
fn take_reminder_due(&mut self, window_id: &str, interval: u64) -> bool {
|
||||||
|
self.model_requests_since_delivery = self.model_requests_since_delivery.saturating_add(1);
|
||||||
|
let reminder_is_due = self.last_window_id.as_deref() != Some(window_id)
|
||||||
|
|| self.model_requests_since_delivery >= interval;
|
||||||
|
|
||||||
|
if reminder_is_due {
|
||||||
|
self.model_requests_since_delivery = 0;
|
||||||
|
self.last_window_id = Some(window_id.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
reminder_is_due
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn maybe_record_current_time_reminder(
|
||||||
|
sess: &Session,
|
||||||
|
turn_context: &TurnContext,
|
||||||
|
window_id: &str,
|
||||||
|
) -> CodexResult<()> {
|
||||||
|
let Some(config) = turn_context.config.current_time_reminder else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let reminder_is_due = {
|
||||||
|
let mut state = sess.state.lock().await;
|
||||||
|
state
|
||||||
|
.current_time_reminder
|
||||||
|
.take_reminder_due(window_id, config.reminder_interval_model_requests)
|
||||||
|
};
|
||||||
|
if !reminder_is_due {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_time = sess
|
||||||
|
.services
|
||||||
|
.time_provider
|
||||||
|
.current_time(sess.thread_id)
|
||||||
|
.await
|
||||||
|
.map_err(|err| CodexErr::Fatal(format!("failed to read current time: {err:#}")))?;
|
||||||
|
|
||||||
|
let response_item =
|
||||||
|
ContextualUserFragment::into(crate::context::CurrentTimeReminder::new(current_time));
|
||||||
|
sess.record_conversation_items(turn_context, std::slice::from_ref(&response_item))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -226,34 +226,50 @@ pub(crate) async fn run_turn(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Construct the input that we will send to the model.
|
let sampling_request_result: CodexResult<_> = async {
|
||||||
let sampling_request_input: Vec<ResponseItem> = async {
|
super::time_reminder::maybe_record_current_time_reminder(
|
||||||
sess.clone_history()
|
sess.as_ref(),
|
||||||
.await
|
turn_context.as_ref(),
|
||||||
.for_prompt(&turn_context.model_info.input_modalities)
|
&window_id,
|
||||||
}
|
)
|
||||||
.instrument(trace_span!("run_turn.prepare_sampling_request_input"))
|
.await?;
|
||||||
.await;
|
|
||||||
|
|
||||||
let responses_metadata = turn_context.turn_metadata_state.to_responses_metadata(
|
// Construct the input that we will send to the model.
|
||||||
sess.installation_id.clone(),
|
let sampling_request_input: Vec<ResponseItem> = async {
|
||||||
window_id,
|
sess.clone_history()
|
||||||
CodexResponsesRequestKind::Turn,
|
.await
|
||||||
);
|
.for_prompt(&turn_context.model_info.input_modalities)
|
||||||
let tokens_before_sampling = sess.get_total_token_usage().await;
|
}
|
||||||
match run_sampling_request(
|
.instrument(trace_span!("run_turn.prepare_sampling_request_input"))
|
||||||
Arc::clone(&sess),
|
.await;
|
||||||
Arc::clone(&turn_context),
|
|
||||||
Arc::clone(&turn_extension_data),
|
let responses_metadata = turn_context.turn_metadata_state.to_responses_metadata(
|
||||||
Arc::clone(&turn_diff_tracker),
|
sess.installation_id.clone(),
|
||||||
&mut client_session,
|
window_id,
|
||||||
&responses_metadata,
|
CodexResponsesRequestKind::Turn,
|
||||||
sampling_request_input,
|
);
|
||||||
cancellation_token.child_token(),
|
let tokens_before_sampling = sess.get_total_token_usage().await;
|
||||||
)
|
let (sampling_request_output, sampling_request_input) = run_sampling_request(
|
||||||
.await
|
Arc::clone(&sess),
|
||||||
{
|
Arc::clone(&turn_context),
|
||||||
Ok((sampling_request_output, sampling_request_input)) => {
|
Arc::clone(&turn_extension_data),
|
||||||
|
Arc::clone(&turn_diff_tracker),
|
||||||
|
&mut client_session,
|
||||||
|
&responses_metadata,
|
||||||
|
sampling_request_input,
|
||||||
|
cancellation_token.child_token(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
tokens_before_sampling,
|
||||||
|
sampling_request_output,
|
||||||
|
sampling_request_input,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
match sampling_request_result {
|
||||||
|
Ok((tokens_before_sampling, sampling_request_output, sampling_request_input)) => {
|
||||||
let SamplingRequestResult {
|
let SamplingRequestResult {
|
||||||
needs_follow_up: model_needs_follow_up,
|
needs_follow_up: model_needs_follow_up,
|
||||||
last_agent_message: sampling_request_last_agent_message,
|
last_agent_message: sampling_request_last_agent_message,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::attestation::AttestationProvider;
|
|||||||
use crate::client::ModelClient;
|
use crate::client::ModelClient;
|
||||||
use crate::config::NetworkProxyAuditMetadata;
|
use crate::config::NetworkProxyAuditMetadata;
|
||||||
use crate::config::StartedNetworkProxy;
|
use crate::config::StartedNetworkProxy;
|
||||||
|
use crate::current_time::TimeProvider;
|
||||||
use crate::environment_selection::ThreadEnvironments;
|
use crate::environment_selection::ThreadEnvironments;
|
||||||
use crate::exec_policy::ExecPolicyManager;
|
use crate::exec_policy::ExecPolicyManager;
|
||||||
use crate::guardian::GuardianRejection;
|
use crate::guardian::GuardianRejection;
|
||||||
@@ -79,6 +80,7 @@ pub(crate) struct SessionServices {
|
|||||||
pub(crate) live_thread: Option<LiveThread>,
|
pub(crate) live_thread: Option<LiveThread>,
|
||||||
pub(crate) thread_store: Arc<dyn ThreadStore>,
|
pub(crate) thread_store: Arc<dyn ThreadStore>,
|
||||||
pub(crate) attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
pub(crate) attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||||||
|
pub(crate) time_provider: Arc<dyn TimeProvider>,
|
||||||
/// Session-scoped model client shared across turns.
|
/// Session-scoped model client shared across turns.
|
||||||
pub(crate) model_client: ModelClient,
|
pub(crate) model_client: ModelClient,
|
||||||
pub(crate) code_mode_service: CodeModeService,
|
pub(crate) code_mode_service: CodeModeService,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use super::auto_compact_window::AutoCompactWindowSnapshot;
|
|||||||
use crate::context_manager::ContextManager;
|
use crate::context_manager::ContextManager;
|
||||||
use crate::session::PreviousTurnSettings;
|
use crate::session::PreviousTurnSettings;
|
||||||
use crate::session::session::SessionConfiguration;
|
use crate::session::session::SessionConfiguration;
|
||||||
|
use crate::session::time_reminder::CurrentTimeReminderState;
|
||||||
use crate::session_startup_prewarm::SessionStartupPrewarmHandle;
|
use crate::session_startup_prewarm::SessionStartupPrewarmHandle;
|
||||||
use codex_protocol::protocol::RateLimitSnapshot;
|
use codex_protocol::protocol::RateLimitSnapshot;
|
||||||
use codex_protocol::protocol::TokenUsage;
|
use codex_protocol::protocol::TokenUsage;
|
||||||
@@ -36,6 +37,7 @@ pub(crate) struct SessionState {
|
|||||||
auto_compact_window: AutoCompactWindow,
|
auto_compact_window: AutoCompactWindow,
|
||||||
/// Startup prewarmed session prepared during session initialization.
|
/// Startup prewarmed session prepared during session initialization.
|
||||||
pub(crate) startup_prewarm: Option<SessionStartupPrewarmHandle>,
|
pub(crate) startup_prewarm: Option<SessionStartupPrewarmHandle>,
|
||||||
|
pub(crate) current_time_reminder: CurrentTimeReminderState,
|
||||||
pub(crate) active_connector_selection: HashSet<String>,
|
pub(crate) active_connector_selection: HashSet<String>,
|
||||||
pub(crate) pending_session_start_sources: VecDeque<codex_hooks::SessionStartSource>,
|
pub(crate) pending_session_start_sources: VecDeque<codex_hooks::SessionStartSource>,
|
||||||
granted_permissions_by_environment_id: HashMap<String, AdditionalPermissionProfile>,
|
granted_permissions_by_environment_id: HashMap<String, AdditionalPermissionProfile>,
|
||||||
@@ -56,6 +58,7 @@ impl SessionState {
|
|||||||
previous_turn_settings: None,
|
previous_turn_settings: None,
|
||||||
auto_compact_window: AutoCompactWindow::new(),
|
auto_compact_window: AutoCompactWindow::new(),
|
||||||
startup_prewarm: None,
|
startup_prewarm: None,
|
||||||
|
current_time_reminder: CurrentTimeReminderState::default(),
|
||||||
active_connector_selection: HashSet::new(),
|
active_connector_selection: HashSet::new(),
|
||||||
pending_session_start_sources: VecDeque::new(),
|
pending_session_start_sources: VecDeque::new(),
|
||||||
granted_permissions_by_environment_id: HashMap::new(),
|
granted_permissions_by_environment_id: HashMap::new(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::attestation::AttestationProvider;
|
|||||||
use crate::codex_thread::CodexThread;
|
use crate::codex_thread::CodexThread;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::config::ThreadStoreConfig;
|
use crate::config::ThreadStoreConfig;
|
||||||
|
use crate::current_time::TimeProvider;
|
||||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||||
use crate::environment_selection::default_thread_environment_selections;
|
use crate::environment_selection::default_thread_environment_selections;
|
||||||
use crate::mcp::McpManager;
|
use crate::mcp::McpManager;
|
||||||
@@ -215,6 +216,7 @@ pub(crate) struct ThreadManagerState {
|
|||||||
user_instructions_provider: Arc<dyn UserInstructionsProvider>,
|
user_instructions_provider: Arc<dyn UserInstructionsProvider>,
|
||||||
thread_store: Arc<dyn ThreadStore>,
|
thread_store: Arc<dyn ThreadStore>,
|
||||||
attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||||||
|
external_time_provider: Option<Arc<dyn TimeProvider>>,
|
||||||
session_source: SessionSource,
|
session_source: SessionSource,
|
||||||
installation_id: String,
|
installation_id: String,
|
||||||
analytics_events_client: Option<AnalyticsEventsClient>,
|
analytics_events_client: Option<AnalyticsEventsClient>,
|
||||||
@@ -269,6 +271,7 @@ impl ThreadManager {
|
|||||||
state_db: Option<StateDbHandle>,
|
state_db: Option<StateDbHandle>,
|
||||||
installation_id: String,
|
installation_id: String,
|
||||||
attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||||||
|
external_time_provider: Option<Arc<dyn TimeProvider>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let codex_home = config.codex_home.clone();
|
let codex_home = config.codex_home.clone();
|
||||||
let restriction_product = session_source.restriction_product();
|
let restriction_product = session_source.restriction_product();
|
||||||
@@ -300,6 +303,7 @@ impl ThreadManager {
|
|||||||
user_instructions_provider,
|
user_instructions_provider,
|
||||||
thread_store,
|
thread_store,
|
||||||
attestation_provider,
|
attestation_provider,
|
||||||
|
external_time_provider,
|
||||||
auth_manager,
|
auth_manager,
|
||||||
session_source,
|
session_source,
|
||||||
installation_id,
|
installation_id,
|
||||||
@@ -405,6 +409,7 @@ impl ThreadManager {
|
|||||||
),
|
),
|
||||||
thread_store,
|
thread_store,
|
||||||
attestation_provider: None,
|
attestation_provider: None,
|
||||||
|
external_time_provider: None,
|
||||||
auth_manager,
|
auth_manager,
|
||||||
session_source: SessionSource::Exec,
|
session_source: SessionSource::Exec,
|
||||||
installation_id,
|
installation_id,
|
||||||
@@ -1466,6 +1471,7 @@ impl ThreadManagerState {
|
|||||||
analytics_events_client: self.analytics_events_client.clone(),
|
analytics_events_client: self.analytics_events_client.clone(),
|
||||||
thread_store: Arc::clone(&self.thread_store),
|
thread_store: Arc::clone(&self.thread_store),
|
||||||
attestation_provider: self.attestation_provider.clone(),
|
attestation_provider: self.attestation_provider.clone(),
|
||||||
|
external_time_provider: self.external_time_provider.clone(),
|
||||||
inherited_multi_agent_version: multi_agent_version,
|
inherited_multi_agent_version: multi_agent_version,
|
||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -440,6 +440,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
|||||||
/*state_db*/ None,
|
/*state_db*/ None,
|
||||||
TEST_INSTALLATION_ID.to_string(),
|
TEST_INSTALLATION_ID.to_string(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
let selected_root_init = |id: &str, environment_id: &str| {
|
let selected_root_init = |id: &str, environment_id: &str| {
|
||||||
let mut init = codex_extension_api::ExtensionDataInit::new();
|
let mut init = codex_extension_api::ExtensionDataInit::new();
|
||||||
@@ -548,6 +549,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
|||||||
/*state_db*/ None,
|
/*state_db*/ None,
|
||||||
TEST_INSTALLATION_ID.to_string(),
|
TEST_INSTALLATION_ID.to_string(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
let selected_cwd =
|
let selected_cwd =
|
||||||
AbsolutePathBuf::try_from(config.cwd.as_path().join("selected")).expect("absolute path");
|
AbsolutePathBuf::try_from(config.cwd.as_path().join("selected")).expect("absolute path");
|
||||||
@@ -671,6 +673,7 @@ async fn explicit_installation_id_skips_codex_home_file() {
|
|||||||
state_db.clone(),
|
state_db.clone(),
|
||||||
installation_id.clone(),
|
installation_id.clone(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let thread = manager
|
let thread = manager
|
||||||
@@ -711,6 +714,7 @@ async fn resume_active_thread_from_rollout_returns_running_thread() {
|
|||||||
/*state_db*/ None,
|
/*state_db*/ None,
|
||||||
TEST_INSTALLATION_ID.to_string(),
|
TEST_INSTALLATION_ID.to_string(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let source = manager
|
let source = manager
|
||||||
@@ -770,6 +774,7 @@ async fn resume_stopped_thread_from_rollout_spawns_new_thread() {
|
|||||||
/*state_db*/ None,
|
/*state_db*/ None,
|
||||||
TEST_INSTALLATION_ID.to_string(),
|
TEST_INSTALLATION_ID.to_string(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let source = manager
|
let source = manager
|
||||||
@@ -836,6 +841,7 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
|
|||||||
state_db.clone(),
|
state_db.clone(),
|
||||||
TEST_INSTALLATION_ID.to_string(),
|
TEST_INSTALLATION_ID.to_string(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let source = manager
|
let source = manager
|
||||||
@@ -929,6 +935,7 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
|
|||||||
state_db,
|
state_db,
|
||||||
TEST_INSTALLATION_ID.to_string(),
|
TEST_INSTALLATION_ID.to_string(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let source = manager
|
let source = manager
|
||||||
@@ -1033,6 +1040,7 @@ async fn new_uses_active_provider_for_model_refresh() {
|
|||||||
/*state_db*/ None,
|
/*state_db*/ None,
|
||||||
TEST_INSTALLATION_ID.to_string(),
|
TEST_INSTALLATION_ID.to_string(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = manager.list_models(RefreshStrategy::Online).await;
|
let _ = manager.list_models(RefreshStrategy::Online).await;
|
||||||
@@ -1254,6 +1262,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
|
|||||||
state_db.clone(),
|
state_db.clone(),
|
||||||
TEST_INSTALLATION_ID.to_string(),
|
TEST_INSTALLATION_ID.to_string(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let source = manager
|
let source = manager
|
||||||
@@ -1362,6 +1371,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
|
|||||||
state_db.clone(),
|
state_db.clone(),
|
||||||
TEST_INSTALLATION_ID.to_string(),
|
TEST_INSTALLATION_ID.to_string(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let source = manager
|
let source = manager
|
||||||
@@ -1460,6 +1470,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
|
|||||||
state_db.clone(),
|
state_db.clone(),
|
||||||
TEST_INSTALLATION_ID.to_string(),
|
TEST_INSTALLATION_ID.to_string(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let source = manager
|
let source = manager
|
||||||
|
|||||||
@@ -4279,6 +4279,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
|
|||||||
state_db.clone(),
|
state_db.clone(),
|
||||||
"11111111-1111-4111-8111-111111111111".to_string(),
|
"11111111-1111-4111-8111-111111111111".to_string(),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let parent = manager
|
let parent = manager
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use codex_config::CloudConfigBundleLoader;
|
|||||||
use codex_core::CodexThread;
|
use codex_core::CodexThread;
|
||||||
use codex_core::StartThreadOptions;
|
use codex_core::StartThreadOptions;
|
||||||
use codex_core::ThreadManager;
|
use codex_core::ThreadManager;
|
||||||
|
use codex_core::TimeProvider;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::resolve_installation_id;
|
use codex_core::resolve_installation_id;
|
||||||
use codex_core::shell::Shell;
|
use codex_core::shell::Shell;
|
||||||
@@ -261,6 +262,7 @@ pub struct TestCodexBuilder {
|
|||||||
extensions: Arc<ExtensionRegistry<Config>>,
|
extensions: Arc<ExtensionRegistry<Config>>,
|
||||||
user_instructions_provider: Option<Arc<dyn UserInstructionsProvider>>,
|
user_instructions_provider: Option<Arc<dyn UserInstructionsProvider>>,
|
||||||
supports_openai_form_elicitation: bool,
|
supports_openai_form_elicitation: bool,
|
||||||
|
external_time_provider: Option<Arc<dyn TimeProvider>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestCodexBuilder {
|
impl TestCodexBuilder {
|
||||||
@@ -362,6 +364,11 @@ impl TestCodexBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_external_time_provider(mut self, provider: Arc<dyn TimeProvider>) -> Self {
|
||||||
|
self.external_time_provider = Some(provider);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_windows_cmd_shell(self) -> Self {
|
pub fn with_windows_cmd_shell(self) -> Self {
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
self.with_user_shell(get_shell_by_model_provided_path(&PathBuf::from("cmd.exe")))
|
self.with_user_shell(get_shell_by_model_provided_path(&PathBuf::from("cmd.exe")))
|
||||||
@@ -568,6 +575,7 @@ impl TestCodexBuilder {
|
|||||||
state_db.clone(),
|
state_db.clone(),
|
||||||
installation_id,
|
installation_id,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ self.external_time_provider.clone(),
|
||||||
);
|
);
|
||||||
let thread_manager = Arc::new(thread_manager);
|
let thread_manager = Arc::new(thread_manager);
|
||||||
let user_shell_override = self.user_shell_override.clone();
|
let user_shell_override = self.user_shell_override.clone();
|
||||||
@@ -1172,6 +1180,7 @@ pub fn test_codex() -> TestCodexBuilder {
|
|||||||
extensions: empty_extension_registry(),
|
extensions: empty_extension_registry(),
|
||||||
user_instructions_provider: None,
|
user_instructions_provider: None,
|
||||||
supports_openai_form_elicitation: false,
|
supports_openai_form_elicitation: false,
|
||||||
|
external_time_provider: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1259,6 +1259,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
|
|||||||
/*state_db*/ None,
|
/*state_db*/ None,
|
||||||
installation_id,
|
installation_id,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
let NewThread { thread: codex, .. } = thread_manager
|
let NewThread { thread: codex, .. } = thread_manager
|
||||||
.start_thread(config.clone())
|
.start_thread(config.clone())
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::AtomicI64;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use chrono::DateTime;
|
||||||
|
use chrono::Utc;
|
||||||
|
use codex_core::TimeFuture;
|
||||||
|
use codex_core::TimeProvider;
|
||||||
|
use codex_core::config::CurrentTimeReminderConfig;
|
||||||
|
use codex_features::CurrentTimeSource;
|
||||||
|
use codex_features::Feature;
|
||||||
|
use codex_model_provider_info::built_in_model_providers;
|
||||||
|
use codex_protocol::ThreadId;
|
||||||
|
use codex_protocol::models::PermissionProfile;
|
||||||
|
use codex_protocol::protocol::CodexErrorInfo;
|
||||||
|
use codex_protocol::protocol::EventMsg;
|
||||||
|
use codex_protocol::protocol::Op;
|
||||||
|
use codex_protocol::user_input::UserInput;
|
||||||
|
use core_test_support::assert_regex_match;
|
||||||
|
use core_test_support::responses::ResponsesRequest;
|
||||||
|
use core_test_support::responses::ev_assistant_message;
|
||||||
|
use core_test_support::responses::ev_completed;
|
||||||
|
use core_test_support::responses::ev_function_call;
|
||||||
|
use core_test_support::responses::ev_response_created;
|
||||||
|
use core_test_support::responses::mount_sse_once;
|
||||||
|
use core_test_support::responses::mount_sse_sequence;
|
||||||
|
use core_test_support::responses::sse;
|
||||||
|
use core_test_support::responses::start_mock_server;
|
||||||
|
use core_test_support::skip_if_no_network;
|
||||||
|
use core_test_support::test_codex::test_codex;
|
||||||
|
use core_test_support::wait_for_event;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
const FIRST_REMINDER: &str = "It is 2026-06-17 17:34:15 UTC.";
|
||||||
|
const SECOND_REMINDER: &str = "It is 2026-06-17 17:35:15 UTC.";
|
||||||
|
const FIRST_TIME_UNIX_SECONDS: i64 = 1_781_717_655;
|
||||||
|
|
||||||
|
struct TestTimeProvider(AtomicI64);
|
||||||
|
|
||||||
|
impl Default for TestTimeProvider {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(AtomicI64::new(FIRST_TIME_UNIX_SECONDS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeProvider for TestTimeProvider {
|
||||||
|
fn current_time(&self, _thread_id: ThreadId) -> TimeFuture<'_> {
|
||||||
|
let timestamp = self.0.fetch_add(60, Ordering::Relaxed);
|
||||||
|
Box::pin(async move {
|
||||||
|
Ok(DateTime::<Utc>::from_timestamp(timestamp, 0)
|
||||||
|
.expect("test timestamp should be valid"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FailingTimeProvider;
|
||||||
|
|
||||||
|
impl TimeProvider for FailingTimeProvider {
|
||||||
|
fn current_time(&self, _thread_id: ThreadId) -> TimeFuture<'_> {
|
||||||
|
Box::pin(async { Err(anyhow!("test clock unavailable")) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_time_reminders(request: &ResponsesRequest) -> Vec<String> {
|
||||||
|
request
|
||||||
|
.message_input_texts("developer")
|
||||||
|
.into_iter()
|
||||||
|
.filter(|text| text.starts_with("It is "))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enable_current_time_reminder(
|
||||||
|
config: &mut codex_core::config::Config,
|
||||||
|
interval: u64,
|
||||||
|
clock_source: CurrentTimeSource,
|
||||||
|
) {
|
||||||
|
config
|
||||||
|
.features
|
||||||
|
.enable(Feature::CurrentTimeReminder)
|
||||||
|
.expect("test config should allow current-time reminders");
|
||||||
|
config.current_time_reminder = Some(CurrentTimeReminderConfig {
|
||||||
|
reminder_interval_model_requests: interval,
|
||||||
|
clock_source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn current_time_reminders_follow_request_interval_and_persist_in_history() -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let tool_args = json!({
|
||||||
|
"command": "echo current time",
|
||||||
|
"timeout_ms": 1_000,
|
||||||
|
});
|
||||||
|
let responses = mount_sse_sequence(
|
||||||
|
&server,
|
||||||
|
vec![
|
||||||
|
sse(vec![
|
||||||
|
ev_response_created("resp-1"),
|
||||||
|
ev_function_call(
|
||||||
|
"current-time-tool-call",
|
||||||
|
"shell_command",
|
||||||
|
&serde_json::to_string(&tool_args)?,
|
||||||
|
),
|
||||||
|
ev_completed("resp-1"),
|
||||||
|
]),
|
||||||
|
sse(vec![
|
||||||
|
ev_response_created("resp-2"),
|
||||||
|
ev_assistant_message("msg-2", "done"),
|
||||||
|
ev_completed("resp-2"),
|
||||||
|
]),
|
||||||
|
sse(vec![ev_response_created("resp-3"), ev_completed("resp-3")]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let test = test_codex()
|
||||||
|
.with_config(|config| {
|
||||||
|
enable_current_time_reminder(config, /*interval*/ 2, CurrentTimeSource::External)
|
||||||
|
})
|
||||||
|
.with_external_time_provider(Arc::new(TestTimeProvider::default()))
|
||||||
|
.build(&server)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
test.submit_turn_with_permission_profile("first turn", PermissionProfile::Disabled)
|
||||||
|
.await?;
|
||||||
|
test.submit_turn("second turn").await?;
|
||||||
|
|
||||||
|
let requests = responses.requests();
|
||||||
|
assert_eq!(requests.len(), 3);
|
||||||
|
assert_eq!(current_time_reminders(&requests[0]), vec![FIRST_REMINDER]);
|
||||||
|
assert_eq!(current_time_reminders(&requests[1]), vec![FIRST_REMINDER]);
|
||||||
|
assert_eq!(
|
||||||
|
current_time_reminders(&requests[2]),
|
||||||
|
vec![FIRST_REMINDER, SECOND_REMINDER]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn system_time_source_adds_current_time_reminder() -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let responses = mount_sse_once(
|
||||||
|
&server,
|
||||||
|
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let test = test_codex()
|
||||||
|
.with_config(|config| {
|
||||||
|
enable_current_time_reminder(config, /*interval*/ 1, CurrentTimeSource::System)
|
||||||
|
})
|
||||||
|
.build(&server)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
test.submit_turn("what time is it?").await?;
|
||||||
|
|
||||||
|
let reminders = current_time_reminders(&responses.single_request());
|
||||||
|
assert_eq!(reminders.len(), 1);
|
||||||
|
assert_regex_match(
|
||||||
|
r"^It is \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC\.$",
|
||||||
|
&reminders[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn current_time_reminder_is_refreshed_after_compaction() -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let responses = mount_sse_sequence(
|
||||||
|
&server,
|
||||||
|
vec![
|
||||||
|
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||||
|
sse(vec![
|
||||||
|
ev_response_created("resp-compact"),
|
||||||
|
ev_assistant_message("msg-compact", "compact summary"),
|
||||||
|
ev_completed("resp-compact"),
|
||||||
|
]),
|
||||||
|
sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let mut model_provider = built_in_model_providers(/*openai_base_url*/ None)["openai"].clone();
|
||||||
|
model_provider.name = "OpenAI-compatible test provider".to_string();
|
||||||
|
model_provider.base_url = Some(format!("{}/v1", server.uri()));
|
||||||
|
model_provider.supports_websockets = false;
|
||||||
|
let test = test_codex()
|
||||||
|
.with_config(move |config| {
|
||||||
|
config.model_provider = model_provider;
|
||||||
|
enable_current_time_reminder(config, /*interval*/ 50, CurrentTimeSource::External);
|
||||||
|
})
|
||||||
|
.with_external_time_provider(Arc::new(TestTimeProvider::default()))
|
||||||
|
.build(&server)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
test.submit_turn("before compact").await?;
|
||||||
|
test.codex.submit(Op::Compact).await?;
|
||||||
|
wait_for_event(&test.codex, |event| {
|
||||||
|
matches!(event, EventMsg::TurnComplete(_))
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test.submit_turn("after compact").await?;
|
||||||
|
|
||||||
|
let requests = responses.requests();
|
||||||
|
assert_eq!(requests.len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
current_time_reminders(&requests[2]),
|
||||||
|
vec![SECOND_REMINDER],
|
||||||
|
"a new context window should force a fresh reminder before the next model request"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn time_provider_failure_stops_before_inference() -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let responses = mount_sse_once(
|
||||||
|
&server,
|
||||||
|
sse(vec![
|
||||||
|
ev_response_created("unused-response"),
|
||||||
|
ev_completed("unused-response"),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let test = test_codex()
|
||||||
|
.with_config(|config| {
|
||||||
|
enable_current_time_reminder(config, /*interval*/ 1, CurrentTimeSource::External)
|
||||||
|
})
|
||||||
|
.with_external_time_provider(Arc::new(FailingTimeProvider))
|
||||||
|
.build(&server)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
test.codex
|
||||||
|
.submit(Op::UserInput {
|
||||||
|
items: vec![UserInput::Text {
|
||||||
|
text: "fail before inference".into(),
|
||||||
|
text_elements: Vec::new(),
|
||||||
|
}],
|
||||||
|
final_output_json_schema: None,
|
||||||
|
responsesapi_client_metadata: None,
|
||||||
|
additional_context: Default::default(),
|
||||||
|
thread_settings: Default::default(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let EventMsg::Error(error) =
|
||||||
|
wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await
|
||||||
|
else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
error.message,
|
||||||
|
"Fatal error: failed to read current time: test clock unavailable"
|
||||||
|
);
|
||||||
|
assert_eq!(error.codex_error_info, Some(CodexErrorInfo::Other));
|
||||||
|
|
||||||
|
wait_for_event(&test.codex, |event| {
|
||||||
|
matches!(event, EventMsg::TurnComplete(_))
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(responses.requests().is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ mod compact;
|
|||||||
mod compact_remote;
|
mod compact_remote;
|
||||||
mod compact_remote_parity;
|
mod compact_remote_parity;
|
||||||
mod compact_resume_fork;
|
mod compact_resume_fork;
|
||||||
|
mod current_time_reminder;
|
||||||
mod deprecation_notice;
|
mod deprecation_notice;
|
||||||
mod exec;
|
mod exec;
|
||||||
mod exec_policy;
|
mod exec_policy;
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ impl MessageProcessor {
|
|||||||
state_db.clone(),
|
state_db.clone(),
|
||||||
installation_id,
|
installation_id,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
));
|
));
|
||||||
Self {
|
Self {
|
||||||
outgoing,
|
outgoing,
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
state_db,
|
state_db,
|
||||||
installation_id,
|
installation_id,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
/*external_time_provider*/ None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let NewThread {
|
let NewThread {
|
||||||
|
|||||||
Reference in new issue
Block a user