merge: memory prompt guidance
This commit is contained in:
commit
bdb52b1ec7
|
|
@ -376,11 +376,21 @@ impl PromptCatalog {
|
|||
|
||||
/// Render `PodPrompt::ResidentKnowledgeSection` with `{{ entries }}`
|
||||
/// (a pre-formatted list block authored by the caller).
|
||||
pub fn resident_knowledge_section(&self, entries: &str) -> Result<String, CatalogError> {
|
||||
self.render(
|
||||
PodPrompt::ResidentKnowledgeSection,
|
||||
single("entries", entries),
|
||||
)
|
||||
pub fn resident_knowledge_section(
|
||||
&self,
|
||||
entries: &str,
|
||||
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 }}`
|
||||
|
|
@ -537,6 +547,7 @@ mod tests {
|
|||
for rendered in [compact, extract, consolidate] {
|
||||
assert!(!rendered.contains("### Memory and knowledge"));
|
||||
assert!(!rendered.contains("Do not query memory every turn"));
|
||||
assert!(!rendered.contains("Strong lookup triggers include"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ impl SystemPromptTemplate {
|
|||
ctx.resident_summary,
|
||||
ctx.resident_knowledge,
|
||||
ctx.resident_workflows,
|
||||
ToolCapabilities::from_tool_names(&ctx.tool_names),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -199,17 +200,79 @@ impl<'a> SystemPromptContext<'a> {
|
|||
.collect::<Vec<_>>(),
|
||||
),
|
||||
);
|
||||
root.insert(
|
||||
"tool_capabilities".into(),
|
||||
ToolCapabilities::from_tool_names(&self.tool_names).to_minijinja_value(),
|
||||
);
|
||||
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
|
||||
/// section to `body`. The Rust side owns the layout (blank-line
|
||||
/// separators, trailing-whitespace trim); each section's header + body
|
||||
/// comes from the prompt catalog (`PodPrompt::WorkingBoundariesSection`
|
||||
/// / `PodPrompt::AgentsMdSection`) so that wording can be overridden
|
||||
/// per-pack without touching this function.
|
||||
pub fn append_trailing_section(
|
||||
fn append_trailing_section(
|
||||
body: &str,
|
||||
prompts: &PromptCatalog,
|
||||
scope: &Scope,
|
||||
|
|
@ -217,6 +280,7 @@ pub fn append_trailing_section(
|
|||
resident_summary: Option<&str>,
|
||||
resident_knowledge: Option<&[ResidentKnowledgeEntry]>,
|
||||
resident_workflows: Option<&[ResidentWorkflowEntry]>,
|
||||
tool_capabilities: ToolCapabilities,
|
||||
) -> Result<String, SystemPromptError> {
|
||||
let mut out = String::with_capacity(body.len() + 256);
|
||||
out.push_str(body);
|
||||
|
|
@ -247,7 +311,11 @@ pub fn append_trailing_section(
|
|||
if !entries.is_empty() {
|
||||
out.push('\n');
|
||||
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('\n');
|
||||
}
|
||||
|
|
@ -414,6 +482,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
|
||||
/// tests, so every `ctx()` can hand out a `&'static PromptCatalog`
|
||||
/// reference without forcing test bodies to create one per call.
|
||||
|
|
@ -438,13 +520,15 @@ mod tests {
|
|||
let dir = TempDir::new().unwrap();
|
||||
let scope = build_scope(dir.path());
|
||||
let rendered = tmpl
|
||||
.render(&ctx(dir.path(), &scope, vec!["Read".into()], None))
|
||||
.render(&ctx(dir.path(), &scope, memory_tool_names(), None))
|
||||
.unwrap();
|
||||
// Builtin default body must expose the tool and language policies.
|
||||
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("Do not query memory every turn"));
|
||||
assert!(rendered.contains("MemoryWrite"));
|
||||
assert!(rendered.contains("## Language"));
|
||||
assert!(rendered.contains("`language`: `match the user's language"));
|
||||
// Trailing section must be present.
|
||||
|
|
@ -452,6 +536,56 @@ mod tests {
|
|||
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]
|
||||
fn instruction_prefix_addressing_user() {
|
||||
let (_tmp, loader) = user_loader_with("greet.md", "HELLO from {{ cwd }}");
|
||||
|
|
@ -706,12 +840,32 @@ mod tests {
|
|||
assert!(rendered.contains("- alpha: first record"));
|
||||
// Newline in description is folded to a space (one entry per line).
|
||||
assert!(rendered.contains("- beta: second record with newline"));
|
||||
assert!(!rendered.contains("KnowledgeQuery"));
|
||||
assert!(!rendered.contains("MemoryRead"));
|
||||
// Resident section sits *after* the working-boundaries header.
|
||||
let pos_boundaries = rendered.find("## Working boundaries").unwrap();
|
||||
let pos_resident = rendered.find("## Resident knowledge").unwrap();
|
||||
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]
|
||||
fn trailing_section_renders_resident_workflows() {
|
||||
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.
|
||||
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
|
||||
|
||||
For past decisions, prior requests, durable preferences, project history, or why something was done, use targeted lookup instead of guessing from vague recollection.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
{% if tool_capabilities.memory_records and tool_capabilities.knowledge_query %}Use memory and knowledge proactively{% elif tool_capabilities.memory_records %}Use memory proactively{% else %}Use knowledge proactively{% endif %} when the request may depend on prior project decisions, historical rationale, durable user preferences, recently completed tickets, or established workflow/policy conventions.
|
||||
{% if tool_capabilities.memory_query and tool_capabilities.knowledge_query %}Prefer a small targeted `MemoryQuery` / `KnowledgeQuery` before relying on vague recollection.
|
||||
{% elif tool_capabilities.memory_query %}Prefer a small targeted `MemoryQuery` before relying on vague recollection.
|
||||
{% 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
|
||||
|
||||
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 }}\
|
||||
"""
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user