mirror of
https://github.com/openai/codex.git
synced 2026-09-15 12:08:01 +00:00
Add context snapshots for async questions and plugin refresh (#44948)
## What changed - Add a multi-turn scenario covering `request_user_input_async`, continued work while awaiting an answer, and delivery of the answer into the active turn. - Add a scenario covering plugin configuration reload in an existing thread, including discovery and use of newly installed skills and MCP tools across turns. - Render explicit tool output names and namespaces in context snapshots, with a regression test ensuring outputs do not inherit metadata from their calls. GitOrigin-RevId: 64fca8cb51a3a697fa84bf444ce55b8408961ab5
This commit is contained in:
@@ -673,6 +673,16 @@ fn render_item(
|
||||
format!("{index:02}:custom_tool_call/{name}:{input}")
|
||||
}
|
||||
"function_call_output" | "custom_tool_call_output" => {
|
||||
let name = item
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.map(|_| format!("/{}", call_name(item)))
|
||||
.or_else(|| {
|
||||
item.get("namespace")
|
||||
.and_then(Value::as_str)
|
||||
.map(|namespace| format!("[namespace={namespace}]"))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let output = item
|
||||
.get("output")
|
||||
.map(|output| match output {
|
||||
@@ -701,7 +711,7 @@ fn render_item(
|
||||
_ => "<NON_TEXT_OUTPUT>".to_string(),
|
||||
})
|
||||
.unwrap_or_else(|| "<NO_OUTPUT>".to_string());
|
||||
format!("{index:02}:{kind}:{output}")
|
||||
format!("{index:02}:{kind}{name}:{output}")
|
||||
}
|
||||
"local_shell_call" => {
|
||||
let command = item
|
||||
|
||||
@@ -46,6 +46,36 @@ fn lite_tool_catalog_and_code_calls_are_visible() {
|
||||
assert!(rendered.contains("03:custom_tool_call_output:success=true:plan updated"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_outputs_show_only_their_own_names_and_namespaces() {
|
||||
let items = [
|
||||
json!({ "type": "function_call", "call_id": "lookup", "namespace": "collaboration", "name": "lookup", "arguments": "{}" }),
|
||||
json!({ "type": "custom_tool_call", "call_id": "custom", "namespace": "functions", "name": "exec", "input": "" }),
|
||||
json!({ "type": "function_call_output", "call_id": "lookup", "output": "ordinary function result" }),
|
||||
json!({ "type": "custom_tool_call_output", "call_id": "custom", "output": "ordinary custom result" }),
|
||||
json!({ "type": "custom_tool_call_output", "call_id": "custom", "name": "exec", "output": "Code Mode notification" }),
|
||||
json!({ "type": "function_call_output", "call_id": "lookup", "name": "lookup", "output": "explicit name only" }),
|
||||
json!({ "type": "function_call_output", "name": "notifications", "namespace": "slack", "output": "standalone" }),
|
||||
json!({ "type": "function_call_output", "call_id": "lookup", "namespace": "slack", "output": "explicit namespace only" }),
|
||||
];
|
||||
let rendered = render_test_items(&items, &ContextSnapshotOptions::default());
|
||||
let outputs = rendered
|
||||
.lines()
|
||||
.filter(|line| line.contains("call_output"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
outputs,
|
||||
[
|
||||
"02:function_call_output:ordinary function result",
|
||||
"03:custom_tool_call_output:ordinary custom result",
|
||||
"04:custom_tool_call_output/exec:Code Mode notification",
|
||||
"05:function_call_output/lookup:explicit name only",
|
||||
"06:function_call_output/slack.notifications:standalone",
|
||||
"07:function_call_output[namespace=slack]:explicit namespace only",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_compaction_payload_changes_are_visible_without_exposing_contents() {
|
||||
let render = |encrypted_content| {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//! A multi-turn Astra scenario whose actual requests expose the context around remote compaction.
|
||||
//! Multi-turn Astra scenarios snapshot the model-visible request history of shipped features.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::Engine;
|
||||
@@ -11,6 +12,8 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_context_fragments::AnsweredQuestion;
|
||||
use codex_context_fragments::ContextualUserFragment;
|
||||
use codex_core::TurnInputRequest;
|
||||
use codex_core::config::Config;
|
||||
use codex_extension_api::ExtensionRegistry;
|
||||
@@ -18,6 +21,8 @@ use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_protocol::items::AgentMessageDelivery;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
@@ -25,6 +30,7 @@ use codex_skills_extension::SkillsExtensionConfig;
|
||||
use codex_skills_extension::install;
|
||||
use core_test_support::context_snapshot;
|
||||
use core_test_support::context_snapshot::ContextSnapshotOptions;
|
||||
use core_test_support::context_snapshot::SnapshotEntry;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_custom_tool_call;
|
||||
@@ -36,12 +42,16 @@ use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use core_test_support::stdio_server_bin;
|
||||
use core_test_support::streaming_sse::StreamingSseChunk;
|
||||
use core_test_support::streaming_sse::start_streaming_sse_server;
|
||||
use core_test_support::test_codex::executor_path_uri;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use core_test_support::wait_for_event_match;
|
||||
use core_test_support::wait_for_mcp_server;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
const ONE_PIXEL_PNG_BASE64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
|
||||
|
||||
@@ -160,6 +170,129 @@ fn configure_scenario_catalog(config: &mut Config) {
|
||||
config.orchestrator_skills_enabled = false;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn astra_asks_an_async_question_and_receives_the_answer_while_working() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let question = "Who should receive the launch update?";
|
||||
let (release_continuation, continuation_gate) = oneshot::channel();
|
||||
let mut working_message = ev_assistant_message("working", "I drafted a short launch update.");
|
||||
working_message["item"]["phase"] = json!("commentary");
|
||||
let (streaming, _completions) = start_streaming_sse_server(vec![
|
||||
vec![StreamingSseChunk {
|
||||
gate: None,
|
||||
body: sse(vec![
|
||||
ev_response_created("question-response"),
|
||||
ev_function_call_with_namespace(
|
||||
"audience-question",
|
||||
"functions",
|
||||
"request_user_input_async",
|
||||
&json!({"questions": [{"title": question, "options": ["Internal team", "Customers"]}]}).to_string(),
|
||||
),
|
||||
ev_completed("question-response"),
|
||||
]),
|
||||
}],
|
||||
vec![
|
||||
StreamingSseChunk {
|
||||
gate: None,
|
||||
body: sse(vec![ev_response_created("working-response"), working_message]),
|
||||
},
|
||||
StreamingSseChunk {
|
||||
gate: Some(continuation_gate),
|
||||
body: sse(vec![ev_completed("working-response")]),
|
||||
},
|
||||
],
|
||||
vec![StreamingSseChunk {
|
||||
gate: None,
|
||||
body: sse(vec![
|
||||
ev_response_created("answered-response"),
|
||||
ev_assistant_message("answered", "Here is the launch update for customers."),
|
||||
ev_completed("answered-response"),
|
||||
]),
|
||||
}],
|
||||
vec![StreamingSseChunk {
|
||||
gate: None,
|
||||
body: sse(vec![
|
||||
ev_response_created("follow-up-response"),
|
||||
ev_assistant_message("follow-up", "The email subject is: Launch update."),
|
||||
ev_completed("follow-up-response"),
|
||||
]),
|
||||
}],
|
||||
])
|
||||
.await;
|
||||
let config_server = start_mock_server().await;
|
||||
let base_url = format!("{}/v1", streaming.uri());
|
||||
let test = test_codex()
|
||||
.with_model("gpt-6-astra")
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| {
|
||||
configure_scenario_catalog(config);
|
||||
config.model_provider.base_url = Some(base_url);
|
||||
// The gated mock records raw request bodies for the shared snapshot renderer.
|
||||
config
|
||||
.features
|
||||
.disable(Feature::EnableRequestCompression)
|
||||
.expect("disable compression for the gated mock");
|
||||
})
|
||||
.build_with_auto_env(&config_server)
|
||||
.await?;
|
||||
|
||||
test.codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![text(
|
||||
"Draft a short launch update. Ask me who it is for and keep working while I answer.",
|
||||
)]))
|
||||
.await?;
|
||||
let turn_id = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::TurnStarted(event) => Some(event.turn_id.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::ItemCompleted(event)
|
||||
if matches!(&event.item, TurnItem::AgentMessage(message)
|
||||
if message.delivery == Some(AgentMessageDelivery::Async)))
|
||||
})
|
||||
.await;
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(/*secs*/ 10),
|
||||
streaming.wait_for_request_count(/*count*/ 2),
|
||||
)
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::ItemCompleted(event)
|
||||
if matches!(&event.item, TurnItem::AgentMessage(message) if message.id == "working"))
|
||||
})
|
||||
.await;
|
||||
|
||||
let answer = format!("{}Customers", AnsweredQuestion::new(question).render());
|
||||
test.codex
|
||||
.steer_turn(TurnInputRequest::user_input(vec![text(&answer)]), turn_id)
|
||||
.await?;
|
||||
release_continuation.send(()).expect("release continuation");
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
test.submit_turn("Now give it an email subject.").await?;
|
||||
|
||||
let requests = streaming
|
||||
.requests()
|
||||
.await
|
||||
.iter()
|
||||
.map(|body| serde_json::from_slice(body))
|
||||
.collect::<serde_json::Result<Vec<_>>>()?;
|
||||
let entries = requests.iter().map(SnapshotEntry::body).collect::<Vec<_>>();
|
||||
insta::assert_snapshot!(
|
||||
"astra_async_question_and_answer",
|
||||
context_snapshot::format_context_snapshot(
|
||||
"Astra asks who a launch update is for, keeps working, and receives the user's answer in the active turn.",
|
||||
&entries,
|
||||
&ContextSnapshotOptions::default().rewrite_known_segments(),
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn astra_kickoff_with_skills_plugins_and_remote_compaction() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -386,3 +519,118 @@ text(`MCP: ${ping.structuredContent?.echo ?? "missing"}`);"#,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn astra_refreshes_plugin_tools_and_skills_in_an_existing_thread() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
core_test_support::skip_if_remote!(Ok(()), "plugin and MCP fixtures use host-local paths");
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
let config = "[features]\nplugins = true\n\n[skills.bundled]\nenabled = false\n";
|
||||
fs::write(home.path().join("config.toml"), config)?;
|
||||
let lookup = r#"text(ALL_TOOLS.filter(({ name }) => name === "mcp__notes__echo").map(({ name }) => name));"#;
|
||||
let mock = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("before-refresh"),
|
||||
ev_custom_tool_call("before-lookup", "exec", lookup),
|
||||
ev_completed("before-refresh"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("missing-tool", "The Notes tool is not installed yet."),
|
||||
ev_completed("missing-tool-response"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("after-refresh"),
|
||||
ev_custom_tool_call("after-lookup", "exec", lookup),
|
||||
ev_completed("after-refresh"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("first-echo"),
|
||||
ev_custom_tool_call(
|
||||
"first-echo-call",
|
||||
"exec",
|
||||
r#"const result = await tools.mcp__notes__echo({ message: "Mira owns the kickoff" }); text(result.structuredContent?.echo);"#,
|
||||
),
|
||||
ev_completed("first-echo"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("owner", "The Notes plugin confirms Mira owns the kickoff."),
|
||||
ev_completed("owner-response"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("follow-up"),
|
||||
ev_custom_tool_call(
|
||||
"second-echo-call",
|
||||
"exec",
|
||||
r#"const result = await tools.mcp__notes__echo({ message: "The kickoff is Friday" }); text(result.structuredContent?.echo);"#,
|
||||
),
|
||||
ev_completed("follow-up"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("deadline", "The Notes plugin confirms the kickoff is Friday."),
|
||||
ev_completed("deadline-response"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let mut builder = test_codex()
|
||||
.with_model("gpt-6-astra")
|
||||
.with_home(Arc::clone(&home))
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_extensions(skills_extensions())
|
||||
.with_config(configure_scenario_catalog);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("Check whether the Notes plugin echo tool is available.")
|
||||
.await?;
|
||||
|
||||
let plugin_root = home.path().join("plugins/cache/test/notes/local");
|
||||
fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
|
||||
fs::write(
|
||||
plugin_root.join(".codex-plugin/plugin.json"),
|
||||
json!({ "name": "notes", "description": "Look up and summarize team notes" }).to_string(),
|
||||
)?;
|
||||
fs::write(
|
||||
plugin_root.join(".mcp.json"),
|
||||
json!({ "mcpServers": { "notes": { "command": stdio_server_bin()?, "cwd": "." } } })
|
||||
.to_string(),
|
||||
)?;
|
||||
let skill = write_skill(
|
||||
&plugin_root.join("skills/summarize"),
|
||||
"summarize",
|
||||
"Summarize team notes",
|
||||
"State the owner and date from the notes.",
|
||||
)?;
|
||||
fs::write(
|
||||
home.path().join("config.toml"),
|
||||
format!("{config}\n[plugins.\"notes@test\"]\nenabled = true\n"),
|
||||
)?;
|
||||
test.codex.submit(Op::ReloadUserConfig).await?;
|
||||
test.codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![
|
||||
text("I installed Notes. Use $notes:summarize and the Notes tool to check that Mira owns the kickoff."),
|
||||
plugin("notes"),
|
||||
selected_skill("notes:summarize", &skill),
|
||||
]))
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
test.submit_text_turn("Use the Notes tool again to check that the kickoff is Friday.")
|
||||
.await?;
|
||||
|
||||
insta::assert_snapshot!(
|
||||
"astra_plugin_refresh",
|
||||
context_snapshot::format_request_history_snapshot(
|
||||
"Astra checks for Notes, refreshes its installed plugin without restarting, and uses the new skill and Code Mode MCP tool across turns.",
|
||||
&mock.requests(),
|
||||
&ContextSnapshotOptions::default().include_request_settings(),
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios.rs
|
||||
expression: "context_snapshot::format_context_snapshot(\"Astra asks who a launch update is for, keeps working, and receives the user's answer in the active turn.\",\n&entries, &ContextSnapshotOptions::default().rewrite_known_segments(),)"
|
||||
---
|
||||
Scenario: Astra asks who a launch update is for, keeps working, and receives the user's answer in the active turn.
|
||||
|
||||
## Window 1
|
||||
-- request 1 (request) --
|
||||
00:additional_tools/developer (3; hash=d4f3e164a92d9bf1):
|
||||
- namespace/functions; hash=e7db0a45c4ec3157
|
||||
- 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
|
||||
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=43f195d9cc99ee15
|
||||
- function/followup_task
|
||||
- function/interrupt_agent
|
||||
- function/list_agents
|
||||
- function/send_message
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
01:message/developer:
|
||||
You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803]
|
||||
|
||||
# When to ask the user for permission
|
||||
|
||||
Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388]
|
||||
|
||||
User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c]
|
||||
|
||||
You MUST complete the work that is already authorized and necessary to make the proposed action concrete... in the session or implied from the task instruction. [hash=821281c2d48149b3]
|
||||
|
||||
Do not use tools to send messages to others (e.g. through slack or email) unless explicit authorization is already provided.
|
||||
|
||||
The user gets very frustrated when you stop and ask for confirmation or permission, so make sure to expl... commentary and final, after any permission question. [hash=7e6ab67a40648d1d]
|
||||
|
||||
# Autonomy and persistence
|
||||
|
||||
<OMITTED 139 LINES; ~4454 TOKENS; hash=e00e6ed8fb28f55f>
|
||||
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool...re searchable by `tools_search` will be listed by it. [hash=4f9c0d35da3968a5]
|
||||
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
|
||||
|
||||
# Plugins
|
||||
|
||||
A plugin is a local bundle of skills, MCP servers, and apps.
|
||||
|
||||
## How to use plugins
|
||||
|
||||
- Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list.
|
||||
- MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from.
|
||||
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
|
||||
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
|
||||
- Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0]
|
||||
- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e]
|
||||
|
||||
02:message/developer[2]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <collaboration_mode># Collaboration Mode: Default
|
||||
|
||||
You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.
|
||||
|
||||
Your active mode changes only when new developer instructions with a different `<collaboration_mode>...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812]
|
||||
|
||||
## request_user_input availability
|
||||
|
||||
Use the `request_user_input` tool only when it is listed in the available tools for this turn.
|
||||
|
||||
In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions.
|
||||
|
||||
Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work.
|
||||
|
||||
If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked.
|
||||
|
||||
Never use the `request_user_input` tool for permission requests or permission-related escalations.
|
||||
|
||||
If explicit user input is required for another reason before progress can safely continue, do not use th... as a textual assistant message.</collaboration_mode> [hash=8213052bde4a62b2]
|
||||
03:message/developer:
|
||||
<multi_agent_role>You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals.
|
||||
|
||||
At the start of your turn, you are the active agent.
|
||||
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents.
|
||||
All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
|
||||
|
||||
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202]
|
||||
`send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers.
|
||||
Child agents can also spawn their own sub-agents.
|
||||
You can decide how much context you want to propagate to your sub-agents with the `fork_turns` parameter.
|
||||
|
||||
You will receive messages in the analysis channel in the form:
|
||||
```
|
||||
Message Type: MESSAGE | FINAL_ANSWER
|
||||
Task name: <recipient>
|
||||
Sender: <author>
|
||||
Payload:
|
||||
<payload text>
|
||||
```
|
||||
They may be addressed as to=/root
|
||||
|
||||
Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_m...ed with a `tools` namespace in the developer message. [hash=2798139748f66a39]
|
||||
|
||||
All agents share the same directory. In detail:
|
||||
- All agents have access to the same container and filesystem as you.
|
||||
- All agents use the same current working directory.
|
||||
- As a result, edits made by one agent are immediately visible to all other agents.
|
||||
|
||||
When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling.
|
||||
|
||||
There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you.
|
||||
|
||||
Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d...ne"` or a positive integer string.</multi_agent_role> [hash=c7f52e90688299ce]
|
||||
04:message/developer:
|
||||
<multi_agent_mode>Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work.</multi_agent_mode> [hash=2b6e037067b4390c]
|
||||
05:message/user:
|
||||
<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
06:message/user:
|
||||
Draft a short launch update. Ask me who it is for and keep working while I answer.
|
||||
-- request 2 (request) --
|
||||
07:function_call/functions.request_user_input_async:{"questions":[{"options":["Internal team","Customers"],"title":"Who should receive the launch update?"}]}
|
||||
08:function_call_output:{"accepted":true}
|
||||
-- request 3 (request) --
|
||||
09:message/assistant:
|
||||
I drafted a short launch update.
|
||||
10:message/user:
|
||||
> Who should receive the launch update?
|
||||
|
||||
Customers
|
||||
-- request 4 (request) --
|
||||
11:message/assistant:
|
||||
Here is the launch update for customers.
|
||||
12:message/developer:
|
||||
<PERMISSIONS_INSTRUCTIONS>
|
||||
13:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
14:message/user:
|
||||
Now give it an email subject.
|
||||
@@ -0,0 +1,324 @@
|
||||
---
|
||||
source: core/tests/suite/scenarios.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"Astra checks for Notes, refreshes its installed plugin without restarting, and uses the new skill and Code Mode MCP tool across turns.\",\n&mock.requests(),\n&ContextSnapshotOptions::default().include_request_settings(),)"
|
||||
---
|
||||
Scenario: Astra checks for Notes, refreshes its installed plugin without restarting, and uses the new skill and Code Mode MCP tool across turns.
|
||||
|
||||
## Window 1
|
||||
Settings:
|
||||
include: ["reasoning.encrypted_content"]
|
||||
model: "gpt-6-astra"
|
||||
parallel_tool_calls: false
|
||||
prompt_cache_key: "<PROMPT_CACHE_KEY 1>"
|
||||
reasoning: {"context":"all_turns","effort":"low"}
|
||||
store: false
|
||||
stream: true
|
||||
text: {"verbosity":"low"}
|
||||
tool_choice: "auto"
|
||||
-- request 1 (turn) --
|
||||
00:additional_tools/developer (3; hash=d4f3e164a92d9bf1):
|
||||
- namespace/functions; hash=e7db0a45c4ec3157
|
||||
- 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
|
||||
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=43f195d9cc99ee15
|
||||
- function/followup_task
|
||||
- function/interrupt_agent
|
||||
- function/list_agents
|
||||
- function/send_message
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
01:message/developer:
|
||||
You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803]
|
||||
|
||||
# When to ask the user for permission
|
||||
|
||||
Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388]
|
||||
|
||||
User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c]
|
||||
|
||||
You MUST complete the work that is already authorized and necessary to make the proposed action concrete... in the session or implied from the task instruction. [hash=821281c2d48149b3]
|
||||
|
||||
Do not use tools to send messages to others (e.g. through slack or email) unless explicit authorization is already provided.
|
||||
|
||||
The user gets very frustrated when you stop and ask for confirmation or permission, so make sure to expl... commentary and final, after any permission question. [hash=7e6ab67a40648d1d]
|
||||
|
||||
# Autonomy and persistence
|
||||
|
||||
<OMITTED 139 LINES; ~4454 TOKENS; hash=e00e6ed8fb28f55f>
|
||||
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool...re searchable by `tools_search` will be listed by it. [hash=4f9c0d35da3968a5]
|
||||
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
|
||||
|
||||
# Plugins
|
||||
|
||||
A plugin is a local bundle of skills, MCP servers, and apps.
|
||||
|
||||
## How to use plugins
|
||||
|
||||
- Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list.
|
||||
- MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from.
|
||||
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
|
||||
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
|
||||
- Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0]
|
||||
- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e]
|
||||
|
||||
02:message/developer[2]:
|
||||
[01] <permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b]
|
||||
Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.
|
||||
</permissions instructions>
|
||||
[02] <collaboration_mode># Collaboration Mode: Default
|
||||
|
||||
You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.
|
||||
|
||||
Your active mode changes only when new developer instructions with a different `<collaboration_mode>...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812]
|
||||
|
||||
## request_user_input availability
|
||||
|
||||
Use the `request_user_input` tool only when it is listed in the available tools for this turn.
|
||||
|
||||
In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions.
|
||||
|
||||
Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work.
|
||||
|
||||
If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked.
|
||||
|
||||
Never use the `request_user_input` tool for permission requests or permission-related escalations.
|
||||
|
||||
If explicit user input is required for another reason before progress can safely continue, do not use th... as a textual assistant message.</collaboration_mode> [hash=8213052bde4a62b2]
|
||||
03:message/developer:
|
||||
<multi_agent_role>You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals.
|
||||
|
||||
At the start of your turn, you are the active agent.
|
||||
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents.
|
||||
All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
|
||||
|
||||
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202]
|
||||
`send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers.
|
||||
Child agents can also spawn their own sub-agents.
|
||||
You can decide how much context you want to propagate to your sub-agents with the `fork_turns` parameter.
|
||||
|
||||
You will receive messages in the analysis channel in the form:
|
||||
```
|
||||
Message Type: MESSAGE | FINAL_ANSWER
|
||||
Task name: <recipient>
|
||||
Sender: <author>
|
||||
Payload:
|
||||
<payload text>
|
||||
```
|
||||
They may be addressed as to=/root
|
||||
|
||||
Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_m...ed with a `tools` namespace in the developer message. [hash=2798139748f66a39]
|
||||
|
||||
All agents share the same directory. In detail:
|
||||
- All agents have access to the same container and filesystem as you.
|
||||
- All agents use the same current working directory.
|
||||
- As a result, edits made by one agent are immediately visible to all other agents.
|
||||
|
||||
When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling.
|
||||
|
||||
There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you.
|
||||
|
||||
Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d...ne"` or a positive integer string.</multi_agent_role> [hash=c7f52e90688299ce]
|
||||
04:message/developer:
|
||||
<multi_agent_mode>Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work.</multi_agent_mode> [hash=2b6e037067b4390c]
|
||||
05:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><WORKSPACE_ROOT 1></root></workspace_roots><permission_profile type...e="unrestricted" /></permission_profile></filesystem> [hash=0592734c170299c1]
|
||||
</environment_context>
|
||||
06:message/user:
|
||||
Check whether the Notes plugin echo tool is available.
|
||||
-- request 2 (turn) --
|
||||
07:custom_tool_call/exec:text(ALL_TOOLS.filter(({ name }) => name === "mcp__notes__echo").map(({ name }) => name));
|
||||
08:custom_tool_call_output:Script completed
|
||||
Wall time <DURATION> seconds
|
||||
Output:
|
||||
| []
|
||||
|
||||
## Window 2 (after request 2: input diverged at item 00)
|
||||
Settings: same as window 1
|
||||
-- request 3 (turn) --
|
||||
00:additional_tools/developer (3; hash=61ace4f77de8d87f):
|
||||
- namespace/functions; hash=8c17932a39355f2d
|
||||
- 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
|
||||
- namespace/collaboration: Tools for spawning and managing sub-agents.; hash=43f195d9cc99ee15
|
||||
- function/followup_task
|
||||
- function/interrupt_agent
|
||||
- function/list_agents
|
||||
- function/send_message
|
||||
- function/spawn_agent
|
||||
- function/wait_agent
|
||||
01:message/developer:
|
||||
You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803]
|
||||
|
||||
# When to ask the user for permission
|
||||
|
||||
Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388]
|
||||
|
||||
User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c]
|
||||
|
||||
You MUST complete the work that is already authorized and necessary to make the proposed action concrete... in the session or implied from the task instruction. [hash=821281c2d48149b3]
|
||||
|
||||
Do not use tools to send messages to others (e.g. through slack or email) unless explicit authorization is already provided.
|
||||
|
||||
The user gets very frustrated when you stop and ask for confirmation or permission, so make sure to expl... commentary and final, after any permission question. [hash=7e6ab67a40648d1d]
|
||||
|
||||
# Autonomy and persistence
|
||||
|
||||
<OMITTED 139 LINES; ~4454 TOKENS; hash=e00e6ed8fb28f55f>
|
||||
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool...re searchable by `tools_search` will be listed by it. [hash=4f9c0d35da3968a5]
|
||||
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
|
||||
|
||||
# Plugins
|
||||
|
||||
A plugin is a local bundle of skills, MCP servers, and apps.
|
||||
|
||||
## How to use plugins
|
||||
|
||||
- Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list.
|
||||
- MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from.
|
||||
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
|
||||
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
|
||||
- Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0]
|
||||
- Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e]
|
||||
|
||||
02:message/developer[2]:
|
||||
[01] <permissions instructions>
|
||||
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b]
|
||||
Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.
|
||||
</permissions instructions>
|
||||
[02] <collaboration_mode># Collaboration Mode: Default
|
||||
|
||||
You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.
|
||||
|
||||
Your active mode changes only when new developer instructions with a different `<collaboration_mode>...<...by themselves. Known mode names are Default and Plan. [hash=e75f6b12fc52d812]
|
||||
|
||||
## request_user_input availability
|
||||
|
||||
Use the `request_user_input` tool only when it is listed in the available tools for this turn.
|
||||
|
||||
In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions.
|
||||
|
||||
Use the `request_user_input` tool only for optional questions where the answer would materially improve the quality of the work.
|
||||
|
||||
If `request_user_input` returns no answers, continue with best judgment instead of asking again or treating the turn as blocked.
|
||||
|
||||
Never use the `request_user_input` tool for permission requests or permission-related escalations.
|
||||
|
||||
If explicit user input is required for another reason before progress can safely continue, do not use th... as a textual assistant message.</collaboration_mode> [hash=8213052bde4a62b2]
|
||||
03:message/developer:
|
||||
<multi_agent_role>You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals.
|
||||
|
||||
At the start of your turn, you are the active agent.
|
||||
You can spawn sub-agents to handle subtasks, and those sub-agents can spawn their own sub-agents.
|
||||
All agents in the team, including the agents that you can assign tasks to, are equally intelligent and capable, and have access to the same set of tools.
|
||||
|
||||
You can use `spawn_agent` to create a new agent, `followup_task` to give an existing agent a new task an...message to a running agent without triggering a turn. [hash=7d40ff8b58977202]
|
||||
`send_message` calls may be read by a human, so ensure they are legible. Always put proper spaces between words and/or numbers.
|
||||
Child agents can also spawn their own sub-agents.
|
||||
You can decide how much context you want to propagate to your sub-agents with the `fork_turns` parameter.
|
||||
|
||||
You will receive messages in the analysis channel in the form:
|
||||
```
|
||||
Message Type: MESSAGE | FINAL_ANSWER
|
||||
Task name: <recipient>
|
||||
Sender: <author>
|
||||
Payload:
|
||||
<payload text>
|
||||
```
|
||||
They may be addressed as to=/root
|
||||
|
||||
Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_m...ed with a `tools` namespace in the developer message. [hash=2798139748f66a39]
|
||||
|
||||
All agents share the same directory. In detail:
|
||||
- All agents have access to the same container and filesystem as you.
|
||||
- All agents use the same current working directory.
|
||||
- As a result, edits made by one agent are immediately visible to all other agents.
|
||||
|
||||
When calling `wait_agent`, prefer longer waits (minutes) to avoid busy polling.
|
||||
|
||||
There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you.
|
||||
|
||||
Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d...ne"` or a positive integer string.</multi_agent_role> [hash=c7f52e90688299ce]
|
||||
04:message/developer:
|
||||
<multi_agent_mode>Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work.</multi_agent_mode> [hash=2b6e037067b4390c]
|
||||
05:message/user:
|
||||
<environment_context>
|
||||
<cwd><CWD></cwd>
|
||||
<shell><HOST_SHELL></shell>
|
||||
<current_date><CURRENT_DATE></current_date>
|
||||
<timezone><HOST_TIMEZONE></timezone>
|
||||
<filesystem><workspace_roots><root><WORKSPACE_ROOT 1></root></workspace_roots><permission_profile type...e="unrestricted" /></permission_profile></filesystem> [hash=0592734c170299c1]
|
||||
</environment_context>
|
||||
06:message/user:
|
||||
Check whether the Notes plugin echo tool is available.
|
||||
07:custom_tool_call/exec:text(ALL_TOOLS.filter(({ name }) => name === "mcp__notes__echo").map(({ name }) => name));
|
||||
08:custom_tool_call_output:Script completed
|
||||
Wall time <DURATION> seconds
|
||||
Output:
|
||||
| []
|
||||
09:message/assistant:
|
||||
The Notes tool is not installed yet.
|
||||
10:message/developer:
|
||||
<skills_instructions>
|
||||
## Skills
|
||||
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list ...ed into an absolute path using the skill roots table. [hash=ca27c3957f42502b]
|
||||
### Skill roots
|
||||
- `r0` = `<PLUGINS_CACHE>/test`
|
||||
### Available skills
|
||||
- notes:summarize: Summarize team notes (file: r0/notes/local/skills/summarize/SKILL.md)
|
||||
</skills_instructions>
|
||||
11:message/user:
|
||||
I installed Notes. Use $notes:summarize and the Notes tool to check that Mira owns the kickoff.
|
||||
12:message/user:
|
||||
<skill>
|
||||
<name>notes:summarize</name>
|
||||
<path><PLUGINS_CACHE>/test/notes/local/skills/summarize/SKILL.md</path>
|
||||
---
|
||||
name: summarize
|
||||
description: Summarize team notes
|
||||
---
|
||||
|
||||
State the owner and date from the notes.
|
||||
|
||||
</skill>
|
||||
13:message/developer:
|
||||
Capabilities from the `notes` plugin:
|
||||
- Skills from this plugin are prefixed with `notes:`.
|
||||
- MCP servers from this plugin available in this session: `notes`.
|
||||
Use these plugin-associated capabilities to help solve the task.
|
||||
-- request 4 (turn) --
|
||||
14:custom_tool_call/exec:text(ALL_TOOLS.filter(({ name }) => name === "mcp__notes__echo").map(({ name }) => name));
|
||||
15:custom_tool_call_output:Script completed
|
||||
Wall time <DURATION> seconds
|
||||
Output:
|
||||
| ["mcp__notes__echo"]
|
||||
-- request 5 (turn) --
|
||||
16:custom_tool_call/exec:const result = await tools.mcp__notes__echo({ message: "Mira owns the kickoff" }); text(result.structuredContent?.echo);
|
||||
17:custom_tool_call_output:Script completed
|
||||
Wall time <DURATION> seconds
|
||||
Output:
|
||||
| ECHOING: Mira owns the kickoff
|
||||
-- request 6 (turn) --
|
||||
18:message/assistant:
|
||||
The Notes plugin confirms Mira owns the kickoff.
|
||||
19:message/user:
|
||||
Use the Notes tool again to check that the kickoff is Friday.
|
||||
-- request 7 (turn) --
|
||||
20:custom_tool_call/exec:const result = await tools.mcp__notes__echo({ message: "The kickoff is Friday" }); text(result.structuredContent?.echo);
|
||||
21:custom_tool_call_output:Script completed
|
||||
Wall time <DURATION> seconds
|
||||
Output:
|
||||
| ECHOING: The kickoff is Friday
|
||||
Reference in New Issue
Block a user