plans/2025-04-02-system-context-rendering-v2.md
Modify the system context rendering mechanism to:
The current implementation renders the system context only once during agent initialization, which means time-sensitive information like current_time is not updated. Additionally, the system context cannot use dynamic variables from the conversation state.
Modify the TemplateService trait in crates/forge_domain/src/services.rs to include variables in the render_system method signature:
async fn render_system(
&self,
agent: &Agent,
prompt: &Template,
variables: &HashMap<String, Value>,
) -> anyhow::Result<String>;
Update the implementation in crates/forge_services/src/template.rs to handle variables in system context rendering:
async fn render_system(
&self,
_agent: &Agent,
prompt: &Template,
variables: &HashMap<String, Value>,
) -> anyhow::Result<String> {
let env = self.infra.environment_service().get_environment();
// Build the walker, only setting max_depth if a value was provided
let mut walker = Walker::max_all();
// Only set max_depth if the value is provided
// Create maximum depth for file walker, defaulting to 1 if not specified
walker = walker.max_depth(_agent.max_walker_depth.unwrap_or(1));
let mut files = walker
.cwd(env.cwd.clone())
.get()
.await?
.iter()
.map(|f| f.path.to_string())
.collect::<Vec<_>>();
// Sort the files alphabetically to ensure consistent ordering
files.sort();
// Get current date and time with timezone
let current_time = Local::now().format("%Y-%m-%d %H:%M:%S %:z").to_string();
// Create the context with README content for all agents
let ctx = SystemContext {
current_time,
env: Some(env),
tool_information: Some(self.tool_service.usage_prompt()),
tool_supported: _agent.tool_supported.unwrap_or_default(),
files,
readme: README_CONTENT.to_string(),
custom_rules: _agent.custom_rules.as_ref().cloned().unwrap_or_default(),
variables: variables.clone(), // Add the variables
};
// Render the template with the context
let result = self.hb.render_template(prompt.template.as_str(), &ctx)?;
Ok(result)
}
Modify the SystemContext struct in crates/forge_domain/src/system_context.rs to include variables:
#[derive(Debug, Setters, Clone, Serialize, Deserialize)]
#[setters(strip_option)]
pub struct SystemContext {
// Current date and time at the time of context creation
pub current_time: String,
// Environment information to be included in the system context
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<Environment>,
// Information about available tools that can be used by the agent
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_information: Option<String>,
/// Indicates whether the agent supports tools.
/// This value is populated directly from the Agent configuration.
#[serde(default)]
pub tool_supported: bool,
// List of file paths that are relevant for the agent context
#[serde(skip_serializing_if = "Vec::is_empty")]
pub files: Vec<String>,
// README content to provide project context to the agent
pub readme: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub custom_rules: String,
// Variables to pass to the system context
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub variables: HashMap<String, Value>,
}
// Update the Default implementation if it exists
impl Default for SystemContext {
fn default() -> Self {
Self {
current_time: String::new(),
env: None,
tool_information: None,
tool_supported: false,
files: Vec::new(),
readme: String::new(),
custom_rules: String::new(),
variables: HashMap::new(),
}
}
}
Modify the orchestrator's conversation loop in crates/forge_domain/src/orch.rs to re-render the system context on each iteration:
async fn init_agent(&self, agent_id: &AgentId, event: &Event) -> anyhow::Result<()> {
let conversation = self.get_conversation().await?;
debug!(
conversation_id = %conversation.id,
agent = %agent_id,
event = ?event,
"Initializing agent"
);
let agent = conversation.workflow.get_agent(agent_id)?;
let mut context = if agent.ephemeral.unwrap_or_default() {
self.init_agent_context(agent).await?
} else {
match conversation.context(&agent.id) {
Some(context) => context.clone(),
None => self.init_agent_context(agent).await?,
}
};
if let Some(temperature) = agent.temperature {
context = context.temperature(temperature);
}
let content = if let Some(user_prompt) = &agent.user_prompt {
// Get conversation variables from the conversation
let variables = &conversation.variables;
// Use the consolidated render_event method which handles suggestions and
// variables
self.services
.template_service()
.render_event(agent, user_prompt, event, variables)
.await?
} else {
// Use the raw event value as content if no user_prompt is provided
event.value.to_string()
};
if !content.is_empty() {
context = context.add_message(ContextMessage::user(content));
}
// Process attachments
let attachments = self
.services
.attachment_service()
.attachments(&event.value.to_string())
.await?;
for attachment in attachments.into_iter() {
match attachment.content_type {
ContentType::Image => {
context = context.add_message(ContextMessage::Image(attachment.content));
}
ContentType::Text => {
let content = format!(
"<file_content path=\"{}\">{}</file_content>",
attachment.path, attachment.content
);
context = context.add_message(ContextMessage::user(content));
}
}
}
self.set_context(&agent.id, context.clone()).await?;
loop {
// Get the latest conversation variables
let variables = self.conversation.read().await.variables.clone();
// Re-render system prompt if present
if let Some(system_prompt) = &agent.system_prompt {
let system_message = self
.services
.template_service()
.render_system(agent, system_prompt, &variables)
.await?;
context = context.set_first_system_message(system_message);
}
// Set context for the current loop iteration
self.set_context(&agent.id, context.clone()).await?;
let response = self
.services
.provider_service()
.chat(
agent
.model
.as_ref()
.ok_or(Error::MissingModel(agent.id.clone()))?,
context.clone(),
)
.await?;
let ChatCompletionResult { tool_calls, content } =
self.collect_messages(agent, response).await?;
// Get all tool results using the helper function
let tool_results = self.get_all_tool_results(agent, &tool_calls).await?;
context = context
.add_message(ContextMessage::assistant(content, Some(tool_calls)))
.add_tool_results(tool_results.clone());
// Check if context requires compression
context = self.compactor.compact_context(agent, context).await?;
self.set_context(&agent.id, context.clone()).await?;
self.sync_conversation().await?;
if tool_results.is_empty() {
break;
}
}
self.complete_turn(&agent.id).await?;
self.sync_conversation().await?;
Ok(())
}
Add tests for the updated render_system method with variables:
// In forge_services/src/template.rs or a test file
#[tokio::test]
async fn test_render_system_with_variables() {
// Create a test agent
let agent = Agent::new("test-agent")
.system_prompt(Template::from_string("{{current_time}} - {{variables.test_var}}"));
// Create test variables
let mut variables = HashMap::new();
variables.insert("test_var".to_string(), json!("test_value"));
// Render the system prompt with variables
let result = template_service
.render_system(&agent, agent.system_prompt.as_ref().unwrap(), &variables)
.await
.unwrap();
// Verify the result contains both the current time and the variable
assert!(result.contains("test_value"));
}
The implementation will be considered successful if: