Organize context fragments (#18794)

Organize context fragments under `core/context`. Implement same trait on
all of them.
This commit is contained in:
pakrym-oai
2026-04-20 22:39:17 -07:00
committed by GitHub
Unverified
parent ab26554a3a
commit 4c2e730488
36 changed files with 452 additions and 559 deletions
@@ -0,0 +1,100 @@
use codex_protocol::items::HookPromptItem;
use codex_protocol::items::parse_hook_prompt_fragment;
use codex_protocol::models::ContentItem;
use super::EnvironmentContext;
use super::FragmentRegistration;
use super::FragmentRegistrationProxy;
use super::SkillInstructions;
use super::SubagentNotification;
use super::TurnAborted;
use super::UserInstructions;
use super::UserShellCommand;
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();
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(|fragment| fragment.matches_text(text))
}
/// Returns whether a contextual user fragment should be omitted from memory
/// stage-1 inputs.
///
/// We exclude injected `AGENTS.md` instructions and skill payloads because
/// they are prompt scaffolding rather than conversation content, so they do
/// not improve the resulting memory. We keep environment context and
/// subagent notifications because they can carry useful execution context or
/// subtask outcomes that should remain visible to memory generation.
pub(crate) fn is_memory_excluded_contextual_user_fragment(content_item: &ContentItem) -> bool {
let ContentItem::InputText { text } = content_item else {
return false;
};
MEMORY_EXCLUDED_CONTEXTUAL_USER_FRAGMENTS
.iter()
.any(|fragment| fragment.matches_text(text))
}
pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool {
let ContentItem::InputText { text } = content_item else {
return false;
};
parse_hook_prompt_fragment(text).is_some() || is_standard_contextual_user_text(text)
}
pub(crate) fn parse_visible_hook_prompt_message(
id: Option<&String>,
content: &[ContentItem],
) -> Option<HookPromptItem> {
let mut fragments = Vec::new();
for content_item in content {
let ContentItem::InputText { text } = content_item else {
return None;
};
if let Some(fragment) = parse_hook_prompt_fragment(text) {
fragments.push(fragment);
continue;
}
if is_standard_contextual_user_text(text) {
continue;
}
return None;
}
if fragments.is_empty() {
return None;
}
Some(HookPromptItem::from_fragments(id, fragments))
}
#[cfg(test)]
#[path = "contextual_user_message_tests.rs"]
mod tests;
@@ -0,0 +1,99 @@
use super::*;
use crate::context::ContextualUserFragment;
use codex_protocol::items::HookPromptFragment;
use codex_protocol::items::build_hook_prompt_message;
use codex_protocol::models::ResponseItem;
#[test]
fn detects_environment_context_fragment() {
assert!(is_contextual_user_fragment(&ContentItem::InputText {
text: "<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>".to_string(),
}));
}
#[test]
fn detects_agents_instructions_fragment() {
assert!(is_contextual_user_fragment(&ContentItem::InputText {
text: "# AGENTS.md instructions for /tmp\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>"
.to_string(),
}));
}
#[test]
fn detects_subagent_notification_fragment_case_insensitively() {
assert!(SubagentNotification::matches_text(
"<SUBAGENT_NOTIFICATION>{}</subagent_notification>"
));
}
#[test]
fn ignores_regular_user_text() {
assert!(!is_contextual_user_fragment(&ContentItem::InputText {
text: "hello".to_string(),
}));
}
#[test]
fn classifies_memory_excluded_fragments() {
let cases = [
(
"# AGENTS.md instructions for /tmp\n\n<INSTRUCTIONS>\nbody\n</INSTRUCTIONS>",
true,
),
(
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
true,
),
(
"<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>",
false,
),
(
"<subagent_notification>{\"agent_id\":\"a\",\"status\":\"completed\"}</subagent_notification>",
false,
),
];
for (text, expected) in cases {
assert_eq!(
is_memory_excluded_contextual_user_fragment(&ContentItem::InputText {
text: text.to_string(),
}),
expected,
"{text}",
);
}
}
#[test]
fn detects_hook_prompt_fragment_and_roundtrips_escaping() {
let message = build_hook_prompt_message(&[HookPromptFragment::from_single_hook(
r#"Retry with "waves" & <tides>"#,
"hook-run-1",
)])
.expect("hook prompt message");
let ResponseItem::Message { content, .. } = message else {
panic!("expected hook prompt response item");
};
let [content_item] = content.as_slice() else {
panic!("expected a single content item");
};
assert!(is_contextual_user_fragment(content_item));
let ContentItem::InputText { text } = content_item else {
panic!("expected input text content item");
};
let parsed = parse_visible_hook_prompt_message(/*id*/ None, content.as_slice())
.expect("visible hook prompt");
assert_eq!(
parsed.fragments,
vec![HookPromptFragment {
text: r#"Retry with "waves" & <tides>"#.to_string(),
hook_run_id: "hook-run-1".to_string(),
}],
);
assert!(!text.contains("&quot;waves&quot; & <tides>"));
}
@@ -0,0 +1,209 @@
use crate::session::turn_context::TurnContext;
use crate::shell::Shell;
use codex_protocol::protocol::TurnContextItem;
use codex_protocol::protocol::TurnContextNetworkItem;
use std::path::PathBuf;
use super::ContextualUserFragment;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct EnvironmentContext {
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, 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(crate) fn new(
cwd: Option<PathBuf>,
shell: String,
current_date: Option<String>,
timezone: Option<String>,
network: Option<NetworkContext>,
subagents: Option<String>,
) -> Self {
Self {
cwd,
shell,
current_date,
timezone,
network,
subagents,
}
}
/// 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(crate) fn equals_except_shell(&self, other: &EnvironmentContext) -> bool {
let EnvironmentContext {
cwd,
current_date,
timezone,
network,
subagents,
shell: _,
} = other;
self.cwd == *cwd
&& self.current_date == *current_date
&& self.timezone == *timezone
&& self.network == *network
&& self.subagents == *subagents
}
pub(crate) fn diff_from_turn_context_item(
before: &TurnContextItem,
after: &EnvironmentContext,
) -> Self {
let before_network = Self::network_from_turn_context_item(before);
let cwd = match &after.cwd {
Some(cwd) if before.cwd.as_path() != cwd.as_path() => Some(cwd.clone()),
_ => None,
};
let network = if before_network != after.network {
after.network.clone()
} else {
before_network
};
EnvironmentContext::new(
cwd,
after.shell.clone(),
after.current_date.clone(),
after.timezone.clone(),
network,
/*subagents*/ None,
)
}
pub(crate) fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
Self::new(
Some(turn_context.cwd.to_path_buf()),
shell.name().to_string(),
turn_context.current_date.clone(),
turn_context.timezone.clone(),
Self::network_from_turn_context(turn_context),
/*subagents*/ None,
)
}
pub(crate) fn from_turn_context_item(
turn_context_item: &TurnContextItem,
shell: String,
) -> Self {
Self::new(
Some(turn_context_item.cwd.clone()),
shell,
turn_context_item.current_date.clone(),
turn_context_item.timezone.clone(),
Self::network_from_turn_context_item(turn_context_item),
/*subagents*/ None,
)
}
pub(crate) fn with_subagents(mut self, subagents: String) -> Self {
if !subagents.is_empty() {
self.subagents = Some(subagents);
}
self
}
fn network_from_turn_context(turn_context: &TurnContext) -> Option<NetworkContext> {
let network = turn_context
.config
.config_layer_stack
.requirements()
.network
.as_ref()?;
Some(NetworkContext::new(
network
.domains
.as_ref()
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)
.unwrap_or_default(),
network
.domains
.as_ref()
.and_then(codex_config::NetworkDomainPermissionsToml::denied_domains)
.unwrap_or_default(),
))
}
fn network_from_turn_context_item(
turn_context_item: &TurnContextItem,
) -> Option<NetworkContext> {
let TurnContextNetworkItem {
allowed_domains,
denied_domains,
} = turn_context_item.network.as_ref()?;
Some(NetworkContext::new(
allowed_domains.clone(),
denied_domains.clone(),
))
}
}
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 {
lines.push(format!(" <cwd>{}</cwd>", cwd.to_string_lossy()));
}
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 {
lines.push(format!(" <timezone>{timezone}</timezone>"));
}
match &self.network {
Some(network) => {
lines.push(" <network enabled=\"true\">".to_string());
for allowed in &network.allowed_domains {
lines.push(format!(" <allowed>{allowed}</allowed>"));
}
for denied in &network.denied_domains {
lines.push(format!(" <denied>{denied}</denied>"));
}
lines.push(" </network>".to_string());
}
None => {
// TODO(mbolin): Include this line if it helps the model.
// lines.push(" <network enabled=\"false\" />".to_string());
}
}
if let Some(subagents) = &self.subagents {
lines.push(" <subagents>".to_string());
lines.extend(subagents.lines().map(|line| format!(" {line}")));
lines.push(" </subagents>".to_string());
}
format!("\n{}", lines.join("\n"))
}
}
#[cfg(test)]
#[path = "environment_context_tests.rs"]
mod tests;
@@ -0,0 +1,186 @@
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_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]
fn serialize_workspace_write_environment_context() {
let cwd = test_path_buf("/repo");
let context = EnvironmentContext::new(
Some(cwd.clone()),
fake_shell_name(),
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
/*network*/ None,
/*subagents*/ None,
);
let expected = format!(
r#"<environment_context>
<cwd>{cwd}</cwd>
<shell>bash</shell>
<current_date>2026-02-26</current_date>
<timezone>America/Los_Angeles</timezone>
</environment_context>"#,
cwd = cwd.display(),
);
assert_eq!(context.render(), expected);
}
#[test]
fn serialize_environment_context_with_network() {
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_name(),
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
Some(network),
/*subagents*/ None,
);
let expected = format!(
r#"<environment_context>
<cwd>{}</cwd>
<shell>bash</shell>
<current_date>2026-02-26</current_date>
<timezone>America/Los_Angeles</timezone>
<network enabled="true">
<allowed>api.example.com</allowed>
<allowed>*.openai.com</allowed>
<denied>blocked.example.com</denied>
</network>
</environment_context>"#,
test_path_buf("/repo").display()
);
assert_eq!(context.render(), expected);
}
#[test]
fn serialize_read_only_environment_context() {
let context = EnvironmentContext::new(
/*cwd*/ None,
fake_shell_name(),
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.render(), expected);
}
#[test]
fn equals_except_shell_compares_cwd() {
let context1 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
fake_shell_name(),
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
/*subagents*/ None,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
fake_shell_name(),
/*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_name(),
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
/*subagents*/ None,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo2")),
fake_shell_name(),
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
/*subagents*/ None,
);
assert!(!context1.equals_except_shell(&context2));
}
#[test]
fn equals_except_shell_ignores_shell() {
let context1 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
"bash".to_string(),
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
/*subagents*/ None,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
"zsh".to_string(),
/*current_date*/ None,
/*timezone*/ None,
/*network*/ None,
/*subagents*/ None,
);
assert!(context1.equals_except_shell(&context2));
}
#[test]
fn serialize_environment_context_with_subagents() {
let context = EnvironmentContext::new(
Some(test_path_buf("/repo")),
fake_shell_name(),
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
/*network*/ None,
Some("- agent-1: atlas\n- agent-2".to_string()),
);
let expected = format!(
r#"<environment_context>
<cwd>{}</cwd>
<shell>bash</shell>
<current_date>2026-02-26</current_date>
<timezone>America/Los_Angeles</timezone>
<subagents>
- agent-1: atlas
- agent-2
</subagents>
</environment_context>"#,
test_path_buf("/repo").display()
);
assert_eq!(context.render(), expected);
}
+82
View File
@@ -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,
}
}
}
+21
View File
@@ -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,
})
)
}
}
+26
View File
@@ -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,
)
}
}