feat: remove active knowledge support
This commit is contained in:
@@ -108,15 +108,6 @@ impl WorkerHandle {
|
||||
is_dir: c.is_dir,
|
||||
})
|
||||
.collect(),
|
||||
protocol::CompletionKind::Knowledge => self
|
||||
.shared_state
|
||||
.list_knowledge_completions(prefix)
|
||||
.into_iter()
|
||||
.map(|c| protocol::CompletionEntry {
|
||||
value: c.slug,
|
||||
is_dir: false,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,13 +322,6 @@ impl WorkerController {
|
||||
if let Some(fs_for_view) = fs_for_view {
|
||||
shared_state.set_fs_view(crate::fs_view::WorkerFsView::new(fs_for_view));
|
||||
}
|
||||
shared_state.set_knowledge(
|
||||
worker
|
||||
.knowledge_completions()
|
||||
.into_iter()
|
||||
.map(|slug| crate::shared_state::KnowledgeCandidate { slug })
|
||||
.collect(),
|
||||
);
|
||||
runtime_dir.write_manifest(&manifest_toml).await?;
|
||||
runtime_dir.write_status(&shared_state).await?;
|
||||
|
||||
@@ -752,7 +736,7 @@ where
|
||||
|
||||
// Memory tools require both explicit feature exposure and memory storage
|
||||
// configuration. This keeps resident-memory config separate from the
|
||||
// model-visible Memory*/Knowledge* tool surface.
|
||||
// model-visible Memory* tool surface.
|
||||
if feature_config.memory.enabled {
|
||||
let mem = memory_config.as_ref().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
@@ -775,8 +759,7 @@ where
|
||||
worker.register_tool(memory::tool::write_tool(layout.clone()));
|
||||
worker.register_tool(memory::tool::edit_tool(layout.clone()));
|
||||
worker.register_tool(memory::tool::delete_tool(layout.clone()));
|
||||
worker.register_tool(memory::tool::memory_query_tool(layout.clone(), query_cfg));
|
||||
worker.register_tool(memory::tool::knowledge_query_tool(layout, query_cfg));
|
||||
worker.register_tool(memory::tool::memory_query_tool(layout, query_cfg));
|
||||
}
|
||||
|
||||
// Worker-orchestration tools (SpawnWorker + the four comm tools) share
|
||||
|
||||
@@ -83,11 +83,6 @@ pub enum WorkerPrompt {
|
||||
/// AGENTS.md section when memory is enabled, summary injection is enabled,
|
||||
/// and `memory/summary.md` has a valid non-empty body.
|
||||
ResidentMemorySummarySection,
|
||||
/// Trailing `## Resident knowledge` section, appended after the
|
||||
/// resident memory summary when memory is enabled, Knowledge resident
|
||||
/// injection is enabled, and at least one `knowledge/*` record advertises
|
||||
/// `model_invokation: true`.
|
||||
ResidentKnowledgeSection,
|
||||
/// Trailing Worker orchestration guidance, appended when registered tools
|
||||
/// include Worker-management capabilities.
|
||||
WorkerOrchestrationGuidanceSection,
|
||||
@@ -110,7 +105,6 @@ impl WorkerPrompt {
|
||||
Self::WorkingBoundariesSection => "working_boundaries_section",
|
||||
Self::AgentsMdSection => "agents_md_section",
|
||||
Self::ResidentMemorySummarySection => "resident_memory_summary_section",
|
||||
Self::ResidentKnowledgeSection => "resident_knowledge_section",
|
||||
Self::WorkerOrchestrationGuidanceSection => "worker_orchestration_guidance_section",
|
||||
Self::TicketEventCompanionNotice => "ticket_event_companion_notice",
|
||||
Self::SpawnWorkerToolDescription => "spawn_worker_tool_description",
|
||||
@@ -130,7 +124,6 @@ impl WorkerPrompt {
|
||||
WorkerPrompt::WorkingBoundariesSection,
|
||||
WorkerPrompt::AgentsMdSection,
|
||||
WorkerPrompt::ResidentMemorySummarySection,
|
||||
WorkerPrompt::ResidentKnowledgeSection,
|
||||
WorkerPrompt::WorkerOrchestrationGuidanceSection,
|
||||
WorkerPrompt::TicketEventCompanionNotice,
|
||||
WorkerPrompt::SpawnWorkerToolDescription,
|
||||
@@ -146,7 +139,6 @@ impl WorkerPrompt {
|
||||
"working_boundaries_section",
|
||||
"agents_md_section",
|
||||
"resident_memory_summary_section",
|
||||
"resident_knowledge_section",
|
||||
"worker_orchestration_guidance_section",
|
||||
"ticket_event_companion_notice",
|
||||
"spawn_worker_tool_description",
|
||||
@@ -384,25 +376,6 @@ impl PromptCatalog {
|
||||
)
|
||||
}
|
||||
|
||||
/// Render `WorkerPrompt::ResidentKnowledgeSection` with `{{ entries }}`
|
||||
/// (a pre-formatted list block authored by the caller).
|
||||
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(WorkerPrompt::ResidentKnowledgeSection, Value::from(m))
|
||||
}
|
||||
|
||||
/// Render `WorkerPrompt::WorkerOrchestrationGuidanceSection` (no inputs).
|
||||
pub fn worker_orchestration_guidance_section(&self) -> Result<String, CatalogError> {
|
||||
self.render(
|
||||
@@ -554,7 +527,6 @@ mod tests {
|
||||
let extract = cat.memory_extract_system("Japanese").unwrap();
|
||||
let consolidate = cat.memory_consolidation_system("Japanese").unwrap();
|
||||
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"));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, SecondsFormat, Utc};
|
||||
use manifest::Scope;
|
||||
use memory::ResidentKnowledgeEntry;
|
||||
use minijinja::value::Value;
|
||||
use minijinja::{Environment, ErrorKind, UndefinedBehavior};
|
||||
use thiserror::Error;
|
||||
@@ -125,7 +124,6 @@ impl SystemPromptTemplate {
|
||||
ctx.scope,
|
||||
ctx.agents_md.as_deref(),
|
||||
ctx.resident_summary,
|
||||
ctx.resident_knowledge,
|
||||
ToolCapabilities::from_tool_names(&ctx.tool_names),
|
||||
)
|
||||
}
|
||||
@@ -159,11 +157,6 @@ pub struct SystemPromptContext<'a> {
|
||||
/// frontmatter stripped. `None` disables the resident summary section;
|
||||
/// empty strings are ignored by the trailing-section formatter.
|
||||
pub resident_summary: Option<&'a str>,
|
||||
/// Resident-injection candidates from `<workspace>/knowledge/*` whose
|
||||
/// frontmatter has `model_invokation: true`. `None` disables the
|
||||
/// section entirely (memory disabled, or a consolidation worker that opts
|
||||
/// out); `Some(&[])` also yields no section.
|
||||
pub resident_knowledge: Option<&'a [ResidentKnowledgeEntry]>,
|
||||
/// Catalog used to render the fixed trailing section headers.
|
||||
/// Passed by reference so callers do not give up ownership across
|
||||
/// the short-lived render borrow.
|
||||
@@ -208,7 +201,6 @@ impl<'a> SystemPromptContext<'a> {
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
struct ToolCapabilities {
|
||||
memory_query: bool,
|
||||
knowledge_query: bool,
|
||||
memory_read: bool,
|
||||
memory_write: bool,
|
||||
memory_edit: bool,
|
||||
@@ -227,7 +219,6 @@ impl ToolCapabilities {
|
||||
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,
|
||||
@@ -253,7 +244,7 @@ impl ToolCapabilities {
|
||||
}
|
||||
|
||||
fn memory_any(self) -> bool {
|
||||
self.memory_records() || self.knowledge_query
|
||||
self.memory_records()
|
||||
}
|
||||
|
||||
fn memory_mutation(self) -> bool {
|
||||
@@ -274,7 +265,6 @@ impl ToolCapabilities {
|
||||
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));
|
||||
@@ -297,7 +287,6 @@ fn append_trailing_section(
|
||||
scope: &Scope,
|
||||
agents_md: Option<&str>,
|
||||
resident_summary: Option<&str>,
|
||||
resident_knowledge: Option<&[ResidentKnowledgeEntry]>,
|
||||
tool_capabilities: ToolCapabilities,
|
||||
) -> Result<String, SystemPromptError> {
|
||||
let mut out = String::with_capacity(body.len() + 256);
|
||||
@@ -325,19 +314,6 @@ fn append_trailing_section(
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
if let Some(entries) = resident_knowledge {
|
||||
if !entries.is_empty() {
|
||||
out.push('\n');
|
||||
let formatted = format_resident_knowledge_entries(entries);
|
||||
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');
|
||||
}
|
||||
}
|
||||
if tool_capabilities.worker_management() {
|
||||
out.push('\n');
|
||||
let section = prompts.worker_orchestration_guidance_section()?;
|
||||
@@ -352,36 +328,6 @@ fn append_trailing_section(
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `- <slug>: <description>` per line. Description newlines are folded
|
||||
/// to spaces so a single entry stays on one row in the rendered prompt.
|
||||
fn format_resident_knowledge_entries(entries: &[ResidentKnowledgeEntry]) -> String {
|
||||
format_resident_entries(
|
||||
entries
|
||||
.iter()
|
||||
.map(|e| (e.slug.as_str(), e.description.as_str())),
|
||||
)
|
||||
}
|
||||
|
||||
fn format_resident_entries<'a>(entries: impl Iterator<Item = (&'a str, &'a str)>) -> String {
|
||||
let mut out = String::new();
|
||||
for (i, (slug, description)) in entries.enumerate() {
|
||||
if i > 0 {
|
||||
out.push('\n');
|
||||
}
|
||||
out.push_str("- ");
|
||||
out.push_str(slug);
|
||||
out.push_str(": ");
|
||||
for ch in description.chars() {
|
||||
if ch == '\n' || ch == '\r' {
|
||||
out.push(' ');
|
||||
} else {
|
||||
out.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Bridge used by [`Worker::ensure_system_prompt_materialized`] so tests
|
||||
/// can construct a synthetic context without going through a full Worker.
|
||||
#[doc(hidden)]
|
||||
@@ -426,7 +372,6 @@ mod tests {
|
||||
tool_names: tools,
|
||||
agents_md,
|
||||
resident_summary: None,
|
||||
resident_knowledge: None,
|
||||
prompts: test_prompts(),
|
||||
}
|
||||
}
|
||||
@@ -444,16 +389,11 @@ mod tests {
|
||||
tool_names: Vec::new(),
|
||||
agents_md: None,
|
||||
resident_summary: summary,
|
||||
resident_knowledge: None,
|
||||
prompts: test_prompts(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ctx_with_resident<'a>(
|
||||
cwd: &'a Path,
|
||||
scope: &'a Scope,
|
||||
resident: &'a [ResidentKnowledgeEntry],
|
||||
) -> SystemPromptContext<'a> {
|
||||
fn ctx_with_resident<'a>(cwd: &'a Path, scope: &'a Scope) -> SystemPromptContext<'a> {
|
||||
SystemPromptContext {
|
||||
now: fixed_now(),
|
||||
cwd: cwd.display().to_string().into(),
|
||||
@@ -462,7 +402,6 @@ mod tests {
|
||||
tool_names: Vec::new(),
|
||||
agents_md: None,
|
||||
resident_summary: None,
|
||||
resident_knowledge: Some(resident),
|
||||
prompts: test_prompts(),
|
||||
}
|
||||
}
|
||||
@@ -470,7 +409,6 @@ mod tests {
|
||||
fn memory_tool_names() -> Vec<String> {
|
||||
[
|
||||
"MemoryQuery",
|
||||
"KnowledgeQuery",
|
||||
"MemoryRead",
|
||||
"MemoryWrite",
|
||||
"MemoryEdit",
|
||||
@@ -522,8 +460,8 @@ mod tests {
|
||||
.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("small targeted `MemoryQuery` / `KnowledgeQuery`"));
|
||||
assert!(rendered.contains("### Memory"));
|
||||
assert!(rendered.contains("small targeted `MemoryQuery`"));
|
||||
assert!(rendered.contains("Strong lookup triggers include"));
|
||||
assert!(rendered.contains("MemoryRead(kind=summary)"));
|
||||
assert!(rendered.contains("Do not query memory every turn"));
|
||||
@@ -550,9 +488,8 @@ mod tests {
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert!(!rendered.contains("### Memory and knowledge"));
|
||||
assert!(!rendered.contains("### Memory"));
|
||||
assert!(!rendered.contains("MemoryQuery"));
|
||||
assert!(!rendered.contains("KnowledgeQuery"));
|
||||
assert!(!rendered.contains("MemoryRead"));
|
||||
assert!(!rendered.contains("MemoryWrite"));
|
||||
assert!(!rendered.contains("MemoryEdit"));
|
||||
@@ -576,10 +513,9 @@ mod tests {
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert!(rendered.contains("### Memory and knowledge"));
|
||||
assert!(rendered.contains("### Memory"));
|
||||
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"));
|
||||
@@ -838,75 +774,4 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(!rendered.contains("Resident memory summary"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_section_omits_resident_knowledge_when_none() {
|
||||
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 rendered = tmpl.render(&ctx(dir.path(), &scope, vec![], None)).unwrap();
|
||||
assert!(!rendered.contains("Resident knowledge"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_section_omits_resident_knowledge_when_empty_slice() {
|
||||
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 rendered = tmpl
|
||||
.render(&ctx_with_resident(dir.path(), &scope, &[]))
|
||||
.unwrap();
|
||||
assert!(!rendered.contains("Resident knowledge"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_section_renders_resident_knowledge_entries() {
|
||||
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 = vec![
|
||||
ResidentKnowledgeEntry {
|
||||
slug: "alpha".into(),
|
||||
description: "first record".into(),
|
||||
},
|
||||
ResidentKnowledgeEntry {
|
||||
slug: "beta".into(),
|
||||
description: "second record\nwith newline".into(),
|
||||
},
|
||||
];
|
||||
let rendered = tmpl
|
||||
.render(&ctx_with_resident(dir.path(), &scope, &entries))
|
||||
.unwrap();
|
||||
assert!(rendered.contains("## Resident knowledge"));
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@ use session_store::SegmentId;
|
||||
|
||||
use crate::fs_view::WorkerFsView;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct KnowledgeCandidate {
|
||||
pub slug: String,
|
||||
}
|
||||
|
||||
/// Shared state between WorkerController and runtime directory.
|
||||
///
|
||||
/// Controller updates this in-memory; RuntimeDir writes the status
|
||||
@@ -20,7 +15,7 @@ pub struct KnowledgeCandidate {
|
||||
/// IPC layer could answer `Method::GetHistory`. Those reads now go
|
||||
/// directly through the session-log sink (`Event::Snapshot` +
|
||||
/// live events), so this struct holds only status, identity,
|
||||
/// greeting, and completion lookup hubs.
|
||||
/// greeting, and filesystem completion lookup hubs.
|
||||
pub struct WorkerSharedState {
|
||||
pub worker_name: String,
|
||||
pub segment_id: SegmentId,
|
||||
@@ -34,7 +29,6 @@ pub struct WorkerSharedState {
|
||||
/// (only relevant for unit tests that build a `WorkerSharedState`
|
||||
/// directly without spinning up a controller).
|
||||
fs_view: OnceLock<WorkerFsView>,
|
||||
knowledge: OnceLock<Vec<KnowledgeCandidate>>,
|
||||
}
|
||||
|
||||
impl WorkerSharedState {
|
||||
@@ -51,7 +45,6 @@ impl WorkerSharedState {
|
||||
greeting,
|
||||
status: RwLock::new(WorkerStatus::Idle),
|
||||
fs_view: OnceLock::new(),
|
||||
knowledge: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,23 +60,6 @@ impl WorkerSharedState {
|
||||
self.fs_view.get()
|
||||
}
|
||||
|
||||
pub fn set_knowledge(&self, knowledge: Vec<KnowledgeCandidate>) {
|
||||
let _ = self.knowledge.set(knowledge);
|
||||
}
|
||||
|
||||
pub fn list_knowledge_completions(&self, prefix: &str) -> Vec<KnowledgeCandidate> {
|
||||
self.knowledge
|
||||
.get()
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter(|candidate| candidate.slug.starts_with(prefix))
|
||||
.cloned()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn set_status(&self, status: WorkerStatus) {
|
||||
if let Ok(mut s) = self.status.write() {
|
||||
*s = status;
|
||||
@@ -165,35 +141,4 @@ mod tests {
|
||||
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(parsed["state"], "running");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn knowledge_completions_empty_when_unset() {
|
||||
let state = test_state();
|
||||
assert!(state.list_knowledge_completions("").is_empty());
|
||||
assert!(state.list_knowledge_completions("foo").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn knowledge_completions_filter_by_prefix() {
|
||||
let state = test_state();
|
||||
state.set_knowledge(vec![
|
||||
KnowledgeCandidate {
|
||||
slug: "alpha".into(),
|
||||
},
|
||||
KnowledgeCandidate {
|
||||
slug: "alphabet".into(),
|
||||
},
|
||||
KnowledgeCandidate {
|
||||
slug: "beta".into(),
|
||||
},
|
||||
]);
|
||||
let all = state.list_knowledge_completions("");
|
||||
assert_eq!(all.len(), 3);
|
||||
let alpha = state.list_knowledge_completions("alpha");
|
||||
assert_eq!(
|
||||
alpha.iter().map(|c| c.slug.as_str()).collect::<Vec<_>>(),
|
||||
vec!["alpha", "alphabet"]
|
||||
);
|
||||
assert!(state.list_knowledge_completions("zzz").is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
+14
-253
@@ -502,16 +502,15 @@ pub struct Worker<C: LlmClient, St: Store> {
|
||||
/// [`Self::from_manifest`], or defaults to the builtin pack when a
|
||||
/// Worker is constructed through lower-level paths that have no loader.
|
||||
prompts: Arc<PromptCatalog>,
|
||||
/// Memory workspace layout used for Memory/Knowledge record operations.
|
||||
/// Memory workspace layout used for Memory record operations.
|
||||
memory_layout: Option<memory::WorkspaceLayout>,
|
||||
/// When true (default), the system-prompt assembler may append the
|
||||
/// workspace memory summary (`memory/summary.md`). Internal disposable
|
||||
/// workers disable this so resident memory exposure is opt-in per Worker.
|
||||
inject_resident_summary: bool,
|
||||
/// When true (default), the system-prompt assembler may append resident
|
||||
/// Knowledge descriptions. This is intentionally independent from
|
||||
/// resident context. This is intentionally independent from
|
||||
/// summary residency: each section has its own gate.
|
||||
inject_resident_knowledge: bool,
|
||||
/// extract (memory.extract) reentry guard. `true` while an extract
|
||||
/// worker is running; subsequent triggers are skipped per spec
|
||||
/// (`docs/plan/memory.md` §Extract 並走防止). `Arc<AtomicBool>` so
|
||||
@@ -617,7 +616,6 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Worker<C, St>
|
||||
runtime_ticket_role: None,
|
||||
prompts: self.prompts.clone(),
|
||||
inject_resident_summary: self.inject_resident_summary,
|
||||
inject_resident_knowledge: self.inject_resident_knowledge,
|
||||
extract_in_flight: self.extract_in_flight.clone(),
|
||||
consolidation_in_flight: self.consolidation_in_flight.clone(),
|
||||
extract_pointer: self.extract_pointer.clone(),
|
||||
@@ -806,7 +804,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
prompts,
|
||||
memory_layout: None,
|
||||
inject_resident_summary: true,
|
||||
inject_resident_knowledge: true,
|
||||
extract_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
consolidation_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
extract_pointer: Arc::new(Mutex::new(None)),
|
||||
@@ -833,11 +830,9 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
///
|
||||
/// Default `true`: normal Workers may expose each resident section according
|
||||
/// to its own gate and manifest settings. Internal disposable workers set
|
||||
/// this to `false` so summary and Knowledge residency are both
|
||||
/// suppressed while explicit tools remain available.
|
||||
pub fn set_resident_injection(&mut self, enabled: bool) {
|
||||
pub fn set_resident_memory_injection(&mut self, enabled: bool) {
|
||||
self.inject_resident_summary = enabled;
|
||||
self.inject_resident_knowledge = enabled;
|
||||
}
|
||||
|
||||
/// Toggle `memory/summary.md` resident injection in the system prompt.
|
||||
@@ -845,14 +840,8 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
self.inject_resident_summary = enabled;
|
||||
}
|
||||
|
||||
/// Toggle resident Knowledge injection in the system prompt.
|
||||
pub fn set_resident_knowledge_injection(&mut self, enabled: bool) {
|
||||
self.inject_resident_knowledge = enabled;
|
||||
}
|
||||
|
||||
/// Shared handle to the prompt catalog. Cheap to clone (`Arc`).
|
||||
pub fn prompts(&self) -> &Arc<PromptCatalog> {
|
||||
&self.prompts
|
||||
pub fn prompts(&self) -> Arc<PromptCatalog> {
|
||||
Arc::clone(&self.prompts)
|
||||
}
|
||||
|
||||
/// The current segment ID. Read lock-free from the shared session
|
||||
@@ -1450,9 +1439,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
let alerter = self.alerter.clone();
|
||||
let tool_names: Vec<String> = {
|
||||
let worker = self.engine.as_mut().expect("worker present");
|
||||
// Materialise any pending tool factories so the template sees the
|
||||
// full list of tool names. Redundant with the flush inside
|
||||
// `Engine::lock()`; safe because `flush_pending` is idempotent.
|
||||
worker.tool_server_handle().flush_pending();
|
||||
worker
|
||||
.tool_server_handle()
|
||||
@@ -1472,11 +1458,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Resident-injection collection. Each resident section has its own
|
||||
// gate so summary and Knowledge residency remain conceptually independent.
|
||||
// Internal workers can still opt out of both resident sections.
|
||||
// Owned values live for the duration of `render` below; the
|
||||
// context borrows from them.
|
||||
let memory_layout = self.memory_layout.as_ref();
|
||||
let inject_summary = self.inject_resident_summary
|
||||
&& memory_layout.is_some()
|
||||
@@ -1491,21 +1472,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let inject_resident_knowledge = self.inject_resident_knowledge && memory_layout.is_some();
|
||||
let resident: Vec<memory::ResidentKnowledgeEntry> = if inject_resident_knowledge {
|
||||
memory_layout
|
||||
.map(memory::collect_resident_knowledge)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let resident_slice: Option<&[memory::ResidentKnowledgeEntry]> = if inject_resident_knowledge
|
||||
{
|
||||
Some(&resident)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let resident_exposure_snapshots = self.resident_exposure_snapshots(&resident);
|
||||
let worker_language = worker_language(&self.manifest.engine);
|
||||
let scope_snapshot = self.scope.snapshot();
|
||||
let cwd_for_prompt = self
|
||||
@@ -1520,7 +1486,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
tool_names,
|
||||
agents_md: agents_md_read.and_then(|read| read.body),
|
||||
resident_summary: resident_summary.as_deref(),
|
||||
resident_knowledge: resident_slice,
|
||||
prompts: &self.prompts,
|
||||
};
|
||||
let rendered = template
|
||||
@@ -1530,7 +1495,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
.as_mut()
|
||||
.expect("worker present")
|
||||
.set_system_prompt(rendered);
|
||||
self.append_resident_exposure_event(resident_exposure_snapshots);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1700,11 +1664,10 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
})?;
|
||||
self.user_segments.push(input.clone());
|
||||
|
||||
// Resolve `@<path>` file refs and `#<slug>` Knowledge refs to system
|
||||
// messages stashed for the WorkerInterceptor to attach right after the
|
||||
// user message. Resolution failures are non-fatal alerts.
|
||||
let mut attachments = self.resolve_file_refs(&input);
|
||||
attachments.extend(self.resolve_knowledge_refs(&input));
|
||||
// Resolve `@<path>` file refs to system messages stashed for the
|
||||
// WorkerInterceptor to attach right after the user message. Resolution
|
||||
// failures are non-fatal alerts.
|
||||
let attachments = self.resolve_file_refs(&input);
|
||||
let flattened = self.flatten_segments(&input);
|
||||
if !attachments.is_empty() {
|
||||
*self
|
||||
@@ -1783,113 +1746,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
out
|
||||
}
|
||||
|
||||
fn resolve_knowledge_refs(&self, segments: &[Segment]) -> Vec<SystemItem> {
|
||||
let Some(layout) = self.memory_layout.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for seg in segments {
|
||||
let Segment::KnowledgeRef { slug } = seg else {
|
||||
continue;
|
||||
};
|
||||
let parsed = match memory::Slug::parse(slug.clone()) {
|
||||
Ok(slug) => slug,
|
||||
Err(e) => {
|
||||
self.alert(
|
||||
AlertLevel::Warn,
|
||||
AlertSource::Worker,
|
||||
format!("knowledge ref #{slug} has invalid slug: {e}"),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let path = layout.knowledge_path(&parsed);
|
||||
let bytes = match std::fs::read(&path) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
self.alert(
|
||||
AlertLevel::Warn,
|
||||
AlertSource::Worker,
|
||||
format!("knowledge ref #{slug} could not be read: {e}"),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let raw = String::from_utf8_lossy(&bytes).into_owned();
|
||||
let body_text = match memory::schema::split_frontmatter(&raw) {
|
||||
Ok((_yaml, body)) => body,
|
||||
Err(e) => {
|
||||
self.alert(
|
||||
AlertLevel::Warn,
|
||||
AlertSource::Worker,
|
||||
format!("knowledge ref #{slug} has invalid frontmatter: {e}"),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let snapshot = memory::snapshot_record_from_bytes(
|
||||
memory::workspace::RecordKind::Knowledge,
|
||||
slug.clone(),
|
||||
&bytes,
|
||||
);
|
||||
self.append_memory_use_event(memory::UsageSource::KnowledgeRef, vec![snapshot]);
|
||||
let body = format!("[Knowledge #{}]\n{}", slug, body_text.trim_end());
|
||||
out.push(SystemItem::Knowledge {
|
||||
slug: slug.clone(),
|
||||
body,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn resident_exposure_snapshots(
|
||||
&self,
|
||||
knowledge: &[memory::ResidentKnowledgeEntry],
|
||||
) -> Vec<memory::UsageRecordSnapshot> {
|
||||
let Some(layout) = self.memory_layout.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut snapshots = Vec::new();
|
||||
for entry in knowledge {
|
||||
match memory::snapshot_record_from_layout(
|
||||
layout,
|
||||
memory::workspace::RecordKind::Knowledge,
|
||||
&entry.slug,
|
||||
) {
|
||||
Ok(snapshot) => snapshots.push(snapshot),
|
||||
Err(err) => {
|
||||
warn!(knowledge = %entry.slug, error = %err, "failed to snapshot resident knowledge exposure")
|
||||
}
|
||||
}
|
||||
}
|
||||
snapshots
|
||||
}
|
||||
|
||||
fn append_memory_use_event(
|
||||
&self,
|
||||
source: memory::UsageSource,
|
||||
records: Vec<memory::UsageRecordSnapshot>,
|
||||
) {
|
||||
let Some(layout) = self.memory_layout.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) =
|
||||
memory::append_use_event(layout, self.segment_id().to_string(), source, records)
|
||||
{
|
||||
warn!(error = %err, "failed to append memory usage event");
|
||||
}
|
||||
}
|
||||
|
||||
fn append_resident_exposure_event(&self, records: Vec<memory::UsageRecordSnapshot>) {
|
||||
let Some(layout) = self.memory_layout.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) =
|
||||
memory::append_resident_exposure_event(layout, self.segment_id().to_string(), records)
|
||||
{
|
||||
warn!(error = %err, "failed to append resident exposure event");
|
||||
}
|
||||
}
|
||||
/// Stage the post-interruption cleanup at the front of worker
|
||||
/// history: close every unanswered `Item::ToolCall` with a synthetic
|
||||
/// `Item::ToolResult` (Anthropic wire-validity), then append a
|
||||
@@ -1947,16 +1803,9 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn knowledge_completions(&self) -> Vec<String> {
|
||||
self.memory_layout
|
||||
.as_ref()
|
||||
.map(memory::list_knowledge_slugs)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Flatten a typed segment list into the single string the Engine
|
||||
/// receives as the user message, and emit user-facing alerts for
|
||||
/// segments that fall through to placeholder (Knowledge refs without a resolver, or unknown variants from a newer client).
|
||||
/// segments that fall through to placeholder (unknown variants from a newer client).
|
||||
/// `FileRef` is handled separately by `resolve_file_refs`. The text
|
||||
/// reconstruction itself comes from `Segment::flatten_to_text`,
|
||||
/// shared with replay paths that should not re-alert.
|
||||
@@ -1964,18 +1813,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
for seg in segments {
|
||||
match seg {
|
||||
Segment::Text { .. } | Segment::Paste { .. } | Segment::FileRef { .. } => {}
|
||||
Segment::KnowledgeRef { slug } => {
|
||||
if self.memory_layout.is_none() {
|
||||
self.alert(
|
||||
AlertLevel::Warn,
|
||||
AlertSource::Worker,
|
||||
format!(
|
||||
"knowledge ref #{slug} cannot be resolved \
|
||||
because memory is disabled; passed to LLM as placeholder"
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Segment::Unknown => {
|
||||
self.alert(
|
||||
AlertLevel::Warn,
|
||||
@@ -3678,8 +3515,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
// directly under the workspace via WorkspaceLayout. Resident section
|
||||
// injection is a Worker-level concern; this disposable Engine is built
|
||||
// without it by construction, in keeping with `docs/plan/memory.md`
|
||||
// §Consolidation のKnowledgeアクセス (agent pulls knowledge through
|
||||
// the search tool instead of via system-prompt residency).
|
||||
let query_cfg = memory::tool::QueryConfig::from(memory_cfg);
|
||||
worker.register_tool(memory::tool::read_tool_with_usage(
|
||||
layout.clone(),
|
||||
@@ -3689,10 +3524,6 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||
worker.register_tool(memory::tool::edit_tool(layout.clone()));
|
||||
worker.register_tool(memory::tool::delete_tool(layout.clone()));
|
||||
worker.register_tool(memory::tool::memory_query_tool(layout.clone(), query_cfg));
|
||||
worker.register_tool(memory::tool::knowledge_query_tool(
|
||||
layout.clone(),
|
||||
query_cfg,
|
||||
));
|
||||
|
||||
let tidy = consolidate::collect_tidy_hints(&layout);
|
||||
let usage_report = match memory::build_usage_report(&layout) {
|
||||
@@ -4020,7 +3851,6 @@ where
|
||||
prompts: common.prompts,
|
||||
memory_layout: common.memory_layout,
|
||||
inject_resident_summary: true,
|
||||
inject_resident_knowledge: true,
|
||||
extract_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
consolidation_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
extract_pointer: Arc::new(Mutex::new(None)),
|
||||
@@ -4127,7 +3957,6 @@ where
|
||||
prompts: common.prompts,
|
||||
memory_layout: common.memory_layout,
|
||||
inject_resident_summary: true,
|
||||
inject_resident_knowledge: true,
|
||||
extract_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
consolidation_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
extract_pointer: Arc::new(Mutex::new(None)),
|
||||
@@ -4360,7 +4189,6 @@ where
|
||||
prompts: common.prompts,
|
||||
memory_layout: common.memory_layout,
|
||||
inject_resident_summary: true,
|
||||
inject_resident_knowledge: true,
|
||||
extract_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
consolidation_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
extract_pointer: Arc::new(Mutex::new(extract_pointer)),
|
||||
@@ -4876,10 +4704,6 @@ fn preview_segments(segments: &[Segment]) -> String {
|
||||
preview.push('@');
|
||||
preview.push_str(path);
|
||||
}
|
||||
Segment::KnowledgeRef { slug } => {
|
||||
preview.push('#');
|
||||
preview.push_str(slug);
|
||||
}
|
||||
Segment::Unknown => preview.push_str("[unknown input segment]"),
|
||||
}
|
||||
}
|
||||
@@ -5972,15 +5796,11 @@ mod build_summary_prompt_tests {
|
||||
#[derive(Clone, Copy)]
|
||||
struct ResidentInjectionGates {
|
||||
summary: bool,
|
||||
knowledge: bool,
|
||||
}
|
||||
|
||||
impl ResidentInjectionGates {
|
||||
fn all(enabled: bool) -> Self {
|
||||
Self {
|
||||
summary: enabled,
|
||||
knowledge: enabled,
|
||||
}
|
||||
Self { summary: enabled }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6002,7 +5822,7 @@ mod build_summary_prompt_tests {
|
||||
summary_doc: Option<&str>,
|
||||
memory_config: Option<manifest::MemoryConfig>,
|
||||
gates: ResidentInjectionGates,
|
||||
include_knowledge: bool,
|
||||
_unused: bool,
|
||||
) -> String {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
|
||||
@@ -6012,14 +5832,6 @@ mod build_summary_prompt_tests {
|
||||
std::fs::create_dir_all(cwd.join(".yoi/memory")).unwrap();
|
||||
std::fs::write(cwd.join(".yoi/memory/summary.md"), doc).unwrap();
|
||||
}
|
||||
if include_knowledge {
|
||||
std::fs::create_dir_all(cwd.join(".yoi/knowledge")).unwrap();
|
||||
std::fs::write(
|
||||
cwd.join(".yoi/knowledge/resident-policy.md"),
|
||||
knowledge_doc("knowledge resident desc"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let mut manifest = minimal_manifest();
|
||||
manifest.memory = memory_config;
|
||||
let scope = Scope::writable(&cwd).unwrap();
|
||||
@@ -6039,12 +5851,7 @@ mod build_summary_prompt_tests {
|
||||
.memory
|
||||
.as_ref()
|
||||
.map(|mem| memory::WorkspaceLayout::resolve(mem, &cwd));
|
||||
if gates.summary == gates.knowledge {
|
||||
worker.set_resident_injection(gates.summary);
|
||||
} else {
|
||||
worker.set_resident_summary_injection(gates.summary);
|
||||
worker.set_resident_knowledge_injection(gates.knowledge);
|
||||
}
|
||||
worker.set_resident_memory_injection(gates.summary);
|
||||
let template = SystemPromptTemplate::parse(
|
||||
"$yoi/default",
|
||||
crate::prompt::loader::PromptLoader::builtins_only(),
|
||||
@@ -6059,12 +5866,6 @@ mod build_summary_prompt_tests {
|
||||
format!("---\nupdated_at: 2026-01-01T00:00:00Z\n---\n{body}")
|
||||
}
|
||||
|
||||
fn knowledge_doc(description: &str) -> String {
|
||||
format!(
|
||||
"---\ncreated_at: 2026-01-01T00:00:00Z\nupdated_at: 2026-01-01T00:00:00Z\nkind: policy\ndescription: \"{description}\"\nmodel_invokation: true\nuser_invocable: true\nlast_sources: []\n---\nbody\n",
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resident_summary_body_is_injected_without_frontmatter() {
|
||||
let rendered = render_system_prompt_with_summary(
|
||||
@@ -6129,53 +5930,13 @@ mod build_summary_prompt_tests {
|
||||
let prompt = render_system_prompt_with_resident_sections(
|
||||
Some(&summary_doc("resident summary marker")),
|
||||
Some(manifest::MemoryConfig::default()),
|
||||
ResidentInjectionGates {
|
||||
summary: false,
|
||||
knowledge: true,
|
||||
},
|
||||
ResidentInjectionGates { summary: false },
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(!prompt.contains("Resident memory summary"));
|
||||
assert!(!prompt.contains("resident summary marker"));
|
||||
assert!(prompt.contains("Resident knowledge"));
|
||||
assert!(prompt.contains("knowledge resident desc"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn knowledge_gate_false_keeps_resident_summary() {
|
||||
let prompt = render_system_prompt_with_resident_sections(
|
||||
Some(&summary_doc("resident summary marker")),
|
||||
Some(manifest::MemoryConfig::default()),
|
||||
ResidentInjectionGates {
|
||||
summary: true,
|
||||
knowledge: false,
|
||||
},
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(prompt.contains("Resident memory summary"));
|
||||
assert!(prompt.contains("resident summary marker"));
|
||||
assert!(!prompt.contains("Resident knowledge"));
|
||||
assert!(!prompt.contains("knowledge resident desc"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resident_injection_opt_out_omits_all_resident_sections() {
|
||||
let prompt = render_system_prompt_with_resident_sections(
|
||||
Some(&summary_doc("resident summary marker")),
|
||||
Some(manifest::MemoryConfig::default()),
|
||||
ResidentInjectionGates::all(false),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(!prompt.contains("Resident memory summary"));
|
||||
assert!(!prompt.contains("resident summary marker"));
|
||||
assert!(!prompt.contains("Resident knowledge"));
|
||||
assert!(!prompt.contains("knowledge resident desc"));
|
||||
}
|
||||
|
||||
fn minimal_manifest() -> WorkerManifest {
|
||||
|
||||
Reference in New Issue
Block a user