feat: add nick name to sub-agents (#12320)
Adding random nick name to sub-agents. Used for UX At the same time, also storing and wiring the role of the sub-agent
This commit is contained in:
parent
03ff04cd65
commit
0f9eed3a6f
39 changed files with 1125 additions and 109 deletions
|
|
@ -6377,6 +6377,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
@ -6524,6 +6538,20 @@
|
|||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
|
|
|||
|
|
@ -9431,6 +9431,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
@ -15092,6 +15106,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
@ -15218,6 +15246,20 @@
|
|||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
|
|
|||
|
|
@ -123,6 +123,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
|
|||
|
|
@ -123,6 +123,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
|
|||
|
|
@ -716,6 +716,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
@ -777,6 +791,20 @@
|
|||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
|
|
|||
|
|
@ -489,6 +489,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
@ -550,6 +564,20 @@
|
|||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
|
|
|||
|
|
@ -489,6 +489,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
@ -550,6 +564,20 @@
|
|||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
|
|
|||
|
|
@ -716,6 +716,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
@ -777,6 +791,20 @@
|
|||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
|
|
|||
|
|
@ -489,6 +489,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
@ -550,6 +564,20 @@
|
|||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
|
|
|||
|
|
@ -716,6 +716,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
@ -777,6 +791,20 @@
|
|||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
|
|
|||
|
|
@ -489,6 +489,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
@ -550,6 +564,20 @@
|
|||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
|
|
|||
|
|
@ -489,6 +489,20 @@
|
|||
"properties": {
|
||||
"thread_spawn": {
|
||||
"properties": {
|
||||
"agent_nickname": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agent_role": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"depth": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
|
|
@ -550,6 +564,20 @@
|
|||
},
|
||||
"Thread": {
|
||||
"properties": {
|
||||
"agentNickname": {
|
||||
"description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"agentRole": {
|
||||
"description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cliVersion": {
|
||||
"description": "Version of the CLI that created the thread.",
|
||||
"type": "string"
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type SubAgentSource = "review" | "compact" | { "thread_spawn": { parent_thread_id: ThreadId, depth: number, } } | "memory_consolidation" | { "other": string };
|
||||
export type SubAgentSource = "review" | "compact" | { "thread_spawn": { parent_thread_id: ThreadId, depth: number, agent_nickname: string | null, agent_role: string | null, } } | "memory_consolidation" | { "other": string };
|
||||
|
|
|
|||
|
|
@ -43,6 +43,14 @@ cliVersion: string,
|
|||
* Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).
|
||||
*/
|
||||
source: SessionSource,
|
||||
/**
|
||||
* Optional random unique nickname assigned to an AgentControl-spawned sub-agent.
|
||||
*/
|
||||
agentNickname: string | null,
|
||||
/**
|
||||
* Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
*/
|
||||
agentRole: string | null,
|
||||
/**
|
||||
* Optional Git metadata captured when the thread was created.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2206,6 +2206,10 @@ pub struct Thread {
|
|||
pub cli_version: String,
|
||||
/// Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).
|
||||
pub source: SessionSource,
|
||||
/// Optional random unique nickname assigned to an AgentControl-spawned sub-agent.
|
||||
pub agent_nickname: Option<String>,
|
||||
/// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
pub agent_role: Option<String>,
|
||||
/// Optional Git metadata captured when the thread was created.
|
||||
pub git_info: Option<GitInfo>,
|
||||
/// Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read`
|
||||
|
|
|
|||
|
|
@ -224,6 +224,7 @@ Experimental API: `thread/start`, `thread/resume`, and `thread/fork` accept `per
|
|||
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`).
|
||||
- `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default).
|
||||
- `cwd` — restrict results to threads whose session cwd exactly matches this path.
|
||||
- Responses include `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available.
|
||||
|
||||
Example:
|
||||
|
||||
|
|
@ -235,7 +236,7 @@ Example:
|
|||
} }
|
||||
{ "id": 20, "result": {
|
||||
"data": [
|
||||
{ "id": "thr_a", "preview": "Create a TUI", "modelProvider": "openai", "createdAt": 1730831111, "updatedAt": 1730831111, "status": { "type": "notLoaded" } },
|
||||
{ "id": "thr_a", "preview": "Create a TUI", "modelProvider": "openai", "createdAt": 1730831111, "updatedAt": 1730831111, "status": { "type": "notLoaded" }, "agentNickname": "Atlas", "agentRole": "explorer" },
|
||||
{ "id": "thr_b", "preview": "Fix tests", "modelProvider": "openai", "createdAt": 1730750000, "updatedAt": 1730750000, "status": { "type": "notLoaded" } }
|
||||
],
|
||||
"nextCursor": "opaque-token-or-null"
|
||||
|
|
@ -271,7 +272,7 @@ When `nextCursor` is `null`, you’ve reached the final page.
|
|||
|
||||
### Example: Read a thread
|
||||
|
||||
Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want the rollout history loaded into `thread.turns`.
|
||||
Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want the rollout history loaded into `thread.turns`. The returned thread includes `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available.
|
||||
|
||||
```json
|
||||
{ "method": "thread/read", "id": 22, "params": { "threadId": "thr_123" } }
|
||||
|
|
|
|||
|
|
@ -6658,6 +6658,8 @@ async fn read_summary_from_state_db_context_by_thread_id(
|
|||
metadata.cwd,
|
||||
metadata.cli_version,
|
||||
metadata.source,
|
||||
metadata.agent_nickname,
|
||||
metadata.agent_role,
|
||||
metadata.git_sha,
|
||||
metadata.git_branch,
|
||||
metadata.git_origin_url,
|
||||
|
|
@ -6678,9 +6680,12 @@ async fn summary_from_thread_list_item(
|
|||
.unwrap_or_else(|| fallback_provider.to_string());
|
||||
let cwd = it.cwd?;
|
||||
let cli_version = it.cli_version.unwrap_or_default();
|
||||
let source = it
|
||||
.source
|
||||
.unwrap_or(codex_protocol::protocol::SessionSource::Unknown);
|
||||
let source = with_thread_spawn_agent_metadata(
|
||||
it.source
|
||||
.unwrap_or(codex_protocol::protocol::SessionSource::Unknown),
|
||||
it.agent_nickname.clone(),
|
||||
it.agent_role.clone(),
|
||||
);
|
||||
return Some(ConversationSummary {
|
||||
conversation_id: thread_id,
|
||||
path: it.path,
|
||||
|
|
@ -6735,13 +6740,17 @@ fn summary_from_state_db_metadata(
|
|||
cwd: PathBuf,
|
||||
cli_version: String,
|
||||
source: String,
|
||||
agent_nickname: Option<String>,
|
||||
agent_role: Option<String>,
|
||||
git_sha: Option<String>,
|
||||
git_branch: Option<String>,
|
||||
git_origin_url: Option<String>,
|
||||
) -> ConversationSummary {
|
||||
let preview = first_user_message.unwrap_or_default();
|
||||
let source = serde_json::from_value(serde_json::Value::String(source))
|
||||
let source = serde_json::from_str(&source)
|
||||
.or_else(|_| serde_json::from_value(serde_json::Value::String(source.clone())))
|
||||
.unwrap_or(codex_protocol::protocol::SessionSource::Unknown);
|
||||
let source = with_thread_spawn_agent_metadata(source, agent_nickname, agent_role);
|
||||
let git_info = if git_sha.is_none() && git_branch.is_none() && git_origin_url.is_none() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -6901,6 +6910,35 @@ fn map_git_info(git_info: &CoreGitInfo) -> ConversationGitInfo {
|
|||
}
|
||||
}
|
||||
|
||||
fn with_thread_spawn_agent_metadata(
|
||||
source: codex_protocol::protocol::SessionSource,
|
||||
agent_nickname: Option<String>,
|
||||
agent_role: Option<String>,
|
||||
) -> codex_protocol::protocol::SessionSource {
|
||||
if agent_nickname.is_none() && agent_role.is_none() {
|
||||
return source;
|
||||
}
|
||||
|
||||
match source {
|
||||
codex_protocol::protocol::SessionSource::SubAgent(
|
||||
codex_protocol::protocol::SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_nickname: existing_agent_nickname,
|
||||
agent_role: existing_agent_role,
|
||||
},
|
||||
) => codex_protocol::protocol::SessionSource::SubAgent(
|
||||
codex_protocol::protocol::SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_nickname: agent_nickname.or(existing_agent_nickname),
|
||||
agent_role: agent_role.or(existing_agent_role),
|
||||
},
|
||||
),
|
||||
_ => source,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_datetime(timestamp: Option<&str>) -> Option<DateTime<Utc>> {
|
||||
timestamp.and_then(|ts| {
|
||||
chrono::DateTime::parse_from_rfc3339(ts)
|
||||
|
|
@ -6937,6 +6975,8 @@ fn build_thread_from_snapshot(
|
|||
path,
|
||||
cwd: config_snapshot.cwd.clone(),
|
||||
cli_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
agent_nickname: config_snapshot.session_source.get_nickname(),
|
||||
agent_role: config_snapshot.session_source.get_agent_role(),
|
||||
source: config_snapshot.session_source.clone().into(),
|
||||
git_info: None,
|
||||
turns: Vec::new(),
|
||||
|
|
@ -6975,6 +7015,8 @@ pub(crate) fn summary_to_thread(summary: ConversationSummary) -> Thread {
|
|||
path: Some(path),
|
||||
cwd,
|
||||
cli_version,
|
||||
agent_nickname: source.get_nickname(),
|
||||
agent_role: source.get_agent_role(),
|
||||
source: source.into(),
|
||||
git_info,
|
||||
turns: Vec::new(),
|
||||
|
|
@ -6986,6 +7028,7 @@ mod tests {
|
|||
use super::*;
|
||||
use anyhow::Result;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -7131,6 +7174,41 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_from_state_db_metadata_preserves_agent_nickname() -> Result<()> {
|
||||
let conversation_id = ThreadId::from_string("bfd12a78-5900-467b-9bc5-d3d35df08191")?;
|
||||
let source =
|
||||
serde_json::to_string(&SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: ThreadId::from_string("ad7f0408-99b8-4f6e-a46f-bd0eec433370")?,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}))?;
|
||||
|
||||
let summary = summary_from_state_db_metadata(
|
||||
conversation_id,
|
||||
PathBuf::from("/tmp/rollout.jsonl"),
|
||||
Some("hi".to_string()),
|
||||
"2025-09-05T16:53:11Z".to_string(),
|
||||
"2025-09-05T16:53:12Z".to_string(),
|
||||
"test-provider".to_string(),
|
||||
PathBuf::from("/"),
|
||||
"0.0.0".to_string(),
|
||||
source,
|
||||
Some("atlas".to_string()),
|
||||
Some("explorer".to_string()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let thread = summary_to_thread(summary);
|
||||
|
||||
assert_eq!(thread.agent_nickname, Some("atlas".to_string()));
|
||||
assert_eq!(thread.agent_role, Some("explorer".to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn removing_one_listener_does_not_cancel_other_subscriptions_for_same_thread()
|
||||
-> Result<()> {
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@ mod tests {
|
|||
let spawn = CoreSessionSource::SubAgent(CoreSubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
|
||||
assert!(source_kind_matches(
|
||||
|
|
|
|||
|
|
@ -622,6 +622,8 @@ mod tests {
|
|||
path: None,
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
cli_version: "test".to_string(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
source,
|
||||
git_info: None,
|
||||
turns: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ pub fn create_fake_rollout_with_source(
|
|||
originator: "codex".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
|
|
@ -158,6 +160,8 @@ pub fn create_fake_rollout_with_text_elements(
|
|||
originator: "codex".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
|
|
|
|||
|
|
@ -582,6 +582,8 @@ async fn thread_list_filters_by_source_kind_subagent_thread_spawn() -> Result<()
|
|||
CoreSessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
)?;
|
||||
|
||||
|
|
@ -643,6 +645,8 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
|
|||
CoreSessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
)?;
|
||||
let other_id = create_fake_rollout_with_source(
|
||||
|
|
|
|||
100
codex-rs/core/src/agent/agent_names.txt
Normal file
100
codex-rs/core/src/agent/agent_names.txt
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
Atlas
|
||||
Nova
|
||||
Orion
|
||||
Iris
|
||||
Milo
|
||||
Juno
|
||||
Mars
|
||||
Vesta
|
||||
Luna
|
||||
Sol
|
||||
Eros
|
||||
Nyx
|
||||
Zeus
|
||||
Hera
|
||||
Ares
|
||||
Ajax
|
||||
Echo
|
||||
Leto
|
||||
Rhea
|
||||
Gaia
|
||||
Hades
|
||||
Apollo
|
||||
Pallas
|
||||
Circe
|
||||
Nereus
|
||||
Triton
|
||||
Selene
|
||||
Helios
|
||||
Castor
|
||||
Pollux
|
||||
Astra
|
||||
Aura
|
||||
Thalia
|
||||
Clio
|
||||
Erato
|
||||
Euterpe
|
||||
Urania
|
||||
Calliope
|
||||
Minos
|
||||
Linus
|
||||
Cato
|
||||
Brutus
|
||||
Seneca
|
||||
Ovid
|
||||
Virgil
|
||||
Horace
|
||||
Remus
|
||||
Romulus
|
||||
Titus
|
||||
Lucian
|
||||
Felix
|
||||
Maximus
|
||||
Octavia
|
||||
Claudia
|
||||
Livia
|
||||
Aelia
|
||||
Aurel
|
||||
Cassia
|
||||
Sabina
|
||||
Flavia
|
||||
Numa
|
||||
Ceres
|
||||
Diana
|
||||
Venus
|
||||
Pluto
|
||||
Pan
|
||||
Ramius
|
||||
Aether
|
||||
Chaos
|
||||
Logos
|
||||
Ethos
|
||||
Tethys
|
||||
Chiron
|
||||
Talos
|
||||
Icarus
|
||||
Daedalus
|
||||
Hyacinth
|
||||
Adonis
|
||||
Perseus
|
||||
Theseus
|
||||
Argos
|
||||
Lemnos
|
||||
Delos
|
||||
Sparta
|
||||
Attica
|
||||
Corinth
|
||||
Thebes
|
||||
Ephyra
|
||||
Achaea
|
||||
Cyrene
|
||||
Sidon
|
||||
Tyre
|
||||
Ilium
|
||||
Etrus
|
||||
Vercos
|
||||
Aurex
|
||||
Novian
|
||||
Helion
|
||||
Casson
|
||||
Aurelix
|
||||
|
|
@ -3,7 +3,9 @@ use crate::agent::guards::Guards;
|
|||
use crate::agent::status::is_final;
|
||||
use crate::error::CodexErr;
|
||||
use crate::error::Result as CodexResult;
|
||||
use crate::find_thread_path_by_id_str;
|
||||
use crate::session_prefix::format_subagent_notification_message;
|
||||
use crate::state_db;
|
||||
use crate::thread_manager::ThreadManagerState;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::Op;
|
||||
|
|
@ -11,11 +13,20 @@ use codex_protocol::protocol::SessionSource;
|
|||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Weak;
|
||||
use tokio::sync::watch;
|
||||
|
||||
const AGENT_NAMES: &str = include_str!("agent_names.txt");
|
||||
|
||||
fn agent_nickname_list() -> Vec<&'static str> {
|
||||
AGENT_NAMES
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Control-plane handle for multi-agent operations.
|
||||
/// `AgentControl` is held by each session (via `SessionServices`). It provides capability to
|
||||
/// spawn new agents and the inter-agent communication layer.
|
||||
|
|
@ -48,7 +59,24 @@ impl AgentControl {
|
|||
session_source: Option<SessionSource>,
|
||||
) -> CodexResult<ThreadId> {
|
||||
let state = self.upgrade()?;
|
||||
let reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let session_source = match session_source {
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_role,
|
||||
..
|
||||
})) => {
|
||||
let agent_nickname = reservation.reserve_agent_nickname(&agent_nickname_list())?;
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_nickname: Some(agent_nickname),
|
||||
agent_role,
|
||||
}))
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
let notification_source = session_source.clone();
|
||||
|
||||
// The same `AgentControl` is sent to spawn the thread.
|
||||
|
|
@ -77,12 +105,51 @@ impl AgentControl {
|
|||
pub(crate) async fn resume_agent_from_rollout(
|
||||
&self,
|
||||
config: crate::config::Config,
|
||||
rollout_path: PathBuf,
|
||||
thread_id: ThreadId,
|
||||
session_source: SessionSource,
|
||||
) -> CodexResult<ThreadId> {
|
||||
let state = self.upgrade()?;
|
||||
let reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let session_source = match session_source {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
..
|
||||
}) => {
|
||||
// Collab resume callers rebuild a placeholder ThreadSpawn source. Rehydrate the
|
||||
// stored nickname/role from sqlite when available; otherwise leave both unset.
|
||||
let (resumed_agent_nickname, resumed_agent_role) =
|
||||
if let Some(state_db_ctx) = state_db::get_state_db(&config, None).await {
|
||||
match state_db_ctx.get_thread(thread_id).await {
|
||||
Ok(Some(metadata)) => (metadata.agent_nickname, metadata.agent_role),
|
||||
Ok(None) | Err(_) => (None, None),
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let reserved_agent_nickname = resumed_agent_nickname
|
||||
.as_deref()
|
||||
.map(|agent_nickname| {
|
||||
reservation.reserve_agent_nickname_with_preference(
|
||||
&agent_nickname_list(),
|
||||
Some(agent_nickname),
|
||||
)
|
||||
})
|
||||
.transpose()?;
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_nickname: reserved_agent_nickname,
|
||||
agent_role: resumed_agent_role,
|
||||
})
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
let notification_source = session_source.clone();
|
||||
let rollout_path =
|
||||
find_thread_path_by_id_str(config.codex_home.as_path(), &thread_id.to_string())
|
||||
.await?
|
||||
.ok_or_else(|| CodexErr::ThreadNotFound(thread_id))?;
|
||||
|
||||
let resumed_thread = state
|
||||
.resume_thread_from_rollout_with_source(
|
||||
|
|
@ -234,6 +301,8 @@ mod tests {
|
|||
use crate::agent::agent_status_from_event;
|
||||
use crate::config::Config;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config_loader::LoaderOverrides;
|
||||
use crate::features::Feature;
|
||||
use crate::session_prefix::SUBAGENT_NOTIFICATION_OPEN_TAG;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
|
|
@ -261,6 +330,12 @@ mod tests {
|
|||
let config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.cli_overrides(cli_overrides)
|
||||
.loader_overrides(LoaderOverrides {
|
||||
#[cfg(target_os = "macos")]
|
||||
managed_preferences_base64: Some(String::new()),
|
||||
macos_managed_config_requirements_base64: Some(String::new()),
|
||||
..LoaderOverrides::default()
|
||||
})
|
||||
.build()
|
||||
.await
|
||||
.expect("load default test config");
|
||||
|
|
@ -441,11 +516,7 @@ mod tests {
|
|||
let control = AgentControl::default();
|
||||
let (_home, config) = test_config().await;
|
||||
let err = control
|
||||
.resume_agent_from_rollout(
|
||||
config,
|
||||
PathBuf::from("/tmp/missing-rollout.jsonl"),
|
||||
SessionSource::Exec,
|
||||
)
|
||||
.resume_agent_from_rollout(config, ThreadId::new(), SessionSource::Exec)
|
||||
.await
|
||||
.expect_err("resume_agent should fail without a manager");
|
||||
assert_eq!(
|
||||
|
|
@ -717,12 +788,6 @@ mod tests {
|
|||
.spawn_agent(config.clone(), text_input("hello"), None)
|
||||
.await
|
||||
.expect("spawn_agent should succeed");
|
||||
let rollout_path = manager
|
||||
.get_thread(resumable_id)
|
||||
.await
|
||||
.expect("thread should exist")
|
||||
.rollout_path()
|
||||
.expect("rollout path should exist");
|
||||
let _ = control
|
||||
.shutdown_agent(resumable_id)
|
||||
.await
|
||||
|
|
@ -734,7 +799,7 @@ mod tests {
|
|||
.expect("spawn_agent should succeed for active slot");
|
||||
|
||||
let err = control
|
||||
.resume_agent_from_rollout(config, rollout_path, SessionSource::Exec)
|
||||
.resume_agent_from_rollout(config, resumable_id, SessionSource::Exec)
|
||||
.await
|
||||
.expect_err("resume should respect max threads");
|
||||
let CodexErr::AgentLimitReached {
|
||||
|
|
@ -766,9 +831,8 @@ mod tests {
|
|||
);
|
||||
let control = manager.agent_control();
|
||||
|
||||
let missing_rollout = config.codex_home.join("sessions/missing-rollout.jsonl");
|
||||
let _ = control
|
||||
.resume_agent_from_rollout(config.clone(), missing_rollout, SessionSource::Exec)
|
||||
.resume_agent_from_rollout(config.clone(), ThreadId::new(), SessionSource::Exec)
|
||||
.await
|
||||
.expect_err("resume should fail for missing rollout path");
|
||||
|
||||
|
|
@ -795,6 +859,8 @@ mod tests {
|
|||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
)
|
||||
.await
|
||||
|
|
@ -812,4 +878,176 @@ mod tests {
|
|||
|
||||
assert_eq!(wait_for_subagent_notification(&parent_thread).await, true);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_thread_subagent_gets_random_nickname_in_session_source() {
|
||||
let harness = AgentControlHarness::new().await;
|
||||
let (parent_thread_id, _parent_thread) = harness.start_thread().await;
|
||||
|
||||
let child_thread_id = harness
|
||||
.control
|
||||
.spawn_agent(
|
||||
harness.config.clone(),
|
||||
text_input("hello child"),
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.expect("child spawn should succeed");
|
||||
|
||||
let child_thread = harness
|
||||
.manager
|
||||
.get_thread(child_thread_id)
|
||||
.await
|
||||
.expect("child thread should be registered");
|
||||
let snapshot = child_thread.config_snapshot().await;
|
||||
|
||||
let SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: seen_parent_thread_id,
|
||||
depth,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
}) = snapshot.session_source
|
||||
else {
|
||||
panic!("expected thread-spawn sub-agent source");
|
||||
};
|
||||
assert_eq!(seen_parent_thread_id, parent_thread_id);
|
||||
assert_eq!(depth, 1);
|
||||
assert!(agent_nickname.is_some());
|
||||
assert_eq!(agent_role, Some("explorer".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
||||
let (home, mut config) = test_config().await;
|
||||
config.features.enable(Feature::Sqlite);
|
||||
let manager = ThreadManager::with_models_provider_and_home_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.clone(),
|
||||
);
|
||||
let control = manager.agent_control();
|
||||
let harness = AgentControlHarness {
|
||||
_home: home,
|
||||
config,
|
||||
manager,
|
||||
control,
|
||||
};
|
||||
let (parent_thread_id, _parent_thread) = harness.start_thread().await;
|
||||
|
||||
let child_thread_id = harness
|
||||
.control
|
||||
.spawn_agent(
|
||||
harness.config.clone(),
|
||||
text_input("hello child"),
|
||||
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: Some("explorer".to_string()),
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.expect("child spawn should succeed");
|
||||
|
||||
let child_thread = harness
|
||||
.manager
|
||||
.get_thread(child_thread_id)
|
||||
.await
|
||||
.expect("child thread should exist");
|
||||
let mut status_rx = harness
|
||||
.control
|
||||
.subscribe_status(child_thread_id)
|
||||
.await
|
||||
.expect("status subscription should succeed");
|
||||
if matches!(status_rx.borrow().clone(), AgentStatus::PendingInit) {
|
||||
timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
status_rx
|
||||
.changed()
|
||||
.await
|
||||
.expect("child status should advance past pending init");
|
||||
if !matches!(status_rx.borrow().clone(), AgentStatus::PendingInit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("child should initialize before shutdown");
|
||||
}
|
||||
let original_snapshot = child_thread.config_snapshot().await;
|
||||
let original_nickname = original_snapshot
|
||||
.session_source
|
||||
.get_nickname()
|
||||
.expect("spawned sub-agent should have a nickname");
|
||||
let state_db = child_thread
|
||||
.state_db()
|
||||
.expect("sqlite state db should be available for nickname resume test");
|
||||
timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
if let Ok(Some(metadata)) = state_db.get_thread(child_thread_id).await
|
||||
&& metadata.agent_nickname.is_some()
|
||||
&& metadata.agent_role.as_deref() == Some("explorer")
|
||||
{
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("child thread metadata should be persisted to sqlite before shutdown");
|
||||
|
||||
let _ = harness
|
||||
.control
|
||||
.shutdown_agent(child_thread_id)
|
||||
.await
|
||||
.expect("child shutdown should submit");
|
||||
|
||||
let resumed_thread_id = harness
|
||||
.control
|
||||
.resume_agent_from_rollout(
|
||||
harness.config.clone(),
|
||||
child_thread_id,
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("resume should succeed");
|
||||
assert_eq!(resumed_thread_id, child_thread_id);
|
||||
|
||||
let resumed_snapshot = harness
|
||||
.manager
|
||||
.get_thread(resumed_thread_id)
|
||||
.await
|
||||
.expect("resumed child thread should exist")
|
||||
.config_snapshot()
|
||||
.await;
|
||||
let SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: resumed_parent_thread_id,
|
||||
depth: resumed_depth,
|
||||
agent_nickname: resumed_nickname,
|
||||
agent_role: resumed_role,
|
||||
}) = resumed_snapshot.session_source
|
||||
else {
|
||||
panic!("expected thread-spawn sub-agent source");
|
||||
};
|
||||
assert_eq!(resumed_parent_thread_id, parent_thread_id);
|
||||
assert_eq!(resumed_depth, 1);
|
||||
assert_eq!(resumed_nickname, Some(original_nickname));
|
||||
assert_eq!(resumed_role, Some("explorer".to_string()));
|
||||
|
||||
let _ = harness
|
||||
.control
|
||||
.shutdown_agent(resumed_thread_id)
|
||||
.await
|
||||
.expect("resumed child shutdown should submit");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ use crate::error::Result;
|
|||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use rand::prelude::IndexedRandom;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
|
@ -17,10 +19,18 @@ use std::sync::atomic::Ordering;
|
|||
/// is).
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Guards {
|
||||
threads_set: Mutex<HashSet<ThreadId>>,
|
||||
active_agents: Mutex<ActiveAgents>,
|
||||
total_count: AtomicUsize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ActiveAgents {
|
||||
threads_set: HashSet<ThreadId>,
|
||||
thread_agent_nicknames: HashMap<ThreadId, String>,
|
||||
used_agent_nicknames: HashSet<String>,
|
||||
nickname_reset_count: usize,
|
||||
}
|
||||
|
||||
fn session_depth(session_source: &SessionSource) -> i32 {
|
||||
match session_source {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) => *depth,
|
||||
|
|
@ -52,28 +62,69 @@ impl Guards {
|
|||
Ok(SpawnReservation {
|
||||
state: Arc::clone(self),
|
||||
active: true,
|
||||
reserved_agent_nickname: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn release_spawned_thread(&self, thread_id: ThreadId) {
|
||||
let removed = {
|
||||
let mut threads = self
|
||||
.threads_set
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
threads.remove(&thread_id)
|
||||
let removed = active_agents.threads_set.remove(&thread_id);
|
||||
active_agents.thread_agent_nicknames.remove(&thread_id);
|
||||
removed
|
||||
};
|
||||
if removed {
|
||||
self.total_count.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
fn register_spawned_thread(&self, thread_id: ThreadId) {
|
||||
let mut threads = self
|
||||
.threads_set
|
||||
fn register_spawned_thread(&self, thread_id: ThreadId, agent_nickname: Option<String>) {
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
threads.insert(thread_id);
|
||||
active_agents.threads_set.insert(thread_id);
|
||||
if let Some(agent_nickname) = agent_nickname {
|
||||
active_agents
|
||||
.used_agent_nicknames
|
||||
.insert(agent_nickname.clone());
|
||||
active_agents
|
||||
.thread_agent_nicknames
|
||||
.insert(thread_id, agent_nickname);
|
||||
}
|
||||
}
|
||||
|
||||
fn reserve_agent_nickname(&self, names: &[&str], preferred: Option<&str>) -> Option<String> {
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let agent_nickname = if let Some(preferred) = preferred {
|
||||
preferred.to_string()
|
||||
} else {
|
||||
if names.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let available_names: Vec<&str> = names
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|name| !active_agents.used_agent_nicknames.contains(*name))
|
||||
.collect();
|
||||
if let Some(name) = available_names.choose(&mut rand::rng()) {
|
||||
(*name).to_string()
|
||||
} else {
|
||||
active_agents.used_agent_nicknames.clear();
|
||||
active_agents.nickname_reset_count += 1;
|
||||
names.choose(&mut rand::rng())?.to_string()
|
||||
}
|
||||
};
|
||||
active_agents
|
||||
.used_agent_nicknames
|
||||
.insert(agent_nickname.clone());
|
||||
Some(agent_nickname)
|
||||
}
|
||||
|
||||
fn try_increment_spawned(&self, max_threads: usize) -> bool {
|
||||
|
|
@ -98,11 +149,41 @@ impl Guards {
|
|||
pub(crate) struct SpawnReservation {
|
||||
state: Arc<Guards>,
|
||||
active: bool,
|
||||
reserved_agent_nickname: Option<String>,
|
||||
}
|
||||
|
||||
impl SpawnReservation {
|
||||
pub(crate) fn commit(mut self, thread_id: ThreadId) {
|
||||
self.state.register_spawned_thread(thread_id);
|
||||
pub(crate) fn reserve_agent_nickname(&mut self, names: &[&str]) -> Result<String> {
|
||||
self.reserve_agent_nickname_with_preference(names, None)
|
||||
}
|
||||
|
||||
pub(crate) fn reserve_agent_nickname_with_preference(
|
||||
&mut self,
|
||||
names: &[&str],
|
||||
preferred: Option<&str>,
|
||||
) -> Result<String> {
|
||||
let agent_nickname = self
|
||||
.state
|
||||
.reserve_agent_nickname(names, preferred)
|
||||
.ok_or_else(|| {
|
||||
CodexErr::UnsupportedOperation("no available agent nicknames".to_string())
|
||||
})?;
|
||||
self.reserved_agent_nickname = Some(agent_nickname.clone());
|
||||
Ok(agent_nickname)
|
||||
}
|
||||
|
||||
pub(crate) fn commit(self, thread_id: ThreadId) {
|
||||
self.commit_with_agent_nickname(thread_id, None);
|
||||
}
|
||||
|
||||
pub(crate) fn commit_with_agent_nickname(
|
||||
mut self,
|
||||
thread_id: ThreadId,
|
||||
agent_nickname: Option<String>,
|
||||
) {
|
||||
let agent_nickname = self.reserved_agent_nickname.take().or(agent_nickname);
|
||||
self.state
|
||||
.register_spawned_thread(thread_id, agent_nickname);
|
||||
self.active = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -130,6 +211,8 @@ mod tests {
|
|||
let session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: ThreadId::new(),
|
||||
depth: 1,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
let child_depth = next_thread_spawn_depth(&session_source);
|
||||
assert_eq!(child_depth, 2);
|
||||
|
|
@ -232,4 +315,83 @@ mod tests {
|
|||
.expect("slot released after second thread removal");
|
||||
drop(reservation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_spawn_keeps_nickname_marked_used() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
|
||||
let agent_nickname = reservation
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("reserve agent name");
|
||||
assert_eq!(agent_nickname, "alpha");
|
||||
drop(reservation);
|
||||
|
||||
let mut reservation = guards.reserve_spawn_slot(None).expect("reserve slot");
|
||||
let agent_nickname = reservation
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.expect("unused name should still be preferred");
|
||||
assert_eq!(agent_nickname, "beta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_nickname_resets_used_pool_when_exhausted() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
assert_eq!(first_name, "alpha");
|
||||
|
||||
let mut second = guards
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("name should be reused after pool reset");
|
||||
assert_eq!(second_name, "alpha");
|
||||
let active_agents = guards
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(active_agents.nickname_reset_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn released_nickname_stays_used_until_pool_reset() {
|
||||
let guards = Arc::new(Guards::default());
|
||||
|
||||
let mut first = guards.reserve_spawn_slot(None).expect("reserve first slot");
|
||||
let first_name = first
|
||||
.reserve_agent_nickname(&["alpha"])
|
||||
.expect("reserve first agent name");
|
||||
let first_id = ThreadId::new();
|
||||
first.commit(first_id);
|
||||
assert_eq!(first_name, "alpha");
|
||||
|
||||
guards.release_spawned_thread(first_id);
|
||||
|
||||
let mut second = guards
|
||||
.reserve_spawn_slot(None)
|
||||
.expect("reserve second slot");
|
||||
let second_name = second
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.expect("released name should still be marked used");
|
||||
assert_eq!(second_name, "beta");
|
||||
let second_id = ThreadId::new();
|
||||
second.commit(second_id);
|
||||
guards.release_spawned_thread(second_id);
|
||||
|
||||
let mut third = guards.reserve_spawn_slot(None).expect("reserve third slot");
|
||||
let third_name = third
|
||||
.reserve_agent_nickname(&["alpha", "beta"])
|
||||
.expect("pool reset should permit a duplicate");
|
||||
assert!(third_name == "alpha" || third_name == "beta");
|
||||
let active_agents = guards
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
assert_eq!(active_agents.nickname_reset_count, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,6 +172,8 @@ mod tests {
|
|||
originator: "test_originator".to_string(),
|
||||
cli_version: "test_version".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ pub struct ThreadsPage {
|
|||
}
|
||||
|
||||
/// Summary information for a thread rollout file.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
pub struct ThreadItem {
|
||||
/// Absolute path to the rollout file.
|
||||
pub path: PathBuf,
|
||||
|
|
@ -57,6 +57,10 @@ pub struct ThreadItem {
|
|||
pub git_origin_url: Option<String>,
|
||||
/// Session source from session metadata.
|
||||
pub source: Option<SessionSource>,
|
||||
/// Random unique nickname from session metadata for AgentControl-spawned sub-agents.
|
||||
pub agent_nickname: Option<String>,
|
||||
/// Role (agent_role) from session metadata for AgentControl-spawned sub-agents.
|
||||
pub agent_role: Option<String>,
|
||||
/// Model provider from session metadata.
|
||||
pub model_provider: Option<String>,
|
||||
/// CLI version from session metadata.
|
||||
|
|
@ -87,6 +91,8 @@ struct HeadTailSummary {
|
|||
git_sha: Option<String>,
|
||||
git_origin_url: Option<String>,
|
||||
source: Option<SessionSource>,
|
||||
agent_nickname: Option<String>,
|
||||
agent_role: Option<String>,
|
||||
model_provider: Option<String>,
|
||||
cli_version: Option<String>,
|
||||
created_at: Option<String>,
|
||||
|
|
@ -715,6 +721,8 @@ async fn build_thread_item(
|
|||
git_sha,
|
||||
git_origin_url,
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
cli_version,
|
||||
created_at,
|
||||
|
|
@ -733,6 +741,8 @@ async fn build_thread_item(
|
|||
git_sha,
|
||||
git_origin_url,
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
cli_version,
|
||||
created_at,
|
||||
|
|
@ -1017,6 +1027,8 @@ async fn read_head_summary(path: &Path, head_limit: usize) -> io::Result<HeadTai
|
|||
RolloutItem::SessionMeta(session_meta_line) => {
|
||||
if !summary.saw_session_meta {
|
||||
summary.source = Some(session_meta_line.meta.source.clone());
|
||||
summary.agent_nickname = session_meta_line.meta.agent_nickname.clone();
|
||||
summary.agent_role = session_meta_line.meta.agent_role.clone();
|
||||
summary.model_provider = session_meta_line.meta.model_provider.clone();
|
||||
summary.thread_id = Some(session_meta_line.meta.id);
|
||||
summary.cwd = Some(session_meta_line.meta.cwd.clone());
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ pub(crate) fn builder_from_session_meta(
|
|||
session_meta.meta.source.clone(),
|
||||
);
|
||||
builder.model_provider = session_meta.meta.model_provider.clone();
|
||||
builder.agent_nickname = session_meta.meta.agent_nickname.clone();
|
||||
builder.agent_role = session_meta.meta.agent_role.clone();
|
||||
builder.cwd = session_meta.meta.cwd.clone();
|
||||
builder.cli_version = Some(session_meta.meta.cli_version.clone());
|
||||
builder.sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
|
|
@ -512,6 +514,8 @@ mod tests {
|
|||
originator: "cli".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::default(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
|
|
@ -660,6 +664,8 @@ mod tests {
|
|||
originator: "cli".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
source: SessionSource::default(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("test-provider".to_string()),
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
|
|
|
|||
|
|
@ -391,6 +391,8 @@ impl RolloutRecorder {
|
|||
cwd: config.cwd.clone(),
|
||||
originator: originator().value,
|
||||
cli_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
agent_nickname: source.get_nickname(),
|
||||
agent_role: source.get_agent_role(),
|
||||
source,
|
||||
model_provider: Some(config.model_provider_id.clone()),
|
||||
base_instructions: Some(base_instructions),
|
||||
|
|
@ -929,9 +931,12 @@ impl From<codex_state::ThreadsPage> for ThreadsPage {
|
|||
git_sha: item.git_sha,
|
||||
git_origin_url: item.git_origin_url,
|
||||
source: Some(
|
||||
serde_json::from_value(Value::String(item.source))
|
||||
serde_json::from_str(item.source.as_str())
|
||||
.or_else(|_| serde_json::from_value(Value::String(item.source)))
|
||||
.unwrap_or(SessionSource::Unknown),
|
||||
),
|
||||
agent_nickname: item.agent_nickname,
|
||||
agent_role: item.agent_role,
|
||||
model_provider: Some(item.model_provider),
|
||||
cli_version: Some(item.cli_version),
|
||||
created_at: Some(item.created_at.to_rfc3339_opts(SecondsFormat::Secs, true)),
|
||||
|
|
|
|||
|
|
@ -560,6 +560,8 @@ async fn test_list_conversations_latest_first() {
|
|||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-01-03T12-00-00".into()),
|
||||
|
|
@ -574,6 +576,8 @@ async fn test_list_conversations_latest_first() {
|
|||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-01-02T12-00-00".into()),
|
||||
|
|
@ -588,6 +592,8 @@ async fn test_list_conversations_latest_first() {
|
|||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-01-01T12-00-00".into()),
|
||||
|
|
@ -695,6 +701,8 @@ async fn test_pagination_cursor() {
|
|||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-05T09-00-00".into()),
|
||||
|
|
@ -709,6 +717,8 @@ async fn test_pagination_cursor() {
|
|||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-04T09-00-00".into()),
|
||||
|
|
@ -759,6 +769,8 @@ async fn test_pagination_cursor() {
|
|||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-03T09-00-00".into()),
|
||||
|
|
@ -773,6 +785,8 @@ async fn test_pagination_cursor() {
|
|||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-02T09-00-00".into()),
|
||||
|
|
@ -814,6 +828,8 @@ async fn test_pagination_cursor() {
|
|||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-01T09-00-00".into()),
|
||||
|
|
@ -894,6 +910,8 @@ async fn test_get_thread_contents() {
|
|||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some(ts.into()),
|
||||
|
|
@ -1086,6 +1104,8 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
|
|||
originator: "test_originator".into(),
|
||||
cli_version: "test_version".into(),
|
||||
source: SessionSource::VSCode,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("test-provider".into()),
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
|
|
@ -1203,6 +1223,8 @@ async fn test_stable_ordering_same_second_pagination() {
|
|||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some(ts.to_string()),
|
||||
|
|
@ -1217,6 +1239,8 @@ async fn test_stable_ordering_same_second_pagination() {
|
|||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some(ts.to_string()),
|
||||
|
|
@ -1258,6 +1282,8 @@ async fn test_stable_ordering_same_second_pagination() {
|
|||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: Some(SessionSource::VSCode),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some(ts.to_string()),
|
||||
|
|
|
|||
|
|
@ -156,7 +156,11 @@ mod spawn {
|
|||
.spawn_agent(
|
||||
config,
|
||||
input_items,
|
||||
Some(thread_spawn_source(session.conversation_id, child_depth)),
|
||||
Some(thread_spawn_source(
|
||||
session.conversation_id,
|
||||
child_depth,
|
||||
role_name,
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.map_err(collab_spawn_error);
|
||||
|
|
@ -284,7 +288,6 @@ mod send_input {
|
|||
mod resume_agent {
|
||||
use super::*;
|
||||
use crate::agent::next_thread_spawn_depth;
|
||||
use crate::rollout::find_thread_path_by_id_str;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -331,15 +334,7 @@ mod resume_agent {
|
|||
.await;
|
||||
let error = if matches!(status, AgentStatus::NotFound) {
|
||||
// If the thread is no longer active, attempt to restore it from rollout.
|
||||
match try_resume_closed_agent(
|
||||
&session,
|
||||
&turn,
|
||||
receiver_thread_id,
|
||||
&args.id,
|
||||
child_depth,
|
||||
)
|
||||
.await
|
||||
{
|
||||
match try_resume_closed_agent(&session, &turn, receiver_thread_id, child_depth).await {
|
||||
Ok(resumed_status) => {
|
||||
status = resumed_status;
|
||||
None
|
||||
|
|
@ -388,33 +383,16 @@ mod resume_agent {
|
|||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
receiver_thread_id: ThreadId,
|
||||
receiver_id: &str,
|
||||
child_depth: i32,
|
||||
) -> Result<AgentStatus, FunctionCallError> {
|
||||
let rollout_path = find_thread_path_by_id_str(
|
||||
turn.config.codex_home.as_path(),
|
||||
receiver_id,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"tool failed: failed to locate rollout for agent {receiver_thread_id}: {err}"
|
||||
))
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"agent with id {receiver_thread_id} not found"
|
||||
))
|
||||
})?;
|
||||
|
||||
let config = build_agent_resume_config(turn.as_ref(), child_depth)?;
|
||||
let resumed_thread_id = session
|
||||
.services
|
||||
.agent_control
|
||||
.resume_agent_from_rollout(
|
||||
config,
|
||||
rollout_path,
|
||||
thread_spawn_source(session.conversation_id, child_depth),
|
||||
receiver_thread_id,
|
||||
thread_spawn_source(session.conversation_id, child_depth, None),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| collab_agent_error(receiver_thread_id, err))?;
|
||||
|
|
@ -733,10 +711,16 @@ fn collab_agent_error(agent_id: ThreadId, err: CodexErr) -> FunctionCallError {
|
|||
}
|
||||
}
|
||||
|
||||
fn thread_spawn_source(parent_thread_id: ThreadId, depth: i32) -> SessionSource {
|
||||
fn thread_spawn_source(
|
||||
parent_thread_id: ThreadId,
|
||||
depth: i32,
|
||||
agent_role: Option<&str>,
|
||||
) -> SessionSource {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
agent_nickname: None,
|
||||
agent_role: agent_role.map(str::to_string),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1068,6 +1052,8 @@ mod tests {
|
|||
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: session.conversation_id,
|
||||
depth: DEFAULT_AGENT_MAX_DEPTH,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
|
||||
let invocation = invocation(
|
||||
|
|
@ -1104,6 +1090,8 @@ mod tests {
|
|||
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: session.conversation_id,
|
||||
depth: DEFAULT_AGENT_MAX_DEPTH,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
|
||||
let invocation = invocation(
|
||||
|
|
@ -1487,6 +1475,8 @@ mod tests {
|
|||
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id: session.conversation_id,
|
||||
depth: DEFAULT_AGENT_MAX_DEPTH,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
});
|
||||
|
||||
let invocation = invocation(
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
|
|||
originator: "test_originator".to_string(),
|
||||
cli_version: "test_version".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
|
|
@ -107,6 +109,8 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re
|
|||
originator: "test_originator".to_string(),
|
||||
cli_version: "test_version".to_string(),
|
||||
source: SessionSource::Cli,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
|
|||
originator: "test".to_string(),
|
||||
cli_version: "test".to_string(),
|
||||
source: SessionSource::default(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: Some(dynamic_tools_for_hook),
|
||||
|
|
|
|||
|
|
@ -1861,6 +1861,10 @@ pub enum SubAgentSource {
|
|||
ThreadSpawn {
|
||||
parent_thread_id: ThreadId,
|
||||
depth: i32,
|
||||
#[serde(default)]
|
||||
agent_nickname: Option<String>,
|
||||
#[serde(default, alias = "agent_type")]
|
||||
agent_role: Option<String>,
|
||||
},
|
||||
MemoryConsolidation,
|
||||
Other(String),
|
||||
|
|
@ -1879,6 +1883,26 @@ impl fmt::Display for SessionSource {
|
|||
}
|
||||
}
|
||||
|
||||
impl SessionSource {
|
||||
pub fn get_nickname(&self) -> Option<String> {
|
||||
match self {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_nickname, .. }) => {
|
||||
agent_nickname.clone()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_agent_role(&self) -> Option<String> {
|
||||
match self {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_role, .. }) => {
|
||||
agent_role.clone()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SubAgentSource {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
|
|
@ -1888,6 +1912,7 @@ impl fmt::Display for SubAgentSource {
|
|||
SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
depth,
|
||||
..
|
||||
} => {
|
||||
write!(f, "thread_spawn_{parent_thread_id}_d{depth}")
|
||||
}
|
||||
|
|
@ -1912,6 +1937,12 @@ pub struct SessionMeta {
|
|||
pub cli_version: String,
|
||||
#[serde(default)]
|
||||
pub source: SessionSource,
|
||||
/// Optional random unique nickname assigned to an AgentControl-spawned sub-agent.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_nickname: Option<String>,
|
||||
/// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
#[serde(default, alias = "agent_type", skip_serializing_if = "Option::is_none")]
|
||||
pub agent_role: Option<String>,
|
||||
pub model_provider: Option<String>,
|
||||
/// base_instructions for the session. This *should* always be present when creating a new session,
|
||||
/// but may be missing for older sessions. If not present, fall back to rendering the base_instructions
|
||||
|
|
@ -1931,6 +1962,8 @@ impl Default for SessionMeta {
|
|||
originator: String::new(),
|
||||
cli_version: String::new(),
|
||||
source: SessionSource::default(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE threads ADD COLUMN agent_nickname TEXT;
|
||||
ALTER TABLE threads ADD COLUMN agent_role TEXT;
|
||||
|
|
@ -37,6 +37,8 @@ fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &Sessi
|
|||
}
|
||||
metadata.id = meta_line.meta.id;
|
||||
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();
|
||||
if let Some(provider) = meta_line.meta.model_provider.as_deref() {
|
||||
metadata.model_provider = provider.to_string();
|
||||
}
|
||||
|
|
@ -216,6 +218,8 @@ mod tests {
|
|||
created_at,
|
||||
updated_at: created_at,
|
||||
source: "cli".to_string(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: "openai".to_string(),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ pub struct ThreadMetadata {
|
|||
pub updated_at: DateTime<Utc>,
|
||||
/// The session source (stringified enum).
|
||||
pub source: String,
|
||||
/// Optional random unique nickname assigned to an AgentControl-spawned sub-agent.
|
||||
pub agent_nickname: Option<String>,
|
||||
/// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
pub agent_role: Option<String>,
|
||||
/// The model provider identifier.
|
||||
pub model_provider: String,
|
||||
/// The working directory for the thread.
|
||||
|
|
@ -101,6 +105,10 @@ pub struct ThreadMetadataBuilder {
|
|||
pub updated_at: Option<DateTime<Utc>>,
|
||||
/// The session source.
|
||||
pub source: SessionSource,
|
||||
/// Optional random unique nickname assigned to the session.
|
||||
pub agent_nickname: Option<String>,
|
||||
/// Optional role (agent_role) assigned to the session.
|
||||
pub agent_role: Option<String>,
|
||||
/// The model provider identifier, if known.
|
||||
pub model_provider: Option<String>,
|
||||
/// The working directory for the thread.
|
||||
|
|
@ -135,6 +143,8 @@ impl ThreadMetadataBuilder {
|
|||
created_at,
|
||||
updated_at: None,
|
||||
source,
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
cwd: PathBuf::new(),
|
||||
cli_version: None,
|
||||
|
|
@ -163,6 +173,8 @@ impl ThreadMetadataBuilder {
|
|||
created_at,
|
||||
updated_at,
|
||||
source,
|
||||
agent_nickname: self.agent_nickname.clone(),
|
||||
agent_role: self.agent_role.clone(),
|
||||
model_provider: self
|
||||
.model_provider
|
||||
.clone()
|
||||
|
|
@ -201,6 +213,12 @@ impl ThreadMetadata {
|
|||
if self.source != other.source {
|
||||
diffs.push("source");
|
||||
}
|
||||
if self.agent_nickname != other.agent_nickname {
|
||||
diffs.push("agent_nickname");
|
||||
}
|
||||
if self.agent_role != other.agent_role {
|
||||
diffs.push("agent_role");
|
||||
}
|
||||
if self.model_provider != other.model_provider {
|
||||
diffs.push("model_provider");
|
||||
}
|
||||
|
|
@ -252,6 +270,8 @@ pub(crate) struct ThreadRow {
|
|||
created_at: i64,
|
||||
updated_at: i64,
|
||||
source: String,
|
||||
agent_nickname: Option<String>,
|
||||
agent_role: Option<String>,
|
||||
model_provider: String,
|
||||
cwd: String,
|
||||
cli_version: String,
|
||||
|
|
@ -274,6 +294,8 @@ impl ThreadRow {
|
|||
created_at: row.try_get("created_at")?,
|
||||
updated_at: row.try_get("updated_at")?,
|
||||
source: row.try_get("source")?,
|
||||
agent_nickname: row.try_get("agent_nickname")?,
|
||||
agent_role: row.try_get("agent_role")?,
|
||||
model_provider: row.try_get("model_provider")?,
|
||||
cwd: row.try_get("cwd")?,
|
||||
cli_version: row.try_get("cli_version")?,
|
||||
|
|
@ -300,6 +322,8 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
|||
created_at,
|
||||
updated_at,
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
cwd,
|
||||
cli_version,
|
||||
|
|
@ -319,6 +343,8 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
|||
created_at: epoch_seconds_to_datetime(created_at)?,
|
||||
updated_at: epoch_seconds_to_datetime(updated_at)?,
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
cwd: PathBuf::from(cwd),
|
||||
cli_version,
|
||||
|
|
|
|||
|
|
@ -211,6 +211,8 @@ SELECT
|
|||
created_at,
|
||||
updated_at,
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
cwd,
|
||||
cli_version,
|
||||
|
|
@ -310,6 +312,8 @@ SELECT
|
|||
created_at,
|
||||
updated_at,
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
cwd,
|
||||
cli_version,
|
||||
|
|
@ -691,6 +695,8 @@ INSERT INTO threads (
|
|||
created_at,
|
||||
updated_at,
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
cwd,
|
||||
cli_version,
|
||||
|
|
@ -704,12 +710,14 @@ INSERT INTO threads (
|
|||
git_sha,
|
||||
git_branch,
|
||||
git_origin_url
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
rollout_path = excluded.rollout_path,
|
||||
created_at = excluded.created_at,
|
||||
updated_at = excluded.updated_at,
|
||||
source = excluded.source,
|
||||
agent_nickname = excluded.agent_nickname,
|
||||
agent_role = excluded.agent_role,
|
||||
model_provider = excluded.model_provider,
|
||||
cwd = excluded.cwd,
|
||||
cli_version = excluded.cli_version,
|
||||
|
|
@ -730,6 +738,8 @@ ON CONFLICT(id) DO UPDATE SET
|
|||
.bind(datetime_to_epoch_seconds(metadata.created_at))
|
||||
.bind(datetime_to_epoch_seconds(metadata.updated_at))
|
||||
.bind(metadata.source.as_str())
|
||||
.bind(metadata.agent_nickname.as_deref())
|
||||
.bind(metadata.agent_role.as_deref())
|
||||
.bind(metadata.model_provider.as_str())
|
||||
.bind(metadata.cwd.display().to_string())
|
||||
.bind(metadata.cli_version.as_str())
|
||||
|
|
@ -3092,6 +3102,8 @@ VALUES (?, ?, ?, ?, ?)
|
|||
created_at: now,
|
||||
updated_at: now,
|
||||
source: "cli".to_string(),
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
cwd,
|
||||
cli_version: "0.0.0".to_string(),
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ SELECT
|
|||
created_at,
|
||||
updated_at,
|
||||
source,
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
cwd,
|
||||
cli_version,
|
||||
|
|
|
|||
|
|
@ -1327,17 +1327,10 @@ mod tests {
|
|||
fn make_item(path: &str, ts: &str, preview: &str) -> ThreadItem {
|
||||
ThreadItem {
|
||||
path: PathBuf::from(path),
|
||||
thread_id: None,
|
||||
first_user_message: Some(preview.to_string()),
|
||||
cwd: None,
|
||||
git_branch: None,
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: None,
|
||||
model_provider: None,
|
||||
cli_version: None,
|
||||
created_at: Some(ts.to_string()),
|
||||
updated_at: Some(ts.to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1462,17 +1455,10 @@ mod tests {
|
|||
fn head_to_row_uses_first_user_message() {
|
||||
let item = ThreadItem {
|
||||
path: PathBuf::from("/tmp/a.jsonl"),
|
||||
thread_id: None,
|
||||
first_user_message: Some("real question".to_string()),
|
||||
cwd: None,
|
||||
git_branch: None,
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: None,
|
||||
model_provider: None,
|
||||
cli_version: None,
|
||||
created_at: Some("2025-01-01T00:00:00Z".into()),
|
||||
updated_at: Some("2025-01-01T00:00:00Z".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let row = head_to_row(&item);
|
||||
assert_eq!(row.preview, "real question");
|
||||
|
|
@ -1483,31 +1469,17 @@ mod tests {
|
|||
// Construct two items with different timestamps and real user text.
|
||||
let a = ThreadItem {
|
||||
path: PathBuf::from("/tmp/a.jsonl"),
|
||||
thread_id: None,
|
||||
first_user_message: Some("A".to_string()),
|
||||
cwd: None,
|
||||
git_branch: None,
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: None,
|
||||
model_provider: None,
|
||||
cli_version: None,
|
||||
created_at: Some("2025-01-01T00:00:00Z".into()),
|
||||
updated_at: Some("2025-01-01T00:00:00Z".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let b = ThreadItem {
|
||||
path: PathBuf::from("/tmp/b.jsonl"),
|
||||
thread_id: None,
|
||||
first_user_message: Some("B".to_string()),
|
||||
cwd: None,
|
||||
git_branch: None,
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: None,
|
||||
model_provider: None,
|
||||
cli_version: None,
|
||||
created_at: Some("2025-01-02T00:00:00Z".into()),
|
||||
updated_at: Some("2025-01-02T00:00:00Z".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let rows = rows_from_items(vec![a, b]);
|
||||
assert_eq!(rows.len(), 2);
|
||||
|
|
@ -1520,17 +1492,10 @@ mod tests {
|
|||
fn row_uses_tail_timestamp_for_updated_at() {
|
||||
let item = ThreadItem {
|
||||
path: PathBuf::from("/tmp/a.jsonl"),
|
||||
thread_id: None,
|
||||
first_user_message: Some("Hello".to_string()),
|
||||
cwd: None,
|
||||
git_branch: None,
|
||||
git_sha: None,
|
||||
git_origin_url: None,
|
||||
source: None,
|
||||
model_provider: None,
|
||||
cli_version: None,
|
||||
created_at: Some("2025-01-01T00:00:00Z".into()),
|
||||
updated_at: Some("2025-01-01T01:00:00Z".into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let row = head_to_row(&item);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue