prompt: close whitespace-control include validation gap

This commit is contained in:
2026-08-14 13:58:11 +09:00
parent a6f92104fa
commit a1f6a6bad5
7 changed files with 46 additions and 25 deletions
Generated
+1 -1
View File
@@ -4795,7 +4795,6 @@ dependencies = [
"fs4", "fs4",
"llm-engine", "llm-engine",
"manifest", "manifest",
"minijinja",
"protocol", "protocol",
"pulldown-cmark", "pulldown-cmark",
"ratatui", "ratatui",
@@ -4809,6 +4808,7 @@ dependencies = [
"toml", "toml",
"unicode-width", "unicode-width",
"uuid", "uuid",
"worker",
] ]
[[package]] [[package]]
+2
View File
@@ -1372,6 +1372,8 @@ fn parse_static_template_includes(template: &str, source: &str) -> Result<Vec<St
break; break;
}; };
let body = after_open[..close].trim(); let body = after_open[..close].trim();
let body = body.strip_prefix('-').unwrap_or(body).trim_start();
let body = body.strip_suffix('-').unwrap_or(body).trim_end();
if body.starts_with("include") { if body.starts_with("include") {
let argument = body["include".len()..].trim(); let argument = body["include".len()..].trim();
let bytes = argument.as_bytes(); let bytes = argument.as_bytes();
+1 -1
View File
@@ -25,7 +25,7 @@ session-store = { workspace = true }
fs4 = { workspace = true } fs4 = { workspace = true }
ticket = { workspace = true } ticket = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
minijinja = "2.19.0" worker = { path = "../worker" }
pulldown-cmark = { version = "0.13.3", default-features = false } pulldown-cmark = { version = "0.13.3", default-features = false }
llm-engine.workspace = true llm-engine.workspace = true
+4 -11
View File
@@ -69,8 +69,7 @@ use render::{PanelListRow, row_hit_boxes};
const MAX_ENTRIES: usize = 50; const MAX_ENTRIES: usize = 50;
const CLOSED_VISIBLE_ROWS: usize = 3; const CLOSED_VISIBLE_ROWS: usize = 3;
const ORCHESTRATOR_IDLE_QUEUE_NOTICE_TEMPLATE: &str = const ORCHESTRATOR_IDLE_QUEUE_NOTICE_PROMPT: &str = "panel.orchestrator_idle_queue_notice";
include_str!("../../../../resources/prompts/panel/orchestrator_idle_queue_notice.md");
const ORCHESTRATOR_QUEUE_ATTENTION_MAX_TICKETS: usize = 6; const ORCHESTRATOR_QUEUE_ATTENTION_MAX_TICKETS: usize = 6;
const ORCHESTRATOR_QUEUE_ATTENTION_MAX_TEXT_CHARS: usize = 120; const ORCHESTRATOR_QUEUE_ATTENTION_MAX_TEXT_CHARS: usize = 120;
const ORCHESTRATOR_QUEUE_ATTENTION_MAX_MESSAGE_CHARS: usize = 2_400; const ORCHESTRATOR_QUEUE_ATTENTION_MAX_MESSAGE_CHARS: usize = 2_400;
@@ -3791,15 +3790,9 @@ fn orchestrator_queue_template_ticket(
fn render_orchestrator_queue_attention_template( fn render_orchestrator_queue_attention_template(
context: &OrchestratorQueueTemplateContext, context: &OrchestratorQueueTemplateContext,
) -> Result<String, minijinja::Error> { ) -> Result<String, worker::CatalogError> {
let mut env = minijinja::Environment::new(); worker::PromptCatalog::builtins_only()?
env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict); .render_serializable(ORCHESTRATOR_IDLE_QUEUE_NOTICE_PROMPT, context)
env.add_template(
"orchestrator_idle_queue_notice",
ORCHESTRATOR_IDLE_QUEUE_NOTICE_TEMPLATE,
)?;
env.get_template("orchestrator_idle_queue_notice")?
.render(context)
} }
fn orchestrator_work_set_detail( fn orchestrator_work_set_detail(
+10 -2
View File
@@ -230,6 +230,14 @@ impl PromptCatalog {
self.projection.templates.contains_key(key) self.projection.templates.contains_key(key)
} }
pub fn render_serializable<T: Serialize + ?Sized>(
&self,
key: &str,
context: &T,
) -> Result<String, CatalogError> {
self.render_name(key, Value::from_serialize(context))
}
pub fn render_name(&self, key: &str, ctx: Value) -> Result<String, CatalogError> { pub fn render_name(&self, key: &str, ctx: Value) -> Result<String, CatalogError> {
let template = self let template = self
.env .env
@@ -538,7 +546,7 @@ mod tests {
#[test] #[test]
fn graph_rejects_dynamic_legacy_missing_and_cycles() { fn graph_rejects_dynamic_legacy_missing_and_cycles() {
let invalid = BTreeMap::from([ let invalid = BTreeMap::from([
("a".into(), "{% include target %}".into()), ("a".into(), "{%- include target -%}".into()),
("target".into(), "ok".into()), ("target".into(), "ok".into()),
]); ]);
assert!(validate_prompt_templates(&invalid).is_err()); assert!(validate_prompt_templates(&invalid).is_err());
@@ -546,7 +554,7 @@ mod tests {
let legacy = BTreeMap::from([("a".into(), "{% include \"legacy/default\" %}".into())]); let legacy = BTreeMap::from([("a".into(), "{% include \"legacy/default\" %}".into())]);
assert!(validate_prompt_templates(&legacy).is_err()); assert!(validate_prompt_templates(&legacy).is_err());
let missing = BTreeMap::from([("a".into(), "{% include \"missing\" %}".into())]); let missing = BTreeMap::from([("a".into(), "{%- include \"missing\" -%}".into())]);
assert!(validate_prompt_templates(&missing).is_err()); assert!(validate_prompt_templates(&missing).is_err());
let cycle = BTreeMap::from([ let cycle = BTreeMap::from([
@@ -130,8 +130,8 @@ mod tests {
.unwrap()]) .unwrap()])
.unwrap(); .unwrap();
for source in [ for source in [
r#"{ prompts = { common = { language = "{% include target %}"; }; }; }"#, r#"{ prompts = { common = { language = "{%- include target -%}"; }; }; }"#,
r#"{ prompts = { common = { language = "{% include \"missing\" %}"; }; }; }"#, r#"{ prompts = { common = { language = "{%- include \"missing\" -%}"; }; }; }"#,
r#"{ prompts = { common = { language = "{% include \"common.workspace\" %}"; workspace = "{% include \"common.language\" %}"; }; }; }"#, r#"{ prompts = { common = { language = "{% include \"common.workspace\" %}"; workspace = "{% include \"common.language\" %}"; }; }; }"#,
] { ] {
let snapshot = ConfigTreeSnapshot::from_entries( let snapshot = ConfigTreeSnapshot::from_entries(
+26 -8
View File
@@ -240,10 +240,7 @@ impl ServerConfig {
} }
const ORCHESTRATOR_ATTENTION_TICKET_LIMIT: usize = 20; const ORCHESTRATOR_ATTENTION_TICKET_LIMIT: usize = 20;
const ORCHESTRATOR_ATTENTION_PROMPT: &str = include_str!(concat!( const ORCHESTRATOR_ATTENTION_PROMPT_NAME: &str = "internal.workspace_orchestrator_queue_attention";
env!("CARGO_MANIFEST_DIR"),
"/../../resources/prompts/internal/workspace_orchestrator_queue_attention.md"
));
#[derive(Clone)] #[derive(Clone)]
pub struct WorkspaceApi { pub struct WorkspaceApi {
@@ -4811,10 +4808,31 @@ fn dispatch_orchestrator_queue_attention(api: &WorkspaceApi) {
} else { } else {
format!("Additional queued Tickets omitted from this notice: {omitted}\n") format!("Additional queued Tickets omitted from this notice: {omitted}\n")
}; };
let content = ORCHESTRATOR_ATTENTION_PROMPT let Ok(Some(config_state)) = api
.replace("{{omitted_line}}", &omitted_line) .config_store
.replace("{{workspace_id}}", &api.config.workspace_id) .load_workspace_config(&api.config.workspace_id)
.replace("{{ticket_lines}}", &shown); else {
return;
};
let Ok(projection) =
crate::prompt_settings::project_prompts_from_workspace_config(&config_state)
else {
return;
};
let Ok(catalog) = worker::PromptCatalog::from_projection(projection) else {
return;
};
let content = match catalog.render_serializable(
ORCHESTRATOR_ATTENTION_PROMPT_NAME,
&BTreeMap::from([
("omitted_line", omitted_line.as_str()),
("workspace_id", api.config.workspace_id.as_str()),
("ticket_lines", shown.as_str()),
]),
) {
Ok(content) => content,
Err(_) => return,
};
let accepted = api let accepted = api
.runtime .runtime
.send_input( .send_input(