This commit is contained in:
jif-oai
2026-02-03 18:06:54 +00:00
parent 0b2b8c9012
commit 365c5b76fc
13 changed files with 212 additions and 72 deletions

View File

@@ -16,6 +16,7 @@ use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::UserInput;
use codex_protocol::artificial_messages::ArtificialMessage;
use codex_protocol::config_types::Personality;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
@@ -439,9 +440,10 @@ async fn thread_resume_accepts_personality_override() -> Result<()> {
let request = response_mock.single_request();
let developer_texts = request.message_input_texts("developer");
assert!(
developer_texts
.iter()
.any(|text| text.contains("<personality_spec>")),
developer_texts.iter().any(|text| matches!(
ArtificialMessage::parse(text),
Ok(ArtificialMessage::PersonalitySpec { .. })
)),
"expected a personality update message in developer input, got {developer_texts:?}"
);
let instructions_text = request.instructions_text();

View File

@@ -37,6 +37,7 @@ use codex_app_server_protocol::UserInput as V2UserInput;
use codex_core::features::FEATURES;
use codex_core::features::Feature;
use codex_core::protocol_config_types::ReasoningSummary;
use codex_protocol::artificial_messages::ArtificialMessage;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality;
@@ -475,9 +476,10 @@ async fn turn_start_accepts_personality_override_v2() -> Result<()> {
}
assert!(
developer_texts
.iter()
.any(|text| text.contains("<personality_spec>")),
developer_texts.iter().any(|text| matches!(
ArtificialMessage::parse(text),
Ok(ArtificialMessage::PersonalitySpec { .. })
)),
"expected personality update message in developer input, got {developer_texts:?}"
);
@@ -578,17 +580,19 @@ async fn turn_start_change_personality_mid_thread_v2() -> Result<()> {
let first_developer_texts = requests[0].message_input_texts("developer");
assert!(
first_developer_texts
.iter()
.all(|text| !text.contains("<personality_spec>")),
first_developer_texts.iter().all(|text| !matches!(
ArtificialMessage::parse(text),
Ok(ArtificialMessage::PersonalitySpec { .. })
)),
"expected no personality update message in first request, got {first_developer_texts:?}"
);
let second_developer_texts = requests[1].message_input_texts("developer");
assert!(
second_developer_texts
.iter()
.any(|text| text.contains("<personality_spec>")),
second_developer_texts.iter().any(|text| matches!(
ArtificialMessage::parse(text),
Ok(ArtificialMessage::PersonalitySpec { .. })
)),
"expected personality update message in second request, got {second_developer_texts:?}"
);

View File

@@ -1523,8 +1523,9 @@ impl Session {
warn!("execpolicy amendment for {sub_id} had no command prefix");
return;
};
let text = format!("Approved command prefix saved:\n{prefixes}");
let message: ResponseItem = DeveloperInstructions::new(text.clone()).into();
let body = format!("Approved command prefix saved:\n{prefixes}");
let message =
ArtificialMessage::ExecPolicyAmendment { body: body.clone() }.to_response_item();
if let Some(turn_context) = self.turn_context_for_sub_id(sub_id).await {
self.record_conversation_items(&turn_context, std::slice::from_ref(&message))
@@ -1535,7 +1536,9 @@ impl Session {
if self
.inject_response_items(vec![ResponseInputItem::Message {
role: "developer".to_string(),
content: vec![ContentItem::InputText { text }],
content: vec![ContentItem::InputText {
text: ArtificialMessage::ExecPolicyAmendment { body }.render(),
}],
}])
.await
.is_err()
@@ -1805,7 +1808,7 @@ impl Session {
.counter("codex.model_warning", 1, &[]);
let item = ArtificialMessage::ModelWarning {
body: message.into(),
};
}
.to_response_item();
self.record_conversation_items(ctx, &[item]).await;

View File

@@ -8,7 +8,7 @@ use crate::truncate::approx_token_count;
use crate::truncate::approx_tokens_from_byte_count;
use crate::truncate::truncate_function_output_items_with_policy;
use crate::truncate::truncate_text;
use crate::user_shell_command::is_user_shell_command_text;
use codex_protocol::artificial_messages::ArtificialMessage;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
@@ -378,7 +378,12 @@ pub(crate) fn is_user_turn_boundary(item: &ResponseItem) -> bool {
for content_item in content {
match content_item {
ContentItem::InputText { text } => {
if is_session_prefix(text) || is_user_shell_command_text(text) {
if is_session_prefix(text)
|| matches!(
ArtificialMessage::parse(text),
Ok(ArtificialMessage::UserShellCommand { .. })
)
{
return false;
}
}

View File

@@ -456,7 +456,12 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
"# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>",
),
user_input_text_msg(&demo_skill_message()),
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
user_input_text_msg(
&ArtificialMessage::UserShellCommand {
body: "echo 42".to_string(),
}
.render(),
),
user_input_text_msg("turn 1 user"),
assistant_msg("turn 1 assistant"),
user_input_text_msg("turn 2 user"),
@@ -473,7 +478,12 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
"# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>",
),
user_input_text_msg(&demo_skill_message()),
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
user_input_text_msg(
&ArtificialMessage::UserShellCommand {
body: "echo 42".to_string(),
}
.render(),
),
user_input_text_msg("turn 1 user"),
assistant_msg("turn 1 assistant"),
];
@@ -487,7 +497,12 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
"# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>",
),
user_input_text_msg(&demo_skill_message()),
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
user_input_text_msg(
&ArtificialMessage::UserShellCommand {
body: "echo 42".to_string(),
}
.render(),
),
];
let mut history = create_history_with_items(vec![
@@ -497,7 +512,12 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
"# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>",
),
user_input_text_msg(&demo_skill_message()),
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
user_input_text_msg(
&ArtificialMessage::UserShellCommand {
body: "echo 42".to_string(),
}
.render(),
),
user_input_text_msg("turn 1 user"),
assistant_msg("turn 1 assistant"),
user_input_text_msg("turn 2 user"),
@@ -513,7 +533,12 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
"# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>",
),
user_input_text_msg(&demo_skill_message()),
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
user_input_text_msg(
&ArtificialMessage::UserShellCommand {
body: "echo 42".to_string(),
}
.render(),
),
user_input_text_msg("turn 1 user"),
assistant_msg("turn 1 assistant"),
user_input_text_msg("turn 2 user"),

View File

@@ -1,3 +1,4 @@
use codex_protocol::artificial_messages::ArtificialMessage;
use codex_protocol::items::AgentMessageContent;
use codex_protocol::items::AgentMessageItem;
use codex_protocol::items::ReasoningItem;
@@ -20,7 +21,6 @@ use uuid::Uuid;
use crate::instructions::SkillInstructions;
use crate::instructions::UserInstructions;
use crate::session_prefix::is_session_prefix;
use crate::user_shell_command::is_user_shell_command_text;
use crate::web_search::web_search_action_detail;
fn parse_user_message(message: &[ContentItem]) -> Option<UserMessageItem> {
@@ -43,7 +43,12 @@ fn parse_user_message(message: &[ContentItem]) -> Option<UserMessageItem> {
{
continue;
}
if is_session_prefix(text) || is_user_shell_command_text(text) {
if is_session_prefix(text)
|| matches!(
ArtificialMessage::parse(text),
Ok(ArtificialMessage::UserShellCommand { .. })
)
{
return None;
}
content.push(UserInput::Text {
@@ -332,10 +337,13 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "<user_shell_command>echo 42</user_shell_command>".to_string(),
text: ArtificialMessage::UserShellCommand {
body: "echo 42".to_string(),
}
.render(),
}],
end_turn: None,
phase: None,
phase: None,
},
];

View File

@@ -1,5 +1,6 @@
use std::time::Duration;
use codex_protocol::artificial_messages::ArtificialMessage;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
@@ -7,15 +8,6 @@ use crate::codex::TurnContext;
use crate::exec::ExecToolCallOutput;
use crate::tools::format_exec_output_str;
pub const USER_SHELL_COMMAND_OPEN: &str = "<user_shell_command>";
pub const USER_SHELL_COMMAND_CLOSE: &str = "</user_shell_command>";
pub fn is_user_shell_command_text(text: &str) -> bool {
let trimmed = text.trim_start();
let lowered = trimmed.to_ascii_lowercase();
lowered.starts_with(USER_SHELL_COMMAND_OPEN)
}
fn format_duration_line(duration: Duration) -> String {
let duration_seconds = duration.as_secs_f64();
format!("Duration: {duration_seconds:.4} seconds")
@@ -48,7 +40,7 @@ pub fn format_user_shell_command_record(
turn_context: &TurnContext,
) -> String {
let body = format_user_shell_command_body(command, exec_output, turn_context);
format!("{USER_SHELL_COMMAND_OPEN}\n{body}\n{USER_SHELL_COMMAND_CLOSE}")
ArtificialMessage::UserShellCommand { body }.render()
}
pub fn user_shell_command_record_item(
@@ -72,16 +64,9 @@ mod tests {
use super::*;
use crate::codex::make_session_and_context;
use crate::exec::StreamOutput;
use codex_protocol::models::ContentItem;
use pretty_assertions::assert_eq;
#[test]
fn detects_user_shell_command_text_variants() {
assert!(is_user_shell_command_text(
"<user_shell_command>\necho hi\n</user_shell_command>"
));
assert!(!is_user_shell_command_text("echo hi"));
}
#[tokio::test]
async fn formats_basic_record() {
let exec_output = ExecToolCallOutput {
@@ -102,7 +87,7 @@ mod tests {
};
assert_eq!(
text,
"<user_shell_command>\n<command>\necho hi\n</command>\n<result>\nExit code: 0\nDuration: 1.0000 seconds\nOutput:\nhi\n</result>\n</user_shell_command>"
"<user_shell_cmd><command>\necho hi\n</command>\n<result>\nExit code: 0\nDuration: 1.0000 seconds\nOutput:\nhi\n</result></user_shell_cmd>"
);
}
@@ -120,7 +105,7 @@ mod tests {
let record = format_user_shell_command_record("false", &exec_output, &turn_context);
assert_eq!(
record,
"<user_shell_command>\n<command>\nfalse\n</command>\n<result>\nExit code: 42\nDuration: 0.1200 seconds\nOutput:\ncombined output wins\n</result>\n</user_shell_command>"
"<user_shell_cmd><command>\nfalse\n</command>\n<result>\nExit code: 42\nDuration: 0.1200 seconds\nOutput:\ncombined output wins\n</result></user_shell_cmd>"
);
}
}

View File

@@ -1,6 +1,4 @@
use anyhow::Result;
use codex_core::protocol::COLLABORATION_MODE_CLOSE_TAG;
use codex_core::protocol::COLLABORATION_MODE_OPEN_TAG;
use codex_core::protocol::EventMsg;
use codex_core::protocol::Op;
use codex_protocol::artificial_messages::ArtificialMessage;
@@ -61,7 +59,10 @@ fn developer_texts(input: &[Value]) -> Vec<String> {
}
fn collab_xml(text: &str) -> String {
format!("{COLLABORATION_MODE_OPEN_TAG}{text}{COLLABORATION_MODE_CLOSE_TAG}")
ArtificialMessage::CollaborationMode {
body: text.to_string(),
}
.render()
}
fn count_exact(texts: &[String], target: &str) -> usize {

View File

@@ -1,13 +1,12 @@
use anyhow::Result;
use codex_core::config::Constrained;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::COLLABORATION_MODE_CLOSE_TAG;
use codex_core::protocol::COLLABORATION_MODE_OPEN_TAG;
use codex_core::protocol::EventMsg;
use codex_core::protocol::Op;
use codex_core::protocol::RolloutItem;
use codex_core::protocol::RolloutLine;
use codex_core::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
use codex_protocol::artificial_messages::ArtificialMessage;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
@@ -34,7 +33,10 @@ fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMod
}
fn collab_xml(text: &str) -> String {
format!("{COLLABORATION_MODE_OPEN_TAG}{text}{COLLABORATION_MODE_CLOSE_TAG}")
ArtificialMessage::CollaborationMode {
body: text.to_string(),
}
.render()
}
async fn read_rollout_text(path: &Path) -> anyhow::Result<String> {

View File

@@ -6,6 +6,7 @@ use codex_core::protocol::AskForApproval;
use codex_core::protocol::EventMsg;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_protocol::artificial_messages::ArtificialMessage;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo;
@@ -46,6 +47,13 @@ fn sse_completed(id: &str) -> String {
sse(vec![ev_response_created(id), ev_completed(id)])
}
fn is_personality_spec_message(text: &str) -> bool {
matches!(
ArtificialMessage::parse(text),
Ok(ArtificialMessage::PersonalitySpec { .. })
)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn personality_does_not_mutate_base_instructions_without_template() {
let codex_home = TempDir::new().expect("create temp dir");
@@ -116,7 +124,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res
assert!(
!developer_texts
.iter()
.any(|text| text.contains("<personality_spec>")),
.any(|text| is_personality_spec_message(text)),
"did not expect a personality update message when personality is None"
);
@@ -169,7 +177,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result<
let developer_texts = request.message_input_texts("developer");
for text in developer_texts {
assert!(
!text.contains("<personality_spec>"),
!is_personality_spec_message(&text),
"expected no personality update message in developer input"
);
}
@@ -258,7 +266,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
let developer_texts = request.message_input_texts("developer");
let personality_text = developer_texts
.iter()
.find(|text| text.contains("<personality_spec>"))
.find(|text| is_personality_spec_message(text))
.expect("expected personality update message in developer input");
assert!(
@@ -355,7 +363,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
let developer_texts = request.message_input_texts("developer");
let personality_text = developer_texts
.iter()
.find(|text| text.contains("<personality_spec>"));
.find(|text| is_personality_spec_message(text));
assert!(
personality_text.is_none(),
"expected no personality preamble for unchanged personality, got {personality_text:?}"
@@ -461,7 +469,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
let developer_texts = request.message_input_texts("developer");
let personality_text = developer_texts
.iter()
.find(|text| text.contains("<personality_spec>"));
.find(|text| is_personality_spec_message(text));
assert!(
personality_text.is_none(),
"expected no personality preamble, got {personality_text:?}"
@@ -835,7 +843,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
let developer_texts = request.message_input_texts("developer");
let personality_text = developer_texts
.iter()
.find(|text| text.contains("<personality_spec>"))
.find(|text| is_personality_spec_message(text))
.expect("expected personality update message in developer input");
assert!(

View File

@@ -7,6 +7,7 @@ use codex_core::protocol::ExecOutputStream;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::TurnAbortReason;
use codex_protocol::artificial_messages::ArtificialMessage;
use core_test_support::assert_regex_match;
use core_test_support::responses;
use core_test_support::responses::ev_assistant_message;
@@ -187,12 +188,17 @@ async fn user_shell_command_history_is_persisted_and_shared_with_model() -> anyh
let command_message = request
.message_input_texts("user")
.into_iter()
.find(|text| text.contains("<user_shell_command>"))
.find(|text| {
matches!(
ArtificialMessage::parse(text),
Ok(ArtificialMessage::UserShellCommand { .. })
)
})
.expect("command message recorded in request");
let command_message = command_message.replace("\r\n", "\n");
let escaped_command = escape(&command);
let expected_pattern = format!(
r"(?m)\A<user_shell_command>\n<command>\n{escaped_command}\n</command>\n<result>\nExit code: 0\nDuration: [0-9]+(?:\.[0-9]+)? seconds\nOutput:\nnot-set\n</result>\n</user_shell_command>\z"
r"(?m)\A<user_shell_cmd><command>\n{escaped_command}\n</command>\n<result>\nExit code: 0\nDuration: [0-9]+(?:\.[0-9]+)? seconds\nOutput:\nnot-set\n</result></user_shell_cmd>\z"
);
assert_regex_match(&expected_pattern, &command_message);
@@ -244,7 +250,12 @@ async fn user_shell_command_output_is_truncated_in_history() -> anyhow::Result<(
let command_message = request
.message_input_texts("user")
.into_iter()
.find(|text| text.contains("<user_shell_command>"))
.find(|text| {
matches!(
ArtificialMessage::parse(text),
Ok(ArtificialMessage::UserShellCommand { .. })
)
})
.expect("command message recorded in request");
let command_message = command_message.replace("\r\n", "\n");
@@ -255,7 +266,7 @@ async fn user_shell_command_output_is_truncated_in_history() -> anyhow::Result<(
let escaped_command = escape(&command);
let escaped_truncated_body = escape(&truncated_body);
let expected_pattern = format!(
r"(?m)\A<user_shell_command>\n<command>\n{escaped_command}\n</command>\n<result>\nExit code: 0\nDuration: [0-9]+(?:\.[0-9]+)? seconds\nOutput:\n{escaped_truncated_body}\n</result>\n</user_shell_command>\z"
r"(?m)\A<user_shell_cmd><command>\n{escaped_command}\n</command>\n<result>\nExit code: 0\nDuration: [0-9]+(?:\.[0-9]+)? seconds\nOutput:\n{escaped_truncated_body}\n</result></user_shell_cmd>\z"
);
assert_regex_match(&expected_pattern, &command_message);

View File

@@ -3,7 +3,8 @@
//! The `artificial_messages!` macro invocation in this module defines:
//!
//! - The `ArtificialMessage` enum variants declared below (`Skill`, `ModelWarning`,
//! `Permission`) with their configured string fields.
//! `Permission`, `UserShellCommand`, `CollaborationMode`, `PersonalitySpec`,
//! `ExecPolicyAmendment`) with their configured string fields.
//! - `ArtificialMessage::tag(&self) -> &'static str`: returns the top-level XML tag for
//! the current variant (for example `skill`).
//! - `ArtificialMessage::role(&self) -> &'static str`: returns the role that should be
@@ -60,6 +61,34 @@ artificial_messages! {
fields: {
raw(body)
}
},
UserShellCommand {
tag: TAG_USER_SHELL_COMMAND,
role: "user",
fields: {
raw(body)
}
},
CollaborationMode {
tag: TAG_COLLABORATION_MODE,
role: "developer",
fields: {
raw(body)
}
},
PersonalitySpec {
tag: TAG_SPEC,
role: "developer",
fields: {
raw(body)
}
},
ExecPolicyAmendment {
tag: TAG_EXEC_POLICY_AMENDMENT,
role: "developer",
fields: {
raw(body)
}
}
}
@@ -148,4 +177,64 @@ mod tests {
ArtificialMessageParseError::UnknownTopLevelTag("unknown".to_string())
);
}
#[test]
fn render_and_parse_user_shell_command_round_trip() {
let message = ArtificialMessage::UserShellCommand {
body: "<command>echo hi</command>".to_string(),
};
let rendered = message.render();
assert_eq!(
rendered,
"<user_shell_cmd><command>echo hi</command></user_shell_cmd>"
);
let parsed = ArtificialMessage::parse(&rendered).expect("parse user shell command");
assert_eq!(parsed, message);
}
#[test]
fn render_and_parse_collaboration_mode_round_trip() {
let message = ArtificialMessage::CollaborationMode {
body: "plan first".to_string(),
};
let rendered = message.render();
assert_eq!(
rendered,
"<collaboration_mode>plan first</collaboration_mode>"
);
let parsed = ArtificialMessage::parse(&rendered).expect("parse collaboration mode");
assert_eq!(parsed, message);
}
#[test]
fn render_and_parse_personality_spec_round_trip() {
let message = ArtificialMessage::PersonalitySpec {
body: "be pragmatic".to_string(),
};
let rendered = message.render();
assert_eq!(
rendered,
"<personality_spec>be pragmatic</personality_spec>"
);
let parsed = ArtificialMessage::parse(&rendered).expect("parse personality spec");
assert_eq!(parsed, message);
}
#[test]
fn render_and_parse_exec_policy_amendment_round_trip() {
let message = ArtificialMessage::ExecPolicyAmendment {
body: "Approved command prefix saved:\n- `echo`".to_string(),
};
let rendered = message.render();
assert_eq!(
rendered,
"<exec_policy_amendment>Approved command prefix saved:\n- `echo`</exec_policy_amendment>"
);
let parsed = ArtificialMessage::parse(&rendered).expect("parse exec policy amendment");
assert_eq!(parsed, message);
}
}

View File

@@ -12,8 +12,6 @@ use crate::artificial_messages::ArtificialMessage;
use crate::config_types::CollaborationMode;
use crate::config_types::SandboxMode;
use crate::protocol::AskForApproval;
use crate::protocol::COLLABORATION_MODE_CLOSE_TAG;
use crate::protocol::COLLABORATION_MODE_OPEN_TAG;
use crate::protocol::NetworkAccess;
use crate::protocol::SandboxPolicy;
use crate::protocol::WritableRoot;
@@ -276,10 +274,10 @@ impl DeveloperInstructions {
}
pub fn personality_spec_message(spec: String) -> Self {
let message = format!(
"<personality_spec> The user has requested a new communication style. Future messages should adhere to the following personality: \n{spec} </personality_spec>"
let body = format!(
" The user has requested a new communication style. Future messages should adhere to the following personality: \n{spec} "
);
DeveloperInstructions::new(message)
DeveloperInstructions::new(ArtificialMessage::PersonalitySpec { body }.render())
}
pub fn from_policy(
@@ -323,9 +321,8 @@ impl DeveloperInstructions {
.as_ref()
.filter(|instructions| !instructions.is_empty())
.map(|instructions| {
DeveloperInstructions::new(format!(
"{COLLABORATION_MODE_OPEN_TAG}{instructions}{COLLABORATION_MODE_CLOSE_TAG}"
))
let body = instructions.to_string();
DeveloperInstructions::new(ArtificialMessage::CollaborationMode { body }.render())
})
}