use crate::{tools::ToolResult, tools::contract::tool_name};
use serde_json::Value;
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ContextInjectionDefinition {
pub(crate) name: &'static str,
pub(crate) tag: &'static str,
pub(crate) command: &'static str,
pub(crate) description: &'static str,
}
const DEFINITIONS: [ContextInjectionDefinition; 2] = [
ContextInjectionDefinition {
name: "tree",
tag: "#tree",
command: "tree",
description: "current directory tree",
},
ContextInjectionDefinition {
name: "git-status",
tag: "#git-status",
command: "git status",
description: "current Git status",
},
];
pub(crate) fn definitions() -> &'static [ContextInjectionDefinition] {
&DEFINITIONS
}
pub(crate) fn injection_tags_in_prompt(prompt: &str) -> Vec<ContextInjectionDefinition> {
let mut seen = HashSet::new();
let mut matches = Vec::new();
for (index, _) in prompt.match_indices('#') {
for definition in definitions() {
if standalone_tag_at(prompt, index, definition.tag) && seen.insert(definition.name) {
matches.push(*definition);
}
}
}
matches
}
pub(crate) fn expand_prompt_with_context_injections(
prompt: &str,
mut run: impl FnMut(&ContextInjectionDefinition) -> ToolResult,
) -> Option<String> {
let definitions = injection_tags_in_prompt(prompt);
if definitions.is_empty() {
return None;
}
let results = definitions
.iter()
.map(|definition| (*definition, run(definition)))
.collect::<Vec<_>>();
Some(render_effective_prompt(prompt, &results))
}
fn standalone_tag_at(prompt: &str, start: usize, tag: &str) -> bool {
let end = start + tag.len();
prompt
.as_bytes()
.get(start..end)
.is_some_and(|candidate| candidate == tag.as_bytes())
&& prompt[..start]
.bytes()
.next_back()
.is_none_or(|byte| !is_identifier_byte(byte))
&& prompt[end..]
.bytes()
.next()
.is_none_or(|byte| !is_identifier_byte(byte))
}
fn is_identifier_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-')
}
fn render_effective_prompt(
prompt: &str,
results: &[(ContextInjectionDefinition, ToolResult)],
) -> String {
let mut rendered = prompt.to_string();
rendered.push_str("\n\n<context_injections>");
for (definition, result) in results {
rendered.push('\n');
rendered.push_str(&format!(
"<context_injection name=\"{}\" tag=\"{}\" command=\"{}\" success=\"{}\">\n",
definition.name, definition.tag, definition.command, result.success
));
if result.tool_name != tool_name::BASH {
rendered.push_str("tool_error:\n");
rendered.push_str(&result.content);
rendered.push('\n');
} else {
rendered.push_str("stdout:\n");
rendered.push_str(metadata_string(&result.metadata, "stdout").unwrap_or_default());
rendered.push_str("\nstderr:\n");
rendered.push_str(metadata_string(&result.metadata, "stderr").unwrap_or_default());
rendered.push_str("\nstatus:\n");
rendered.push_str(&format!("success: {}\n", result.success));
rendered.push_str(&format!(
"exit_code: {}\n",
metadata_exit_code(&result.metadata).unwrap_or_else(|| "unknown".to_string())
));
rendered.push_str(&format!(
"timed_out: {}\n",
metadata_bool(&result.metadata, "timed_out")
.map(|value| value.to_string())
.unwrap_or_else(|| "unknown".to_string())
));
rendered.push_str(&format!(
"stdout_truncated: {}\n",
metadata_bool(&result.metadata, "stdout_truncated")
.map(|value| value.to_string())
.unwrap_or_else(|| "unknown".to_string())
));
rendered.push_str(&format!(
"stderr_truncated: {}\n",
metadata_bool(&result.metadata, "stderr_truncated")
.map(|value| value.to_string())
.unwrap_or_else(|| "unknown".to_string())
));
if !result.success && result.content != "stdout:\n\nstderr:\n" {
rendered.push_str("content:\n");
rendered.push_str(&result.content);
rendered.push('\n');
}
}
rendered.push_str("</context_injection>");
}
rendered.push_str("\n</context_injections>");
rendered
}
fn metadata_string<'a>(metadata: &'a Value, key: &str) -> Option<&'a str> {
metadata.get(key)?.as_str()
}
fn metadata_bool(metadata: &Value, key: &str) -> Option<bool> {
metadata.get(key)?.as_bool()
}
fn metadata_exit_code(metadata: &Value) -> Option<String> {
let value = metadata.get("exit_code")?;
if value.is_null() {
return Some("none".to_string());
}
value.as_i64().map(|code| code.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::ToolResultDisplay;
use serde_json::json;
fn result(success: bool, stdout: &str, stderr: &str, exit_code: Option<i32>) -> ToolResult {
ToolResult {
tool_name: tool_name::BASH.to_string(),
success,
content: format!("stdout:\n{stdout}\nstderr:\n{stderr}"),
metadata: json!({
"exit_code": exit_code,
"timed_out": false,
"stdout": stdout,
"stderr": stderr,
"stdout_truncated": false,
"stderr_truncated": false,
"stdout_limit_bytes": 65536,
"stderr_limit_bytes": 16384,
"cleanup_warning": null,
}),
display: ToolResultDisplay::default(),
}
}
#[test]
fn parser_recognizes_supported_standalone_tags_in_first_seen_order_once() {
let tags = injection_tags_in_prompt("use #git-status then (#tree), then #git-status again");
let names = tags.iter().map(|tag| tag.name).collect::<Vec<_>>();
assert_eq!(names, vec!["git-status", "tree"]);
}
#[test]
fn parser_ignores_unknown_bare_issue_heading_and_embedded_forms() {
let ignored = [
"#",
"#40",
"# Heading",
"abc#tree",
"#treehouse",
"#git-status-old",
"#unknown #vibes",
];
for prompt in ignored {
assert!(
injection_tags_in_prompt(prompt).is_empty(),
"unexpected tag in {prompt:?}"
);
}
}
#[test]
fn parser_allows_supported_tags_adjacent_to_punctuation() {
let tags = injection_tags_in_prompt("see #tree, then (#git-status).");
let names = tags.iter().map(|tag| tag.name).collect::<Vec<_>>();
assert_eq!(names, vec!["tree", "git-status"]);
}
#[test]
fn no_supported_tags_returns_none_without_running_commands() {
let expanded = expand_prompt_with_context_injections("plain prompt #40", |_| {
panic!("runner should not execute")
});
assert_eq!(expanded, None);
}
#[test]
fn expansion_renders_command_context_blocks() {
let expanded = expand_prompt_with_context_injections("inspect #tree", |definition| {
assert_eq!(definition.command, "tree");
result(true, "file.rs\n", "", Some(0))
})
.unwrap();
assert!(expanded.starts_with("inspect #tree\n\n<context_injections>"));
assert!(expanded.contains(
"<context_injection name=\"tree\" tag=\"#tree\" command=\"tree\" success=\"true\">"
));
assert!(expanded.contains("stdout:\nfile.rs\n\nstderr:\n"));
assert!(expanded.contains("exit_code: 0"));
}
#[test]
fn expansion_renders_failure_status_and_content() {
let expanded = expand_prompt_with_context_injections("inspect #git-status", |_| {
result(false, "", "fatal: not a git repository\n", Some(128))
})
.unwrap();
assert!(expanded.contains("success=\"false\""));
assert!(expanded.contains("stderr:\nfatal: not a git repository\n"));
assert!(expanded.contains("exit_code: 128"));
assert!(expanded.contains("content:\nstdout:\n\nstderr:\nfatal: not a git repository\n"));
}
}