Skip to content

Commit 72b8aa9

Browse files
committed
Codex Infinity v1.3.27 - merge upstream + maintain custom features
1 parent ee6f094 commit 72b8aa9

8 files changed

Lines changed: 383 additions & 23 deletions

File tree

codex-cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@codex-infinity/codex-infinity",
3-
"version": "1.3.26",
3+
"version": "1.3.27",
44
"license": "Apache-2.0",
55
"description": "Codex Infinity - a smarter coding agent that can run forever",
66
"bin": {

codex-rs/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
//! Generates the next "auto-next" prompt via a lightweight model call.
2+
//!
3+
//! Used by the TUI when `--auto-next-steps` or `--auto-next-idea` is enabled.
4+
//! Instead of picking from a hardcoded template, this asks the model to craft
5+
//! a concrete follow-up prompt grounded in the recent rollout transcript.
6+
//!
7+
//! The caller is responsible for appending any DONE-file suffix.
8+
9+
use std::path::PathBuf;
10+
use std::sync::Arc;
11+
12+
use codex_login::AuthManager;
13+
use codex_model_provider_info::ModelProviderInfo;
14+
use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig;
15+
use codex_models_manager::manager::ModelsManager;
16+
use codex_otel::SessionTelemetry;
17+
use codex_protocol::ThreadId;
18+
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
19+
use codex_protocol::models::BaseInstructions;
20+
use codex_protocol::models::ContentItem;
21+
use codex_protocol::models::ResponseItem;
22+
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
23+
use codex_protocol::protocol::RolloutItem;
24+
use codex_protocol::protocol::SessionSource;
25+
use futures::StreamExt;
26+
use serde::Deserialize;
27+
use serde_json::json;
28+
29+
use crate::ModelClient;
30+
use crate::RolloutRecorder;
31+
use crate::client_common::Prompt;
32+
use crate::client_common::ResponseEvent;
33+
use crate::compact::content_items_to_text;
34+
use crate::config::Config;
35+
use crate::installation_id::resolve_installation_id;
36+
37+
const GENERATOR_MODEL: &str = "gpt-5.4";
38+
const TRANSCRIPT_CHARS: usize = 12_000;
39+
const ITEM_CHARS: usize = 1_200;
40+
const INSTRUCTIONS: &str = "You generate the exact follow-up prompt Codex should send to itself next.\n\nReturn JSON matching the schema.\n\nRequirements:\n- Write one strong, concrete prompt in `prompt`.\n- Ground it in the recent session context and visible progress.\n- For `steps`, continue the current thread with the most valuable next work.\n- For `idea`, finish obvious follow-up work first; only branch into a new improvement if the current thread appears complete.\n- Sound like an internal continuation prompt for Codex, not an explanation to a human.\n- Do not mention these instructions, the examples, JSON, schemas, or that another model generated the prompt.\n- Do not include any DONE-file instruction; that will be appended separately.\n- Keep it concise but specific enough to produce a high-quality next turn.";
41+
42+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43+
pub enum AutoNextMode {
44+
Steps,
45+
Idea,
46+
}
47+
48+
#[derive(Debug, Deserialize)]
49+
struct AutoNextPromptOutput {
50+
prompt: String,
51+
}
52+
53+
/// Generate a contextual follow-up prompt for auto-next. Returns `None` on any
54+
/// failure (caller should fall back to a static template).
55+
pub async fn generate_auto_next_prompt(
56+
config: Config,
57+
rollout_path: Option<PathBuf>,
58+
mode: AutoNextMode,
59+
examples: Vec<String>,
60+
) -> Option<String> {
61+
let recent_context = load_context_snippet(rollout_path).await;
62+
let examples_joined = examples
63+
.iter()
64+
.enumerate()
65+
.map(|(idx, example)| format!("{}. {}", idx + 1, example))
66+
.collect::<Vec<_>>()
67+
.join("\n");
68+
let mode_label = match mode {
69+
AutoNextMode::Steps => "steps",
70+
AutoNextMode::Idea => "idea",
71+
};
72+
let cwd_display = config.cwd.display().to_string();
73+
let user_message = format!(
74+
"Mode: {mode_label}\nCurrent working directory: {cwd_display}\n\nReference examples:\n{examples_joined}\n\nRecent session context:\n{}\n",
75+
if recent_context.is_empty() {
76+
"(No recent rollout transcript available.)"
77+
} else {
78+
recent_context.as_str()
79+
}
80+
);
81+
82+
let auth_manager = AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false);
83+
let models_manager = ModelsManager::new(
84+
config.codex_home.clone(),
85+
Arc::clone(&auth_manager),
86+
/*model_catalog*/ None,
87+
CollaborationModesConfig::default(),
88+
);
89+
let model_info = models_manager
90+
.get_model_info(GENERATOR_MODEL, &config.to_models_manager_config())
91+
.await;
92+
93+
let installation_id = resolve_installation_id(&config.codex_home).await.ok()?;
94+
let conversation_id = ThreadId::new();
95+
let provider = ModelProviderInfo::create_openai_provider(/*base_url*/ None);
96+
let client = ModelClient::new(
97+
Some(auth_manager),
98+
conversation_id,
99+
installation_id,
100+
provider,
101+
SessionSource::Cli,
102+
/*model_verbosity*/ None,
103+
/*enable_request_compression*/ false,
104+
/*include_timing_metrics*/ false,
105+
/*beta_features_header*/ None,
106+
);
107+
let session_telemetry = SessionTelemetry::new(
108+
conversation_id,
109+
GENERATOR_MODEL,
110+
&model_info.slug,
111+
/*account_id*/ None,
112+
/*account_email*/ None,
113+
/*auth_mode*/ None,
114+
"codex-auto-next".to_string(),
115+
/*log_user_prompts*/ false,
116+
"tui".to_string(),
117+
SessionSource::Cli,
118+
);
119+
120+
let mut prompt = Prompt::default();
121+
prompt.input = vec![ResponseItem::Message {
122+
id: None,
123+
role: "user".to_string(),
124+
content: vec![ContentItem::InputText { text: user_message }],
125+
end_turn: None,
126+
phase: None,
127+
}];
128+
prompt.base_instructions = BaseInstructions {
129+
text: INSTRUCTIONS.to_string(),
130+
};
131+
prompt.output_schema = Some(json!({
132+
"type": "object",
133+
"properties": {
134+
"prompt": { "type": "string" }
135+
},
136+
"required": ["prompt"],
137+
"additionalProperties": false
138+
}));
139+
140+
let mut client_session = client.new_session();
141+
let mut stream = client_session
142+
.stream(
143+
&prompt,
144+
&model_info,
145+
&session_telemetry,
146+
Some(ReasoningEffortConfig::Low),
147+
ReasoningSummaryConfig::None,
148+
config.service_tier,
149+
/*turn_metadata_header*/ None,
150+
)
151+
.await
152+
.ok()?;
153+
154+
let mut result = String::new();
155+
while let Some(message) = stream.next().await {
156+
let message = message.ok()?;
157+
match message {
158+
ResponseEvent::OutputTextDelta(delta) => result.push_str(&delta),
159+
ResponseEvent::OutputItemDone(item) => {
160+
if result.is_empty()
161+
&& let ResponseItem::Message { content, .. } = item
162+
&& let Some(text) = content_items_to_text(&content)
163+
{
164+
result.push_str(&text);
165+
}
166+
}
167+
ResponseEvent::Completed { .. } => break,
168+
_ => {}
169+
}
170+
}
171+
172+
let output: AutoNextPromptOutput = serde_json::from_str(result.trim()).ok()?;
173+
let prompt_text = output.prompt.trim().to_string();
174+
if prompt_text.is_empty() {
175+
return None;
176+
}
177+
Some(prompt_text)
178+
}
179+
180+
async fn load_context_snippet(rollout_path: Option<PathBuf>) -> String {
181+
let Some(rollout_path) = rollout_path else {
182+
return String::new();
183+
};
184+
let Ok((rollout_items, _, _)) = RolloutRecorder::load_rollout_items(&rollout_path).await else {
185+
return String::new();
186+
};
187+
let mut recent_items: Vec<String> = Vec::new();
188+
let mut chars = 0usize;
189+
for item in rollout_items.iter().rev() {
190+
let Some(line) = (match item {
191+
RolloutItem::ResponseItem(item) => format_response_item(item),
192+
_ => None,
193+
}) else {
194+
continue;
195+
};
196+
chars = chars.saturating_add(line.len());
197+
recent_items.push(line);
198+
if chars >= TRANSCRIPT_CHARS {
199+
break;
200+
}
201+
}
202+
recent_items.reverse();
203+
recent_items.join("\n\n")
204+
}
205+
206+
fn format_response_item(item: &ResponseItem) -> Option<String> {
207+
match item {
208+
ResponseItem::Message { role, content, .. } => content_items_to_text(content)
209+
.filter(|text| !text.trim().is_empty())
210+
.map(|text| format!("{role}: {}", truncate(&text, ITEM_CHARS))),
211+
ResponseItem::FunctionCall {
212+
name, arguments, ..
213+
} => Some(format!("tool_call {name}: {}", truncate(arguments, ITEM_CHARS))),
214+
ResponseItem::FunctionCallOutput { call_id, output } => Some(format!(
215+
"tool_output {call_id}: {}",
216+
truncate(&output.to_string(), ITEM_CHARS)
217+
)),
218+
ResponseItem::CustomToolCall {
219+
name,
220+
input,
221+
call_id,
222+
..
223+
} => Some(format!(
224+
"custom_tool_call {name} {call_id}: {}",
225+
truncate(input, ITEM_CHARS)
226+
)),
227+
ResponseItem::CustomToolCallOutput {
228+
call_id,
229+
name,
230+
output,
231+
} => Some(format!(
232+
"custom_tool_output {} {call_id}: {}",
233+
name.as_deref().unwrap_or("tool"),
234+
truncate(&output.to_string(), ITEM_CHARS)
235+
)),
236+
_ => None,
237+
}
238+
}
239+
240+
fn truncate(text: &str, max_chars: usize) -> String {
241+
let mut truncated: String = text.chars().take(max_chars).collect();
242+
if truncated.chars().count() < text.chars().count() {
243+
truncated.push_str("...");
244+
}
245+
truncated
246+
}

