memory: gate prompt guidance
This commit is contained in:
parent
cff858ec23
commit
03256db913
|
|
@ -376,11 +376,21 @@ impl PromptCatalog {
|
||||||
|
|
||||||
/// Render `PodPrompt::ResidentKnowledgeSection` with `{{ entries }}`
|
/// Render `PodPrompt::ResidentKnowledgeSection` with `{{ entries }}`
|
||||||
/// (a pre-formatted list block authored by the caller).
|
/// (a pre-formatted list block authored by the caller).
|
||||||
pub fn resident_knowledge_section(&self, entries: &str) -> Result<String, CatalogError> {
|
pub fn resident_knowledge_section(
|
||||||
self.render(
|
&self,
|
||||||
PodPrompt::ResidentKnowledgeSection,
|
entries: &str,
|
||||||
single("entries", entries),
|
knowledge_query_available: bool,
|
||||||
)
|
memory_read_available: bool,
|
||||||
|
) -> Result<String, CatalogError> {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
let mut m: BTreeMap<&'static str, Value> = BTreeMap::new();
|
||||||
|
m.insert("entries", Value::from(entries));
|
||||||
|
m.insert(
|
||||||
|
"knowledge_query_available",
|
||||||
|
Value::from(knowledge_query_available),
|
||||||
|
);
|
||||||
|
m.insert("memory_read_available", Value::from(memory_read_available));
|
||||||
|
self.render(PodPrompt::ResidentKnowledgeSection, Value::from(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render `PodPrompt::ResidentWorkflowsSection` with `{{ entries }}`
|
/// Render `PodPrompt::ResidentWorkflowsSection` with `{{ entries }}`
|
||||||
|
|
@ -537,6 +547,7 @@ mod tests {
|
||||||
for rendered in [compact, extract, consolidate] {
|
for rendered in [compact, extract, consolidate] {
|
||||||
assert!(!rendered.contains("### Memory and knowledge"));
|
assert!(!rendered.contains("### Memory and knowledge"));
|
||||||
assert!(!rendered.contains("Do not query memory every turn"));
|
assert!(!rendered.contains("Do not query memory every turn"));
|
||||||
|
assert!(!rendered.contains("Strong lookup triggers include"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ impl SystemPromptTemplate {
|
||||||
let body = tmpl
|
let body = tmpl
|
||||||
.render(ctx.to_minijinja_value())
|
.render(ctx.to_minijinja_value())
|
||||||
.map_err(|e| SystemPromptError::Render(e.to_string()))?;
|
.map_err(|e| SystemPromptError::Render(e.to_string()))?;
|
||||||
append_trailing_section(
|
append_trailing_section_with_capabilities(
|
||||||
&body,
|
&body,
|
||||||
ctx.prompts,
|
ctx.prompts,
|
||||||
ctx.scope,
|
ctx.scope,
|
||||||
|
|
@ -125,6 +125,7 @@ impl SystemPromptTemplate {
|
||||||
ctx.resident_summary,
|
ctx.resident_summary,
|
||||||
ctx.resident_knowledge,
|
ctx.resident_knowledge,
|
||||||
ctx.resident_workflows,
|
ctx.resident_workflows,
|
||||||
|
ToolCapabilities::from_tool_names(&ctx.tool_names),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -199,10 +200,72 @@ impl<'a> SystemPromptContext<'a> {
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
root.insert(
|
||||||
|
"tool_capabilities".into(),
|
||||||
|
ToolCapabilities::from_tool_names(&self.tool_names).to_minijinja_value(),
|
||||||
|
);
|
||||||
Value::from(root)
|
Value::from(root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
struct ToolCapabilities {
|
||||||
|
memory_query: bool,
|
||||||
|
knowledge_query: bool,
|
||||||
|
memory_read: bool,
|
||||||
|
memory_write: bool,
|
||||||
|
memory_edit: bool,
|
||||||
|
memory_delete: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToolCapabilities {
|
||||||
|
fn from_tool_names(names: &[String]) -> Self {
|
||||||
|
let mut capabilities = Self::default();
|
||||||
|
for name in names {
|
||||||
|
match name.as_str() {
|
||||||
|
"MemoryQuery" => capabilities.memory_query = true,
|
||||||
|
"KnowledgeQuery" => capabilities.knowledge_query = true,
|
||||||
|
"MemoryRead" => capabilities.memory_read = true,
|
||||||
|
"MemoryWrite" => capabilities.memory_write = true,
|
||||||
|
"MemoryEdit" => capabilities.memory_edit = true,
|
||||||
|
"MemoryDelete" => capabilities.memory_delete = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
capabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_records(self) -> bool {
|
||||||
|
self.memory_query
|
||||||
|
|| self.memory_read
|
||||||
|
|| self.memory_write
|
||||||
|
|| self.memory_edit
|
||||||
|
|| self.memory_delete
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_any(self) -> bool {
|
||||||
|
self.memory_records() || self.knowledge_query
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_mutation(self) -> bool {
|
||||||
|
self.memory_write || self.memory_edit || self.memory_delete
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_minijinja_value(self) -> Value {
|
||||||
|
let mut map: BTreeMap<&'static str, Value> = BTreeMap::new();
|
||||||
|
map.insert("memory_any", Value::from(self.memory_any()));
|
||||||
|
map.insert("memory_records", Value::from(self.memory_records()));
|
||||||
|
map.insert("memory_query", Value::from(self.memory_query));
|
||||||
|
map.insert("knowledge_query", Value::from(self.knowledge_query));
|
||||||
|
map.insert("memory_read", Value::from(self.memory_read));
|
||||||
|
map.insert("memory_write", Value::from(self.memory_write));
|
||||||
|
map.insert("memory_edit", Value::from(self.memory_edit));
|
||||||
|
map.insert("memory_delete", Value::from(self.memory_delete));
|
||||||
|
map.insert("memory_mutation", Value::from(self.memory_mutation()));
|
||||||
|
Value::from(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the final system prompt by appending the fixed trailing
|
/// Build the final system prompt by appending the fixed trailing
|
||||||
/// section to `body`. The Rust side owns the layout (blank-line
|
/// section to `body`. The Rust side owns the layout (blank-line
|
||||||
/// separators, trailing-whitespace trim); each section's header + body
|
/// separators, trailing-whitespace trim); each section's header + body
|
||||||
|
|
@ -217,6 +280,28 @@ pub fn append_trailing_section(
|
||||||
resident_summary: Option<&str>,
|
resident_summary: Option<&str>,
|
||||||
resident_knowledge: Option<&[ResidentKnowledgeEntry]>,
|
resident_knowledge: Option<&[ResidentKnowledgeEntry]>,
|
||||||
resident_workflows: Option<&[ResidentWorkflowEntry]>,
|
resident_workflows: Option<&[ResidentWorkflowEntry]>,
|
||||||
|
) -> Result<String, SystemPromptError> {
|
||||||
|
append_trailing_section_with_capabilities(
|
||||||
|
body,
|
||||||
|
prompts,
|
||||||
|
scope,
|
||||||
|
agents_md,
|
||||||
|
resident_summary,
|
||||||
|
resident_knowledge,
|
||||||
|
resident_workflows,
|
||||||
|
ToolCapabilities::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_trailing_section_with_capabilities(
|
||||||
|
body: &str,
|
||||||
|
prompts: &PromptCatalog,
|
||||||
|
scope: &Scope,
|
||||||
|
agents_md: Option<&str>,
|
||||||
|
resident_summary: Option<&str>,
|
||||||
|
resident_knowledge: Option<&[ResidentKnowledgeEntry]>,
|
||||||
|
resident_workflows: Option<&[ResidentWorkflowEntry]>,
|
||||||
|
tool_capabilities: ToolCapabilities,
|
||||||
) -> Result<String, SystemPromptError> {
|
) -> Result<String, SystemPromptError> {
|
||||||
let mut out = String::with_capacity(body.len() + 256);
|
let mut out = String::with_capacity(body.len() + 256);
|
||||||
out.push_str(body);
|
out.push_str(body);
|
||||||
|
|
@ -247,7 +332,11 @@ pub fn append_trailing_section(
|
||||||
if !entries.is_empty() {
|
if !entries.is_empty() {
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
let formatted = format_resident_knowledge_entries(entries);
|
let formatted = format_resident_knowledge_entries(entries);
|
||||||
let section = prompts.resident_knowledge_section(&formatted)?;
|
let section = prompts.resident_knowledge_section(
|
||||||
|
&formatted,
|
||||||
|
tool_capabilities.knowledge_query,
|
||||||
|
tool_capabilities.memory_read,
|
||||||
|
)?;
|
||||||
out.push_str(section.trim_end_matches(&['\n', ' '][..]));
|
out.push_str(section.trim_end_matches(&['\n', ' '][..]));
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
}
|
}
|
||||||
|
|
@ -414,6 +503,20 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn memory_tool_names() -> Vec<String> {
|
||||||
|
[
|
||||||
|
"MemoryQuery",
|
||||||
|
"KnowledgeQuery",
|
||||||
|
"MemoryRead",
|
||||||
|
"MemoryWrite",
|
||||||
|
"MemoryEdit",
|
||||||
|
"MemoryDelete",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(String::from)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Lazily-initialised builtin catalog shared across system-prompt
|
/// Lazily-initialised builtin catalog shared across system-prompt
|
||||||
/// tests, so every `ctx()` can hand out a `&'static PromptCatalog`
|
/// tests, so every `ctx()` can hand out a `&'static PromptCatalog`
|
||||||
/// reference without forcing test bodies to create one per call.
|
/// reference without forcing test bodies to create one per call.
|
||||||
|
|
@ -438,13 +541,15 @@ mod tests {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
let scope = build_scope(dir.path());
|
let scope = build_scope(dir.path());
|
||||||
let rendered = tmpl
|
let rendered = tmpl
|
||||||
.render(&ctx(dir.path(), &scope, vec!["Read".into()], None))
|
.render(&ctx(dir.path(), &scope, memory_tool_names(), None))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Builtin default body must expose the tool and language policies.
|
// Builtin default body must expose the tool and language policies.
|
||||||
assert!(rendered.contains("### Memory and knowledge"));
|
assert!(rendered.contains("### Memory and knowledge"));
|
||||||
assert!(rendered.contains("MemoryQuery"));
|
assert!(rendered.contains("small targeted `MemoryQuery` / `KnowledgeQuery`"));
|
||||||
|
assert!(rendered.contains("Strong lookup triggers include"));
|
||||||
assert!(rendered.contains("MemoryRead(kind=summary)"));
|
assert!(rendered.contains("MemoryRead(kind=summary)"));
|
||||||
assert!(rendered.contains("Do not query memory every turn"));
|
assert!(rendered.contains("Do not query memory every turn"));
|
||||||
|
assert!(rendered.contains("MemoryWrite"));
|
||||||
assert!(rendered.contains("## Language"));
|
assert!(rendered.contains("## Language"));
|
||||||
assert!(rendered.contains("`language`: `match the user's language"));
|
assert!(rendered.contains("`language`: `match the user's language"));
|
||||||
// Trailing section must be present.
|
// Trailing section must be present.
|
||||||
|
|
@ -452,6 +557,56 @@ mod tests {
|
||||||
assert!(rendered.contains("Readable:"));
|
assert!(rendered.contains("Readable:"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instruction_default_omits_memory_guidance_without_memory_tools() {
|
||||||
|
let loader = PromptLoader::builtins_only();
|
||||||
|
let tmpl = SystemPromptTemplate::parse("$insomnia/default", loader).unwrap();
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let scope = build_scope(dir.path());
|
||||||
|
let rendered = tmpl
|
||||||
|
.render(&ctx(
|
||||||
|
dir.path(),
|
||||||
|
&scope,
|
||||||
|
vec!["Read".into(), "Edit".into()],
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!rendered.contains("### Memory and knowledge"));
|
||||||
|
assert!(!rendered.contains("MemoryQuery"));
|
||||||
|
assert!(!rendered.contains("KnowledgeQuery"));
|
||||||
|
assert!(!rendered.contains("MemoryRead"));
|
||||||
|
assert!(!rendered.contains("MemoryWrite"));
|
||||||
|
assert!(!rendered.contains("MemoryEdit"));
|
||||||
|
assert!(!rendered.contains("MemoryDelete"));
|
||||||
|
assert!(rendered.contains("## Language"));
|
||||||
|
assert!(rendered.contains("## Working boundaries"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn memory_guidance_names_only_available_memory_tools() {
|
||||||
|
let loader = PromptLoader::builtins_only();
|
||||||
|
let tmpl = SystemPromptTemplate::parse("$insomnia/default", loader).unwrap();
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let scope = build_scope(dir.path());
|
||||||
|
let rendered = tmpl
|
||||||
|
.render(&ctx(
|
||||||
|
dir.path(),
|
||||||
|
&scope,
|
||||||
|
vec!["MemoryQuery".into(), "MemoryRead".into()],
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(rendered.contains("### Memory and knowledge"));
|
||||||
|
assert!(rendered.contains("small targeted `MemoryQuery`"));
|
||||||
|
assert!(rendered.contains("MemoryRead(kind=summary)"));
|
||||||
|
assert!(!rendered.contains("KnowledgeQuery"));
|
||||||
|
assert!(!rendered.contains("MemoryWrite"));
|
||||||
|
assert!(!rendered.contains("MemoryEdit"));
|
||||||
|
assert!(!rendered.contains("MemoryDelete"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn instruction_prefix_addressing_user() {
|
fn instruction_prefix_addressing_user() {
|
||||||
let (_tmp, loader) = user_loader_with("greet.md", "HELLO from {{ cwd }}");
|
let (_tmp, loader) = user_loader_with("greet.md", "HELLO from {{ cwd }}");
|
||||||
|
|
@ -706,12 +861,32 @@ mod tests {
|
||||||
assert!(rendered.contains("- alpha: first record"));
|
assert!(rendered.contains("- alpha: first record"));
|
||||||
// Newline in description is folded to a space (one entry per line).
|
// Newline in description is folded to a space (one entry per line).
|
||||||
assert!(rendered.contains("- beta: second record with newline"));
|
assert!(rendered.contains("- beta: second record with newline"));
|
||||||
|
assert!(!rendered.contains("KnowledgeQuery"));
|
||||||
|
assert!(!rendered.contains("MemoryRead"));
|
||||||
// Resident section sits *after* the working-boundaries header.
|
// Resident section sits *after* the working-boundaries header.
|
||||||
let pos_boundaries = rendered.find("## Working boundaries").unwrap();
|
let pos_boundaries = rendered.find("## Working boundaries").unwrap();
|
||||||
let pos_resident = rendered.find("## Resident knowledge").unwrap();
|
let pos_resident = rendered.find("## Resident knowledge").unwrap();
|
||||||
assert!(pos_resident > pos_boundaries);
|
assert!(pos_resident > pos_boundaries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trailing_section_mentions_resident_knowledge_tools_when_available() {
|
||||||
|
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
||||||
|
let tmpl = SystemPromptTemplate::parse("$user/body", loader).unwrap();
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let scope = build_scope(dir.path());
|
||||||
|
let entries = [ResidentKnowledgeEntry {
|
||||||
|
slug: "alpha".into(),
|
||||||
|
description: "first record".into(),
|
||||||
|
}];
|
||||||
|
let mut context = ctx_with_resident(dir.path(), &scope, &entries);
|
||||||
|
context.tool_names = memory_tool_names();
|
||||||
|
let rendered = tmpl.render(&context).unwrap();
|
||||||
|
|
||||||
|
assert!(rendered.contains("## Resident knowledge"));
|
||||||
|
assert!(rendered.contains("KnowledgeQuery / MemoryRead"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn trailing_section_renders_resident_workflows() {
|
fn trailing_section_renders_resident_workflows() {
|
||||||
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,20 @@ When searching, use grep/glob primitives rather than shell pipelines.
|
||||||
|
|
||||||
You can run multiple tools simultaneously by calling them within a single response.
|
You can run multiple tools simultaneously by calling them within a single response.
|
||||||
It is recommended to run tools that handle asynchronous processing, such as queries and readings, in batches.
|
It is recommended to run tools that handle asynchronous processing, such as queries and readings, in batches.
|
||||||
|
{% if tool_capabilities.memory_any %}
|
||||||
|
|
||||||
### Memory and knowledge
|
### Memory and knowledge
|
||||||
|
|
||||||
For past decisions, prior requests, durable preferences, project history, or why something was done, use targeted lookup instead of guessing from vague recollection.
|
Use memory and knowledge proactively when the request may depend on prior project decisions, historical rationale, durable user preferences, recently completed tickets, or established workflow/policy conventions.
|
||||||
Use `MemoryQuery` for durable memory records (summary, decisions, requests), `KnowledgeQuery` for project knowledge, `MemoryRead(kind=summary)` for the full memory summary, and `MemoryRead` on returned slugs when excerpts are insufficient.
|
{% if tool_capabilities.memory_query and tool_capabilities.knowledge_query %}Prefer a small targeted `MemoryQuery` / `KnowledgeQuery` before relying on vague recollection.
|
||||||
Resident memory and knowledge are helpful context but may be stale; current user instructions, repository files, tickets, git history, and session logs are authoritative for exact current state.
|
{% elif tool_capabilities.memory_query %}Prefer a small targeted `MemoryQuery` before relying on vague recollection.
|
||||||
Do not query memory every turn, and normally prefer read/query tools; use `MemoryWrite`, `MemoryEdit`, or `MemoryDelete` only when explicitly asked or in a memory maintenance worker.
|
{% elif tool_capabilities.knowledge_query %}Prefer a small targeted `KnowledgeQuery` before relying on vague recollection.
|
||||||
|
{% endif %}
|
||||||
|
Strong lookup triggers include: the user says "recently", "previously", "that decision", "the ticket", "why", "policy", or "workflow"; you are about to make a design recommendation; you are reviewing, merging, closing, or rescoping a work item; or you are about to assert project history from memory.
|
||||||
|
{% if tool_capabilities.memory_read %}
|
||||||
|
Use `MemoryRead(kind=summary)` for the full memory summary, and `MemoryRead` on returned slugs when excerpts are insufficient.
|
||||||
|
{% endif %}
|
||||||
|
{% if tool_capabilities.memory_records and tool_capabilities.knowledge_query %}Resident memory and knowledge are{% elif tool_capabilities.knowledge_query %}Resident knowledge is{% else %}Resident memory is{% endif %} helpful context but may be stale; current user instructions, repository files, tickets, git history, and session logs are authoritative for exact current state.
|
||||||
|
Do not query memory every turn or mechanically. Skip memory lookup for purely local facts answered by current repository files, command output, or current user instructions.
|
||||||
|
{% if tool_capabilities.memory_mutation %}Normally prefer read/query tools; use available mutation tools ({% if tool_capabilities.memory_write %}`MemoryWrite`{% endif %}{% if tool_capabilities.memory_edit %}{% if tool_capabilities.memory_write %}, {% endif %}`MemoryEdit`{% endif %}{% if tool_capabilities.memory_delete %}{% if tool_capabilities.memory_write or tool_capabilities.memory_edit %}, {% endif %}`MemoryDelete`{% endif %}) only when explicitly asked or in a memory maintenance worker.
|
||||||
|
{% endif %}{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ resident_knowledge_section = """\
|
||||||
---
|
---
|
||||||
## Resident knowledge
|
## Resident knowledge
|
||||||
|
|
||||||
The following knowledge records are advertised resident. Use the KnowledgeQuery / MemoryRead tools to fetch the full body when relevant.
|
The following knowledge records are advertised resident.{% if knowledge_query_available and memory_read_available %} Use the KnowledgeQuery / MemoryRead tools to fetch the full body when relevant.{% elif knowledge_query_available %} Use KnowledgeQuery to search related knowledge records when relevant.{% elif memory_read_available %} Use MemoryRead on a known knowledge slug when the full body is required.{% endif %}
|
||||||
|
|
||||||
{{ entries }}\
|
{{ entries }}\
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user