mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: change multi-agent to use path-like system instead of uuids (#15313)
This PR add an URI-based system to reference agents within a tree. This comes from a sync between research and engineering. The main agent (the one manually spawned by a user) is always called `/root`. Any sub-agent spawned by it will be `/root/agent_1` for example where `agent_1` is chosen by the model. Any agent can contact any agents using the path. Paths can be used either in absolute or relative to the calling agents Resume is not supported for now on this new path
This commit is contained in:
@@ -50,6 +50,7 @@ fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &Sessi
|
||||
metadata.source = enum_to_string(&meta_line.meta.source);
|
||||
metadata.agent_nickname = meta_line.meta.agent_nickname.clone();
|
||||
metadata.agent_role = meta_line.meta.agent_role.clone();
|
||||
metadata.agent_path = meta_line.meta.agent_path.clone();
|
||||
if let Some(provider) = meta_line.meta.model_provider.as_deref() {
|
||||
metadata.model_provider = provider.to_string();
|
||||
}
|
||||
@@ -251,6 +252,7 @@ mod tests {
|
||||
originator: "codex_cli_rs".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
@@ -377,6 +379,7 @@ mod tests {
|
||||
originator: "codex_cli_rs".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
@@ -402,6 +405,7 @@ mod tests {
|
||||
created_at,
|
||||
updated_at: created_at,
|
||||
source: "cli".to_string(),
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: "openai".to_string(),
|
||||
|
||||
@@ -69,6 +69,8 @@ pub struct ThreadMetadata {
|
||||
pub agent_nickname: Option<String>,
|
||||
/// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
pub agent_role: Option<String>,
|
||||
/// Optional canonical agent path assigned to an AgentControl-spawned sub-agent.
|
||||
pub agent_path: Option<String>,
|
||||
/// The model provider identifier.
|
||||
pub model_provider: String,
|
||||
/// The latest observed model for the thread.
|
||||
@@ -116,6 +118,8 @@ pub struct ThreadMetadataBuilder {
|
||||
pub agent_nickname: Option<String>,
|
||||
/// Optional role (agent_role) assigned to the session.
|
||||
pub agent_role: Option<String>,
|
||||
/// Optional canonical agent path assigned to the session.
|
||||
pub agent_path: Option<String>,
|
||||
/// The model provider identifier, if known.
|
||||
pub model_provider: Option<String>,
|
||||
/// The working directory for the thread.
|
||||
@@ -152,6 +156,7 @@ impl ThreadMetadataBuilder {
|
||||
source,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
agent_path: None,
|
||||
model_provider: None,
|
||||
cwd: PathBuf::new(),
|
||||
cli_version: None,
|
||||
@@ -182,6 +187,10 @@ impl ThreadMetadataBuilder {
|
||||
source,
|
||||
agent_nickname: self.agent_nickname.clone(),
|
||||
agent_role: self.agent_role.clone(),
|
||||
agent_path: self
|
||||
.agent_path
|
||||
.clone()
|
||||
.or_else(|| self.source.get_agent_path().map(Into::into)),
|
||||
model_provider: self
|
||||
.model_provider
|
||||
.clone()
|
||||
@@ -241,6 +250,9 @@ impl ThreadMetadata {
|
||||
if self.agent_role != other.agent_role {
|
||||
diffs.push("agent_role");
|
||||
}
|
||||
if self.agent_path != other.agent_path {
|
||||
diffs.push("agent_path");
|
||||
}
|
||||
if self.model_provider != other.model_provider {
|
||||
diffs.push("model_provider");
|
||||
}
|
||||
@@ -300,6 +312,7 @@ pub(crate) struct ThreadRow {
|
||||
source: String,
|
||||
agent_nickname: Option<String>,
|
||||
agent_role: Option<String>,
|
||||
agent_path: Option<String>,
|
||||
model_provider: String,
|
||||
model: Option<String>,
|
||||
reasoning_effort: Option<String>,
|
||||
@@ -326,6 +339,7 @@ impl ThreadRow {
|
||||
source: row.try_get("source")?,
|
||||
agent_nickname: row.try_get("agent_nickname")?,
|
||||
agent_role: row.try_get("agent_role")?,
|
||||
agent_path: row.try_get("agent_path")?,
|
||||
model_provider: row.try_get("model_provider")?,
|
||||
model: row.try_get("model")?,
|
||||
reasoning_effort: row.try_get("reasoning_effort")?,
|
||||
@@ -356,6 +370,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort,
|
||||
@@ -379,6 +394,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort: reasoning_effort
|
||||
@@ -447,6 +463,7 @@ mod tests {
|
||||
source: "cli".to_string(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
agent_path: None,
|
||||
model_provider: "openai".to_string(),
|
||||
model: Some("gpt-5".to_string()),
|
||||
reasoning_effort: reasoning_effort.map(str::to_string),
|
||||
@@ -474,6 +491,7 @@ mod tests {
|
||||
source: "cli".to_string(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
agent_path: None,
|
||||
model_provider: "openai".to_string(),
|
||||
model: Some("gpt-5".to_string()),
|
||||
reasoning_effort,
|
||||
|
||||
@@ -166,6 +166,7 @@ SELECT
|
||||
created_at,
|
||||
updated_at,
|
||||
source,
|
||||
agent_path,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
|
||||
@@ -50,6 +50,7 @@ pub(super) fn test_thread_metadata(
|
||||
source: "cli".to_string(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
agent_path: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
model: Some("gpt-5".to_string()),
|
||||
reasoning_effort: Some(ReasoningEffort::Medium),
|
||||
|
||||
@@ -13,6 +13,7 @@ SELECT
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort,
|
||||
@@ -142,6 +143,62 @@ ON CONFLICT(child_thread_id) DO UPDATE SET
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find a direct spawned child of `parent_thread_id` by canonical agent path.
|
||||
pub async fn find_thread_spawn_child_by_path(
|
||||
&self,
|
||||
parent_thread_id: ThreadId,
|
||||
agent_path: &str,
|
||||
) -> anyhow::Result<Option<ThreadId>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT threads.id
|
||||
FROM thread_spawn_edges
|
||||
JOIN threads ON threads.id = thread_spawn_edges.child_thread_id
|
||||
WHERE thread_spawn_edges.parent_thread_id = ?
|
||||
AND threads.agent_path = ?
|
||||
ORDER BY threads.id
|
||||
LIMIT 2
|
||||
"#,
|
||||
)
|
||||
.bind(parent_thread_id.to_string())
|
||||
.bind(agent_path)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
one_thread_id_from_rows(rows, agent_path)
|
||||
}
|
||||
|
||||
/// Find a spawned descendant of `root_thread_id` by canonical agent path.
|
||||
pub async fn find_thread_spawn_descendant_by_path(
|
||||
&self,
|
||||
root_thread_id: ThreadId,
|
||||
agent_path: &str,
|
||||
) -> anyhow::Result<Option<ThreadId>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
WITH RECURSIVE subtree(child_thread_id) AS (
|
||||
SELECT child_thread_id
|
||||
FROM thread_spawn_edges
|
||||
WHERE parent_thread_id = ?
|
||||
UNION ALL
|
||||
SELECT edge.child_thread_id
|
||||
FROM thread_spawn_edges AS edge
|
||||
JOIN subtree ON edge.parent_thread_id = subtree.child_thread_id
|
||||
)
|
||||
SELECT threads.id
|
||||
FROM subtree
|
||||
JOIN threads ON threads.id = subtree.child_thread_id
|
||||
WHERE threads.agent_path = ?
|
||||
ORDER BY threads.id
|
||||
LIMIT 2
|
||||
"#,
|
||||
)
|
||||
.bind(root_thread_id.to_string())
|
||||
.bind(agent_path)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
one_thread_id_from_rows(rows, agent_path)
|
||||
}
|
||||
|
||||
async fn list_thread_spawn_children_matching(
|
||||
&self,
|
||||
parent_thread_id: ThreadId,
|
||||
@@ -293,6 +350,7 @@ SELECT
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort,
|
||||
@@ -393,6 +451,7 @@ INSERT INTO threads (
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort,
|
||||
@@ -409,7 +468,7 @@ INSERT INTO threads (
|
||||
git_branch,
|
||||
git_origin_url,
|
||||
memory_mode
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO NOTHING
|
||||
"#,
|
||||
)
|
||||
@@ -420,6 +479,7 @@ ON CONFLICT(id) DO NOTHING
|
||||
.bind(metadata.source.as_str())
|
||||
.bind(metadata.agent_nickname.as_deref())
|
||||
.bind(metadata.agent_role.as_deref())
|
||||
.bind(metadata.agent_path.as_deref())
|
||||
.bind(metadata.model_provider.as_str())
|
||||
.bind(metadata.model.as_deref())
|
||||
.bind(
|
||||
@@ -518,6 +578,7 @@ INSERT INTO threads (
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort,
|
||||
@@ -534,7 +595,7 @@ INSERT INTO threads (
|
||||
git_branch,
|
||||
git_origin_url,
|
||||
memory_mode
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
rollout_path = excluded.rollout_path,
|
||||
created_at = excluded.created_at,
|
||||
@@ -542,6 +603,7 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
source = excluded.source,
|
||||
agent_nickname = excluded.agent_nickname,
|
||||
agent_role = excluded.agent_role,
|
||||
agent_path = excluded.agent_path,
|
||||
model_provider = excluded.model_provider,
|
||||
model = excluded.model,
|
||||
reasoning_effort = excluded.reasoning_effort,
|
||||
@@ -566,6 +628,7 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
.bind(metadata.source.as_str())
|
||||
.bind(metadata.agent_nickname.as_deref())
|
||||
.bind(metadata.agent_role.as_deref())
|
||||
.bind(metadata.agent_path.as_deref())
|
||||
.bind(metadata.model_provider.as_str())
|
||||
.bind(metadata.model.as_deref())
|
||||
.bind(
|
||||
@@ -753,6 +816,26 @@ ON CONFLICT(thread_id, position) DO NOTHING
|
||||
}
|
||||
}
|
||||
|
||||
fn one_thread_id_from_rows(
|
||||
rows: Vec<sqlx::sqlite::SqliteRow>,
|
||||
agent_path: &str,
|
||||
) -> anyhow::Result<Option<ThreadId>> {
|
||||
let mut ids = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let id: String = row.try_get("id")?;
|
||||
ThreadId::try_from(id).map_err(anyhow::Error::from)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
match ids.len() {
|
||||
0 => Ok(None),
|
||||
1 => Ok(ids.pop()),
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"multiple agents found for canonical path `{agent_path}`"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn extract_dynamic_tools(items: &[RolloutItem]) -> Option<Option<Vec<DynamicToolSpec>>> {
|
||||
items.iter().find_map(|item| match item {
|
||||
RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.dynamic_tools.clone()),
|
||||
@@ -942,6 +1025,7 @@ mod tests {
|
||||
originator: String::new(),
|
||||
cli_version: String::new(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
@@ -996,6 +1080,7 @@ mod tests {
|
||||
originator: String::new(),
|
||||
cli_version: String::new(),
|
||||
source: SessionSource::Cli,
|
||||
agent_path: None,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
|
||||
Reference in New Issue
Block a user