mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
Expose parent context for host-managed child startup (#58390)
## What changed Replace `PreparedThreadSpawn` with `PreparedChildContext`. `CodexThread::prepare_child` now captures parent inputs from a live tool invocation without requiring a child request. Hosts choose the child's name, role, and history, bind destination services, and apply child settings. Expose captured instructions, environments, permissions, approval settings, disabled plugins, current inheritable dynamic tools, and an execution-policy snapshot with its original managed requirements. Retain those requirements across MCP configuration refreshes so hosts can assess policy-source compatibility. Remove `ThreadManager::create_prepared_child`; `CodexThread::create_child` handles reserved child startup on the parent's manager with invocation-bound cancellation and cleanup. ## Testing Add coverage for execution-policy snapshots and managed requirements after MCP refresh. Update child-spawn tests to verify captured instructions and permissions, current dynamic-tool declarations, and fresh versus forked history. GitOrigin-RevId: f15b595b44997fa70cd0c9be0e92f470e1f333e1
This commit is contained in:
@@ -63,6 +63,7 @@ pub(crate) use self::runtime::LocalAgentRuntime;
|
||||
pub(crate) use self::watch::StatusSubscription;
|
||||
pub(in crate::agent) use spawn::effective_spawn_config;
|
||||
pub(in crate::agent) use spawn::fresh_child_history_and_tools;
|
||||
pub(in crate::agent) use spawn::inherited_dynamic_tools;
|
||||
pub(in crate::agent) use spawn_guard::PendingSpawn;
|
||||
pub(in crate::agent) use spawn_guard::SpawnOwner;
|
||||
|
||||
|
||||
@@ -87,14 +87,24 @@ pub(in crate::agent) async fn fresh_child_history_and_tools(
|
||||
ThreadHistoryMode::Paginated
|
||||
)
|
||||
.then_some(ThreadHistoryMode::Paginated);
|
||||
let tools = if version == MultiAgentVersion::V2
|
||||
(
|
||||
history_mode,
|
||||
inherited_dynamic_tools(parent, config, version).await,
|
||||
)
|
||||
}
|
||||
|
||||
pub(in crate::agent) async fn inherited_dynamic_tools(
|
||||
parent: &CodexThread,
|
||||
config: &Config,
|
||||
version: MultiAgentVersion,
|
||||
) -> Vec<DynamicToolSpec> {
|
||||
if version == MultiAgentVersion::V2
|
||||
&& config.features.enabled(Feature::MultiAgentV2DynamicTools)
|
||||
{
|
||||
parent.session.dynamic_tools().await.to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
(history_mode, tools)
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalAgentControl {
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
//! Bind host child preparation and startup to a live tool invocation.
|
||||
|
||||
use super::spawn::SpawnDestination;
|
||||
use super::spawn::prepare_spawn;
|
||||
use super::spawn::start_prepared;
|
||||
use super::spawn::create_spawn;
|
||||
use super::types::HostThreadSpawnReservation;
|
||||
use super::types::PreparedThreadSpawn;
|
||||
use super::types::PreparedChildContext;
|
||||
use crate::agent::control::inherited_dynamic_tools;
|
||||
use crate::codex_thread::CodexThread;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::thread_spawn::ChildSpawnSettings;
|
||||
use crate::thread_spawn::ThreadCreateRequest;
|
||||
use crate::thread_spawn::ThreadSpawnError;
|
||||
use crate::thread_spawn::ThreadSpawnResult;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use futures::TryFutureExt;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::future::Future;
|
||||
@@ -195,50 +191,71 @@ pub(crate) async fn create(
|
||||
request: ThreadCreateRequest,
|
||||
reservation: HostThreadSpawnReservation,
|
||||
) -> Result<ThreadSpawnResult, ThreadSpawnError> {
|
||||
with_spawn_request(
|
||||
thread,
|
||||
invocation,
|
||||
request,
|
||||
|request, task_name| async move {
|
||||
let prepared = Box::pin(prepare_spawn(
|
||||
thread,
|
||||
request,
|
||||
task_name,
|
||||
SpawnDestination::SameManager,
|
||||
))
|
||||
.await?;
|
||||
Box::pin(start_prepared(
|
||||
&thread.session.services.local_agent_runtime,
|
||||
prepared,
|
||||
reservation,
|
||||
Some(invocation),
|
||||
))
|
||||
.await
|
||||
},
|
||||
)
|
||||
with_live_invocation(thread, invocation, |step| async move {
|
||||
let (settings, task_name) =
|
||||
crate::thread_spawn::prepare(&thread.session, &step, &invocation.call_id, request)
|
||||
.await?;
|
||||
Box::pin(create_spawn(
|
||||
thread,
|
||||
invocation,
|
||||
settings,
|
||||
task_name,
|
||||
reservation,
|
||||
))
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn prepare(
|
||||
thread: &CodexThread,
|
||||
invocation: &ToolInvocationContext,
|
||||
request: ThreadCreateRequest,
|
||||
) -> Result<PreparedThreadSpawn, ThreadSpawnError> {
|
||||
with_spawn_request(thread, invocation, request, |request, task_name| {
|
||||
prepare_spawn(thread, request, task_name, SpawnDestination::Detached)
|
||||
.map_err(ThreadSpawnError::from)
|
||||
) -> Result<PreparedChildContext, ThreadSpawnError> {
|
||||
with_live_invocation(thread, invocation, |step| async move {
|
||||
let turn = &step.turn;
|
||||
let dynamic_tools =
|
||||
inherited_dynamic_tools(thread, &turn.config, turn.multi_agent_version).await;
|
||||
let state = thread.session.services.local_agent_runtime.upgrade()?;
|
||||
if !std::ptr::eq(
|
||||
state.get_thread(thread.session.thread_id).await?.as_ref(),
|
||||
thread,
|
||||
) {
|
||||
return Err(CodexErr::ThreadNotFound(thread.session.thread_id).into());
|
||||
}
|
||||
let instructions = step.loaded_agents_md.as_deref();
|
||||
#[allow(deprecated)]
|
||||
let cwd = turn.cwd.clone();
|
||||
let (exec_policy, exec_policy_source_requirements) =
|
||||
thread.session.services.exec_policy.snapshot();
|
||||
Ok(PreparedChildContext {
|
||||
originator: turn.originator.clone(),
|
||||
user_instructions: instructions
|
||||
.and_then(|md| md.user_instructions.as_ref())
|
||||
.map(|entry| entry.text.clone()),
|
||||
thread_instructions: instructions
|
||||
.and_then(|md| md.thread_instructions.as_ref())
|
||||
.map(|entry| entry.text.clone()),
|
||||
cwd,
|
||||
environments: step.inherited_environments.inheritable_requests(),
|
||||
selected_capability_roots: step.inherited_environments.selected_capability_roots(),
|
||||
resolved_permission_profile: turn.config.permissions.permission_profile().clone(),
|
||||
approval_policy: step.settings.approval_policy(),
|
||||
approvals_reviewer: step.settings.approvals_reviewer(),
|
||||
exec_policy,
|
||||
exec_policy_source_requirements,
|
||||
dynamic_tools,
|
||||
disabled_plugin_ids: turn.disabled_plugin_ids.clone(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn with_spawn_request<T, F, Fut>(
|
||||
async fn with_live_invocation<T, Fut>(
|
||||
thread: &CodexThread,
|
||||
invocation: &ToolInvocationContext,
|
||||
request: ThreadCreateRequest,
|
||||
operation: F,
|
||||
operation: impl FnOnce(Arc<StepContext>) -> Fut,
|
||||
) -> Result<T, ThreadSpawnError>
|
||||
where
|
||||
F: FnOnce(ChildSpawnSettings, AgentPath) -> Fut,
|
||||
Fut: Future<Output = Result<T, ThreadSpawnError>>,
|
||||
{
|
||||
let session = &thread.session;
|
||||
@@ -261,8 +278,7 @@ where
|
||||
if !thread.is_running() {
|
||||
return Err(CodexErr::ThreadNotFound(session.thread_id).into());
|
||||
}
|
||||
let (request, task_name) = crate::thread_spawn::prepare(session, &step, &invocation.call_id, request).await?;
|
||||
operation(request, task_name).await
|
||||
operation(step).await
|
||||
} => result,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ pub(crate) use invocation::create;
|
||||
pub(crate) use invocation::prepare;
|
||||
pub(crate) use invocation::tool_invocation;
|
||||
pub use mcp_metadata::AgentMcpMetadataRecipient;
|
||||
pub(crate) use spawn::start_prepared;
|
||||
pub use types::HostThreadSpawnReservation;
|
||||
pub use types::PreparedChildContext;
|
||||
pub(crate) use types::PreparedChildInheritance;
|
||||
pub use types::PreparedThreadSpawn;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
//! Capture child startup inputs on the source and start the reserved child at the destination.
|
||||
//! Create a host-reserved child on the invoking thread's manager.
|
||||
|
||||
use super::invocation::HostToolInvocationLookup;
|
||||
use super::invocation::ToolInvocationContext;
|
||||
use super::types::HostThreadSpawnReservation;
|
||||
use super::types::PreparedChildInheritance;
|
||||
use super::types::PreparedThreadSpawn;
|
||||
use crate::agent::control::AgentControlInit;
|
||||
use crate::agent::control::LocalAgentRuntime;
|
||||
use crate::agent::control::PendingSpawn;
|
||||
use crate::agent::control::SpawnOwner;
|
||||
use crate::agent::control::effective_spawn_config;
|
||||
@@ -16,50 +14,29 @@ use crate::agent::history::prepare_forked_history;
|
||||
use crate::agent::types::AgentMetadata;
|
||||
use crate::agent::types::LiveAgent;
|
||||
use crate::codex_thread::CodexThread;
|
||||
use crate::exec_policy::ExecPolicyManager;
|
||||
use crate::session::session::AppServerClientMetadata;
|
||||
use crate::thread_manager::StartThreadOptions;
|
||||
use crate::thread_spawn::ChildSpawnSettings;
|
||||
use crate::thread_spawn::ThreadSpawnError;
|
||||
use crate::thread_spawn::ThreadSpawnResult;
|
||||
use codex_config::McpServerAuth;
|
||||
use codex_features::Feature;
|
||||
use codex_history::InitialHistory;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::error::AgentErrorContext;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result;
|
||||
use codex_protocol::protocol::EnvironmentConfigState;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_thread_store::ListThreadAttachmentsParams;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(super) enum SpawnDestination {
|
||||
SameManager,
|
||||
Detached,
|
||||
}
|
||||
|
||||
pub(super) async fn prepare_spawn(
|
||||
async fn prepare_spawn_inputs(
|
||||
parent: &CodexThread,
|
||||
request: ChildSpawnSettings,
|
||||
agent_path: AgentPath,
|
||||
destination: SpawnDestination,
|
||||
) -> Result<PreparedThreadSpawn> {
|
||||
if matches!(destination, SpawnDestination::Detached)
|
||||
&& (request.config.application_network_policy.is_managed()
|
||||
|| request.config.application_auth_route_config.is_some()
|
||||
|| request
|
||||
.config
|
||||
.config_layer_stack
|
||||
.cloud_config_binding()
|
||||
.is_some())
|
||||
{
|
||||
return Err(CodexErr::InvalidRequest(
|
||||
"prepared spawn requires destination binding for host-managed application authority"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
) -> Result<(
|
||||
StartThreadOptions,
|
||||
PreparedChildInheritance,
|
||||
AppServerClientMetadata,
|
||||
)> {
|
||||
let runtime = &parent.session.services.local_agent_runtime;
|
||||
let _membership = runtime.admit_start()?;
|
||||
let state = runtime.upgrade()?;
|
||||
@@ -68,60 +45,11 @@ pub(super) async fn prepare_spawn(
|
||||
source,
|
||||
options,
|
||||
} = request;
|
||||
if matches!(destination, SpawnDestination::Detached)
|
||||
&& config.features.enabled(Feature::UseXaa)
|
||||
&& config.mcp_enterprise_managed_auth.is_some()
|
||||
&& config.mcp_servers.get().values().any(|server| {
|
||||
server.enabled
|
||||
&& matches!(server.auth, McpServerAuth::EmaAuth)
|
||||
&& server.validate_ema_auth_transport().is_ok()
|
||||
})
|
||||
{
|
||||
return Err(CodexErr::InvalidRequest(
|
||||
"prepared spawn requires destination binding for EMA-authenticated MCP servers".into(),
|
||||
));
|
||||
}
|
||||
let caller = parent.session.thread_id;
|
||||
let instructions = parent.session.inherited_instructions().await;
|
||||
if matches!(destination, SpawnDestination::Detached)
|
||||
&& (instructions.thread_provider.is_some() || instructions.user_provider.is_some())
|
||||
{
|
||||
return Err(CodexErr::InvalidRequest(
|
||||
"prepared spawn requires destination binding for inherited instruction providers"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
let store = &parent.session.services.thread_store;
|
||||
if matches!(destination, SpawnDestination::Detached) && store.supports_thread_attachments() {
|
||||
let page = store
|
||||
.list_thread_attachments(ListThreadAttachmentsParams {
|
||||
thread_id: caller,
|
||||
cursor: None,
|
||||
limit: 1,
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
CodexErr::InvalidRequest(format!("cannot prepare child attachments: {error}"))
|
||||
})?;
|
||||
if !page.attachments.is_empty() {
|
||||
return Err(CodexErr::InvalidRequest(
|
||||
"prepared spawn requires a host handoff for thread attachments".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let snapshot = options.environments.as_ref().ok_or_else(|| {
|
||||
CodexErr::InvalidRequest("prepared spawn requires captured environments".into())
|
||||
})?;
|
||||
let environments = snapshot.inheritable_requests();
|
||||
if matches!(destination, SpawnDestination::Detached)
|
||||
&& environments
|
||||
.iter()
|
||||
.any(|environment| matches!(environment.config, EnvironmentConfigState::Pending))
|
||||
{
|
||||
return Err(CodexErr::InvalidRequest(
|
||||
"prepared spawn requires resolved environment configuration".into(),
|
||||
));
|
||||
}
|
||||
let (initial_history, history_mode, dynamic_tools) = if options.fork_mode.is_some() {
|
||||
let (history, mode) =
|
||||
prepare_forked_history(&state, &source, &options, &config, MultiAgentVersion::V2)
|
||||
@@ -136,7 +64,7 @@ pub(super) async fn prepare_spawn(
|
||||
if !std::ptr::eq(state.get_thread(caller).await?.as_ref(), parent) {
|
||||
return Err(CodexErr::ThreadNotFound(caller));
|
||||
}
|
||||
let inherited_policy = inherited_exec_policy_for_parent(parent, &config).await;
|
||||
let exec_policy = inherited_exec_policy_for_parent(parent, &config).await;
|
||||
let mut thread_options = StartThreadOptions {
|
||||
session_source: Some(source),
|
||||
thread_source: Some(ThreadSource::Subagent),
|
||||
@@ -158,52 +86,37 @@ pub(super) async fn prepare_spawn(
|
||||
thread_options
|
||||
.thread_extension_init
|
||||
.insert(HostToolInvocationLookup);
|
||||
let exec_policy = match destination {
|
||||
SpawnDestination::SameManager => {
|
||||
thread_options.inherited_environments = options.environments;
|
||||
inherited_policy
|
||||
}
|
||||
SpawnDestination::Detached => {
|
||||
thread_options.environments = Some(environments);
|
||||
Some(Arc::new(match inherited_policy {
|
||||
Some(policy) => ExecPolicyManager::new(policy.current()),
|
||||
None => ExecPolicyManager::load(&thread_options.config.config_layer_stack)
|
||||
.await
|
||||
.map_err(|error| CodexErr::InvalidRequest(error.to_string()))?,
|
||||
}))
|
||||
}
|
||||
thread_options.inherited_environments = options.environments;
|
||||
let client_metadata = parent.session.app_server_client_metadata().await;
|
||||
let inheritance = PreparedChildInheritance {
|
||||
instructions,
|
||||
exec_policy,
|
||||
originator: parent.session.originator().await,
|
||||
attachment_source: options.fork_mode.is_some().then_some(caller),
|
||||
};
|
||||
Ok(PreparedThreadSpawn {
|
||||
caller,
|
||||
session_id: parent.session.session_id(),
|
||||
agent_path,
|
||||
client_metadata: parent.session.app_server_client_metadata().await,
|
||||
thread_options,
|
||||
inheritance: PreparedChildInheritance {
|
||||
instructions,
|
||||
exec_policy,
|
||||
originator: parent.config_snapshot().await.originator,
|
||||
attachment_source: (matches!(destination, SpawnDestination::SameManager)
|
||||
&& options.fork_mode.is_some())
|
||||
.then_some(caller),
|
||||
},
|
||||
})
|
||||
Ok((thread_options, inheritance, client_metadata))
|
||||
}
|
||||
|
||||
pub(crate) async fn start_prepared(
|
||||
runtime: &LocalAgentRuntime,
|
||||
prepared: PreparedThreadSpawn,
|
||||
pub(super) async fn create_spawn(
|
||||
parent: &CodexThread,
|
||||
invocation: &ToolInvocationContext,
|
||||
request: ChildSpawnSettings,
|
||||
task_name: AgentPath,
|
||||
reservation: HostThreadSpawnReservation,
|
||||
invocation: Option<&ToolInvocationContext>,
|
||||
) -> std::result::Result<ThreadSpawnResult, ThreadSpawnError> {
|
||||
if reservation.control.identity() != prepared.session_id
|
||||
|| reservation.thread_id == prepared.caller
|
||||
|| SessionId::from(reservation.thread_id) == prepared.session_id
|
||||
let (mut options, inheritance, client_metadata) =
|
||||
Box::pin(prepare_spawn_inputs(parent, request)).await?;
|
||||
let caller = parent.session.thread_id;
|
||||
let session_id = parent.session.session_id();
|
||||
if reservation.control.identity() != session_id
|
||||
|| reservation.thread_id == caller
|
||||
|| SessionId::from(reservation.thread_id) == session_id
|
||||
{
|
||||
return Err(ThreadSpawnError::Preparation(
|
||||
"reservation does not match prepared child".into(),
|
||||
));
|
||||
}
|
||||
let runtime = &parent.session.services.local_agent_runtime;
|
||||
let membership = runtime.admit_start()?;
|
||||
let state = runtime.upgrade()?;
|
||||
if state
|
||||
@@ -218,18 +131,16 @@ pub(crate) async fn start_prepared(
|
||||
))
|
||||
.into());
|
||||
}
|
||||
let task_name = prepared.agent_path;
|
||||
let mut options = prepared.thread_options;
|
||||
options.reserved_thread_id = Some(reservation.thread_id);
|
||||
let child = state
|
||||
.spawn_prepared_child_thread(
|
||||
.spawn_host_child_thread(
|
||||
options,
|
||||
AgentControlInit::Provided {
|
||||
control: reservation.control,
|
||||
runtime: runtime.clone(),
|
||||
},
|
||||
prepared.caller,
|
||||
prepared.inheritance,
|
||||
caller,
|
||||
inheritance,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.with_agent_context(AgentErrorContext::ChildStartup))?;
|
||||
@@ -242,16 +153,14 @@ pub(crate) async fn start_prepared(
|
||||
child.thread.emit_thread_ready_lifecycle().await;
|
||||
child
|
||||
.thread
|
||||
.emit_subagent_session_started(prepared.client_metadata, /*resumed_created_at*/ None)
|
||||
.emit_subagent_session_started(client_metadata, /*resumed_created_at*/ None)
|
||||
.await;
|
||||
if child.session_configured.forked_from_id.is_some() {
|
||||
child.thread.ensure_rollout_materialized().await;
|
||||
}
|
||||
let config = effective_spawn_config(&child.thread).await?;
|
||||
let status = child.thread.io.agent_status.borrow().clone();
|
||||
let _active = invocation
|
||||
.map(ToolInvocationContext::lock_active)
|
||||
.transpose()?;
|
||||
let _active = invocation.lock_active()?;
|
||||
let _membership = pending.disarm();
|
||||
state.notify_thread_created(child.thread_id);
|
||||
Ok(ThreadSpawnResult {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
#![cfg(test)]
|
||||
|
||||
//! Tests live child bindings and the capture boundary for detached child authority.
|
||||
//! Tests same-manager child bindings and invocation lifetime.
|
||||
|
||||
use super::SpawnDestination;
|
||||
use super::prepare_spawn;
|
||||
use crate::CodexThread;
|
||||
use crate::ConfigRefreshOutcome;
|
||||
use crate::HostThreadSpawnReservation;
|
||||
use crate::HostToolInvocationLookup;
|
||||
use crate::ThreadManager;
|
||||
use crate::agent::host::InvocationReservation;
|
||||
use crate::agent::types::SpawnAgentForkMode;
|
||||
use crate::config::Config;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::thread_manager::StartThreadOptions;
|
||||
use crate::thread_spawn::ThreadCreateRequest;
|
||||
@@ -18,8 +16,10 @@ use crate::tools::context::ToolCallSource;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use codex_config::ScopedSkillsConfig;
|
||||
use codex_config::test_support::CloudConfigBundleFixture;
|
||||
use codex_exec_server::ExecServerError;
|
||||
use codex_exec_server::RemoteEnvironmentOptions;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::ThreadId;
|
||||
@@ -27,16 +27,106 @@ use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
use codex_protocol::protocol::EnvironmentConfigState;
|
||||
use codex_tools::ToolName;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::future::poll_fn;
|
||||
use std::net::TcpListener;
|
||||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
use std::time::Duration;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
async fn spawn_fixture() -> anyhow::Result<(tempfile::TempDir, Config, ThreadManager)> {
|
||||
async fn spawn_call(parent: &CodexThread) -> anyhow::Result<ToolInvocation> {
|
||||
parent
|
||||
.thread_extension_data()
|
||||
.insert(HostToolInvocationLookup);
|
||||
let session = &parent.session;
|
||||
let step = session
|
||||
.capture_step_context(session.new_default_turn().await, &CancellationToken::new())
|
||||
.await?;
|
||||
Ok(ToolInvocation {
|
||||
session: Arc::clone(session),
|
||||
turn: Arc::clone(&step.turn),
|
||||
step_context: step,
|
||||
cancellation_token: CancellationToken::new(),
|
||||
tracker: Default::default(),
|
||||
call_id: "spawn-call".into(),
|
||||
tool_name: ToolName::plain("host_spawn"),
|
||||
source: ToolCallSource::Direct,
|
||||
payload: ToolPayload::Function {
|
||||
arguments: "{}".into(),
|
||||
},
|
||||
native_call: None,
|
||||
environments: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prepare_child_retains_loaded_exec_policy_requirements_after_mcp_refresh()
|
||||
-> anyhow::Result<()> {
|
||||
let home = tempfile::tempdir()?;
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
let builder =
|
||||
|| ConfigBuilder::without_managed_config_for_tests().codex_home(home.path().to_path_buf());
|
||||
let mut config = builder()
|
||||
.cloud_config_bundle(CloudConfigBundleFixture::loader_with_enterprise_requirement(
|
||||
"[[rules.prefix_rules]]\npattern = [{ token = 'managed-command' }]\ndecision = 'forbidden'",
|
||||
))
|
||||
.build()
|
||||
.await?;
|
||||
config.features.enable(Feature::MultiAgentV2)?;
|
||||
let manager = ThreadManager::with_models_provider_and_home_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
home.path().to_path_buf(),
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
);
|
||||
let parent = manager
|
||||
.start_thread(StartThreadOptions::new(config))
|
||||
.await?
|
||||
.thread;
|
||||
let startup = parent.config().await;
|
||||
let loaded = startup
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.exec_policy
|
||||
.as_deref()
|
||||
.expect("startup has a managed exec policy");
|
||||
assert_eq!(
|
||||
parent
|
||||
.refresh_mcp_config(Arc::clone(&startup), builder().build().await?)
|
||||
.await,
|
||||
ConfigRefreshOutcome::Published
|
||||
);
|
||||
let call = spawn_call(&parent).await?;
|
||||
assert_eq!(
|
||||
call.turn
|
||||
.config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.exec_policy,
|
||||
None
|
||||
);
|
||||
let reservation = InvocationReservation::default();
|
||||
reservation.reserve(&parent.session, &call.turn, &call.call_id)?;
|
||||
let _guard = reservation.activate(&call);
|
||||
let invocation = parent.tool_invocation(&call.turn.sub_id, &call.call_id)?;
|
||||
let prepared = parent.prepare_child(&invocation).await?;
|
||||
assert_eq!(
|
||||
prepared.exec_policy_source_requirements.as_deref(),
|
||||
Some(loaded)
|
||||
);
|
||||
assert_eq!(
|
||||
prepared
|
||||
.exec_policy
|
||||
.check(&["managed-command".into()], &|_| Decision::Allow)
|
||||
.decision,
|
||||
Decision::Forbidden
|
||||
);
|
||||
parent.shutdown_and_wait().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn same_manager_creation_retains_step_executor_and_shares_exec_policy() -> anyhow::Result<()>
|
||||
{
|
||||
let home = tempfile::tempdir()?;
|
||||
let mut config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.build()
|
||||
.await?;
|
||||
@@ -46,23 +136,6 @@ async fn spawn_fixture() -> anyhow::Result<(tempfile::TempDir, Config, ThreadMan
|
||||
home.path().to_path_buf(),
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
);
|
||||
Ok((home, config, manager))
|
||||
}
|
||||
|
||||
fn child_request(fork_mode: Option<SpawnAgentForkMode>) -> ThreadCreateRequest {
|
||||
ThreadCreateRequest {
|
||||
task_name: "worker".into(),
|
||||
fork_mode,
|
||||
agent_type: None,
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn same_manager_creation_retains_step_executor_and_shares_exec_policy() -> anyhow::Result<()>
|
||||
{
|
||||
let (_home, mut config, manager) = spawn_fixture().await?;
|
||||
config.features.enable(Feature::MultiAgentV2)?;
|
||||
config.features.enable(Feature::DeferredExecutor)?;
|
||||
let environments = manager.environment_manager();
|
||||
@@ -93,28 +166,8 @@ async fn same_manager_creation_retains_step_executor_and_shares_exec_policy() ->
|
||||
})
|
||||
.await?
|
||||
.thread;
|
||||
parent
|
||||
.thread_extension_data()
|
||||
.insert(HostToolInvocationLookup);
|
||||
let session = &parent.session;
|
||||
let step = session
|
||||
.capture_step_context(session.new_default_turn().await, &CancellationToken::new())
|
||||
.await?;
|
||||
let call = ToolInvocation {
|
||||
session: Arc::clone(session),
|
||||
turn: Arc::clone(&step.turn),
|
||||
step_context: step,
|
||||
cancellation_token: CancellationToken::new(),
|
||||
tracker: Default::default(),
|
||||
call_id: "spawn-call".into(),
|
||||
tool_name: ToolName::plain("host_spawn"),
|
||||
source: ToolCallSource::Direct,
|
||||
payload: ToolPayload::Function {
|
||||
arguments: "{}".into(),
|
||||
},
|
||||
native_call: None,
|
||||
environments: Default::default(),
|
||||
};
|
||||
let call = spawn_call(&parent).await?;
|
||||
let reservation = InvocationReservation::default();
|
||||
reservation.reserve(session, &call.turn, &call.call_id)?;
|
||||
assert!(
|
||||
@@ -124,16 +177,24 @@ async fn same_manager_creation_retains_step_executor_and_shares_exec_policy() ->
|
||||
);
|
||||
let guard = reservation.activate(&call);
|
||||
let invocation = parent.tool_invocation(&call.turn.sub_id, &call.call_id)?;
|
||||
let before = parent.prepare_child(&invocation).await?.exec_policy;
|
||||
let amendment = ExecPolicyAmendment::new(vec!["snapshot-tool".into(), "check".into()]);
|
||||
session.persist_execpolicy_amendment(&amendment).await?;
|
||||
let after = parent.prepare_child(&invocation).await?.exec_policy;
|
||||
assert!(!before.get_allowed_prefixes().contains(&amendment.command));
|
||||
assert!(after.get_allowed_prefixes().contains(&amendment.command));
|
||||
environments.upsert_environment("workspace".into(), url, /*connect_timeout*/ None)?;
|
||||
assert!(matches!(
|
||||
parent.prepare_child(&invocation, child_request(/*fork_mode*/ None)).await,
|
||||
Err(ThreadSpawnError::Backend(error)) if error.to_string().contains("resolved environment configuration")
|
||||
));
|
||||
let child_id = ThreadId::new();
|
||||
parent
|
||||
.create_child(
|
||||
&invocation,
|
||||
child_request(/*fork_mode*/ None),
|
||||
ThreadCreateRequest {
|
||||
task_name: "worker".into(),
|
||||
fork_mode: None,
|
||||
agent_type: None,
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
},
|
||||
HostThreadSpawnReservation {
|
||||
thread_id: child_id,
|
||||
control: Arc::clone(&session.services.agent_control),
|
||||
@@ -184,72 +245,10 @@ async fn same_manager_creation_retains_step_executor_and_shares_exec_policy() ->
|
||||
let _next_guard = next.activate(&call);
|
||||
parent.tool_invocation(&call.turn.sub_id, &call.call_id)?;
|
||||
assert!(matches!(
|
||||
parent.prepare_child(&invocation, child_request(/*fork_mode*/ None)).await,
|
||||
parent.prepare_child(&invocation).await,
|
||||
Err(ThreadSpawnError::Preparation(message)) if message == "spawning tool call finished"
|
||||
));
|
||||
child.shutdown_and_wait().await?;
|
||||
parent.shutdown_and_wait().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prepared_spawn_captures_policy_after_fork_history() -> anyhow::Result<()> {
|
||||
let (home, config, manager) = spawn_fixture().await?;
|
||||
let parent = manager
|
||||
.start_thread(StartThreadOptions::new(config))
|
||||
.await?
|
||||
.thread;
|
||||
let session = &parent.session;
|
||||
let step = session
|
||||
.capture_step_context(session.new_default_turn().await, &CancellationToken::new())
|
||||
.await?;
|
||||
let (request, path) = crate::thread_spawn::prepare(
|
||||
session,
|
||||
&step,
|
||||
"fork-call",
|
||||
child_request(Some(SpawnAgentForkMode::FullHistory)),
|
||||
)
|
||||
.await?;
|
||||
let rollout = parent
|
||||
.rollout_path()
|
||||
.expect("local parent has a rollout path");
|
||||
assert!(!rollout.try_exists()?);
|
||||
let mut preparation = std::pin::pin!(prepare_spawn(
|
||||
&parent,
|
||||
request,
|
||||
path,
|
||||
SpawnDestination::Detached,
|
||||
));
|
||||
// Pause the fork as soon as it materializes the parent's rollout, then amend the policy.
|
||||
let materialized = poll_fn(|cx| {
|
||||
if rollout.exists() {
|
||||
return Poll::Ready(());
|
||||
}
|
||||
assert!(
|
||||
preparation.as_mut().poll(cx).is_pending(),
|
||||
"fork finished before materializing history"
|
||||
);
|
||||
Poll::Pending
|
||||
});
|
||||
tokio::time::timeout(Duration::from_secs(10), materialized).await?;
|
||||
let command = vec!["late_fork_policy_test".to_owned()];
|
||||
let parent_policy = &session.services.exec_policy;
|
||||
parent_policy
|
||||
.append_amendment_and_update(home.path(), &ExecPolicyAmendment::new(command.clone()))
|
||||
.await?;
|
||||
let prepared = tokio::time::timeout(Duration::from_secs(10), preparation).await??;
|
||||
let later = vec!["after_prepare_policy_test".to_owned()];
|
||||
parent_policy
|
||||
.append_amendment_and_update(home.path(), &ExecPolicyAmendment::new(later.clone()))
|
||||
.await?;
|
||||
let prefixes = prepared
|
||||
.inheritance
|
||||
.exec_policy
|
||||
.expect("detached policy")
|
||||
.current()
|
||||
.get_allowed_prefixes();
|
||||
assert!(prefixes.contains(&command));
|
||||
assert!(!prefixes.contains(&later));
|
||||
parent.shutdown_and_wait().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,43 +3,47 @@
|
||||
use crate::agent::api::AgentControl;
|
||||
use crate::agents_md_manager::SessionInstructions;
|
||||
use crate::exec_policy::ExecPolicyManager;
|
||||
use crate::session::session::AppServerClientMetadata;
|
||||
use crate::thread_manager::StartThreadOptions;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::RequirementsExecPolicy;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::TurnEnvironmentRequest;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A fresh child ID reserved in the host's tree.
|
||||
/// The host commits membership, serializes startup attempts and deduplicates retries.
|
||||
/// Core derives parent, session and path from [`PreparedThreadSpawn`]; the controller
|
||||
/// supplies runtime coordination, and its `spawn` method is not called.
|
||||
/// Core derives the parent, session and path from the invocation and child request.
|
||||
/// The controller supplies runtime coordination.
|
||||
pub struct HostThreadSpawnReservation {
|
||||
pub thread_id: ThreadId,
|
||||
pub control: Arc<dyn AgentControl>,
|
||||
}
|
||||
|
||||
/// Opaque, in-process snapshot for starting a child without its source runtime.
|
||||
/// Settings and authority reflect preparation time. The destination must supply compatible
|
||||
/// services and register the captured environment IDs with matching working directories and
|
||||
/// capability mappings. Detached preparation rejects thread attachments, live instruction providers,
|
||||
/// enabled EMA-authenticated MCP servers, and unresolved environment configuration.
|
||||
/// The child inherits user and thread instructions and discovers repository instructions
|
||||
/// in its destination environment.
|
||||
pub struct PreparedThreadSpawn {
|
||||
pub(super) caller: ThreadId,
|
||||
pub(super) session_id: SessionId,
|
||||
pub(super) agent_path: AgentPath,
|
||||
pub(super) client_metadata: AppServerClientMetadata,
|
||||
pub(super) thread_options: StartThreadOptions,
|
||||
pub(super) inheritance: PreparedChildInheritance,
|
||||
}
|
||||
|
||||
impl PreparedThreadSpawn {
|
||||
/// Canonical path of the child in the agent tree.
|
||||
pub fn agent_path(&self) -> &AgentPath {
|
||||
&self.agent_path
|
||||
}
|
||||
/// Parent inputs captured from the tool invocation for a host starting or forking a child.
|
||||
/// The host binds the destination's services and applies the requested child settings.
|
||||
/// Repository instructions are discovered at the destination.
|
||||
pub struct PreparedChildContext {
|
||||
pub originator: String,
|
||||
pub user_instructions: Option<String>,
|
||||
pub thread_instructions: Option<String>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub environments: Vec<TurnEnvironmentRequest>,
|
||||
pub selected_capability_roots: Vec<SelectedCapabilityRoot>,
|
||||
pub resolved_permission_profile: PermissionProfile,
|
||||
pub approval_policy: AskForApproval,
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
/// Parent exec policy at preparation time. Inherit it only for compatible policy sources.
|
||||
pub exec_policy: Arc<Policy>,
|
||||
/// Managed requirements used to load the policy, before any session amendments.
|
||||
pub exec_policy_source_requirements: Option<Arc<RequirementsExecPolicy>>,
|
||||
/// Current tool declarations a fresh child can inherit. The host supplies their execution route.
|
||||
pub dynamic_tools: Vec<DynamicToolSpec>,
|
||||
pub disabled_plugin_ids: Vec<String>,
|
||||
}
|
||||
|
||||
pub(crate) struct PreparedChildInheritance {
|
||||
|
||||
@@ -300,10 +300,10 @@ fn candidate_filenames<'a>(config: &'a Config, cwd: &PathUri) -> Vec<&'a str> {
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct LoadedAgentsMd {
|
||||
/// Account- or home-scoped instructions supplied by the host.
|
||||
user_instructions: Option<Instructions>,
|
||||
pub(crate) user_instructions: Option<Instructions>,
|
||||
|
||||
/// Thread-scoped instructions supplied by the host.
|
||||
thread_instructions: Option<Instructions>,
|
||||
pub(crate) thread_instructions: Option<Instructions>,
|
||||
|
||||
/// Ordered instructions and their provenance.
|
||||
entries: Vec<InstructionEntry>,
|
||||
|
||||
@@ -284,7 +284,8 @@ impl CodexThread {
|
||||
/// Creates an idle V2 child here with the invoking step's environments and live application
|
||||
/// bindings. Compatible children share the parent's exec policy.
|
||||
/// Ending the invocation or dropping this future aborts pending startup.
|
||||
/// [`crate::ThreadManager::create_prepared_child`] defines acceptance and cleanup.
|
||||
/// Success publishes the idle child and transfers ownership to the host. An error or dropped
|
||||
/// future schedules cleanup of any provisional child. Already-durable history may remain.
|
||||
pub async fn create_child(
|
||||
&self,
|
||||
invocation: &crate::ToolInvocationContext,
|
||||
@@ -294,15 +295,13 @@ impl CodexThread {
|
||||
crate::agent::host::create(self, invocation, request, reservation).await
|
||||
}
|
||||
|
||||
/// Captures inputs from a live invocation for [`crate::ThreadManager::create_prepared_child`].
|
||||
/// The host owns cancellation and snapshot freshness after this returns. Detached preparation
|
||||
/// rejects managed application network, auth, and cloud config bindings.
|
||||
/// Reads the parent settings for this live tool invocation, including the dynamic tools
|
||||
/// currently inheritable by a fresh child. The host chooses the child's name, role, and history.
|
||||
pub async fn prepare_child(
|
||||
&self,
|
||||
invocation: &crate::ToolInvocationContext,
|
||||
request: crate::ThreadCreateRequest,
|
||||
) -> Result<crate::PreparedThreadSpawn, crate::ThreadSpawnError> {
|
||||
crate::agent::host::prepare(self, invocation, request).await
|
||||
) -> Result<crate::PreparedChildContext, crate::ThreadSpawnError> {
|
||||
crate::agent::host::prepare(self, invocation).await
|
||||
}
|
||||
|
||||
/// Returns extension-owned data attached to this thread runtime.
|
||||
|
||||
@@ -277,6 +277,7 @@ pub enum ExecPolicyUpdateError {
|
||||
|
||||
pub(crate) struct ExecPolicyManager {
|
||||
policy: ArcSwap<Policy>,
|
||||
source_requirements: Option<Arc<RequirementsExecPolicy>>,
|
||||
update_lock: Semaphore,
|
||||
}
|
||||
|
||||
@@ -295,8 +296,16 @@ pub(crate) struct ExecApprovalRequest<'a> {
|
||||
|
||||
impl ExecPolicyManager {
|
||||
pub(crate) fn new(policy: Arc<Policy>) -> Self {
|
||||
Self::from_snapshot(policy, /*source_requirements*/ None)
|
||||
}
|
||||
|
||||
pub(crate) fn from_snapshot(
|
||||
policy: Arc<Policy>,
|
||||
source_requirements: Option<Arc<RequirementsExecPolicy>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
policy: ArcSwap::from(policy),
|
||||
source_requirements,
|
||||
update_lock: Semaphore::new(/*permits*/ 1),
|
||||
}
|
||||
}
|
||||
@@ -307,13 +316,21 @@ impl ExecPolicyManager {
|
||||
if let Some(err) = warning.as_ref() {
|
||||
tracing::warn!("failed to parse rules: {err}");
|
||||
}
|
||||
Ok(Self::new(Arc::new(policy)))
|
||||
let source_requirements = config_stack.requirements().exec_policy.as_deref().cloned();
|
||||
Ok(Self::from_snapshot(
|
||||
Arc::new(policy),
|
||||
source_requirements.map(Arc::new),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn current(&self) -> Arc<Policy> {
|
||||
self.policy.load_full()
|
||||
}
|
||||
|
||||
pub(crate) fn snapshot(&self) -> (Arc<Policy>, Option<Arc<RequirementsExecPolicy>>) {
|
||||
(self.current(), self.source_requirements.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn create_exec_approval_requirement_for_command(
|
||||
&self,
|
||||
|
||||
@@ -156,7 +156,7 @@ mod thread_spawn;
|
||||
pub use agent::host::AgentMcpMetadataRecipient;
|
||||
pub use agent::host::HostThreadSpawnReservation;
|
||||
pub use agent::host::HostToolInvocationLookup;
|
||||
pub use agent::host::PreparedThreadSpawn;
|
||||
pub use agent::host::PreparedChildContext;
|
||||
pub use agent::host::ToolInvocationContext;
|
||||
pub use thread_spawn::ThreadCreateRequest;
|
||||
pub use thread_spawn::ThreadSpawnError;
|
||||
|
||||
@@ -707,15 +707,22 @@ impl Session {
|
||||
instructions
|
||||
};
|
||||
let exec_policy = if isolation == codex_extension_api::SessionIsolation::Isolated {
|
||||
let managed_policy = config
|
||||
let source_requirements = config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.exec_policy
|
||||
.as_deref()
|
||||
.cloned()
|
||||
.map(Arc::new);
|
||||
let managed_policy = source_requirements
|
||||
.as_deref()
|
||||
.map_or_else(codex_execpolicy::Policy::empty, |policy| {
|
||||
policy.as_ref().clone()
|
||||
});
|
||||
Arc::new(ExecPolicyManager::new(Arc::new(managed_policy)))
|
||||
Arc::new(ExecPolicyManager::from_snapshot(
|
||||
Arc::new(managed_policy),
|
||||
source_requirements,
|
||||
))
|
||||
} else if let Some(exec_policy) = &inherited_exec_policy {
|
||||
Arc::clone(exec_policy)
|
||||
} else {
|
||||
|
||||
@@ -1197,27 +1197,6 @@ impl ThreadManager {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Creates a [prepared child](crate::PreparedThreadSpawn) using this manager's services.
|
||||
///
|
||||
/// On `Ok`, the child is loaded and idle, its creation has been published, and the host
|
||||
/// owns it. The host chooses whether to submit work and handles any later delivery failure.
|
||||
///
|
||||
/// An error or a dropped future schedules asynchronous cleanup of any provisional child.
|
||||
/// Already-durable history may remain.
|
||||
pub async fn create_prepared_child(
|
||||
&self,
|
||||
prepared: crate::agent::host::PreparedThreadSpawn,
|
||||
reservation: crate::agent::host::HostThreadSpawnReservation,
|
||||
) -> Result<crate::ThreadSpawnResult, crate::ThreadSpawnError> {
|
||||
crate::agent::host::start_prepared(
|
||||
&self.agent_control().runtime,
|
||||
prepared,
|
||||
reservation,
|
||||
/*invocation*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Starts a fresh internal session associated with an existing parent thread.
|
||||
pub async fn spawn_internal_session(
|
||||
&self,
|
||||
@@ -2138,22 +2117,14 @@ impl ThreadManagerState {
|
||||
Box::pin(self.spawn_thread(request)).await
|
||||
}
|
||||
|
||||
/// Starts from captured inheritance without requiring a live parent runtime.
|
||||
pub(crate) async fn spawn_prepared_child_thread(
|
||||
/// Starts a host-reserved child using inheritance captured from this manager's parent.
|
||||
pub(crate) async fn spawn_host_child_thread(
|
||||
&self,
|
||||
options: StartThreadOptions,
|
||||
agent_control: AgentControlInit,
|
||||
parent_thread_id: ThreadId,
|
||||
inheritance: crate::agent::host::PreparedChildInheritance,
|
||||
) -> CodexResult<NewThread> {
|
||||
if let Some(environments) = &options.environments {
|
||||
crate::environment_selection::validate_environment_ids_and_cwds(
|
||||
&self.environment_manager,
|
||||
environments
|
||||
.iter()
|
||||
.map(|environment| (environment.environment_id.as_str(), &environment.cwd)),
|
||||
)?;
|
||||
}
|
||||
options.reserved_thread_id.ok_or_else(|| {
|
||||
CodexErr::InvalidRequest("prepared child startup requires a reserved thread ID".into())
|
||||
})?;
|
||||
|
||||
@@ -4,12 +4,10 @@ use super::super::agent_control::TestAgentControl;
|
||||
use super::super::agents_md::RecordingThreadInstructionsProvider;
|
||||
use super::*;
|
||||
use anyhow::Context;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_core::AgentControl;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::HostThreadSpawnReservation;
|
||||
use codex_core::HostToolInvocationLookup;
|
||||
use codex_core::PromptCacheKey;
|
||||
use codex_core::SpawnAgentForkMode;
|
||||
use codex_core::StartIfIdleSubmission;
|
||||
use codex_core::ThreadCreateRequest;
|
||||
@@ -24,14 +22,11 @@ use codex_http_client::NetworkPolicy;
|
||||
use codex_http_client::NetworkPolicyController;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallRequest;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::input_text;
|
||||
use codex_protocol::protocol::ThreadHistoryMode;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use codex_thread_store::AddThreadAttachmentParams;
|
||||
use codex_thread_store::ListThreadAttachmentsParams;
|
||||
use codex_thread_store::LoadThreadHistoryParams;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::submit_thread_settings;
|
||||
use core_test_support::test_codex::TestCodexBuilder;
|
||||
@@ -44,7 +39,6 @@ async fn host_spawn_fixture(
|
||||
server: &MockServer,
|
||||
builder: TestCodexBuilder,
|
||||
application_network_policy: NetworkPolicy,
|
||||
prompt_cache_key: PromptCacheKey,
|
||||
thread_instructions_provider: Option<Arc<dyn ThreadInstructionsProvider>>,
|
||||
) -> Result<(TestCodex, Arc<CodexThread>, Arc<TestAgentControl>)> {
|
||||
let root_id = ThreadId::new();
|
||||
@@ -90,7 +84,6 @@ async fn host_spawn_fixture(
|
||||
.thread_extension_init
|
||||
.insert(HostToolInvocationLookup);
|
||||
options.history_mode = Some(ThreadHistoryMode::Legacy);
|
||||
options.prompt_cache_key = prompt_cache_key;
|
||||
options.thread_instructions_provider = thread_instructions_provider;
|
||||
options.reserved_thread_id = Some(root_id);
|
||||
options.disabled_plugin_ids = Some(vec!["captured-plugin@local".into()]);
|
||||
@@ -171,18 +164,6 @@ fn create_request(name: &str, fork_history: bool) -> ThreadCreateRequest {
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_first_turn(child: &CodexThread, prompt: &str) -> Result<()> {
|
||||
let submission = child
|
||||
.start_turn_if_idle(TurnInputRequest::user_input(vec![text(prompt)]))
|
||||
.await?;
|
||||
assert!(
|
||||
matches!(submission, StartIfIdleSubmission::Started { .. }),
|
||||
"expected the new child to be idle, got {submission:?}"
|
||||
);
|
||||
wait_for_successful_turn(child).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case::test_case(false; "fresh")]
|
||||
#[test_case::test_case(true; "forked")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -190,26 +171,26 @@ async fn same_manager_creation_preserves_live_bindings_and_attachments(
|
||||
fork_history: bool,
|
||||
) -> Result<()> {
|
||||
const PR: &str = "openai/codex#123";
|
||||
const INITIAL_RULE: &str = "Initial rules.";
|
||||
const UPDATED_RULE: &str = "Updated shared worker instructions.";
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = start_mock_server().await;
|
||||
let network_controller = NetworkPolicyController::default();
|
||||
let network_policy = network_controller.policy();
|
||||
assert!(network_controller.publish(network_policy.revision(), DestinationPolicy::Unrestricted));
|
||||
let provider =
|
||||
Arc::new(RecordingThreadInstructionsProvider::with_text("Initial rules.").shared());
|
||||
let provider = Arc::new(RecordingThreadInstructionsProvider::with_text(INITIAL_RULE).shared());
|
||||
let (test, parent, controller) = host_spawn_fixture(
|
||||
&server,
|
||||
test_codex(),
|
||||
test_codex().with_config(|config| config.allow_persistent_dynamic_tool_updates = true),
|
||||
network_policy.clone(),
|
||||
PromptCacheKey::SessionDerived,
|
||||
Some(provider.clone()),
|
||||
)
|
||||
.await?;
|
||||
let root_id = test.session_configured.thread_id;
|
||||
let captured = parent.config_snapshot().await;
|
||||
let parent_message = "Start the worker.";
|
||||
let invocation =
|
||||
host_spawn_invocation(&server, &parent, "spawn-worker", "Start the worker.").await?;
|
||||
host_spawn_invocation(&server, &parent, "spawn-worker", parent_message).await?;
|
||||
let context = parent.tool_invocation(&invocation.turn_id, &invocation.call_id)?;
|
||||
test.thread_store
|
||||
.add_thread_attachment(AddThreadAttachmentParams {
|
||||
@@ -228,20 +209,27 @@ async fn same_manager_creation_preserves_live_bindings_and_attachments(
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
provider.set_instructions(Some(Instructions {
|
||||
text: UPDATED_RULE.into(),
|
||||
source: None,
|
||||
}));
|
||||
// Tool inheritance reads the current catalog while invocation settings remain captured.
|
||||
assert_eq!(parent.prepare_child(&context).await?.dynamic_tools.len(), 1);
|
||||
parent.replace_dynamic_tools(Vec::new()).await?;
|
||||
let prepared = parent.prepare_child(&context).await?;
|
||||
assert!(prepared.dynamic_tools.is_empty());
|
||||
assert_eq!(prepared.disabled_plugin_ids, captured.disabled_plugin_ids);
|
||||
assert_eq!(prepared.thread_instructions.as_deref(), Some(INITIAL_RULE));
|
||||
assert_eq!(
|
||||
prepared.resolved_permission_profile,
|
||||
PermissionProfile::read_only()
|
||||
);
|
||||
let child_id = ThreadId::new();
|
||||
let request = || create_request("worker", fork_history);
|
||||
let reservation = || HostThreadSpawnReservation {
|
||||
thread_id: child_id,
|
||||
control: controller.clone(),
|
||||
};
|
||||
// A detached snapshot cannot carry the source's live application policy.
|
||||
assert!(matches!(
|
||||
parent
|
||||
.prepare_child(&context, request())
|
||||
.await,
|
||||
Err(ThreadSpawnError::Backend(error))
|
||||
if error.to_string().contains("host-managed application authority")
|
||||
));
|
||||
let mut mismatched = reservation();
|
||||
mismatched.control = Arc::new(TestAgentControl::new(ThreadId::new()));
|
||||
assert!(matches!(
|
||||
@@ -250,10 +238,6 @@ async fn same_manager_creation_preserves_live_bindings_and_attachments(
|
||||
));
|
||||
assert!(test.thread_manager.get_thread(child_id).await.is_err());
|
||||
|
||||
provider.set_instructions(Some(Instructions {
|
||||
text: UPDATED_RULE.into(),
|
||||
source: None,
|
||||
}));
|
||||
let spawned = parent
|
||||
.create_child(&context, request(), reservation())
|
||||
.await?;
|
||||
@@ -266,7 +250,7 @@ async fn same_manager_creation_preserves_live_bindings_and_attachments(
|
||||
let worker = test.thread_manager.get_thread(child_id).await?;
|
||||
// A handle cannot be used through a different source runtime.
|
||||
assert!(matches!(
|
||||
worker.prepare_child(&context, request()).await,
|
||||
worker.prepare_child(&context).await,
|
||||
Err(ThreadSpawnError::Backend(error))
|
||||
if error.to_string().contains("tool invocation belongs to a different runtime")
|
||||
));
|
||||
@@ -281,11 +265,21 @@ async fn same_manager_creation_preserves_live_bindings_and_attachments(
|
||||
finish_spawn_tool(&server, &parent, &invocation).await?;
|
||||
let child_request =
|
||||
responses::mount_sse_once(&server, responses::sse_completed("worker-done")).await;
|
||||
start_first_turn(&worker, "Report your status.").await?;
|
||||
let submission = worker
|
||||
.start_turn_if_idle(TurnInputRequest::user_input(vec![text(
|
||||
"Report your status.",
|
||||
)]))
|
||||
.await?;
|
||||
assert!(
|
||||
child_request
|
||||
.single_request()
|
||||
.body_contains_text(UPDATED_RULE)
|
||||
matches!(submission, StartIfIdleSubmission::Started { .. }),
|
||||
"expected the new child to be idle, got {submission:?}"
|
||||
);
|
||||
wait_for_successful_turn(&worker).await;
|
||||
let child_request = child_request.single_request();
|
||||
assert!(child_request.body_contains_text(UPDATED_RULE));
|
||||
assert_eq!(
|
||||
child_request.body_contains_text(parent_message),
|
||||
fork_history
|
||||
);
|
||||
let page = test
|
||||
.thread_store
|
||||
@@ -307,265 +301,6 @@ async fn same_manager_creation_preserves_live_bindings_and_attachments(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case::test_case(false, "inherited instruction providers"; "thread instructions")]
|
||||
#[test_case::test_case(true, "EMA-authenticated MCP servers"; "enterprise MCP")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn prepare_child_rejects_live_bindings(ema_enabled: bool, expected: &str) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = start_mock_server().await;
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
std::fs::write(
|
||||
home.path().join("config.toml"),
|
||||
format!(
|
||||
r#"features.use_xaa = true
|
||||
mcp_enterprise_managed_auth.idp = {{ issuer = "https://idp.example", client_id = "test" }}
|
||||
mcp_servers.enterprise = {{ url = "https://mcp.example/mcp", auth = "ema_auth", enabled = {ema_enabled} }}"#
|
||||
),
|
||||
)?;
|
||||
let (test, parent, controller) = host_spawn_fixture(
|
||||
&server,
|
||||
test_codex().with_home(home),
|
||||
NetworkPolicy::unmanaged(),
|
||||
PromptCacheKey::SessionDerived,
|
||||
(!ema_enabled).then(|| {
|
||||
Arc::new(RecordingThreadInstructionsProvider::with_text("Shared rules").shared())
|
||||
as Arc<dyn ThreadInstructionsProvider>
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let call = host_spawn_invocation(&server, &parent, "prepare-worker", "Start a worker.").await?;
|
||||
let context = parent.tool_invocation(&call.turn_id, &call.call_id)?;
|
||||
let request = || create_request("worker", /*fork_history*/ false);
|
||||
let Err(ThreadSpawnError::Backend(error)) = parent.prepare_child(&context, request()).await
|
||||
else {
|
||||
anyhow::bail!("expected detached preparation to reject {expected}");
|
||||
};
|
||||
assert!(matches!(
|
||||
error.details(),
|
||||
CodexErrorDetails::InvalidRequest(message) if message.contains(expected)
|
||||
));
|
||||
let child_id = ThreadId::new();
|
||||
parent
|
||||
.create_child(
|
||||
&context,
|
||||
request(),
|
||||
HostThreadSpawnReservation {
|
||||
thread_id: child_id,
|
||||
control: controller,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
test.thread_manager
|
||||
.get_thread(child_id)
|
||||
.await?
|
||||
.shutdown_and_wait()
|
||||
.await?;
|
||||
parent.shutdown_and_wait().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case::test_case(true, None; "full_history")]
|
||||
#[test_case::test_case(false, Some("host-cache-key"); "fresh_history")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn prepared_spawn_starts_on_an_independent_destination_after_parent_shutdown(
|
||||
fork_history: bool,
|
||||
cache_key: Option<&str>,
|
||||
) -> Result<()> {
|
||||
const PROJECT_RULE: &str = "Run the destination repository checks.";
|
||||
const EARLIER_INPUT: &str = "Remember PRIOR_CONTEXT_5241.";
|
||||
const SPAWN_INPUT: &str =
|
||||
"Remember TRANSFERRED_CONTEXT_7391 and ask the host to start a worker.";
|
||||
const TASK: &str = "Report TRANSFERRED_TASK_8812 and the remembered context.";
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = start_mock_server().await;
|
||||
let (source, parent, source_control) = host_spawn_fixture(
|
||||
&server,
|
||||
test_codex(),
|
||||
NetworkPolicy::unmanaged(),
|
||||
cache_key.map_or(PromptCacheKey::Omit, |key| {
|
||||
PromptCacheKey::Custom(key.into())
|
||||
}),
|
||||
/*thread_instructions_provider*/ None,
|
||||
)
|
||||
.await?;
|
||||
let root_id = source.session_configured.thread_id;
|
||||
let session_id = source.session_configured.session_id;
|
||||
parent
|
||||
.set_app_server_client_info(
|
||||
Some("source-client".into()),
|
||||
Some("1.0".into()),
|
||||
/*mcp_elicitations_auto_deny*/ false,
|
||||
)
|
||||
.await?;
|
||||
let captured = parent.config_snapshot().await;
|
||||
responses::mount_sse_once(&server, sse(vec![ev_completed("remembered")])).await;
|
||||
source.submit_text_turn(EARLIER_INPUT).await?;
|
||||
let invocation = host_spawn_invocation(&server, &parent, "prepare-worker", SPAWN_INPUT).await?;
|
||||
let context = parent.tool_invocation(&invocation.turn_id, &invocation.call_id)?;
|
||||
let prepared = parent
|
||||
.prepare_child(&context, create_request("remote_worker", fork_history))
|
||||
.await?;
|
||||
let child_path = prepared.agent_path().clone();
|
||||
assert_eq!(child_path.as_str(), "/root/remote_worker");
|
||||
let agents_md = source.workspace_path_uri("AGENTS.md")?;
|
||||
|
||||
// Keep only durable storage and referenced paths; startup cannot consult A's runtime,
|
||||
// manager, controller, or environment services after this point.
|
||||
let source_store = Arc::clone(&source.thread_store);
|
||||
let _source_home = Arc::clone(&source.home);
|
||||
let _source_cwd = Arc::clone(&source.cwd);
|
||||
let source_manager = Arc::downgrade(&source.thread_manager);
|
||||
let source_parent = Arc::downgrade(&parent);
|
||||
parent.shutdown_and_wait().await?;
|
||||
source.thread_manager.remove_thread(&root_id).await;
|
||||
drop(parent);
|
||||
drop(source);
|
||||
drop(source_control);
|
||||
assert!(source_manager.upgrade().is_none());
|
||||
assert!(source_parent.upgrade().is_none());
|
||||
|
||||
// Independent runtimes may share durable storage.
|
||||
wiremock::Mock::given(wiremock::matchers::path("/codex/analytics-events/events"))
|
||||
.respond_with(wiremock::ResponseTemplate::new(/*status*/ 200))
|
||||
.mount(&server)
|
||||
.await;
|
||||
let analytics = AnalyticsEventsClient::new(
|
||||
codex_core::test_support::auth_manager_from_auth(
|
||||
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
|
||||
),
|
||||
server.uri(),
|
||||
/*analytics_enabled*/ Some(true),
|
||||
);
|
||||
let destination = test_codex()
|
||||
.with_analytics_events_client(analytics.clone())
|
||||
.with_thread_store(source_store)
|
||||
.with_model("gpt-6-astra")
|
||||
.with_config(configure_scenario_catalog)
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
let manager = &destination.thread_manager;
|
||||
destination.codex.shutdown_and_wait().await?;
|
||||
manager
|
||||
.remove_thread(&destination.session_configured.thread_id)
|
||||
.await;
|
||||
destination
|
||||
.fs()
|
||||
.write_file(
|
||||
&agents_md,
|
||||
PROJECT_RULE.as_bytes().to_vec(),
|
||||
Default::default(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
let child_id = ThreadId::new();
|
||||
// The destination controller shares the tree identity but has no entry for the child path.
|
||||
let destination_control = Arc::new(TestAgentControl::new(root_id));
|
||||
let child_request =
|
||||
responses::mount_sse_once(&server, responses::sse_completed("remote-done")).await;
|
||||
let spawned = manager
|
||||
.create_prepared_child(
|
||||
prepared,
|
||||
HostThreadSpawnReservation {
|
||||
thread_id: child_id,
|
||||
control: destination_control,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
(
|
||||
spawned.agent.thread_id,
|
||||
&spawned.task_name,
|
||||
spawned.config.parent_thread_id,
|
||||
),
|
||||
(child_id, &child_path, Some(root_id))
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
spawned.config.permission_profile,
|
||||
spawned.config.disabled_plugin_ids,
|
||||
),
|
||||
(captured.permission_profile, captured.disabled_plugin_ids),
|
||||
);
|
||||
let worker = manager.get_thread(child_id).await?;
|
||||
start_first_turn(&worker, TASK).await?;
|
||||
analytics.flush().await;
|
||||
let events = responses::received_analytics_events(&server, "codex_thread_initialized").await;
|
||||
let child_starts: Vec<_> = events
|
||||
.iter()
|
||||
.map(|event| &event["event_params"])
|
||||
.filter(|params| params["thread_id"] == json!(child_id))
|
||||
.collect();
|
||||
let [start] = child_starts.as_slice() else {
|
||||
anyhow::bail!("expected one initialization for {child_id}, got {child_starts:?}");
|
||||
};
|
||||
assert_eq!(start["parent_thread_id"], json!(root_id));
|
||||
assert_eq!(start["app_server_client"]["client_name"], "source-client");
|
||||
assert_eq!(start["app_server_client"]["client_version"], "1.0");
|
||||
let requests = responses::received_responses_requests(&server).await;
|
||||
let history = context_snapshot::format_request_history_snapshot(
|
||||
"A prepares a child. After A and its manager shut down, a second manager creates the idle child and then starts its first turn.",
|
||||
&requests,
|
||||
&ContextSnapshotOptions::default().rewrite_known_segments(),
|
||||
);
|
||||
let snapshot_name = if fork_history {
|
||||
"detached_spawn_full_history"
|
||||
} else {
|
||||
"detached_spawn_fresh_history"
|
||||
};
|
||||
insta::assert_snapshot!(snapshot_name, history);
|
||||
let child_request = child_request.single_request();
|
||||
assert_eq!(
|
||||
child_request.body_json().get("prompt_cache_key").cloned(),
|
||||
cache_key.map(|key| json!(key)),
|
||||
);
|
||||
assert!(
|
||||
child_request
|
||||
.inputs_of_type("additional_tools")
|
||||
.iter()
|
||||
.any(|item| {
|
||||
responses::namespace_child_tool(item, "functions", "exec")
|
||||
.and_then(|tool| tool["description"].as_str())
|
||||
.is_some_and(|description| description.contains("host_spawn(args:"))
|
||||
})
|
||||
);
|
||||
let user_messages = child_request.message_input_texts("user");
|
||||
assert!(user_messages.iter().any(|text| text.contains(PROJECT_RULE)));
|
||||
worker.ensure_rollout_materialized().await;
|
||||
worker.flush_rollout().await?;
|
||||
let stored = destination
|
||||
.thread_store
|
||||
.load_latest_model_context(LoadThreadHistoryParams {
|
||||
thread_id: child_id,
|
||||
include_archived: false,
|
||||
})
|
||||
.await?;
|
||||
assert!(stored.items.iter().any(|item| matches!(item,
|
||||
RolloutItem::SessionMeta(meta) if meta.meta.session_id == session_id
|
||||
&& meta.meta.parent_thread_id == Some(root_id)
|
||||
)));
|
||||
for input in [EARLIER_INPUT, SPAWN_INPUT] {
|
||||
let span = input_text(input);
|
||||
let inherited = stored.items.iter().find_map(|item| match item {
|
||||
RolloutItem::ResponseItem(envelope)
|
||||
if matches!(&envelope.item, ResponseItem::Message { role, content, .. }
|
||||
if role == "user" && content.contains(&span)) =>
|
||||
{
|
||||
Some(
|
||||
envelope
|
||||
.metadata
|
||||
.as_ref()
|
||||
.is_some_and(|meta| meta.inherited_user_message),
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(inherited, fork_history.then_some(true), "{input}");
|
||||
}
|
||||
assert!(manager.get_thread(root_id).await.is_err());
|
||||
worker.shutdown_and_wait().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ChildStartupGate {
|
||||
child_id: ThreadId,
|
||||
entered: Notify,
|
||||
@@ -600,7 +335,6 @@ async fn cancelled_creation_cleans_registered_child() -> Result<()> {
|
||||
&server,
|
||||
test_codex().with_extensions(Arc::new(extensions.build())),
|
||||
NetworkPolicy::unmanaged(),
|
||||
PromptCacheKey::SessionDerived,
|
||||
/*thread_instructions_provider*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios_reserved_spawn.rs
|
||||
expression: history
|
||||
---
|
||||
Scenario: A prepares a child. After A and its manager shut down, a second manager creates the idle child and then starts its first turn.
|
||||
|
||||
## Window 1
|
||||
-- request 1 (turn) --
|
||||
00:additional_tools/developer (2; hash=e03063c2cb6ece3a):
|
||||
- namespace/functions; hash=e58195dea5c51dea
|
||||
- custom/exec
|
||||
- function/wait
|
||||
- function/request_user_input
|
||||
- function/request_user_input_async
|
||||
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
|
||||
- function/sleep
|
||||
01:message/developer:
|
||||
<MODEL_INSTRUCTIONS>
|
||||
02:message/developer[2]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <COLLABORATION_MODE>
|
||||
03:message/developer:
|
||||
<MULTI_AGENT_ROLE>
|
||||
04:message/developer:
|
||||
<MULTI_AGENT_MODE>
|
||||
05:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
06:message/user:
|
||||
Remember PRIOR_CONTEXT_5241.
|
||||
07:configuration_update:reasoning={"effort":"low"}
|
||||
-- request 2 (turn) --
|
||||
08:message/user:
|
||||
Remember TRANSFERRED_CONTEXT_7391 and ask the host to start a worker.
|
||||
|
||||
## Window 2 (after request 2: input diverged at item 00)
|
||||
-- request 3 (turn) --
|
||||
00:additional_tools/developer (2; hash=adec8e1bf249cb8f):
|
||||
- namespace/functions; hash=00aa6cf7871d56b9
|
||||
- custom/exec
|
||||
- function/wait
|
||||
- function/request_user_input
|
||||
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
|
||||
- function/sleep
|
||||
01:message/developer:
|
||||
<MODEL_INSTRUCTIONS>
|
||||
02:message/developer[2]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <COLLABORATION_MODE>
|
||||
03:message/developer:
|
||||
<MULTI_AGENT_ROLE>
|
||||
04:message/developer:
|
||||
<MULTI_AGENT_MODE>
|
||||
05:message/user[2]:
|
||||
[01] <AGENTS_MD>
|
||||
[02] <ENVIRONMENT_CONTEXT>
|
||||
06:message/user:
|
||||
Report TRANSFERRED_TASK_8812 and the remembered context.
|
||||
07:configuration_update:reasoning={"effort":"low"}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios_reserved_spawn.rs
|
||||
expression: history
|
||||
---
|
||||
Scenario: A prepares a child. After A and its manager shut down, a second manager creates the idle child and then starts its first turn.
|
||||
|
||||
## Window 1
|
||||
-- request 1 (turn) --
|
||||
00:additional_tools/developer (2; hash=e03063c2cb6ece3a):
|
||||
- namespace/functions; hash=e58195dea5c51dea
|
||||
- custom/exec
|
||||
- function/wait
|
||||
- function/request_user_input
|
||||
- function/request_user_input_async
|
||||
- namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317
|
||||
- function/sleep
|
||||
01:message/developer:
|
||||
<MODEL_INSTRUCTIONS>
|
||||
02:message/developer[2]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <COLLABORATION_MODE>
|
||||
03:message/developer:
|
||||
<MULTI_AGENT_ROLE>
|
||||
04:message/developer:
|
||||
<MULTI_AGENT_MODE>
|
||||
05:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
06:message/user:
|
||||
Remember PRIOR_CONTEXT_5241.
|
||||
07:configuration_update:reasoning={"effort":"low"}
|
||||
-- request 2 (turn) --
|
||||
08:message/user:
|
||||
Remember TRANSFERRED_CONTEXT_7391 and ask the host to start a worker.
|
||||
-- request 3 (turn) --
|
||||
09:function_call/host_spawn:{}
|
||||
10:function_call_output:aborted
|
||||
11:message/developer:
|
||||
You are now a newly spawned child agent. The preceding conversation is inherited parent context. Follow ...are not inherited. Report your result to your parent. [hash=0e586a5d405d39db]
|
||||
12:message/developer:
|
||||
<MULTI_AGENT_ROLE>
|
||||
13:message/developer:
|
||||
The following tools are no longer available. Do not call them:
|
||||
- functions.request_user_input_async
|
||||
14:message/user:
|
||||
<AGENTS_MD>
|
||||
15:message/developer:
|
||||
<MULTI_AGENT_MODE>
|
||||
16:message/user:
|
||||
Report TRANSFERRED_TASK_8812 and the remembered context.
|
||||
17:configuration_update:reasoning={"effort":"low"}
|
||||
Reference in New Issue
Block a user