Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/app-server/src/mcp_refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ mod tests {
Some(state_db.clone()),
"11111111-1111-4111-8111-111111111111".to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
)
});
thread_manager.start_thread(good_config).await?;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ impl MessageProcessor {
outgoing.clone(),
thread_state_manager.clone(),
)),
/*external_time_provider*/ None,
)
});
let models_manager = thread_manager.get_models_manager();
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/codex_delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ pub(crate) async fn run_codex_thread_interactive(
analytics_events_client: Some(parent_session.services.analytics_events_client.clone()),
thread_store: Arc::clone(&parent_session.services.thread_store),
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),
}))
.or_cancel(&cancel_token)
Expand Down
35 changes: 35 additions & 0 deletions codex-rs/core/src/context/current_time_reminder.rs
Original file line number Diff line number Diff line change
@@ -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")
)
}
}
2 changes: 2 additions & 0 deletions codex-rs/core/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod available_plugins_instructions;
mod available_skills_instructions;
mod collaboration_mode_instructions;
mod contextual_user_message;
mod current_time_reminder;
mod environment_context;
mod guardian_followup_review_reminder;
mod hook_additional_context;
Expand Down Expand Up @@ -44,6 +45,7 @@ pub(crate) use codex_core_skills::SkillInstructions;
pub(crate) use collaboration_mode_instructions::CollaborationModeInstructions;
pub(crate) use contextual_user_message::is_contextual_user_fragment;
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 guardian_followup_review_reminder::GuardianFollowupReviewReminder;
pub(crate) use hook_additional_context::HookAdditionalContext;
Expand Down
41 changes: 41 additions & 0 deletions codex-rs/core/src/current_time.rs
Original file line number Diff line number Diff line change
@@ -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"
)
}),
}
}
3 changes: 3 additions & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub mod config;
pub mod connectors;
pub mod context;
mod context_manager;
mod current_time;
mod environment_selection;
pub mod exec;
pub mod exec_env;
Expand Down Expand Up @@ -186,6 +187,8 @@ pub use client_common::ResponseEvent;
pub use client_common::ResponseStream;
pub use codex_prompts::REVIEW_PROMPT;
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 exec_policy::ExecPolicyError;
pub use exec_policy::check_execpolicy_for_warnings;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/prompt_debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub async fn build_prompt_input(
state_db.clone(),
installation_id,
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
let thread = thread_manager.start_thread(config).await?;

Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use crate::context::NetworkRuleSaved;
use crate::context::PermissionsInstructions;
use crate::context::PersonalitySpecInstructions;
use crate::context::RecommendedPluginsInstructions;
use crate::current_time::TimeProvider;
use crate::default_skill_metadata_budget;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::exec_policy::ExecPolicyManager;
Expand Down Expand Up @@ -215,6 +216,7 @@ mod rollout_budget;
mod rollout_reconstruction;
#[allow(clippy::module_inception)]
pub(crate) mod session;
pub(crate) mod time_reminder;
mod token_budget;
pub(crate) mod turn;
pub(crate) mod turn_context;
Expand Down Expand Up @@ -439,6 +441,7 @@ pub(crate) struct CodexSpawnArgs {
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
pub(crate) thread_store: Arc<dyn ThreadStore>,
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>,
}

Expand Down Expand Up @@ -522,6 +525,7 @@ impl Codex {
analytics_events_client,
thread_store,
attestation_provider,
external_time_provider,
inherited_multi_agent_version,
} = args;
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
Expand Down Expand Up @@ -669,6 +673,7 @@ impl Codex {
thread_store,
parent_rollout_thread_trace,
attestation_provider,
external_time_provider,
multi_agent_version,
))
.await
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/core/src/session/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ impl Session {
thread_store: Arc<dyn ThreadStore>,
parent_rollout_thread_trace: ThreadTraceContext,
attestation_provider: Option<Arc<dyn AttestationProvider>>,
external_time_provider: Option<Arc<dyn TimeProvider>>,
multi_agent_version: Option<MultiAgentVersion>,
) -> anyhow::Result<Arc<Self>> {
debug!(
Expand All @@ -516,6 +517,10 @@ impl Session {
}
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 thread_extension_data = codex_extension_api::ExtensionData::new_with_init(
thread_id.to_string(),
Expand Down Expand Up @@ -1022,6 +1027,7 @@ impl Session {
live_thread: live_thread_init.as_ref().cloned(),
thread_store: Arc::clone(&thread_store),
attestation_provider: attestation_provider.clone(),
time_provider,
model_client: ModelClient::new(
Some(Arc::clone(&auth_manager)),
thread_id,
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4867,6 +4867,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
)),
codex_rollout_trace::ThreadTraceContext::disabled(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
Some(config.multi_agent_version_from_features()),
)
.await;
Expand Down Expand Up @@ -5032,6 +5033,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
/*state_db*/ None,
)),
attestation_provider: None,
time_provider: Arc::new(crate::current_time::SystemTimeProvider),
model_client: ModelClient::new(
Some(auth_manager.clone()),
thread_id,
Expand Down Expand Up @@ -5216,6 +5218,7 @@ async fn make_session_with_config_and_rx(
)),
codex_rollout_trace::ThreadTraceContext::disabled(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
Some(config.multi_agent_version_from_features()),
)
.await?;
Expand Down Expand Up @@ -5328,6 +5331,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
)),
codex_rollout_trace::ThreadTraceContext::disabled(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
Some(config.multi_agent_version_from_features()),
)
.await?;
Expand Down Expand Up @@ -7078,6 +7082,7 @@ where
state_db,
)),
attestation_provider: None,
time_provider: Arc::new(crate::current_time::SystemTimeProvider),
model_client: ModelClient::new(
Some(Arc::clone(&auth_manager)),
thread_id,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/session/tests/guardian_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
analytics_events_client: None,
thread_store,
attestation_provider: None,
external_time_provider: None,
inherited_multi_agent_version: None,
})
.await
Expand Down
61 changes: 61 additions & 0 deletions codex-rs/core/src/session/time_reminder.rs
Original file line number Diff line number Diff line change
@@ -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))
Comment thread
pakrym-oai marked this conversation as resolved.
.await;

Ok(())
}
68 changes: 42 additions & 26 deletions codex-rs/core/src/session/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,34 +226,50 @@ pub(crate) async fn run_turn(
)
.await;

// Construct the input that we will send to the model.
let sampling_request_input: Vec<ResponseItem> = async {
sess.clone_history()
.await
.for_prompt(&turn_context.model_info.input_modalities)
let sampling_request_result: CodexResult<_> = async {
super::time_reminder::maybe_record_current_time_reminder(
sess.as_ref(),
turn_context.as_ref(),
&window_id,
)
.await?;

// Construct the input that we will send to the model.
let sampling_request_input: Vec<ResponseItem> = async {
sess.clone_history()
.await
.for_prompt(&turn_context.model_info.input_modalities)
}
.instrument(trace_span!("run_turn.prepare_sampling_request_input"))
.await;

let responses_metadata = turn_context.turn_metadata_state.to_responses_metadata(
sess.installation_id.clone(),
window_id,
CodexResponsesRequestKind::Turn,
);
let tokens_before_sampling = sess.get_total_token_usage().await;
let (sampling_request_output, sampling_request_input) = run_sampling_request(
Arc::clone(&sess),
Arc::clone(&turn_context),
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,
))
}
.instrument(trace_span!("run_turn.prepare_sampling_request_input"))
.await;

let responses_metadata = turn_context.turn_metadata_state.to_responses_metadata(
sess.installation_id.clone(),
window_id,
CodexResponsesRequestKind::Turn,
);
let tokens_before_sampling = sess.get_total_token_usage().await;
match run_sampling_request(
Arc::clone(&sess),
Arc::clone(&turn_context),
Arc::clone(&turn_extension_data),
Arc::clone(&turn_diff_tracker),
&mut client_session,
&responses_metadata,
sampling_request_input,
cancellation_token.child_token(),
)
.await
{
Ok((sampling_request_output, sampling_request_input)) => {
match sampling_request_result {
Ok((tokens_before_sampling, sampling_request_output, sampling_request_input)) => {
let SamplingRequestResult {
needs_follow_up: model_needs_follow_up,
last_agent_message: sampling_request_last_agent_message,
Expand Down
Loading
Loading