This commit is contained in:
jif-oai
2025-12-03 13:16:40 +00:00
parent 2b1956f0ef
commit 42f7f5734f
8 changed files with 77 additions and 123 deletions
+1 -2
View File
@@ -13,7 +13,7 @@ The focus is on:
## 1. Mental Model
- A **Codex session** remains the top‑level unit, with one running `SessionTask` (typically `RegularTask`) at a time.
- A **Codex session** remains the top‑level unit, with one running `SessionTask` at a time.
- Within a session we introduce a **Collaboration Graph**: a set of logical agents that can exchange messages and call tools, orchestrated entirely within the same session.
- The **main agent** corresponds to the existing single agent Codex exposes today. It gets the user’s prompt and controls collaboration via the new `collaboration.*` tools.
- **Child agents** are lightweight logical agents:
@@ -624,7 +624,6 @@ Add collaboration‑related helpers on `Session`:
The single‑agent path (today’s behavior) remains:
- `RegularTask::run` → `run_task` loop.
- `run_task` builds `prompt` from *session* history and calls `run_turn`.
For collaboration, we introduce a **CollaborationTask**:
+12
View File
@@ -18,6 +18,18 @@ You are Codex, based on GPT-5. You are running as a coding agent in the Codex CL
- While you are working, you might notice unexpected changes that you didn't make. If this happens, STOP IMMEDIATELY and ask the user how they would like to proceed.
- **NEVER** use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested or approved by the user.
## Collaboration
You can spawn and coordinate child agents using these tools (only on this model):
- `collaboration_init_agent`: create a direct child with optional instructions/model; depth/agent limits apply. No initial message is sent at this point.
- `collaboration_send`: send a user-message to your direct children by id.
- `collaboration_wait`: run children for up to `max_duration` tokens and surface their last message/status.
- `collaboration_get_state`: see all agents, parents, statuses, and last messages.
- `collaboration_close`: close specific children (and their descendants).
Children inherit your instructions unless overridden. Always `wait` after `send` to drive children forward; keep communication concise and include the expected output format. Use `get_state` if unsure about child ids/status.
A `collaboration_send` is always necessary after a `collaboration_init_agent` to start the child agent working.
## Plan tool
When using the planning tool:
-4
View File
@@ -398,10 +398,6 @@ impl SessionConfiguration {
self.user_instructions.clone()
}
pub(crate) fn approval_policy(&self) -> AskForApproval {
self.approval_policy
}
pub(crate) fn sandbox_policy(&self) -> SandboxPolicy {
self.sandbox_policy.clone()
}
-4
View File
@@ -196,10 +196,6 @@ pub fn find_family_for_model(slug: &str) -> Option<ModelFamily> {
base_instructions: GPT_5_1_CODEX_MAX_INSTRUCTIONS.to_string(),
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
experimental_supported_tools: vec![
"grep_files".to_string(),
"list_dir".to_string(),
"read_file".to_string(),
"test_sync_tool".to_string(),
"collaboration".to_string(),
],
shell_type: ConfigShellToolType::ShellCommand,
+1 -1
View File
@@ -1,7 +1,6 @@
mod collaboration;
mod compact;
mod ghost_snapshot;
mod regular;
mod review;
mod undo;
mod user_shell;
@@ -30,6 +29,7 @@ use crate::state::TaskKind;
use codex_protocol::user_input::UserInput;
pub(crate) use collaboration::CollaborationSupervisor;
pub(crate) use collaboration::CollaborationTask;
pub(crate) use compact::CompactTask;
pub(crate) use ghost_snapshot::GhostSnapshotTask;
pub(crate) use review::ReviewTask;
-33
View File
@@ -1,33 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use tokio_util::sync::CancellationToken;
use crate::codex::TurnContext;
use crate::codex::run_task;
use crate::state::TaskKind;
use codex_protocol::user_input::UserInput;
use super::SessionTask;
use super::SessionTaskContext;
#[derive(Clone, Copy, Default)]
pub(crate) struct RegularTask;
#[async_trait]
impl SessionTask for RegularTask {
fn kind(&self) -> TaskKind {
TaskKind::Regular
}
async fn run(
self: Arc<Self>,
session: Arc<SessionTaskContext>,
ctx: Arc<TurnContext>,
input: Vec<UserInput>,
cancellation_token: CancellationToken,
) -> Option<String> {
let sess = session.clone_session();
run_task(sess, ctx, input, cancellation_token).await
}
}
@@ -25,6 +25,7 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use serde_json::json;
use std::path::PathBuf;
use tracing::info;
#[derive(Debug, Default, Serialize, Deserialize)]
pub(crate) struct ExtraMetadata(pub HashMap<String, serde_json::Value>);
@@ -235,7 +236,7 @@ fn build_agent_system_message(
ResponseItem::Message {
id: None,
role: "system".to_string(),
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: lines.join("\n"),
}],
@@ -269,21 +270,6 @@ fn build_history_for_child(
history
}
async fn record_session_summary(
session: &crate::codex::Session,
turn: &crate::codex::TurnContext,
text: String,
) {
let summary = ResponseItem::Message {
id: None,
role: "assistant".to_string(),
content: vec![ContentItem::InputText { text }],
};
session
.record_conversation_items(turn, std::slice::from_ref(&summary))
.await;
}
pub struct CollaborationHandler;
impl Default for CollaborationHandler {
@@ -456,29 +442,18 @@ async fn handle_init_agent(
extra: ExtraMetadata(extra),
};
let content = format!(
"Initialized agent {} at depth {} in cwd {}",
assigned_id.0,
depth,
turn.cwd.display()
);
let content = format!("Initialized agent {} at depth {}", assigned_id.0, depth);
if !cfg!(test) {
let supervisor = session.ensure_collaboration_supervisor().await;
supervisor.kick_agent(assigned_id).await;
}
record_session_summary(
session.as_ref(),
turn,
format!(
"Agent {} initialized (child of {})",
assigned_id.0, parent.id.0
),
)
.await;
let output = CollaborationInitAgentOutput { content, metadata };
info!(
"collaboration_init_agent: assigned_id={}, parent_id={}, depth={}",
assigned_id.0, parent.id.0, depth
);
serialize_function_output(&output, true)
}
@@ -591,19 +566,14 @@ async fn handle_send(
let success = metadata.message_tool_call_success;
if success {
record_session_summary(
session.as_ref(),
turn,
format!(
"Agent {} sent to {:?}: {}",
sender_id.0, valid_recipients, input.message
),
)
.await;
if success && !valid_recipients.is_empty() {
let supervisor = session.ensure_collaboration_supervisor().await;
for recipient in &valid_recipients {
supervisor.kick_agent(*recipient).await;
for recipient in valid_recipients
.iter()
.copied()
.filter(|recipient| recipient.0 != 0)
{
supervisor.kick_agent(recipient).await;
}
}
@@ -710,9 +680,56 @@ async fn handle_wait(
return serialize_function_output(&output, false);
}
let mut agents_ran = Vec::new();
let supervisor = session.ensure_collaboration_supervisor().await;
let mut rx = supervisor.subscribe();
let snapshot = |agent: &AgentState, delta_tokens: i32, sub_id: Option<String>| {
json!({
"agent_idx": agent.id.0,
"delta_tokens": delta_tokens,
"status": status_label(&agent.status),
"status_detail": status_detail(&agent.status),
"last_agent_message": last_agent_message(&agent.status),
"sub_id": sub_id,
})
};
let mut agents_ran = Vec::new();
{
// Short-circuit if all targets are already idle/terminal.
let collab = session.collaboration_state().lock().await;
if targets.iter().all(|id| {
collab
.agent(*id)
.map(|agent| !matches!(agent.status, AgentLifecycleState::Running))
.unwrap_or(false)
}) {
let mut extra = HashMap::new();
extra.insert(
"agents_ran".to_string(),
json!(
targets
.iter()
.filter_map(|id| collab.agent(*id))
.map(|agent| snapshot(agent, 0, None))
.collect::<Vec<_>>()
),
);
extra.insert("token_budget_exhausted".to_string(), json!(false));
let metadata = CollaborationWaitMetadata {
message_tool_call_success: true,
message_tool_call_error_should_penalize_model: false,
is_wait_success_msg: Some(true),
extra: ExtraMetadata(extra),
};
let output = CollaborationWaitOutput {
content: "Finished waiting.".to_string(),
metadata,
};
return serialize_function_output(&output, true);
}
}
supervisor
.start_agents(targets.clone(), input.max_duration)
.await
@@ -731,29 +748,6 @@ async fn handle_wait(
}
remaining = remaining.saturating_sub(event.delta_tokens);
if let Some(message) = event.last_message.clone() {
record_session_summary(
session.as_ref(),
turn,
format!("Agent {} replied: {}", event.agent.0, message),
)
.await;
} else if let AgentLifecycleState::Error { error } = &event.status {
record_session_summary(
session.as_ref(),
turn,
format!("Agent {} failed: {error}", event.agent.0),
)
.await;
} else if !matches!(event.status, AgentLifecycleState::Error { .. }) {
record_session_summary(
session.as_ref(),
turn,
format!("Agent {} finished turn.", event.agent.0),
)
.await;
}
let detail = status_detail(&event.status);
agents_ran.push(json!({
"agent_idx": event.agent.0,
@@ -949,15 +943,6 @@ async fn handle_close(
)
.await;
}
record_session_summary(
session.as_ref(),
turn,
format!(
"Agent {} closed agents {:?}",
caller_id.0, closed_for_summary
),
)
.await;
serialize_function_output(&output, true)
}
@@ -1173,7 +1158,7 @@ mod tests {
assert_eq!(items.len(), 2);
assert!(matches!(
items.first(),
Some(ResponseItem::Message { role, .. }) if role == "system"
Some(ResponseItem::Message { role, .. }) if role == "user"
));
assert!(matches!(
items.get(1),
-1
View File
@@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;