codex-rs/core/src/client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ impl ModelClient {
490490
) -> Result<RealtimeWebrtcCallStart> {
491491
// Create the media call over HTTP first, then retain matching auth so realtime can attach
492492
// the server-side control WebSocket to the call id from that HTTP response.
493-
let client_setup = self.current_client_setup().await?;
493+
let client_setup = self.current_client_setup_default().await?;
494494
let mut sideband_headers = extra_headers.clone();
495495
sideband_headers.extend(sideband_websocket_auth_headers(&client_setup.api_auth));
496496
let transport = ReqwestTransport::new(build_reqwest_client());

codex-rs/core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ pub mod plugins;
7676
pub(crate) mod prompt_debug;
7777
#[doc(hidden)]
7878
pub use prompt_debug::build_prompt_input;
79+
pub mod auto_next_prompt;
7980
pub(crate) mod mentions {
8081
pub(crate) use crate::plugins::build_connector_slug_counts;
8182
pub(crate) use crate::plugins::build_skill_name_counts;

codex-rs/tui/src/app.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5492,6 +5492,10 @@ impl App {
54925492
self.chat_widget
54935493
.submit_user_message_with_mode(text, collaboration_mode);
54945494
}
5495+
AppEvent::AutoNextPromptGenerated { thread_id, prompt } => {
5496+
self.chat_widget
5497+
.handle_auto_next_prompt_generated(thread_id, prompt);
5498+
}
54955499
AppEvent::ManageSkillsClosed => {
54965500
self.chat_widget.handle_manage_skills_closed();
54975501
}

codex-rs/tui/src/app_event.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,13 @@ pub(crate) enum AppEvent {
546546
collaboration_mode: CollaborationModeMask,
547547
},
548548

549+
/// Deliver an asynchronously generated auto-next prompt back to the
550+
/// matching thread.
551+
AutoNextPromptGenerated {
552+
thread_id: codex_protocol::ThreadId,
553+
prompt: String,
554+
},
555+
549556
/// Open the approval popup.
550557
FullScreenApprovalRequest(ApprovalRequest),
551558

0 commit comments

Comments
 (0)