mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Organize context fragments (#18794)
Organize context fragments under `core/context`. Implement same trait on all of them.
This commit is contained in:
committed by
GitHub
Unverified
parent
ab26554a3a
commit
4c2e730488
Generated
-11
@@ -1941,7 +1941,6 @@ dependencies = [
|
||||
"codex-feedback",
|
||||
"codex-git-utils",
|
||||
"codex-hooks",
|
||||
"codex-instructions",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-model-provider",
|
||||
@@ -2074,7 +2073,6 @@ dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-config",
|
||||
"codex-exec-server",
|
||||
"codex-instructions",
|
||||
"codex-login",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
@@ -2319,15 +2317,6 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-instructions"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-protocol",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-keyring-store"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -31,7 +31,6 @@ members = [
|
||||
"core-plugins",
|
||||
"core-skills",
|
||||
"hooks",
|
||||
"instructions",
|
||||
"secrets",
|
||||
"exec",
|
||||
"exec-server",
|
||||
@@ -143,7 +142,6 @@ codex-install-context = { path = "install-context" }
|
||||
codex-file-search = { path = "file-search" }
|
||||
codex-git-utils = { path = "git-utils" }
|
||||
codex-hooks = { path = "hooks" }
|
||||
codex-instructions = { path = "instructions" }
|
||||
codex-keyring-store = { path = "keyring-store" }
|
||||
codex-linux-sandbox = { path = "linux-sandbox" }
|
||||
codex-lmstudio = { path = "lmstudio" }
|
||||
|
||||
@@ -18,7 +18,6 @@ codex-analytics = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-instructions = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
|
||||
@@ -10,19 +10,24 @@ use codex_analytics::InvocationType;
|
||||
use codex_analytics::SkillInvocation;
|
||||
use codex_analytics::TrackEventsContext;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_instructions::SkillInstructions;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_plugins::mention_syntax::TOOL_MENTION_SIGIL;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SkillInjections {
|
||||
pub items: Vec<ResponseItem>,
|
||||
pub items: Vec<SkillInjection>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SkillInjection {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub contents: String,
|
||||
}
|
||||
|
||||
pub async fn build_skill_injections(
|
||||
mentioned_skills: &[SkillMetadata],
|
||||
loaded_skills: Option<&SkillLoadOutcome>,
|
||||
@@ -56,11 +61,11 @@ pub async fn build_skill_injections(
|
||||
skill_path: skill.path_to_skills_md.to_path_buf(),
|
||||
invocation_type: InvocationType::Explicit,
|
||||
});
|
||||
result.items.push(ResponseItem::from(SkillInstructions {
|
||||
result.items.push(SkillInjection {
|
||||
name: skill.name.clone(),
|
||||
path: skill.path_to_skills_md.to_string_lossy().into_owned(),
|
||||
contents,
|
||||
}));
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
emit_skill_injected_metric(otel, skill, "error");
|
||||
|
||||
@@ -48,7 +48,6 @@ codex-shell-command = { workspace = true }
|
||||
codex-execpolicy = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-hooks = { workspace = true }
|
||||
codex-instructions = { workspace = true }
|
||||
codex-network-proxy = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-plugin = { workspace = true }
|
||||
|
||||
@@ -5,7 +5,8 @@ use crate::agent::agent_status_from_event;
|
||||
use crate::config::AgentRoleConfig;
|
||||
use crate::config::Config;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::contextual_user_message::SUBAGENT_NOTIFICATION_OPEN_TAG;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::SubagentNotification;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -127,7 +128,7 @@ fn has_subagent_notification(history_items: &[ResponseItem]) -> bool {
|
||||
}
|
||||
content.iter().any(|content_item| match content_item {
|
||||
ContentItem::InputText { text } | ContentItem::OutputText { text } => {
|
||||
text.contains(SUBAGENT_NOTIFICATION_OPEN_TAG)
|
||||
SubagentNotification::matches_text(text)
|
||||
}
|
||||
ContentItem::InputImage { .. } => false,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -16,6 +17,7 @@ use wiremock::matchers::path;
|
||||
use super::*;
|
||||
use crate::agent_identity::AgentIdentityManager;
|
||||
use crate::agent_identity::RegisteredAgentTask;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use chrono::Utc;
|
||||
use codex_login::AuthCredentialsStoreMode;
|
||||
@@ -143,11 +145,16 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
|
||||
.await;
|
||||
session
|
||||
.record_into_history(
|
||||
&[
|
||||
crate::contextual_user_message::ENVIRONMENT_CONTEXT_FRAGMENT.into_message(
|
||||
"<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>".to_string(),
|
||||
&[ContextualUserFragment::into(
|
||||
crate::context::EnvironmentContext::new(
|
||||
Some(PathBuf::from("/tmp")),
|
||||
"zsh".to_string(),
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
),
|
||||
],
|
||||
)],
|
||||
&turn_context,
|
||||
)
|
||||
.await;
|
||||
|
||||
+36
-37
@@ -1,50 +1,47 @@
|
||||
use codex_instructions::AGENTS_MD_FRAGMENT;
|
||||
use codex_instructions::ContextualUserFragmentDefinition;
|
||||
use codex_instructions::SKILL_FRAGMENT;
|
||||
use codex_protocol::items::HookPromptItem;
|
||||
use codex_protocol::items::parse_hook_prompt_fragment;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG;
|
||||
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
|
||||
|
||||
pub(crate) const USER_SHELL_COMMAND_OPEN_TAG: &str = "<user_shell_command>";
|
||||
pub(crate) const USER_SHELL_COMMAND_CLOSE_TAG: &str = "</user_shell_command>";
|
||||
pub(crate) const TURN_ABORTED_OPEN_TAG: &str = "<turn_aborted>";
|
||||
pub(crate) const TURN_ABORTED_CLOSE_TAG: &str = "</turn_aborted>";
|
||||
pub(crate) const SUBAGENT_NOTIFICATION_OPEN_TAG: &str = "<subagent_notification>";
|
||||
pub(crate) const SUBAGENT_NOTIFICATION_CLOSE_TAG: &str = "</subagent_notification>";
|
||||
use super::EnvironmentContext;
|
||||
use super::FragmentRegistration;
|
||||
use super::FragmentRegistrationProxy;
|
||||
use super::SkillInstructions;
|
||||
use super::SubagentNotification;
|
||||
use super::TurnAborted;
|
||||
use super::UserInstructions;
|
||||
use super::UserShellCommand;
|
||||
|
||||
pub(crate) const ENVIRONMENT_CONTEXT_FRAGMENT: ContextualUserFragmentDefinition =
|
||||
ContextualUserFragmentDefinition::new(
|
||||
ENVIRONMENT_CONTEXT_OPEN_TAG,
|
||||
ENVIRONMENT_CONTEXT_CLOSE_TAG,
|
||||
);
|
||||
pub(crate) const USER_SHELL_COMMAND_FRAGMENT: ContextualUserFragmentDefinition =
|
||||
ContextualUserFragmentDefinition::new(
|
||||
USER_SHELL_COMMAND_OPEN_TAG,
|
||||
USER_SHELL_COMMAND_CLOSE_TAG,
|
||||
);
|
||||
pub(crate) const TURN_ABORTED_FRAGMENT: ContextualUserFragmentDefinition =
|
||||
ContextualUserFragmentDefinition::new(TURN_ABORTED_OPEN_TAG, TURN_ABORTED_CLOSE_TAG);
|
||||
pub(crate) const SUBAGENT_NOTIFICATION_FRAGMENT: ContextualUserFragmentDefinition =
|
||||
ContextualUserFragmentDefinition::new(
|
||||
SUBAGENT_NOTIFICATION_OPEN_TAG,
|
||||
SUBAGENT_NOTIFICATION_CLOSE_TAG,
|
||||
);
|
||||
static USER_INSTRUCTIONS_REGISTRATION: FragmentRegistrationProxy<UserInstructions> =
|
||||
FragmentRegistrationProxy::new();
|
||||
static ENVIRONMENT_CONTEXT_REGISTRATION: FragmentRegistrationProxy<EnvironmentContext> =
|
||||
FragmentRegistrationProxy::new();
|
||||
static SKILL_INSTRUCTIONS_REGISTRATION: FragmentRegistrationProxy<SkillInstructions> =
|
||||
FragmentRegistrationProxy::new();
|
||||
static USER_SHELL_COMMAND_REGISTRATION: FragmentRegistrationProxy<UserShellCommand> =
|
||||
FragmentRegistrationProxy::new();
|
||||
static TURN_ABORTED_REGISTRATION: FragmentRegistrationProxy<TurnAborted> =
|
||||
FragmentRegistrationProxy::new();
|
||||
static SUBAGENT_NOTIFICATION_REGISTRATION: FragmentRegistrationProxy<SubagentNotification> =
|
||||
FragmentRegistrationProxy::new();
|
||||
|
||||
const CONTEXTUAL_USER_FRAGMENTS: &[ContextualUserFragmentDefinition] = &[
|
||||
AGENTS_MD_FRAGMENT,
|
||||
ENVIRONMENT_CONTEXT_FRAGMENT,
|
||||
SKILL_FRAGMENT,
|
||||
USER_SHELL_COMMAND_FRAGMENT,
|
||||
TURN_ABORTED_FRAGMENT,
|
||||
SUBAGENT_NOTIFICATION_FRAGMENT,
|
||||
static CONTEXTUAL_USER_FRAGMENTS: &[&dyn FragmentRegistration] = &[
|
||||
&USER_INSTRUCTIONS_REGISTRATION,
|
||||
&ENVIRONMENT_CONTEXT_REGISTRATION,
|
||||
&SKILL_INSTRUCTIONS_REGISTRATION,
|
||||
&USER_SHELL_COMMAND_REGISTRATION,
|
||||
&TURN_ABORTED_REGISTRATION,
|
||||
&SUBAGENT_NOTIFICATION_REGISTRATION,
|
||||
];
|
||||
|
||||
static MEMORY_EXCLUDED_CONTEXTUAL_USER_FRAGMENTS: &[&dyn FragmentRegistration] = &[
|
||||
&USER_INSTRUCTIONS_REGISTRATION,
|
||||
&SKILL_INSTRUCTIONS_REGISTRATION,
|
||||
];
|
||||
|
||||
fn is_standard_contextual_user_text(text: &str) -> bool {
|
||||
CONTEXTUAL_USER_FRAGMENTS
|
||||
.iter()
|
||||
.any(|definition| definition.matches_text(text))
|
||||
.any(|fragment| fragment.matches_text(text))
|
||||
}
|
||||
|
||||
/// Returns whether a contextual user fragment should be omitted from memory
|
||||
@@ -59,7 +56,9 @@ pub(crate) fn is_memory_excluded_contextual_user_fragment(content_item: &Content
|
||||
let ContentItem::InputText { text } = content_item else {
|
||||
return false;
|
||||
};
|
||||
AGENTS_MD_FRAGMENT.matches_text(text) || SKILL_FRAGMENT.matches_text(text)
|
||||
MEMORY_EXCLUDED_CONTEXTUAL_USER_FRAGMENTS
|
||||
.iter()
|
||||
.any(|fragment| fragment.matches_text(text))
|
||||
}
|
||||
|
||||
pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool {
|
||||
+4
-4
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use codex_protocol::items::HookPromptFragment;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
@@ -20,10 +21,9 @@ fn detects_agents_instructions_fragment() {
|
||||
|
||||
#[test]
|
||||
fn detects_subagent_notification_fragment_case_insensitively() {
|
||||
assert!(
|
||||
SUBAGENT_NOTIFICATION_FRAGMENT
|
||||
.matches_text("<SUBAGENT_NOTIFICATION>{}</subagent_notification>")
|
||||
);
|
||||
assert!(SubagentNotification::matches_text(
|
||||
"<SUBAGENT_NOTIFICATION>{}</subagent_notification>"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
+62
-71
@@ -1,34 +1,40 @@
|
||||
use crate::contextual_user_message::ENVIRONMENT_CONTEXT_FRAGMENT;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::shell::Shell;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_protocol::protocol::TurnContextNetworkItem;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename = "environment_context", rename_all = "snake_case")]
|
||||
use super::ContextualUserFragment;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct EnvironmentContext {
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub shell: Shell,
|
||||
pub current_date: Option<String>,
|
||||
pub timezone: Option<String>,
|
||||
pub network: Option<NetworkContext>,
|
||||
pub subagents: Option<String>,
|
||||
pub(crate) cwd: Option<PathBuf>,
|
||||
pub(crate) shell: String,
|
||||
pub(crate) current_date: Option<String>,
|
||||
pub(crate) timezone: Option<String>,
|
||||
pub(crate) network: Option<NetworkContext>,
|
||||
pub(crate) subagents: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub(crate) struct NetworkContext {
|
||||
allowed_domains: Vec<String>,
|
||||
denied_domains: Vec<String>,
|
||||
}
|
||||
|
||||
impl NetworkContext {
|
||||
pub(crate) fn new(allowed_domains: Vec<String>, denied_domains: Vec<String>) -> Self {
|
||||
Self {
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvironmentContext {
|
||||
pub fn new(
|
||||
pub(crate) fn new(
|
||||
cwd: Option<PathBuf>,
|
||||
shell: Shell,
|
||||
shell: String,
|
||||
current_date: Option<String>,
|
||||
timezone: Option<String>,
|
||||
network: Option<NetworkContext>,
|
||||
@@ -47,7 +53,7 @@ impl EnvironmentContext {
|
||||
/// Compares two environment contexts, ignoring the shell. Useful when
|
||||
/// comparing turn to turn, since the initial environment_context will
|
||||
/// include the shell, and then it is not configurable from turn to turn.
|
||||
pub fn equals_except_shell(&self, other: &EnvironmentContext) -> bool {
|
||||
pub(crate) fn equals_except_shell(&self, other: &EnvironmentContext) -> bool {
|
||||
let EnvironmentContext {
|
||||
cwd,
|
||||
current_date,
|
||||
@@ -63,39 +69,34 @@ impl EnvironmentContext {
|
||||
&& self.subagents == *subagents
|
||||
}
|
||||
|
||||
pub fn diff_from_turn_context_item(
|
||||
pub(crate) fn diff_from_turn_context_item(
|
||||
before: &TurnContextItem,
|
||||
after: &TurnContext,
|
||||
shell: &Shell,
|
||||
after: &EnvironmentContext,
|
||||
) -> Self {
|
||||
let before_network = Self::network_from_turn_context_item(before);
|
||||
let after_network = Self::network_from_turn_context(after);
|
||||
let cwd = if before.cwd.as_path() != after.cwd.as_path() {
|
||||
Some(after.cwd.to_path_buf())
|
||||
} else {
|
||||
None
|
||||
let cwd = match &after.cwd {
|
||||
Some(cwd) if before.cwd.as_path() != cwd.as_path() => Some(cwd.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let current_date = after.current_date.clone();
|
||||
let timezone = after.timezone.clone();
|
||||
let network = if before_network != after_network {
|
||||
after_network
|
||||
let network = if before_network != after.network {
|
||||
after.network.clone()
|
||||
} else {
|
||||
before_network
|
||||
};
|
||||
EnvironmentContext::new(
|
||||
cwd,
|
||||
shell.clone(),
|
||||
current_date,
|
||||
timezone,
|
||||
after.shell.clone(),
|
||||
after.current_date.clone(),
|
||||
after.timezone.clone(),
|
||||
network,
|
||||
/*subagents*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
|
||||
pub(crate) fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
|
||||
Self::new(
|
||||
Some(turn_context.cwd.to_path_buf()),
|
||||
shell.clone(),
|
||||
shell.name().to_string(),
|
||||
turn_context.current_date.clone(),
|
||||
turn_context.timezone.clone(),
|
||||
Self::network_from_turn_context(turn_context),
|
||||
@@ -103,10 +104,13 @@ impl EnvironmentContext {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_turn_context_item(turn_context_item: &TurnContextItem, shell: &Shell) -> Self {
|
||||
pub(crate) fn from_turn_context_item(
|
||||
turn_context_item: &TurnContextItem,
|
||||
shell: String,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
Some(turn_context_item.cwd.clone()),
|
||||
shell.clone(),
|
||||
shell,
|
||||
turn_context_item.current_date.clone(),
|
||||
turn_context_item.timezone.clone(),
|
||||
Self::network_from_turn_context_item(turn_context_item),
|
||||
@@ -114,7 +118,7 @@ impl EnvironmentContext {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_subagents(mut self, subagents: String) -> Self {
|
||||
pub(crate) fn with_subagents(mut self, subagents: String) -> Self {
|
||||
if !subagents.is_empty() {
|
||||
self.subagents = Some(subagents);
|
||||
}
|
||||
@@ -129,18 +133,18 @@ impl EnvironmentContext {
|
||||
.network
|
||||
.as_ref()?;
|
||||
|
||||
Some(NetworkContext {
|
||||
allowed_domains: network
|
||||
Some(NetworkContext::new(
|
||||
network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
|
||||
.unwrap_or_default(),
|
||||
denied_domains: network
|
||||
network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::denied_domains)
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
fn network_from_turn_context_item(
|
||||
@@ -150,40 +154,33 @@ impl EnvironmentContext {
|
||||
allowed_domains,
|
||||
denied_domains,
|
||||
} = turn_context_item.network.as_ref()?;
|
||||
Some(NetworkContext {
|
||||
allowed_domains: allowed_domains.clone(),
|
||||
denied_domains: denied_domains.clone(),
|
||||
})
|
||||
Some(NetworkContext::new(
|
||||
allowed_domains.clone(),
|
||||
denied_domains.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvironmentContext {
|
||||
/// Serializes the environment context to XML. Libraries like `quick-xml`
|
||||
/// require custom macros to handle Enums with newtypes, so we just do it
|
||||
/// manually, to keep things simple. Output looks like:
|
||||
///
|
||||
/// ```xml
|
||||
/// <environment_context>
|
||||
/// <cwd>...</cwd>
|
||||
/// <shell>...</shell>
|
||||
/// </environment_context>
|
||||
/// ```
|
||||
pub fn serialize_to_xml(self) -> String {
|
||||
impl ContextualUserFragment for EnvironmentContext {
|
||||
const ROLE: &'static str = "user";
|
||||
const START_MARKER: &'static str = codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
|
||||
const END_MARKER: &'static str = codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG;
|
||||
|
||||
fn body(&self) -> String {
|
||||
let mut lines = Vec::new();
|
||||
if let Some(cwd) = self.cwd {
|
||||
if let Some(cwd) = &self.cwd {
|
||||
lines.push(format!(" <cwd>{}</cwd>", cwd.to_string_lossy()));
|
||||
}
|
||||
|
||||
let shell_name = self.shell.name();
|
||||
lines.push(format!(" <shell>{shell_name}</shell>"));
|
||||
if let Some(current_date) = self.current_date {
|
||||
lines.push(format!(" <shell>{}</shell>", self.shell));
|
||||
if let Some(current_date) = &self.current_date {
|
||||
lines.push(format!(" <current_date>{current_date}</current_date>"));
|
||||
}
|
||||
if let Some(timezone) = self.timezone {
|
||||
if let Some(timezone) = &self.timezone {
|
||||
lines.push(format!(" <timezone>{timezone}</timezone>"));
|
||||
}
|
||||
match self.network {
|
||||
Some(ref network) => {
|
||||
match &self.network {
|
||||
Some(network) => {
|
||||
lines.push(" <network enabled=\"true\">".to_string());
|
||||
for allowed in &network.allowed_domains {
|
||||
lines.push(format!(" <allowed>{allowed}</allowed>"));
|
||||
@@ -198,18 +195,12 @@ impl EnvironmentContext {
|
||||
// lines.push(" <network enabled=\"false\" />".to_string());
|
||||
}
|
||||
}
|
||||
if let Some(subagents) = self.subagents {
|
||||
if let Some(subagents) = &self.subagents {
|
||||
lines.push(" <subagents>".to_string());
|
||||
lines.extend(subagents.lines().map(|line| format!(" {line}")));
|
||||
lines.push(" </subagents>".to_string());
|
||||
}
|
||||
ENVIRONMENT_CONTEXT_FRAGMENT.wrap(lines.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EnvironmentContext> for ResponseItem {
|
||||
fn from(ec: EnvironmentContext) -> Self {
|
||||
ENVIRONMENT_CONTEXT_FRAGMENT.into_message(ec.serialize_to_xml())
|
||||
format!("\n{}", lines.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
+23
-111
@@ -3,13 +3,15 @@ use crate::shell::ShellType;
|
||||
use super::*;
|
||||
use core_test_support::test_path_buf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn fake_shell() -> Shell {
|
||||
Shell {
|
||||
fn fake_shell_name() -> String {
|
||||
let shell = crate::shell::Shell {
|
||||
shell_type: ShellType::Bash,
|
||||
shell_path: PathBuf::from("/bin/bash"),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
}
|
||||
};
|
||||
shell.name().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -17,7 +19,7 @@ fn serialize_workspace_write_environment_context() {
|
||||
let cwd = test_path_buf("/repo");
|
||||
let context = EnvironmentContext::new(
|
||||
Some(cwd.clone()),
|
||||
fake_shell(),
|
||||
fake_shell_name(),
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
/*network*/ None,
|
||||
@@ -34,18 +36,18 @@ fn serialize_workspace_write_environment_context() {
|
||||
cwd = cwd.display(),
|
||||
);
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
assert_eq!(context.render(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_environment_context_with_network() {
|
||||
let network = NetworkContext {
|
||||
allowed_domains: vec!["api.example.com".to_string(), "*.openai.com".to_string()],
|
||||
denied_domains: vec!["blocked.example.com".to_string()],
|
||||
};
|
||||
let network = NetworkContext::new(
|
||||
vec!["api.example.com".to_string(), "*.openai.com".to_string()],
|
||||
vec!["blocked.example.com".to_string()],
|
||||
);
|
||||
let context = EnvironmentContext::new(
|
||||
Some(test_path_buf("/repo")),
|
||||
fake_shell(),
|
||||
fake_shell_name(),
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
Some(network),
|
||||
@@ -67,14 +69,14 @@ fn serialize_environment_context_with_network() {
|
||||
test_path_buf("/repo").display()
|
||||
);
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
assert_eq!(context.render(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_read_only_environment_context() {
|
||||
let context = EnvironmentContext::new(
|
||||
/*cwd*/ None,
|
||||
fake_shell(),
|
||||
fake_shell_name(),
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
/*network*/ None,
|
||||
@@ -87,74 +89,14 @@ fn serialize_read_only_environment_context() {
|
||||
<timezone>America/Los_Angeles</timezone>
|
||||
</environment_context>"#;
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_external_sandbox_environment_context() {
|
||||
let context = EnvironmentContext::new(
|
||||
/*cwd*/ None,
|
||||
fake_shell(),
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
);
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
<shell>bash</shell>
|
||||
<current_date>2026-02-26</current_date>
|
||||
<timezone>America/Los_Angeles</timezone>
|
||||
</environment_context>"#;
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_external_sandbox_with_restricted_network_environment_context() {
|
||||
let context = EnvironmentContext::new(
|
||||
/*cwd*/ None,
|
||||
fake_shell(),
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
);
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
<shell>bash</shell>
|
||||
<current_date>2026-02-26</current_date>
|
||||
<timezone>America/Los_Angeles</timezone>
|
||||
</environment_context>"#;
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_full_access_environment_context() {
|
||||
let context = EnvironmentContext::new(
|
||||
/*cwd*/ None,
|
||||
fake_shell(),
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
);
|
||||
|
||||
let expected = r#"<environment_context>
|
||||
<shell>bash</shell>
|
||||
<current_date>2026-02-26</current_date>
|
||||
<timezone>America/Los_Angeles</timezone>
|
||||
</environment_context>"#;
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
assert_eq!(context.render(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_except_shell_compares_cwd() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
fake_shell(),
|
||||
fake_shell_name(),
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
@@ -162,7 +104,7 @@ fn equals_except_shell_compares_cwd() {
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
fake_shell(),
|
||||
fake_shell_name(),
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
@@ -171,33 +113,11 @@ fn equals_except_shell_compares_cwd() {
|
||||
assert!(context1.equals_except_shell(&context2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_except_shell_ignores_sandbox_policy() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
fake_shell(),
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
fake_shell(),
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
);
|
||||
|
||||
assert!(context1.equals_except_shell(&context2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equals_except_shell_compares_cwd_differences() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo1")),
|
||||
fake_shell(),
|
||||
fake_shell_name(),
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
@@ -205,7 +125,7 @@ fn equals_except_shell_compares_cwd_differences() {
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo2")),
|
||||
fake_shell(),
|
||||
fake_shell_name(),
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
@@ -219,11 +139,7 @@ fn equals_except_shell_compares_cwd_differences() {
|
||||
fn equals_except_shell_ignores_shell() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
Shell {
|
||||
shell_type: ShellType::Bash,
|
||||
shell_path: "/bin/bash".into(),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
},
|
||||
"bash".to_string(),
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
@@ -231,11 +147,7 @@ fn equals_except_shell_ignores_shell() {
|
||||
);
|
||||
let context2 = EnvironmentContext::new(
|
||||
Some(PathBuf::from("/repo")),
|
||||
Shell {
|
||||
shell_type: ShellType::Zsh,
|
||||
shell_path: "/bin/zsh".into(),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
},
|
||||
"zsh".to_string(),
|
||||
/*current_date*/ None,
|
||||
/*timezone*/ None,
|
||||
/*network*/ None,
|
||||
@@ -249,7 +161,7 @@ fn equals_except_shell_ignores_shell() {
|
||||
fn serialize_environment_context_with_subagents() {
|
||||
let context = EnvironmentContext::new(
|
||||
Some(test_path_buf("/repo")),
|
||||
fake_shell(),
|
||||
fake_shell_name(),
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
/*network*/ None,
|
||||
@@ -270,5 +182,5 @@ fn serialize_environment_context_with_subagents() {
|
||||
test_path_buf("/repo").display()
|
||||
);
|
||||
|
||||
assert_eq!(context.serialize_to_xml(), expected);
|
||||
assert_eq!(context.render(), expected);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// Type-erased registration for a contextual user fragment.
|
||||
///
|
||||
/// Implementations are used by context filtering code to recognize injected
|
||||
/// fragments without constructing the concrete context payload.
|
||||
pub(crate) trait FragmentRegistration: Sync {
|
||||
fn matches_text(&self, text: &str) -> bool;
|
||||
}
|
||||
|
||||
pub(crate) struct FragmentRegistrationProxy<T> {
|
||||
_marker: PhantomData<fn() -> T>,
|
||||
}
|
||||
|
||||
impl<T> FragmentRegistrationProxy<T> {
|
||||
pub(crate) const fn new() -> Self {
|
||||
Self {
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ContextualUserFragment> FragmentRegistration for FragmentRegistrationProxy<T> {
|
||||
fn matches_text(&self, text: &str) -> bool {
|
||||
T::matches_text(text)
|
||||
}
|
||||
}
|
||||
|
||||
/// Context payload that is injected as a user-authored message fragment.
|
||||
///
|
||||
/// Implementations own the response role, start/end markers used to recognize
|
||||
/// the fragment, and provide the fragment body appended directly after the
|
||||
/// start marker. The default helpers wrap that body and convert it into the
|
||||
/// response item shape expected by model input assembly.
|
||||
pub(crate) trait ContextualUserFragment {
|
||||
const ROLE: &'static str;
|
||||
const START_MARKER: &'static str;
|
||||
const END_MARKER: &'static str;
|
||||
|
||||
fn body(&self) -> String;
|
||||
|
||||
fn matches_text(text: &str) -> bool
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let trimmed = text.trim_start();
|
||||
let starts_with_marker = trimmed
|
||||
.get(..Self::START_MARKER.len())
|
||||
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(Self::START_MARKER));
|
||||
let trimmed = trimmed.trim_end();
|
||||
let ends_with_marker = trimmed
|
||||
.get(trimmed.len().saturating_sub(Self::END_MARKER.len())..)
|
||||
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(Self::END_MARKER));
|
||||
starts_with_marker && ends_with_marker
|
||||
}
|
||||
|
||||
fn render(&self) -> String {
|
||||
format!(
|
||||
"{}{}\n{}",
|
||||
Self::START_MARKER,
|
||||
self.body(),
|
||||
Self::END_MARKER
|
||||
)
|
||||
}
|
||||
|
||||
fn into(self) -> ResponseItem
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: Self::ROLE.to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: self.render(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
mod contextual_user_message;
|
||||
mod environment_context;
|
||||
mod fragment;
|
||||
mod skill_instructions;
|
||||
mod subagent_notification;
|
||||
mod turn_aborted;
|
||||
mod user_instructions;
|
||||
mod user_shell_command;
|
||||
|
||||
pub(crate) use contextual_user_message::is_contextual_user_fragment;
|
||||
pub(crate) use contextual_user_message::is_memory_excluded_contextual_user_fragment;
|
||||
pub(crate) use contextual_user_message::parse_visible_hook_prompt_message;
|
||||
pub(crate) use environment_context::EnvironmentContext;
|
||||
pub(crate) use fragment::ContextualUserFragment;
|
||||
pub(crate) use fragment::FragmentRegistration;
|
||||
pub(crate) use fragment::FragmentRegistrationProxy;
|
||||
pub(crate) use skill_instructions::SkillInstructions;
|
||||
pub(crate) use subagent_notification::SubagentNotification;
|
||||
pub(crate) use turn_aborted::TurnAborted;
|
||||
pub(crate) use user_instructions::UserInstructions;
|
||||
pub(crate) use user_shell_command::UserShellCommand;
|
||||
@@ -0,0 +1,33 @@
|
||||
use codex_core_skills::injection::SkillInjection;
|
||||
|
||||
use super::ContextualUserFragment;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct SkillInstructions {
|
||||
pub(crate) name: String,
|
||||
pub(crate) path: String,
|
||||
pub(crate) contents: String,
|
||||
}
|
||||
|
||||
impl From<&SkillInjection> for SkillInstructions {
|
||||
fn from(skill: &SkillInjection) -> Self {
|
||||
Self {
|
||||
name: skill.name.clone(),
|
||||
path: skill.path.clone(),
|
||||
contents: skill.contents.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for SkillInstructions {
|
||||
const ROLE: &'static str = "user";
|
||||
const START_MARKER: &'static str = "<skill>";
|
||||
const END_MARKER: &'static str = "</skill>";
|
||||
|
||||
fn body(&self) -> String {
|
||||
format!(
|
||||
"\n<name>{}</name>\n<path>{}</path>\n{}",
|
||||
self.name, self.path, self.contents
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
|
||||
use super::ContextualUserFragment;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct SubagentNotification {
|
||||
pub(crate) agent_reference: String,
|
||||
pub(crate) status: AgentStatus,
|
||||
}
|
||||
|
||||
impl SubagentNotification {
|
||||
pub(crate) fn new(agent_reference: impl Into<String>, status: AgentStatus) -> Self {
|
||||
Self {
|
||||
agent_reference: agent_reference.into(),
|
||||
status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for SubagentNotification {
|
||||
const ROLE: &'static str = "user";
|
||||
const START_MARKER: &'static str = "<subagent_notification>";
|
||||
const END_MARKER: &'static str = "</subagent_notification>";
|
||||
|
||||
fn body(&self) -> String {
|
||||
format!(
|
||||
"\n{}",
|
||||
serde_json::json!({
|
||||
"agent_path": &self.agent_reference,
|
||||
"status": &self.status,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use super::ContextualUserFragment;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct TurnAborted {
|
||||
pub(crate) guidance: String,
|
||||
}
|
||||
|
||||
impl TurnAborted {
|
||||
pub(crate) const INTERRUPTED_GUIDANCE: &'static str = "The user interrupted the previous turn on purpose. Any running unified exec processes may still be running in the background. If any tools/commands were aborted, they may have partially executed.";
|
||||
|
||||
pub(crate) fn new(guidance: impl Into<String>) -> Self {
|
||||
Self {
|
||||
guidance: guidance.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for TurnAborted {
|
||||
const ROLE: &'static str = "user";
|
||||
const START_MARKER: &'static str = "<turn_aborted>";
|
||||
const END_MARKER: &'static str = "</turn_aborted>";
|
||||
|
||||
fn body(&self) -> String {
|
||||
format!("\n{}", self.guidance)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use super::ContextualUserFragment;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct UserInstructions {
|
||||
pub(crate) directory: String,
|
||||
pub(crate) text: String,
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for UserInstructions {
|
||||
const ROLE: &'static str = "user";
|
||||
const START_MARKER: &'static str = "# AGENTS.md instructions for ";
|
||||
const END_MARKER: &'static str = "</INSTRUCTIONS>";
|
||||
|
||||
fn body(&self) -> String {
|
||||
format!("{}\n\n<INSTRUCTIONS>\n{}", self.directory, self.text)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use super::ContextualUserFragment;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct UserShellCommand {
|
||||
pub(crate) command: String,
|
||||
pub(crate) exit_code: i32,
|
||||
pub(crate) duration_seconds: f64,
|
||||
pub(crate) output: String,
|
||||
}
|
||||
|
||||
impl UserShellCommand {
|
||||
pub(crate) fn new(
|
||||
command: impl Into<String>,
|
||||
exit_code: i32,
|
||||
duration: Duration,
|
||||
output: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
command: command.into(),
|
||||
exit_code,
|
||||
duration_seconds: duration.as_secs_f64(),
|
||||
output: output.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for UserShellCommand {
|
||||
const ROLE: &'static str = "user";
|
||||
const START_MARKER: &'static str = "<user_shell_command>";
|
||||
const END_MARKER: &'static str = "</user_shell_command>";
|
||||
|
||||
fn body(&self) -> String {
|
||||
format!(
|
||||
"\n<command>\n{}\n</command>\n<result>\nExit code: {}\nDuration: {:.4} seconds\nOutput:\n{}\n</result>",
|
||||
self.command, self.exit_code, self.duration_seconds, self.output,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::environment_context::EnvironmentContext;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::EnvironmentContext;
|
||||
use crate::session::PreviousTurnSettings;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::shell::Shell;
|
||||
@@ -21,14 +22,14 @@ fn build_environment_update_item(
|
||||
}
|
||||
|
||||
let prev = previous?;
|
||||
let prev_context = EnvironmentContext::from_turn_context_item(prev, shell);
|
||||
let prev_context = EnvironmentContext::from_turn_context_item(prev, shell.name().to_string());
|
||||
let next_context = EnvironmentContext::from_turn_context(next, shell);
|
||||
if prev_context.equals_except_shell(&next_context) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ResponseItem::from(
|
||||
EnvironmentContext::diff_from_turn_context_item(prev, next, shell),
|
||||
Some(ContextualUserFragment::into(
|
||||
EnvironmentContext::diff_from_turn_context_item(prev, &next_context),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ use codex_protocol::user_input::UserInput;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::contextual_user_message::is_contextual_user_fragment;
|
||||
use crate::contextual_user_message::parse_visible_hook_prompt_message;
|
||||
use crate::context::is_contextual_user_fragment;
|
||||
use crate::context::parse_visible_hook_prompt_message;
|
||||
use crate::web_search::web_search_action_detail;
|
||||
|
||||
const CONTEXTUAL_DEVELOPER_PREFIXES: &[&str] = &[
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) use codex_instructions::UserInstructions;
|
||||
@@ -27,9 +27,8 @@ mod commit_attribution;
|
||||
pub mod config;
|
||||
pub mod config_loader;
|
||||
pub mod connectors;
|
||||
mod context;
|
||||
mod context_manager;
|
||||
mod contextual_user_message;
|
||||
mod environment_context;
|
||||
pub mod exec;
|
||||
pub mod exec_env;
|
||||
mod exec_policy;
|
||||
@@ -41,7 +40,6 @@ mod git_info_tests;
|
||||
mod guardian;
|
||||
mod hook_runtime;
|
||||
mod installation_id;
|
||||
pub(crate) mod instructions;
|
||||
pub(crate) mod landlock;
|
||||
pub use landlock::spawn_command_under_linux_sandbox;
|
||||
pub(crate) mod mcp;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::Prompt;
|
||||
use crate::RolloutRecorder;
|
||||
use crate::config::Config;
|
||||
use crate::contextual_user_message::is_memory_excluded_contextual_user_fragment;
|
||||
use crate::context::is_memory_excluded_contextual_user_fragment;
|
||||
use crate::memories::metrics;
|
||||
use crate::memories::phase_one;
|
||||
use crate::memories::phase_one::PRUNE_BATCH_SIZE;
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::commit_attribution::commit_message_trailer_instruction;
|
||||
use crate::compact;
|
||||
use crate::config::ManagedFeatures;
|
||||
use crate::connectors;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::default_skill_metadata_budget;
|
||||
use crate::exec_policy::ExecPolicyManager;
|
||||
use crate::installation_id::resolve_installation_id;
|
||||
@@ -153,7 +154,6 @@ use crate::config::StartedNetworkProxy;
|
||||
use crate::config::resolve_web_search_mode_for_turn;
|
||||
use crate::context_manager::ContextManager;
|
||||
use crate::context_manager::TotalTokenUsageBreakdown;
|
||||
use crate::environment_context::EnvironmentContext;
|
||||
use crate::thread_rollout_truncation::initial_history_has_prior_user_turns;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::types::McpServerConfig;
|
||||
@@ -249,9 +249,9 @@ use crate::SkillLoadOutcome;
|
||||
use crate::SkillMetadata;
|
||||
use crate::SkillsManager;
|
||||
use crate::agents_md::AgentsMdManager;
|
||||
use crate::context::UserInstructions;
|
||||
use crate::exec_policy::ExecPolicyUpdateError;
|
||||
use crate::guardian::GuardianReviewSessionManager;
|
||||
use crate::instructions::UserInstructions;
|
||||
use crate::mcp::McpManager;
|
||||
use crate::memories;
|
||||
use crate::network_policy_decision::execpolicy_network_rule_amendment;
|
||||
@@ -2487,7 +2487,7 @@ impl Session {
|
||||
text: user_instructions.to_string(),
|
||||
directory: turn_context.cwd.to_string_lossy().into_owned(),
|
||||
}
|
||||
.serialize_to_text(),
|
||||
.render(),
|
||||
);
|
||||
}
|
||||
if turn_context.config.include_environment_context {
|
||||
@@ -2497,9 +2497,9 @@ impl Session {
|
||||
.format_environment_context_subagents(self.conversation_id)
|
||||
.await;
|
||||
contextual_user_sections.push(
|
||||
EnvironmentContext::from_turn_context(turn_context, shell.as_ref())
|
||||
crate::context::EnvironmentContext::from_turn_context(turn_context, shell.as_ref())
|
||||
.with_subagents(subagents)
|
||||
.serialize_to_xml(),
|
||||
.render(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ use crate::config_loader::NetworkDomainPermissionsToml;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::Sourced;
|
||||
use crate::config_loader::project_trust_key;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::TurnAborted;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::shell::default_user_shell;
|
||||
@@ -6349,7 +6351,7 @@ async fn abort_review_task_emits_exited_then_aborted_and_records_history() {
|
||||
let ContentItem::InputText { text } = content_item else {
|
||||
return false;
|
||||
};
|
||||
text.contains(crate::contextual_user_message::TURN_ABORTED_OPEN_TAG)
|
||||
TurnAborted::matches_text(text)
|
||||
})
|
||||
}),
|
||||
"expected a model-visible turn aborted marker in history after interrupt"
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::compact::run_inline_auto_compact_task;
|
||||
use crate::compact::should_use_remote_compact_task;
|
||||
use crate::compact_remote::run_inline_remote_auto_compact_task;
|
||||
use crate::connectors;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::feedback_tags;
|
||||
use crate::hook_runtime::PendingInputHookDisposition;
|
||||
use crate::hook_runtime::emit_hook_completed_events;
|
||||
@@ -241,7 +242,7 @@ pub(crate) async fn run_turn(
|
||||
turn_context.sub_id.clone(),
|
||||
);
|
||||
let SkillInjections {
|
||||
items: skill_items,
|
||||
items: skill_injections,
|
||||
warnings: skill_warnings,
|
||||
} = build_skill_injections(
|
||||
&mentioned_skills,
|
||||
@@ -257,6 +258,11 @@ pub(crate) async fn run_turn(
|
||||
.await;
|
||||
}
|
||||
|
||||
let skill_items: Vec<ResponseItem> = skill_injections
|
||||
.iter()
|
||||
.map(|skill| ContextualUserFragment::into(crate::context::SkillInstructions::from(skill)))
|
||||
.collect();
|
||||
|
||||
let plugin_items =
|
||||
build_plugin_injections(&mentioned_plugins, &mcp_tools, &available_connectors);
|
||||
let mentioned_plugin_metadata = mentioned_plugins
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
|
||||
/// Helpers for model-visible session state markers that are stored in user-role
|
||||
/// messages but are not user intent.
|
||||
use crate::contextual_user_message::SUBAGENT_NOTIFICATION_FRAGMENT;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::SubagentNotification;
|
||||
|
||||
// Helpers for model-visible session state markers that are stored in user-role
|
||||
// messages but are not user intent.
|
||||
|
||||
// TODO(jif) unify with structured schema
|
||||
pub(crate) fn format_subagent_notification_message(
|
||||
agent_reference: &str,
|
||||
status: &AgentStatus,
|
||||
) -> String {
|
||||
let payload_json = serde_json::json!({
|
||||
"agent_path": agent_reference,
|
||||
"status": status,
|
||||
})
|
||||
.to_string();
|
||||
SUBAGENT_NOTIFICATION_FRAGMENT.wrap(payload_json)
|
||||
SubagentNotification::new(agent_reference, status.clone()).render()
|
||||
}
|
||||
|
||||
pub(crate) fn format_subagent_context_line(
|
||||
|
||||
@@ -19,8 +19,7 @@ use tracing::info_span;
|
||||
use tracing::trace;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::contextual_user_message::TURN_ABORTED_CLOSE_TAG;
|
||||
use crate::contextual_user_message::TURN_ABORTED_OPEN_TAG;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::hook_runtime::PendingInputHookDisposition;
|
||||
use crate::hook_runtime::inspect_pending_input;
|
||||
use crate::hook_runtime::record_additional_contexts;
|
||||
@@ -39,7 +38,6 @@ use codex_otel::TURN_MEMORY_METRIC;
|
||||
use codex_otel::TURN_NETWORK_PROXY_METRIC;
|
||||
use codex_otel::TURN_TOKEN_USAGE_METRIC;
|
||||
use codex_otel::TURN_TOOL_CALL_METRIC;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -62,22 +60,13 @@ pub(crate) use user_shell::UserShellCommandTask;
|
||||
pub(crate) use user_shell::execute_user_shell_command;
|
||||
|
||||
const GRACEFULL_INTERRUPTION_TIMEOUT_MS: u64 = 100;
|
||||
const TURN_ABORTED_INTERRUPTED_GUIDANCE: &str = "The user interrupted the previous turn on purpose. Any running unified exec processes may still be running in the background. If any tools/commands were aborted, they may have partially executed.";
|
||||
|
||||
/// Shared model-visible marker used by both the real interrupt path and
|
||||
/// interrupted fork snapshots.
|
||||
pub(crate) fn interrupted_turn_history_marker() -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: format!(
|
||||
"{TURN_ABORTED_OPEN_TAG}\n{TURN_ABORTED_INTERRUPTED_GUIDANCE}\n{TURN_ABORTED_CLOSE_TAG}"
|
||||
),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
ContextualUserFragment::into(crate::context::TurnAborted::new(
|
||||
crate::context::TurnAborted::INTERRUPTED_GUIDANCE,
|
||||
))
|
||||
}
|
||||
|
||||
fn emit_turn_network_proxy_metric(
|
||||
|
||||
@@ -1,45 +1,27 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
|
||||
use crate::contextual_user_message::USER_SHELL_COMMAND_FRAGMENT;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::UserShellCommand;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::tools::format_exec_output_str;
|
||||
|
||||
fn format_duration_line(duration: Duration) -> String {
|
||||
let duration_seconds = duration.as_secs_f64();
|
||||
format!("Duration: {duration_seconds:.4} seconds")
|
||||
}
|
||||
|
||||
fn format_user_shell_command_body(
|
||||
fn user_shell_command_fragment(
|
||||
command: &str,
|
||||
exec_output: &ExecToolCallOutput,
|
||||
turn_context: &TurnContext,
|
||||
) -> String {
|
||||
let mut sections = Vec::new();
|
||||
sections.push("<command>".to_string());
|
||||
sections.push(command.to_string());
|
||||
sections.push("</command>".to_string());
|
||||
sections.push("<result>".to_string());
|
||||
sections.push(format!("Exit code: {}", exec_output.exit_code));
|
||||
sections.push(format_duration_line(exec_output.duration));
|
||||
sections.push("Output:".to_string());
|
||||
sections.push(format_exec_output_str(
|
||||
exec_output,
|
||||
turn_context.truncation_policy,
|
||||
));
|
||||
sections.push("</result>".to_string());
|
||||
sections.join("\n")
|
||||
) -> UserShellCommand {
|
||||
let output = format_exec_output_str(exec_output, turn_context.truncation_policy);
|
||||
UserShellCommand::new(command, exec_output.exit_code, exec_output.duration, output)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn format_user_shell_command_record(
|
||||
command: &str,
|
||||
exec_output: &ExecToolCallOutput,
|
||||
turn_context: &TurnContext,
|
||||
) -> String {
|
||||
let body = format_user_shell_command_body(command, exec_output, turn_context);
|
||||
USER_SHELL_COMMAND_FRAGMENT.wrap(body)
|
||||
user_shell_command_fragment(command, exec_output, turn_context).render()
|
||||
}
|
||||
|
||||
pub fn user_shell_command_record_item(
|
||||
@@ -47,7 +29,7 @@ pub fn user_shell_command_record_item(
|
||||
exec_output: &ExecToolCallOutput,
|
||||
turn_context: &TurnContext,
|
||||
) -> ResponseItem {
|
||||
USER_SHELL_COMMAND_FRAGMENT.into_message(format_user_shell_command_record(
|
||||
ContextualUserFragment::into(user_shell_command_fragment(
|
||||
command,
|
||||
exec_output,
|
||||
turn_context,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use super::*;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::UserShellCommand;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use codex_protocol::exec_output::StreamOutput;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn detects_user_shell_command_text_variants() {
|
||||
assert!(
|
||||
USER_SHELL_COMMAND_FRAGMENT
|
||||
.matches_text("<user_shell_command>\necho hi\n</user_shell_command>")
|
||||
);
|
||||
assert!(!USER_SHELL_COMMAND_FRAGMENT.matches_text("echo hi"));
|
||||
assert!(UserShellCommand::matches_text(
|
||||
"<user_shell_command>\necho hi\n</user_shell_command>"
|
||||
));
|
||||
assert!(!UserShellCommand::matches_text("echo hi"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "instructions",
|
||||
crate_name = "codex_instructions",
|
||||
compile_data = glob(
|
||||
include = ["**"],
|
||||
exclude = [
|
||||
"BUILD.bazel",
|
||||
"Cargo.toml",
|
||||
],
|
||||
allow_empty = True,
|
||||
) + [
|
||||
"//codex-rs:node-version.txt",
|
||||
],
|
||||
)
|
||||
@@ -1,20 +0,0 @@
|
||||
[package]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
name = "codex-instructions"
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
name = "codex_instructions"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-protocol = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
@@ -1,61 +0,0 @@
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
|
||||
pub(crate) const AGENTS_MD_START_MARKER: &str = "# AGENTS.md instructions for ";
|
||||
pub(crate) const AGENTS_MD_END_MARKER: &str = "</INSTRUCTIONS>";
|
||||
pub(crate) const SKILL_OPEN_TAG: &str = "<skill>";
|
||||
pub(crate) const SKILL_CLOSE_TAG: &str = "</skill>";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ContextualUserFragmentDefinition {
|
||||
start_marker: &'static str,
|
||||
end_marker: &'static str,
|
||||
}
|
||||
|
||||
impl ContextualUserFragmentDefinition {
|
||||
pub const fn new(start_marker: &'static str, end_marker: &'static str) -> Self {
|
||||
Self {
|
||||
start_marker,
|
||||
end_marker,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches_text(&self, text: &str) -> bool {
|
||||
let trimmed = text.trim_start();
|
||||
let starts_with_marker = trimmed
|
||||
.get(..self.start_marker.len())
|
||||
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.start_marker));
|
||||
let trimmed = trimmed.trim_end();
|
||||
let ends_with_marker = trimmed
|
||||
.get(trimmed.len().saturating_sub(self.end_marker.len())..)
|
||||
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.end_marker));
|
||||
starts_with_marker && ends_with_marker
|
||||
}
|
||||
|
||||
pub const fn start_marker(&self) -> &'static str {
|
||||
self.start_marker
|
||||
}
|
||||
|
||||
pub const fn end_marker(&self) -> &'static str {
|
||||
self.end_marker
|
||||
}
|
||||
|
||||
pub fn wrap(&self, body: String) -> String {
|
||||
format!("{}\n{}\n{}", self.start_marker, body, self.end_marker)
|
||||
}
|
||||
|
||||
pub fn into_message(self, text: String) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText { text }],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const AGENTS_MD_FRAGMENT: ContextualUserFragmentDefinition =
|
||||
ContextualUserFragmentDefinition::new(AGENTS_MD_START_MARKER, AGENTS_MD_END_MARKER);
|
||||
pub const SKILL_FRAGMENT: ContextualUserFragmentDefinition =
|
||||
ContextualUserFragmentDefinition::new(SKILL_OPEN_TAG, SKILL_CLOSE_TAG);
|
||||
@@ -1,11 +0,0 @@
|
||||
//! User and skill instruction payloads and contextual user fragment markers for Codex prompts.
|
||||
|
||||
mod fragment;
|
||||
mod user_instructions;
|
||||
|
||||
pub use fragment::AGENTS_MD_FRAGMENT;
|
||||
pub use fragment::ContextualUserFragmentDefinition;
|
||||
pub use fragment::SKILL_FRAGMENT;
|
||||
pub use user_instructions::SkillInstructions;
|
||||
pub use user_instructions::USER_INSTRUCTIONS_PREFIX;
|
||||
pub use user_instructions::UserInstructions;
|
||||
@@ -1,56 +0,0 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use codex_protocol::models::ResponseItem;
|
||||
|
||||
use crate::fragment::AGENTS_MD_FRAGMENT;
|
||||
use crate::fragment::AGENTS_MD_START_MARKER;
|
||||
use crate::fragment::SKILL_FRAGMENT;
|
||||
|
||||
pub const USER_INSTRUCTIONS_PREFIX: &str = AGENTS_MD_START_MARKER;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename = "user_instructions", rename_all = "snake_case")]
|
||||
pub struct UserInstructions {
|
||||
pub directory: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl UserInstructions {
|
||||
pub fn serialize_to_text(&self) -> String {
|
||||
format!(
|
||||
"{prefix}{directory}\n\n<INSTRUCTIONS>\n{contents}\n{suffix}",
|
||||
prefix = AGENTS_MD_FRAGMENT.start_marker(),
|
||||
directory = self.directory,
|
||||
contents = self.text,
|
||||
suffix = AGENTS_MD_FRAGMENT.end_marker(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserInstructions> for ResponseItem {
|
||||
fn from(ui: UserInstructions) -> Self {
|
||||
AGENTS_MD_FRAGMENT.into_message(ui.serialize_to_text())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename = "skill_instructions", rename_all = "snake_case")]
|
||||
pub struct SkillInstructions {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub contents: String,
|
||||
}
|
||||
|
||||
impl From<SkillInstructions> for ResponseItem {
|
||||
fn from(si: SkillInstructions) -> Self {
|
||||
SKILL_FRAGMENT.into_message(SKILL_FRAGMENT.wrap(format!(
|
||||
"<name>{}</name>\n<path>{}</path>\n{}",
|
||||
si.name, si.path, si.contents
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "user_instructions_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,72 +0,0 @@
|
||||
use super::*;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::fragment::AGENTS_MD_FRAGMENT;
|
||||
use crate::fragment::SKILL_FRAGMENT;
|
||||
|
||||
#[test]
|
||||
fn test_user_instructions() {
|
||||
let user_instructions = UserInstructions {
|
||||
directory: "test_directory".to_string(),
|
||||
text: "test_text".to_string(),
|
||||
};
|
||||
let response_item: ResponseItem = user_instructions.into();
|
||||
|
||||
let ResponseItem::Message { role, content, .. } = response_item else {
|
||||
panic!("expected ResponseItem::Message");
|
||||
};
|
||||
|
||||
assert_eq!(role, "user");
|
||||
|
||||
let [ContentItem::InputText { text }] = content.as_slice() else {
|
||||
panic!("expected one InputText content item");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
text,
|
||||
"# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_user_instructions() {
|
||||
assert!(AGENTS_MD_FRAGMENT.matches_text(
|
||||
"# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>"
|
||||
));
|
||||
assert!(!AGENTS_MD_FRAGMENT.matches_text("test_text"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skill_instructions() {
|
||||
let skill_instructions = SkillInstructions {
|
||||
name: "demo-skill".to_string(),
|
||||
path: "skills/demo/SKILL.md".to_string(),
|
||||
contents: "body".to_string(),
|
||||
};
|
||||
let response_item: ResponseItem = skill_instructions.into();
|
||||
|
||||
let ResponseItem::Message { role, content, .. } = response_item else {
|
||||
panic!("expected ResponseItem::Message");
|
||||
};
|
||||
|
||||
assert_eq!(role, "user");
|
||||
|
||||
let [ContentItem::InputText { text }] = content.as_slice() else {
|
||||
panic!("expected one InputText content item");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
text,
|
||||
"<skill>\n<name>demo-skill</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_skill_instructions() {
|
||||
assert!(SKILL_FRAGMENT.matches_text(
|
||||
"<skill>\n<name>demo-skill</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>"
|
||||
));
|
||||
assert!(!SKILL_FRAGMENT.matches_text("regular text"));
|
||||
}
|
||||
Reference in New Issue
Block a user