diff --git a/crates/manifest/src/defaults.rs b/crates/manifest/src/defaults.rs index 05b0b33a..eb703dbc 100644 --- a/crates/manifest/src/defaults.rs +++ b/crates/manifest/src/defaults.rs @@ -98,5 +98,5 @@ pub const COMPACT_DEFAULT_REFERENCE_COUNT: usize = 5; pub const MEMORY_EXTRACT_WORKER_MAX_TURNS: Option = Some(8); /// Default language used by memory extraction / consolidation workers for -/// durable memory and knowledge text. See [`crate::MemoryConfig::language`]. +/// durable memory text. See [`crate::MemoryConfig::language`]. pub const MEMORY_LANGUAGE: &str = "English"; diff --git a/crates/manifest/src/lib.rs b/crates/manifest/src/lib.rs index 5470f239..4c9989a1 100644 --- a/crates/manifest/src/lib.rs +++ b/crates/manifest/src/lib.rs @@ -379,7 +379,7 @@ pub struct MemoryConfig { #[serde(default)] pub workspace_root: Option, /// Maximum number of records returned by `MemoryQuery` / - /// `KnowledgeQuery` per call. `None` ⇒ tool default (20). + /// `MemoryQuery` per call. `None` ⇒ tool default (20). #[serde(default)] pub query_result_limit: Option, /// Lines of context before and after each match in query excerpts. @@ -391,7 +391,7 @@ pub struct MemoryConfig { #[serde(default)] pub inject_summary: Option, /// Language used by memory extraction / consolidation workers for durable - /// memory and knowledge text. Free-form so workspaces can use names like + /// memory text. Free-form so workspaces can use names like /// `English`, `Japanese`, or locale tags. `None` ⇒ /// [`defaults::MEMORY_LANGUAGE`]. #[serde(default)] diff --git a/crates/memory/README.md b/crates/memory/README.md index 63062ffd..6e7c8182 100644 --- a/crates/memory/README.md +++ b/crates/memory/README.md @@ -2,13 +2,13 @@ ## Role -`memory` owns generated memory, Knowledge records, staging/consolidation mechanics, linting, and audit observations. +`memory` owns generated memory, records, staging/consolidation mechanics, linting, and audit observations. ## Boundaries Owns: -- memory/Knowledge record parsing and validation +- memory/record parsing and validation - memory lint subcommand backend behavior - staging and consolidation file mechanics - audit log observation writes diff --git a/crates/memory/src/audit.rs b/crates/memory/src/audit.rs index 3f104c59..21d35d25 100644 --- a/crates/memory/src/audit.rs +++ b/crates/memory/src/audit.rs @@ -321,7 +321,6 @@ pub fn snapshot_records(layout: &WorkspaceLayout) -> BTreeMap layout.decisions_dir(), RecordKind::Request => layout.requests_dir(), - RecordKind::Knowledge | RecordKind::Summary => return, + RecordKind::Summary => return, }; let entries = match std::fs::read_dir(&dir) { Ok(it) => it, @@ -135,13 +133,13 @@ fn push_kind_records(out: &mut String, layout: &WorkspaceLayout, kind: RecordKin fn render_usage_report(report: &UsageReport) -> String { if report.is_empty() { - return "(empty — no explicit memory/knowledge usage events recorded yet. \ + return "(empty — no explicit memory usage events recorded yet. \ Treat this as lack of evidence, not proof that records are unused.)\n" .to_string(); } let json = serde_json::to_string_pretty(report).unwrap_or_else(|_| "{}".to_string()); format!( - "This report is evidence only. Do not make hard Knowledge-creation or tidy-protection decisions from it alone.\n\n```json\n{json}\n```\n" + "This report is evidence only. Do not make hard tidy-protection decisions from it alone.\n\n```json\n{json}\n```\n" ) } @@ -276,7 +274,7 @@ mod tests { assert!(out.contains("Replaced decisions")); assert!(out.contains("Sources overflow")); assert!(out.contains("Similar slug clusters")); - assert!(out.contains("no explicit memory/knowledge usage events")); + assert!(out.contains("no explicit memory usage events")); } #[test] diff --git a/crates/memory/src/consolidate/mod.rs b/crates/memory/src/consolidate/mod.rs index 8fe950fc..3610c83e 100644 --- a/crates/memory/src/consolidate/mod.rs +++ b/crates/memory/src/consolidate/mod.rs @@ -1,19 +1,19 @@ //! consolidation: 統合 + 整理。 //! -//! extract が staging に残した活動ログを `memory/*` / `knowledge/*` に +//! extract が staging に残した活動ログを `memory/*` に //! 統合し、続けて既存 record を `outdated | superseded | unused | noisy` //! の観点で整理する disposable Engine を、Worker 側が組み立てるための //! ヘルパー群を提供する。Worker は次の手順で sub-Engine を構築する: //! //! - [`build_consolidate_input`] を sub-Engine の最初の user 入力に -//! - memory 専用 Tool (read / write / edit) と Knowledge / memory 検索ツールを登録 +//! - memory 専用 Tool (read / write / edit) と memory 検索ツールを登録 //! - [`StagingLock::acquire`] で並走防止 + consumed ID 確定 //! - sub-Engine run 完了後、[`StagingLock::release_with_cleanup`] で //! consumed ID 分の staging のみ削除し、占有ファイルを解放 //! //! system prompt は Worker の `PromptCatalog` //! (`WorkerPrompt::MemoryConsolidationSystem`) で管理される。Usage report は -//! 判断材料として渡すだけで、ここでは Knowledge 化や protection の hard decision はしない +//! 判断材料として渡すだけで、ここでは protection の hard decision はしない //! (`docs/plan/memory.md` §Consolidation / 整理材料)。 mod input; diff --git a/crates/memory/src/consolidate/tidy.rs b/crates/memory/src/consolidate/tidy.rs index 04645fcc..dab471c0 100644 --- a/crates/memory/src/consolidate/tidy.rs +++ b/crates/memory/src/consolidate/tidy.rs @@ -14,9 +14,7 @@ use std::collections::{BTreeMap, BTreeSet}; use crate::Slug; -use crate::schema::{ - DecisionFrontmatter, KnowledgeFrontmatter, RequestFrontmatter, split_frontmatter, -}; +use crate::schema::{DecisionFrontmatter, RequestFrontmatter, split_frontmatter}; use crate::workspace::{RecordKind, WorkspaceLayout}; /// `sources` overflow を flag する閾値。`linter::warnings::SOURCES_OVERFLOW_THRESHOLD` @@ -69,7 +67,6 @@ pub fn collect_tidy_hints(layout: &WorkspaceLayout) -> TidyHints { let decisions = read_kind_records(layout, RecordKind::Decision); let requests = read_kind_records(layout, RecordKind::Request); - let knowledge = read_kind_records(layout, RecordKind::Knowledge); for (slug, content) in &decisions { let fm = parse_yaml::(content); @@ -99,33 +96,18 @@ pub fn collect_tidy_hints(layout: &WorkspaceLayout) -> TidyHints { } } } - for (slug, content) in &knowledge { - if let Some(fm) = parse_yaml::(content) { - if fm.last_sources.len() > SOURCES_OVERFLOW_THRESHOLD { - hints.sources_overflow.push(SourcesOverflow { - kind: RecordKind::Knowledge, - slug: slug.clone(), - count: fm.last_sources.len(), - }); - } - } - } hints.sources_overflow.sort_by(|a, b| { (a.kind.as_str(), a.slug.as_str()).cmp(&(b.kind.as_str(), b.slug.as_str())) }); let decision_slugs: Vec<&str> = decisions.keys().map(|s| s.as_str()).collect(); let request_slugs: Vec<&str> = requests.keys().map(|s| s.as_str()).collect(); - let knowledge_slugs: Vec<&str> = knowledge.keys().map(|s| s.as_str()).collect(); if let Some(c) = cluster_similar(&decision_slugs, RecordKind::Decision) { hints.similar_slug_clusters.extend(c); } if let Some(c) = cluster_similar(&request_slugs, RecordKind::Request) { hints.similar_slug_clusters.extend(c); } - if let Some(c) = cluster_similar(&knowledge_slugs, RecordKind::Knowledge) { - hints.similar_slug_clusters.extend(c); - } hints .similar_slug_clusters .sort_by(|a, b| (a.kind.as_str(), &a.slugs).cmp(&(b.kind.as_str(), &b.slugs))); @@ -133,14 +115,12 @@ pub fn collect_tidy_hints(layout: &WorkspaceLayout) -> TidyHints { hints } -/// `/.yoi/memory//*.md` (Knowledge は -/// `/.yoi/knowledge/*.md`) を slug ごとに `(slug, full content)` +/// `/.yoi/memory//*.md` を slug ごとに `(slug, full content)` /// 化して返す。 fn read_kind_records(layout: &WorkspaceLayout, kind: RecordKind) -> BTreeMap { let dir = match kind { RecordKind::Decision => layout.decisions_dir(), RecordKind::Request => layout.requests_dir(), - RecordKind::Knowledge => layout.knowledge_dir(), RecordKind::Summary => return BTreeMap::new(), }; let mut out: BTreeMap = BTreeMap::new(); diff --git a/crates/memory/src/error.rs b/crates/memory/src/error.rs index 746d2231..735866e7 100644 --- a/crates/memory/src/error.rs +++ b/crates/memory/src/error.rs @@ -8,7 +8,7 @@ use thiserror::Error; /// Top-level error for memory operations that don't fit the lint flow. #[derive(Debug, Error)] pub enum MemoryError { - #[error("path is not under the memory or knowledge tree: {}", .0.display())] + #[error("path is not under the memory tree: {}", .0.display())] OutsideMemoryTree(PathBuf), #[error("path is not absolute: {}", .0.display())] RelativePath(PathBuf), @@ -56,11 +56,6 @@ pub enum LintError { #[error("Decisions `status` must be one of open|resolved|replaced (got `{0}`)")] InvalidStatus(String), - #[error( - "Knowledge with model_invokation: true cannot have description longer than {limit} chars (got {actual})" - )] - DescriptionTooLong { actual: usize, limit: usize }, - #[error("body exceeds the size limit for this record kind: {actual} chars > {limit}")] BodyTooLong { actual: usize, limit: usize }, diff --git a/crates/memory/src/lib.rs b/crates/memory/src/lib.rs index 7bf7d49d..e729980f 100644 --- a/crates/memory/src/lib.rs +++ b/crates/memory/src/lib.rs @@ -1,10 +1,9 @@ -//! Memory subsystem: persistence layer for `memory/*` and `knowledge/*` records. +//! Memory subsystem: persistence layer for `memory/*` records. //! //! Self-contained: provides its own Tool implementations (read/write/edit) -//! that target `/memory/` and `/knowledge/` only, -//! with a pre-write Linter built in. Generic CRUD tools (in the `tools` -//! crate) must not touch these directories — Worker is responsible for -//! denying them at the Scope level when memory is enabled. +//! that target `/memory/` only, with a pre-write Linter built in. +//! Generic CRUD tools (in the `tools` crate) must not touch this directory — +//! Worker is responsible for denying it at the Scope level when memory is enabled. pub mod audit; pub mod consolidate; @@ -22,10 +21,7 @@ pub use error::{LintError, LintWarning, MemoryError}; pub use extract::ExtractPointerPayload; pub use lint_common::{RecordLintError, Slug, is_valid_slug}; pub use linter::{LintReport, Linter}; -pub use resident::{ - ResidentKnowledgeEntry, collect_resident_knowledge, collect_resident_summary, - list_knowledge_slugs, -}; +pub use resident::collect_resident_summary; pub use scope::deny_write_rules; pub use usage::{ UsageEvent, UsageEventKind, UsageRecordSnapshot, UsageReport, UsageReportRecord, UsageSource, diff --git a/crates/memory/src/linter/existing.rs b/crates/memory/src/linter/existing.rs index 8ba10f26..ecab3736 100644 --- a/crates/memory/src/linter/existing.rs +++ b/crates/memory/src/linter/existing.rs @@ -1,4 +1,4 @@ -//! Walks `/memory/{decisions,requests}/` and `/knowledge/` to collect +//! Walks `/memory/{decisions,requests}/` to collect //! the slug set the linter needs for reference-integrity and //! same-slug-duplication checks. //! @@ -10,9 +10,7 @@ use std::io; use std::path::Path; use crate::Slug; -use crate::schema::{ - DecisionFrontmatter, KnowledgeFrontmatter, RequestFrontmatter, split_frontmatter, -}; +use crate::schema::{DecisionFrontmatter, RequestFrontmatter, split_frontmatter}; use crate::workspace::{RecordKind, WorkspaceLayout}; /// Snapshot of every record currently on disk under the workspace. @@ -25,7 +23,6 @@ use crate::workspace::{RecordKind, WorkspaceLayout}; pub struct ExistingRecords { decisions: HashMap, requests: HashSet, - knowledge: HashSet, } #[derive(Debug, Clone)] @@ -38,7 +35,6 @@ impl ExistingRecords { match kind { RecordKind::Decision => self.decisions.contains_key(slug), RecordKind::Request => self.requests.contains(slug), - RecordKind::Knowledge => self.knowledge.contains(slug), RecordKind::Summary => false, } } @@ -51,7 +47,6 @@ impl ExistingRecords { match kind { RecordKind::Decision => self.decisions.keys().collect(), RecordKind::Request => self.requests.iter().collect(), - RecordKind::Knowledge => self.knowledge.iter().collect(), RecordKind::Summary => Vec::new(), } } @@ -73,10 +68,6 @@ pub fn scan_existing(layout: &WorkspaceLayout) -> io::Result { let _ = parse_silent::(path); out.requests.insert(slug); })?; - scan_dir(&layout.knowledge_dir(), |path, slug| { - let _ = parse_silent::(path); - out.knowledge.insert(slug); - })?; Ok(out) } diff --git a/crates/memory/src/linter/frontmatter.rs b/crates/memory/src/linter/frontmatter.rs index bf0a3756..60bf912c 100644 --- a/crates/memory/src/linter/frontmatter.rs +++ b/crates/memory/src/linter/frontmatter.rs @@ -42,9 +42,6 @@ fn parse_missing_field(msg: &str) -> Option<&'static str> { "status", "kind", "description", - "model_invokation", - "user_invocable", - "last_sources", "requires", ]; FIELDS.iter().copied().find(|n| *n == field_name) diff --git a/crates/memory/src/linter/mod.rs b/crates/memory/src/linter/mod.rs index 5ed13f03..e823f1d6 100644 --- a/crates/memory/src/linter/mod.rs +++ b/crates/memory/src/linter/mod.rs @@ -7,7 +7,7 @@ //! collection back to the LLM as `ToolError::InvalidArgument`. //! //! Reference-integrity checks (`replaced_by` / `requires` existence, -//! cycle detection) walk the whole `memory/` and `knowledge/` trees +//! cycle detection) walk the whole `memory/` tree //! each call. No caching; the trees are expected to be small. mod existing; @@ -23,8 +23,7 @@ use serde::de::DeserializeOwned; use crate::error::{LintError, LintWarning}; use crate::schema::{ - DecisionFrontmatter, KnowledgeFrontmatter, RequestFrontmatter, SummaryFrontmatter, - split_frontmatter, + DecisionFrontmatter, RequestFrontmatter, SummaryFrontmatter, split_frontmatter, }; use crate::workspace::{ClassifiedPath, RecordKind, WorkspaceLayout}; @@ -134,9 +133,6 @@ impl Linter { RecordKind::Request => { self.check_request(content, &classified, &mut report); } - RecordKind::Knowledge => { - self.check_knowledge(content, &classified, &mut report); - } RecordKind::Summary => { self.check_kind::(content, &classified, &mut report); } @@ -207,30 +203,6 @@ impl Linter { warnings::check_warnings_with_sources(parsed.body, fm.sources.len(), report); } - - fn check_knowledge(&self, content: &str, cp: &ClassifiedPath, report: &mut LintReport) { - let parsed = match parse_frontmatter::(content) { - Ok(p) => p, - Err(e) => { - report.push_error(e); - return; - } - }; - let fm = parsed.frontmatter; - size::check_body::(parsed.body, report); - - if fm.model_invokation - && fm.description.chars().count() > crate::schema::KNOWLEDGE_DESCRIPTION_HARD_CAP - { - report.push_error(LintError::DescriptionTooLong { - actual: fm.description.chars().count(), - limit: crate::schema::KNOWLEDGE_DESCRIPTION_HARD_CAP, - }); - } - - warnings::check_warnings_with_sources(parsed.body, fm.last_sources.len(), report); - let _ = cp; - } } struct Parsed<'a, F> { @@ -357,37 +329,6 @@ mod tests { ))); } - #[test] - fn knowledge_long_description_with_model_invokation_errors() { - let (dir, linter) = workspace(); - let path = dir.path().join(".yoi/knowledge/foo.md"); - let big_desc = "x".repeat(2000); - let content = format!( - "---\ncreated_at: {now}\nupdated_at: {now}\nkind: rule\ndescription: {big_desc}\nmodel_invokation: true\nuser_invocable: true\nlast_sources: []\n---\nbody\n", - now = iso_now() - ); - let report = linter.lint(&path, &content, WriteMode::Create); - assert!( - report - .errors - .iter() - .any(|e| matches!(e, LintError::DescriptionTooLong { .. })) - ); - } - - #[test] - fn knowledge_long_description_without_model_invokation_ok() { - let (dir, linter) = workspace(); - let path = dir.path().join(".yoi/knowledge/foo.md"); - let big_desc = "x".repeat(2000); - let content = format!( - "---\ncreated_at: {now}\nupdated_at: {now}\nkind: rule\ndescription: {big_desc}\nmodel_invokation: false\nuser_invocable: true\nlast_sources: []\n---\nbody\n", - now = iso_now() - ); - let report = linter.lint(&path, &content, WriteMode::Create); - assert!(!report.has_errors(), "got errors: {:?}", report.errors); - } - #[test] fn summary_path_accepted() { let (dir, linter) = workspace(); diff --git a/crates/memory/src/linter/warnings.rs b/crates/memory/src/linter/warnings.rs index d8a57525..3ded46f8 100644 --- a/crates/memory/src/linter/warnings.rs +++ b/crates/memory/src/linter/warnings.rs @@ -26,7 +26,7 @@ pub fn check_warnings_kindless(_cp: &ClassifiedPath, body: &str, _report: &mut L // size:importance heuristic doesn't apply to a single rolling file. } -/// For kinds with `sources` (Decisions / Requests / Knowledge), consult +/// For kinds with `sources`, consult /// both the body length and the sources count. pub fn check_warnings_with_sources(body: &str, source_count: usize, report: &mut LintReport) { let chars = body.chars().count(); diff --git a/crates/memory/src/resident.rs b/crates/memory/src/resident.rs index 4fb533ca..70a25116 100644 --- a/crates/memory/src/resident.rs +++ b/crates/memory/src/resident.rs @@ -2,46 +2,17 @@ //! //! Surfaces used by the Worker system-prompt assembler: //! -//! - [`collect_resident_knowledge`] — resident-injection candidates -//! (`model_invokation: true`) returned as `(slug, description)` pairs. //! - [`collect_resident_summary`] — the body of //! `/.yoi/memory/summary.md` when it parses as a summary //! record and has non-empty body. -//! - [`list_knowledge_slugs`] — every slug whose file parses, regardless -//! of `model_invokation`. Used by the Worker IPC layer to answer TUI `#` -//! completion (`model_invokation` is a resident-injection flag, not a -//! user-visibility flag). //! //! Files that fail to read or parse are skipped silently — the Linter //! enforces shape on write, so a malformed file here means external //! tampering and we'd rather degrade than panic. -use crate::schema::{KnowledgeFrontmatter, SummaryFrontmatter, split_frontmatter}; +use crate::schema::{SummaryFrontmatter, split_frontmatter}; use crate::workspace::WorkspaceLayout; -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ResidentKnowledgeEntry { - pub slug: String, - pub description: String, -} - -/// Walk `/.yoi/knowledge/*.md` and return entries whose -/// frontmatter has `model_invokation: true`, sorted by slug. A missing -/// directory yields an empty vec. -pub fn collect_resident_knowledge(layout: &WorkspaceLayout) -> Vec { - let mut out: Vec = Vec::new(); - walk_knowledge(layout, |slug, fm| { - if fm.model_invokation { - out.push(ResidentKnowledgeEntry { - slug, - description: fm.description, - }); - } - }); - out.sort_by(|a, b| a.slug.cmp(&b.slug)); - out -} - /// Read `/.yoi/memory/summary.md` for resident prompt /// injection. Returns only the markdown body (frontmatter stripped), and /// degrades to `None` for missing, unreadable, malformed, or empty records. @@ -57,52 +28,6 @@ pub fn collect_resident_summary(layout: &WorkspaceLayout) -> Option { } } -/// Walk `/knowledge/*.md` and return every slug whose -/// frontmatter parses, sorted ascending. Does not filter on -/// `model_invokation`. A missing `knowledge/` directory yields an empty -/// vec. -pub fn list_knowledge_slugs(layout: &WorkspaceLayout) -> Vec { - let mut out: Vec = Vec::new(); - walk_knowledge(layout, |slug, _fm| out.push(slug)); - out.sort(); - out -} - -fn walk_knowledge(layout: &WorkspaceLayout, mut visit: impl FnMut(String, KnowledgeFrontmatter)) { - let dir = layout.knowledge_dir(); - let entries = match std::fs::read_dir(&dir) { - Ok(it) => it, - Err(_) => return, - }; - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_file() { - continue; - } - let name = match path.file_name().and_then(|n| n.to_str()) { - Some(n) => n, - None => continue, - }; - let slug = match name.strip_suffix(".md") { - Some(s) => s.to_string(), - None => continue, - }; - let raw = match std::fs::read_to_string(&path) { - Ok(s) => s, - Err(_) => continue, - }; - let (yaml, _body) = match split_frontmatter(&raw) { - Ok(v) => v, - Err(_) => continue, - }; - let fm: KnowledgeFrontmatter = match serde_yaml::from_str(yaml) { - Ok(f) => f, - Err(_) => continue, - }; - visit(slug, fm); - } -} - #[cfg(test)] mod tests { use super::*; @@ -120,25 +45,8 @@ mod tests { std::fs::write(path, content).unwrap(); } - fn write_knowledge( - dir: &Path, - slug: &str, - description: &str, - model_invokation: bool, - body: &str, - ) { - let path = dir.join(".yoi/knowledge").join(format!("{slug}.md")); - let content = format!( - "---\ncreated_at: {n}\nupdated_at: {n}\nkind: policy\ndescription: \"{description}\"\nmodel_invokation: {flag}\nuser_invocable: true\nlast_sources: []\n---\n{body}", - n = now(), - flag = model_invokation, - ); - std::fs::write(path, content).unwrap(); - } - fn setup() -> (TempDir, WorkspaceLayout) { let dir = TempDir::new().unwrap(); - std::fs::create_dir_all(dir.path().join(".yoi/knowledge")).unwrap(); std::fs::create_dir_all(dir.path().join(".yoi/memory")).unwrap(); let layout = WorkspaceLayout::new(dir.path().to_path_buf()); (dir, layout) @@ -180,98 +88,4 @@ mod tests { write_summary(dir.path(), " \n"); assert!(collect_resident_summary(&layout).is_none()); } - - #[test] - fn missing_knowledge_dir_returns_empty() { - let dir = TempDir::new().unwrap(); - // No knowledge/ directory at all. - let layout = WorkspaceLayout::new(dir.path().to_path_buf()); - assert!(collect_resident_knowledge(&layout).is_empty()); - } - - #[test] - fn picks_only_model_invokation_true() { - let (dir, layout) = setup(); - write_knowledge(dir.path(), "alpha", "alpha desc", true, "body\n"); - write_knowledge(dir.path(), "beta", "beta desc", false, "body\n"); - write_knowledge(dir.path(), "gamma", "gamma desc", true, "body\n"); - - let got = collect_resident_knowledge(&layout); - assert_eq!(got.len(), 2); - assert_eq!(got[0].slug, "alpha"); - assert_eq!(got[0].description, "alpha desc"); - assert_eq!(got[1].slug, "gamma"); - assert_eq!(got[1].description, "gamma desc"); - } - - #[test] - fn entries_are_sorted_by_slug() { - let (dir, layout) = setup(); - write_knowledge(dir.path(), "zeta", "z", true, ""); - write_knowledge(dir.path(), "alpha", "a", true, ""); - write_knowledge(dir.path(), "mu", "m", true, ""); - - let got = collect_resident_knowledge(&layout); - let slugs: Vec<&str> = got.iter().map(|e| e.slug.as_str()).collect(); - assert_eq!(slugs, vec!["alpha", "mu", "zeta"]); - } - - #[test] - fn malformed_frontmatter_is_skipped() { - let (dir, layout) = setup(); - write_knowledge(dir.path(), "good", "ok", true, ""); - // Garbage in frontmatter — must be skipped, not panic. - std::fs::write( - dir.path().join(".yoi/knowledge/bad.md"), - "---\nthis is not yaml: : :\n---\nbody\n", - ) - .unwrap(); - - let got = collect_resident_knowledge(&layout); - assert_eq!(got.len(), 1); - assert_eq!(got[0].slug, "good"); - } - - #[test] - fn non_md_files_ignored() { - let (dir, layout) = setup(); - write_knowledge(dir.path(), "good", "ok", true, ""); - std::fs::write(dir.path().join(".yoi/knowledge/note.txt"), "not markdown\n").unwrap(); - - let got = collect_resident_knowledge(&layout); - assert_eq!(got.len(), 1); - } - - #[test] - fn list_slugs_missing_dir_returns_empty() { - let dir = TempDir::new().unwrap(); - let layout = WorkspaceLayout::new(dir.path().to_path_buf()); - assert!(list_knowledge_slugs(&layout).is_empty()); - } - - #[test] - fn list_slugs_returns_all_regardless_of_model_invokation() { - let (dir, layout) = setup(); - write_knowledge(dir.path(), "alpha", "a", true, ""); - write_knowledge(dir.path(), "beta", "b", false, ""); - write_knowledge(dir.path(), "gamma", "g", true, ""); - - let got = list_knowledge_slugs(&layout); - assert_eq!(got, vec!["alpha", "beta", "gamma"]); - } - - #[test] - fn list_slugs_skips_malformed_and_non_md() { - let (dir, layout) = setup(); - write_knowledge(dir.path(), "good", "ok", true, ""); - std::fs::write( - dir.path().join(".yoi/knowledge/bad.md"), - "---\nthis is not yaml: : :\n---\nbody\n", - ) - .unwrap(); - std::fs::write(dir.path().join(".yoi/knowledge/note.txt"), "not markdown\n").unwrap(); - - let got = list_knowledge_slugs(&layout); - assert_eq!(got, vec!["good"]); - } } diff --git a/crates/memory/src/schema/knowledge.rs b/crates/memory/src/schema/knowledge.rs deleted file mode 100644 index 5e493f5f..00000000 --- a/crates/memory/src/schema/knowledge.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Knowledge frontmatter schema. - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -use crate::schema::common::{Frontmatter, SourceRef}; - -/// Hard cap on `description` length when `model_invokation: true`. -/// Mirrors the agent-skills 1024-char rule for description that lives -/// in resident system-prompt budget. -pub const KNOWLEDGE_DESCRIPTION_HARD_CAP: usize = 1024; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct KnowledgeFrontmatter { - pub created_at: DateTime, - pub updated_at: DateTime, - pub kind: String, - pub description: String, - pub model_invokation: bool, - pub user_invocable: bool, - pub last_sources: Vec, -} - -impl Frontmatter for KnowledgeFrontmatter { - const BODY_LIMIT: usize = 8000; - - fn created_at(&self) -> Option> { - Some(self.created_at) - } - fn updated_at(&self) -> Option> { - Some(self.updated_at) - } -} diff --git a/crates/memory/src/schema/mod.rs b/crates/memory/src/schema/mod.rs index 5f958229..25900fcc 100644 --- a/crates/memory/src/schema/mod.rs +++ b/crates/memory/src/schema/mod.rs @@ -7,12 +7,10 @@ mod common; mod decision; -mod knowledge; mod request; mod summary; pub use common::{Frontmatter, SourceRef, split_frontmatter}; pub use decision::{DecisionFrontmatter, DecisionStatus}; -pub use knowledge::{KNOWLEDGE_DESCRIPTION_HARD_CAP, KnowledgeFrontmatter}; pub use request::RequestFrontmatter; pub use summary::SummaryFrontmatter; diff --git a/crates/memory/src/scope.rs b/crates/memory/src/scope.rs index 0f8927d1..3860229d 100644 --- a/crates/memory/src/scope.rs +++ b/crates/memory/src/scope.rs @@ -13,14 +13,9 @@ use manifest::{Permission, ScopeRule}; use crate::workspace::WorkspaceLayout; -/// Build deny rules that strip Write permission from `/memory/` -/// and `/knowledge/`. Recursive — every descendant is capped at -/// Read for the generic tools. +/// Build a deny rule that strips Write permission from `/.yoi/memory/`. pub fn deny_write_rules(layout: &WorkspaceLayout) -> Vec { - vec![ - deny_write(layout.memory_dir().as_path()), - deny_write(layout.knowledge_dir().as_path()), - ] + vec![deny_write(layout.memory_dir().as_path())] } fn deny_write(target: &Path) -> ScopeRule { @@ -37,13 +32,12 @@ mod tests { use std::path::PathBuf; #[test] - fn deny_targets_memory_and_knowledge() { + fn deny_targets_memory() { let layout = WorkspaceLayout::new(PathBuf::from("/ws")); let rules = deny_write_rules(&layout); - assert_eq!(rules.len(), 2); + assert_eq!(rules.len(), 1); assert_eq!(rules[0].target, PathBuf::from("/ws/.yoi/memory")); assert_eq!(rules[0].permission, Permission::Write); assert!(rules[0].recursive); - assert_eq!(rules[1].target, PathBuf::from("/ws/.yoi/knowledge")); } } diff --git a/crates/memory/src/tool/delete.rs b/crates/memory/src/tool/delete.rs index 12364608..4263c4f0 100644 --- a/crates/memory/src/tool/delete.rs +++ b/crates/memory/src/tool/delete.rs @@ -1,4 +1,4 @@ -//! `MemoryDelete` tool for removing memory / knowledge records with audit logging. +//! `MemoryDelete` tool for removing memory records with audit logging. use std::sync::Arc; @@ -10,7 +10,7 @@ use crate::audit::{AuditStatus, RecordOperationAudit, append_record_operation, f use crate::tool::MemoryToolKind; use crate::workspace::WorkspaceLayout; -const DESCRIPTION: &str = "Delete an existing memory or knowledge record selected by `kind` + `slug`. \ +const DESCRIPTION: &str = "Delete an existing memory record selected by `kind` + `slug`. \ For `summary` omit `slug`; for the others `slug` is required. The delete is audited and cannot target \ workflow or staging/log files."; diff --git a/crates/memory/src/tool/edit.rs b/crates/memory/src/tool/edit.rs index 1b69d006..1b24824c 100644 --- a/crates/memory/src/tool/edit.rs +++ b/crates/memory/src/tool/edit.rs @@ -19,14 +19,14 @@ use crate::linter::{LintReport, Linter, WriteMode}; use crate::tool::MemoryToolKind; use crate::workspace::WorkspaceLayout; -const DESCRIPTION: &str = "Replace a substring in an existing memory or knowledge \ +const DESCRIPTION: &str = "Replace a substring in an existing memory \ record selected by `kind` + `slug`. By default `old_string` must be unique in the \ file; set `replace_all: true` to replace every occurrence. The resulting content \ is re-validated by the memory linter; failure leaves the file untouched."; #[derive(Debug, Deserialize, schemars::JsonSchema)] struct EditParams { - /// Record kind: `summary` | `decision` | `request` | `knowledge`. + /// Record kind: `summary` | `decision` | `request`. kind: MemoryToolKind, /// Slug. Required for everything except `summary`; forbidden for `summary`. #[serde(default)] diff --git a/crates/memory/src/tool/mod.rs b/crates/memory/src/tool/mod.rs index c661f1a4..8ba45c36 100644 --- a/crates/memory/src/tool/mod.rs +++ b/crates/memory/src/tool/mod.rs @@ -22,7 +22,7 @@ use crate::Slug; use crate::workspace::{RecordKind, WorkspaceLayout}; pub use edit::edit_tool; -pub use query::{QueryConfig, knowledge_query_tool, memory_query_tool}; +pub use query::{QueryConfig, memory_query_tool}; pub use read::{read_tool, read_tool_with_usage}; pub use write::write_tool; @@ -34,7 +34,6 @@ pub enum MemoryToolKind { Summary, Decision, Request, - Knowledge, } impl std::fmt::Display for MemoryToolKind { @@ -43,7 +42,6 @@ impl std::fmt::Display for MemoryToolKind { Self::Summary => "summary", Self::Decision => "decision", Self::Request => "request", - Self::Knowledge => "knowledge", }) } } @@ -54,7 +52,6 @@ impl MemoryToolKind { Self::Summary => "summary", Self::Decision => "decision", Self::Request => "request", - Self::Knowledge => "knowledge", } } @@ -63,7 +60,6 @@ impl MemoryToolKind { Self::Summary => RecordKind::Summary, Self::Decision => RecordKind::Decision, Self::Request => RecordKind::Request, - Self::Knowledge => RecordKind::Knowledge, } } @@ -92,7 +88,6 @@ impl MemoryToolKind { Ok(match other { Self::Decision => layout.decision_path(&parsed), Self::Request => layout.request_path(&parsed), - Self::Knowledge => layout.knowledge_path(&parsed), Self::Summary => unreachable!(), }) } diff --git a/crates/memory/src/tool/query.rs b/crates/memory/src/tool/query.rs index 9c7bb8d1..6fff7de2 100644 --- a/crates/memory/src/tool/query.rs +++ b/crates/memory/src/tool/query.rs @@ -1,6 +1,6 @@ -//! `MemoryQuery` / `KnowledgeQuery` tools. +//! `MemoryQuery` tool. //! -//! Both perform a case-insensitive substring scan over markdown record +//! Performs a case-insensitive substring scan over markdown record //! files. With a `query` set, returns `{slug, kind, ..., excerpt}` hits //! with `excerpt_lines` lines of context around each match. With `query` //! omitted, returns one entry per file (no excerpt) so the agent can @@ -10,8 +10,6 @@ //! requests/}`. `.yoi/memory/_staging/`, //! `.yoi/memory/_usage/`, and `.yoi/memory/_logs/` are excluded //! by construction. -//! - `KnowledgeQuery` walks `.yoi/knowledge/*.md` and supports a -//! `kind` filter against the Knowledge frontmatter's `kind` field. //! //! No derived index — the file tree is the source of truth and is //! re-scanned per call. 出現順: within a file by line order, across @@ -25,7 +23,6 @@ use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput}; use serde::{Deserialize, Serialize}; use crate::audit::{AuditStatus, RecordUsageAudit, append_record_usage}; -use crate::schema::{KnowledgeFrontmatter, split_frontmatter}; use crate::workspace::WorkspaceLayout; const DEFAULT_RESULT_LIMIT: usize = 20; @@ -38,14 +35,6 @@ when you don't yet know what's in there. Result count is capped (configurable vi manifest's `[memory]` section). Use the returned `slug` + `kind` with MemoryRead to fetch \ the full record. Workflow and staging directories are not visible."; -const KNOWLEDGE_QUERY_DESCRIPTION: &str = "Inspect knowledge records. With `query` set, \ -returns substring hits with line context; omit `query` to list every record (one entry \ -per file, no excerpt). Optional `kind` filters by the Knowledge frontmatter's `kind` \ -field; records whose frontmatter fails to parse are skipped when `kind` is given. Result \ -count is capped (configurable via the manifest's `[memory]` section). Returns \ -`{slug, kind, description, model_invokation, excerpt}` entries. Use the returned `slug` \ -with MemoryRead (kind=knowledge) for the full record."; - /// Tunables passed in from the manifest. #[derive(Debug, Clone, Copy)] pub struct QueryConfig { @@ -85,17 +74,6 @@ struct MemoryQueryParams { query: Option, } -#[derive(Debug, Deserialize, schemars::JsonSchema)] -struct KnowledgeQueryParams { - /// Optional substring filter. Case-insensitive. Omit to list every - /// knowledge record under the query scope. - #[serde(default)] - query: Option, - /// Optional filter on the Knowledge frontmatter's `kind` field. - #[serde(default)] - kind: Option, -} - #[derive(Debug, Serialize)] struct MemoryRecord { slug: String, @@ -104,26 +82,11 @@ struct MemoryRecord { excerpt: Option, } -#[derive(Debug, Serialize)] -struct KnowledgeRecord { - slug: String, - kind: Option, - description: Option, - model_invokation: Option, - #[serde(skip_serializing_if = "Option::is_none")] - excerpt: Option, -} - struct MemoryQueryTool { layout: WorkspaceLayout, config: QueryConfig, } -struct KnowledgeQueryTool { - layout: WorkspaceLayout, - config: QueryConfig, -} - #[async_trait] impl Tool for MemoryQueryTool { async fn execute( @@ -242,123 +205,6 @@ impl Tool for MemoryQueryTool { } } -#[async_trait] -impl Tool for KnowledgeQueryTool { - async fn execute( - &self, - input_json: &str, - _ctx: llm_engine::tool::ToolExecutionContext, - ) -> Result { - let params: KnowledgeQueryParams = serde_json::from_str(input_json).map_err(|e| { - ToolError::InvalidArgument(format!("invalid KnowledgeQuery input: {e}")) - })?; - let needle = match params.query.as_deref() { - Some(q) => match validate_query(q) { - Ok(q) => Some(q), - Err(err) => { - let _ = append_record_usage( - &self.layout, - RecordUsageAudit { - op: "query".to_string(), - status: AuditStatus::Failed, - kind: "knowledge".to_string(), - slug: None, - path: None, - query: params.query.clone(), - result_count: None, - reason: Some(err.to_string()), - }, - ); - return Err(err); - } - }, - None => None, - }; - let kind_filter = params.kind.as_deref(); - - let mut records: Vec = Vec::new(); - let limit = self.config.result_limit; - let ctx = self.config.excerpt_lines; - - for (path, slug) in list_md_files(&self.layout.knowledge_dir()) { - if records.len() >= limit { - break; - } - let raw = match std::fs::read_to_string(&path) { - Ok(s) => s, - Err(_) => continue, - }; - let fm = parse_knowledge_frontmatter(&raw); - - // kind filter applies to the frontmatter's kind field. - if let Some(filter) = kind_filter { - let matches = fm - .as_ref() - .map(|f| f.kind.as_str() == filter) - .unwrap_or(false); - if !matches { - continue; - } - } - - let kind = fm.as_ref().map(|f| f.kind.clone()); - let description = fm.as_ref().map(|f| f.description.clone()); - let model_invokation = fm.as_ref().map(|f| f.model_invokation); - - match needle.as_deref() { - Some(n) => { - scan_text(&raw, n, ctx, limit - records.len(), |excerpt| { - records.push(KnowledgeRecord { - slug: slug.clone(), - kind: kind.clone(), - description: description.clone(), - model_invokation, - excerpt: Some(excerpt), - }); - }); - } - None => { - records.push(KnowledgeRecord { - slug: slug.clone(), - kind, - description, - model_invokation, - excerpt: None, - }); - } - } - } - - let body = serde_json::to_string_pretty(&records) - .map_err(|e| ToolError::ExecutionFailed(format!("serialize records: {e}")))?; - let summary = match params.query.as_deref() { - Some(q) => format!("{} hit(s) for {q:?}", records.len()), - None => format!("{} record(s)", records.len()), - }; - let _ = append_record_usage( - &self.layout, - RecordUsageAudit { - op: "query".to_string(), - status: AuditStatus::Success, - kind: "knowledge".to_string(), - slug: None, - path: None, - query: params.query.clone(), - result_count: Some(records.len()), - reason: if records.len() >= limit { - Some("result_limit_reached".to_string()) - } else { - None - }, - }, - ); - Ok(ToolOutput { - summary, - content: Some(body), - }) - } -} - fn collect_memory_records( path: &Path, slug: &str, @@ -470,14 +316,6 @@ fn scan_text( } } -/// Best-effort frontmatter parse. Returns `None` if missing/malformed -/// — query still finds matches in the body even when the header is -/// broken. -fn parse_knowledge_frontmatter(raw: &str) -> Option { - let (yaml, _body) = split_frontmatter(raw).ok()?; - serde_yaml::from_str::(yaml).ok() -} - pub fn memory_query_tool(layout: WorkspaceLayout, config: QueryConfig) -> ToolDefinition { Arc::new(move || { let schema = schemars::schema_for!(MemoryQueryParams); @@ -493,21 +331,6 @@ pub fn memory_query_tool(layout: WorkspaceLayout, config: QueryConfig) -> ToolDe }) } -pub fn knowledge_query_tool(layout: WorkspaceLayout, config: QueryConfig) -> ToolDefinition { - Arc::new(move || { - let schema = schemars::schema_for!(KnowledgeQueryParams); - let schema_value = serde_json::to_value(schema).unwrap_or(serde_json::json!({})); - let meta = ToolMeta::new("KnowledgeQuery") - .description(KNOWLEDGE_QUERY_DESCRIPTION) - .input_schema(schema_value); - let tool: Arc = Arc::new(KnowledgeQueryTool { - layout: layout.clone(), - config, - }); - (meta, tool) - }) -} - #[cfg(test)] mod tests { use super::*; @@ -524,7 +347,6 @@ mod tests { std::fs::create_dir_all(dir.path().join(".yoi/memory/decisions")).unwrap(); std::fs::create_dir_all(dir.path().join(".yoi/memory/requests")).unwrap(); std::fs::create_dir_all(dir.path().join(".yoi/memory/_staging")).unwrap(); - std::fs::create_dir_all(dir.path().join(".yoi/knowledge")).unwrap(); (dir, layout) } @@ -537,19 +359,6 @@ mod tests { std::fs::write(path, content).unwrap(); } - fn write_knowledge(dir: &Path, slug: &str, kind: &str, description: &str, body: &str) { - let path = dir.join(".yoi/knowledge").join(format!("{slug}.md")); - let content = format!( - "---\ncreated_at: {n}\nupdated_at: {n}\nkind: {kind}\ndescription: \"{description}\"\nmodel_invokation: false\nuser_invocable: true\nlast_sources: []\n---\n{body}", - n = now() - ); - std::fs::write(path, content).unwrap(); - } - - fn parse_records serde::Deserialize<'de>>(out: &ToolOutput) -> Vec { - serde_json::from_str(out.content.as_ref().unwrap()).unwrap() - } - #[derive(Deserialize)] struct OwnedMemoryRecord { slug: String, @@ -558,16 +367,10 @@ mod tests { excerpt: Option, } - #[derive(Deserialize)] - struct OwnedKnowledgeRecord { - slug: String, - kind: Option, - description: Option, - model_invokation: Option, - #[serde(default)] - excerpt: Option, + fn parse_records(out: &ToolOutput) -> Vec { + let text = out.content.as_ref().unwrap_or(&out.summary); + serde_json::from_str(text).unwrap() } - #[tokio::test] async fn memory_query_finds_decision_body() { let (dir, layout) = setup(); @@ -640,25 +443,12 @@ mod tests { async fn query_hits_do_not_log_usage() { let (dir, layout) = setup(); write_decision(dir.path(), "alpha", "needle line\n"); - write_knowledge( - dir.path(), - "policy", - "policy", - "needle desc", - "needle body\n", - ); - let (_, memory_tool) = memory_query_tool(layout.clone(), QueryConfig::default())(); - let (_, knowledge_tool) = knowledge_query_tool(layout.clone(), QueryConfig::default())(); let inp = serde_json::json!({ "query": "needle" }); memory_tool .execute(&inp.to_string(), Default::default()) .await .unwrap(); - knowledge_tool - .execute(&inp.to_string(), Default::default()) - .await - .unwrap(); let report = crate::usage::build_usage_report(&layout).unwrap(); assert!(report.records.is_empty()); @@ -724,116 +514,4 @@ mod tests { .unwrap_err(); assert!(matches!(err, ToolError::InvalidArgument(_))); } - - #[tokio::test] - async fn knowledge_query_returns_frontmatter_fields() { - let (dir, layout) = setup(); - write_knowledge( - dir.path(), - "policy", - "policy", - "the policy doc", - "Ollama first\n", - ); - let (_, tool) = knowledge_query_tool(layout, QueryConfig::default())(); - let inp = serde_json::json!({ "query": "ollama" }); - let out = tool - .execute(&inp.to_string(), Default::default()) - .await - .unwrap(); - let records: Vec = parse_records(&out); - assert_eq!(records.len(), 1); - assert_eq!(records[0].slug, "policy"); - assert_eq!(records[0].kind.as_deref(), Some("policy")); - assert_eq!(records[0].description.as_deref(), Some("the policy doc")); - assert_eq!(records[0].model_invokation, Some(false)); - assert!( - records[0] - .excerpt - .as_deref() - .unwrap() - .to_lowercase() - .contains("ollama") - ); - } - - #[tokio::test] - async fn knowledge_query_without_query_lists_all_records() { - let (dir, layout) = setup(); - write_knowledge(dir.path(), "p1", "policy", "d1", "body\n"); - write_knowledge(dir.path(), "h1", "howto", "d2", "body\n"); - - let (_, tool) = knowledge_query_tool(layout, QueryConfig::default())(); - let out = tool.execute("{}", Default::default()).await.unwrap(); - let records: Vec = parse_records(&out); - let mut slugs: Vec<&str> = records.iter().map(|r| r.slug.as_str()).collect(); - slugs.sort(); - assert_eq!(slugs, vec!["h1", "p1"]); - assert!(records.iter().all(|r| r.excerpt.is_none())); - } - - #[tokio::test] - async fn knowledge_query_kind_filter() { - let (dir, layout) = setup(); - write_knowledge(dir.path(), "p1", "policy", "d1", "needle\n"); - write_knowledge(dir.path(), "h1", "howto", "d2", "needle\n"); - - let (_, tool) = knowledge_query_tool(layout, QueryConfig::default())(); - let inp = serde_json::json!({ "query": "needle", "kind": "howto" }); - let out = tool - .execute(&inp.to_string(), Default::default()) - .await - .unwrap(); - let records: Vec = parse_records(&out); - assert_eq!(records.len(), 1); - assert_eq!(records[0].slug, "h1"); - } - - #[tokio::test] - async fn knowledge_query_kind_filter_works_without_query() { - let (dir, layout) = setup(); - write_knowledge(dir.path(), "p1", "policy", "d1", "body\n"); - write_knowledge(dir.path(), "h1", "howto", "d2", "body\n"); - - let (_, tool) = knowledge_query_tool(layout, QueryConfig::default())(); - let inp = serde_json::json!({ "kind": "howto" }); - let out = tool - .execute(&inp.to_string(), Default::default()) - .await - .unwrap(); - let records: Vec = parse_records(&out); - assert_eq!(records.len(), 1); - assert_eq!(records[0].slug, "h1"); - assert!(records[0].excerpt.is_none()); - } - - #[tokio::test] - async fn knowledge_query_searches_frontmatter_too() { - let (dir, layout) = setup(); - write_knowledge(dir.path(), "p", "policy", "mentions xyzzy here", "body\n"); - - let (_, tool) = knowledge_query_tool(layout, QueryConfig::default())(); - let inp = serde_json::json!({ "query": "xyzzy" }); - let out = tool - .execute(&inp.to_string(), Default::default()) - .await - .unwrap(); - let records: Vec = parse_records(&out); - assert_eq!(records.len(), 1); - assert_eq!(records[0].slug, "p"); - } - - #[tokio::test] - async fn knowledge_query_no_matches_returns_empty() { - let (dir, layout) = setup(); - write_knowledge(dir.path(), "p", "policy", "d", "no match\n"); - let (_, tool) = knowledge_query_tool(layout, QueryConfig::default())(); - let inp = serde_json::json!({ "query": "absent" }); - let out = tool - .execute(&inp.to_string(), Default::default()) - .await - .unwrap(); - let records: Vec = parse_records(&out); - assert!(records.is_empty()); - } } diff --git a/crates/memory/src/tool/read.rs b/crates/memory/src/tool/read.rs index a29badfd..7e05beb2 100644 --- a/crates/memory/src/tool/read.rs +++ b/crates/memory/src/tool/read.rs @@ -1,8 +1,8 @@ //! `MemoryRead` tool. //! -//! Reads a memory or knowledge record by `(kind, slug)`. Returns +//! Reads a memory record by `(kind, slug)`. Returns //! line-numbered content (1-based), like the generic Read tool. The -//! agent never names a path — `Search` returns `{kind, slug, ...}` +//! agent never names a path — `MemoryQuery` returns `{kind, slug, ...}` //! and that pair feeds straight into Read. use std::sync::Arc; @@ -16,8 +16,8 @@ use crate::tool::MemoryToolKind; use crate::usage::{self, UsageSource}; use crate::workspace::WorkspaceLayout; -const DESCRIPTION: &str = "Read a memory or knowledge record by `kind` + `slug`. \ -`kind` is one of: summary, decision, request, knowledge. \ +const DESCRIPTION: &str = "Read a memory record by `kind` + `slug`. \ +`kind` is one of: summary, decision, request. \ For `summary` omit `slug`; for the others `slug` is required. \ Returns line-numbered output (1-based)."; @@ -25,7 +25,7 @@ const DEFAULT_LIMIT: usize = 2000; #[derive(Debug, Deserialize, schemars::JsonSchema)] struct ReadParams { - /// Record kind: `summary` | `decision` | `request` | `knowledge`. + /// Record kind: `summary` | `decision` | `request`. kind: MemoryToolKind, /// Slug. Required for everything except `summary`; forbidden for `summary`. #[serde(default)] @@ -290,22 +290,6 @@ mod tests { assert!(matches!(err, ToolError::InvalidArgument(_))); } - #[tokio::test] - async fn knowledge_path_resolution() { - let (dir, layout) = setup(); - let path = dir.path().join(".yoi/knowledge/policy.md"); - std::fs::create_dir_all(path.parent().unwrap()).unwrap(); - std::fs::write(&path, "k\n").unwrap(); - - let (_, tool) = read_tool(layout)(); - let inp = serde_json::json!({ "kind": "knowledge", "slug": "policy" }); - let out = tool - .execute(&inp.to_string(), Default::default()) - .await - .unwrap(); - assert!(out.content.unwrap().contains("k")); - } - #[tokio::test] async fn read_logs_explicit_use_when_usage_session_is_set() { let (dir, layout) = setup(); diff --git a/crates/memory/src/tool/write.rs b/crates/memory/src/tool/write.rs index f4ce1926..e10f2dbb 100644 --- a/crates/memory/src/tool/write.rs +++ b/crates/memory/src/tool/write.rs @@ -1,6 +1,6 @@ //! `MemoryWrite` tool. //! -//! Creates or overwrites a memory or knowledge record by `(kind, slug)`. +//! Creates or overwrites a memory record by `(kind, slug)`. //! Pre-write Linter validates frontmatter, slug uniqueness (Create only), //! reference integrity, size limits. On any //! Linter error the tool returns `ToolError::InvalidArgument` with all @@ -19,14 +19,14 @@ use crate::linter::{LintReport, Linter, WriteMode}; use crate::tool::MemoryToolKind; use crate::workspace::WorkspaceLayout; -const DESCRIPTION: &str = "Create or overwrite a memory or knowledge record by \ -`kind` + `slug`. `kind`: summary | decision | request | knowledge. For `summary` \ +const DESCRIPTION: &str = "Create or overwrite a memory record by \ +`kind` + `slug`. `kind`: summary | decision | request. For `summary` \ omit `slug`. Frontmatter is validated before write; on validation failure no \ write occurs and every violation is returned in the error message."; #[derive(Debug, Deserialize, schemars::JsonSchema)] struct WriteParams { - /// Record kind: `summary` | `decision` | `request` | `knowledge`. + /// Record kind: `summary` | `decision` | `request`. kind: MemoryToolKind, /// Slug. Required for everything except `summary`; forbidden for `summary`. #[serde(default)] diff --git a/crates/memory/src/usage.rs b/crates/memory/src/usage.rs index 910fc892..dcf81168 100644 --- a/crates/memory/src/usage.rs +++ b/crates/memory/src/usage.rs @@ -1,9 +1,9 @@ -//! Workspace-local usage event log for memory / knowledge records. +//! Workspace-local usage event log for memory records. //! //! The log is append-only JSONL under the workspace's `.yoi/` tree. It is //! intentionally evidence-only: aggregation reports explicit context reads and //! resident exposure cost telemetry, but it does not classify records as -//! Knowledge candidates or tidy-protected records. +//! tidy-protected records. use std::collections::{BTreeMap, HashMap}; use std::fs::{self, OpenOptions}; @@ -25,7 +25,6 @@ pub enum UsageEventKind { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum UsageSource { MemoryRead, - KnowledgeRef, ResidentInjection, } @@ -33,7 +32,6 @@ impl UsageSource { pub fn as_str(self) -> &'static str { match self { Self::MemoryRead => "MemoryRead", - Self::KnowledgeRef => "KnowledgeRef", Self::ResidentInjection => "ResidentInjection", } } @@ -214,10 +212,6 @@ fn record_path( let slug = crate::Slug::parse(slug.to_string()).map_err(invalid_slug_error)?; Ok(layout.request_path(&slug)) } - RecordKind::Knowledge => { - let slug = crate::Slug::parse(slug.to_string()).map_err(invalid_slug_error)?; - Ok(layout.knowledge_path(&slug)) - } } } @@ -313,7 +307,7 @@ mod tests { fn aggregates_use_and_resident_exposure_separately() { let (_dir, layout) = setup(); let decision = snapshot_record_from_bytes(RecordKind::Decision, "alpha", b"abcd"); - let knowledge = snapshot_record_from_bytes(RecordKind::Knowledge, "policy", b"abcdefgh"); + let request = snapshot_record_from_bytes(RecordKind::Request, "policy", b"abcdefgh"); append_use_event( &layout, @@ -325,18 +319,18 @@ mod tests { append_use_event( &layout, "session-a", - UsageSource::KnowledgeRef, - vec![knowledge.clone()], + UsageSource::MemoryRead, + vec![request.clone()], ) .unwrap(); append_use_event( &layout, "session-b", - UsageSource::KnowledgeRef, - vec![knowledge.clone()], + UsageSource::MemoryRead, + vec![request.clone()], ) .unwrap(); - append_resident_exposure_event(&layout, "session-b", vec![knowledge]).unwrap(); + append_resident_exposure_event(&layout, "session-b", vec![request]).unwrap(); let report = build_usage_report(&layout).unwrap(); let decision = report @@ -349,22 +343,22 @@ mod tests { assert_eq!(decision.resident_exposure_count, 0); assert!(decision.last_used_at.is_some()); - let knowledge = report + let request = report .records .iter() - .find(|r| r.kind == "knowledge" && r.slug == "policy") + .find(|r| r.kind == "request" && r.slug == "policy") .unwrap(); - assert_eq!(knowledge.use_count, 2); - assert_eq!(knowledge.source_breakdown["KnowledgeRef"], 2); - assert_eq!(knowledge.resident_exposure_count, 1); - assert_eq!(knowledge.estimated_tokens_per_injection, 2); - assert_eq!(knowledge.estimated_total_resident_exposure_tokens, 2); + assert_eq!(request.use_count, 2); + assert_eq!(request.source_breakdown["MemoryRead"], 2); + assert_eq!(request.resident_exposure_count, 1); + assert_eq!(request.estimated_tokens_per_injection, 2); + assert_eq!(request.estimated_total_resident_exposure_tokens, 2); } #[test] fn resident_only_record_does_not_increment_use_count() { let (_dir, layout) = setup(); - let snapshot = snapshot_record_from_bytes(RecordKind::Knowledge, "policy", b"abcdefgh"); + let snapshot = snapshot_record_from_bytes(RecordKind::Decision, "policy", b"abcdefgh"); append_resident_exposure_event(&layout, "session", vec![snapshot]).unwrap(); let report = build_usage_report(&layout).unwrap(); diff --git a/crates/memory/src/workspace.rs b/crates/memory/src/workspace.rs index 0bd1512e..f4058994 100644 --- a/crates/memory/src/workspace.rs +++ b/crates/memory/src/workspace.rs @@ -5,7 +5,6 @@ //! `/.yoi/` subdirectory — alongside workspace project records //! generated durable memory. The trees inside it: //! -//! - `/.yoi/knowledge/.md` //! - `/.yoi/memory/summary.md` //! - `/.yoi/memory/decisions/.md` //! - `/.yoi/memory/requests/.md` @@ -26,7 +25,6 @@ use lint_common::RecordLintError; const YOI_DIR: &str = ".yoi"; const MEMORY_DIR: &str = "memory"; -const KNOWLEDGE_DIR: &str = "knowledge"; const SUMMARY_FILE: &str = "summary.md"; const DECISIONS_DIR: &str = "decisions"; const REQUESTS_DIR: &str = "requests"; @@ -42,7 +40,6 @@ pub enum RecordKind { Summary, Decision, Request, - Knowledge, } impl RecordKind { @@ -51,7 +48,6 @@ impl RecordKind { Self::Summary => "summary", Self::Decision => "decision", Self::Request => "request", - Self::Knowledge => "knowledge", } } } @@ -110,10 +106,6 @@ impl WorkspaceLayout { self.yoi_dir().join(MEMORY_DIR) } - pub fn knowledge_dir(&self) -> PathBuf { - self.yoi_dir().join(KNOWLEDGE_DIR) - } - pub fn summary_path(&self) -> PathBuf { self.memory_dir().join(SUMMARY_FILE) } @@ -158,12 +150,8 @@ impl WorkspaceLayout { self.requests_dir().join(format!("{slug}.md")) } - pub fn knowledge_path(&self, slug: &Slug) -> PathBuf { - self.knowledge_dir().join(format!("{slug}.md")) - } - /// Classify a path under the memory tree. Returns `None` if the - /// path is not under `.yoi/memory/` or `.yoi/knowledge/` + /// path is not under `.yoi/memory/` /// of this workspace, or if it lives in /// `_staging/` / `_usage/` / `_logs/` (opaque subsystem-owned trees). /// @@ -173,11 +161,7 @@ impl WorkspaceLayout { /// can surface it as a write violation. pub fn classify(&self, path: &Path) -> Result, LintError> { let memory = self.memory_dir(); - let knowledge = self.knowledge_dir(); - if let Ok(rel) = path.strip_prefix(&knowledge) { - return Ok(Some(classify_kinded_md(rel, RecordKind::Knowledge, path)?)); - } let rel = match path.strip_prefix(&memory) { Ok(r) => r, Err(_) => return Ok(None), @@ -283,16 +267,7 @@ mod tests { } #[test] - fn classifies_knowledge() { - let cp = layout() - .classify(&PathBuf::from("/ws/.yoi/knowledge/x.md")) - .unwrap() - .unwrap(); - assert_eq!(cp.kind, RecordKind::Knowledge); - } - - #[test] - fn staging_returns_none() { + fn staging_tree_is_opaque_to_classifier() { assert!( layout() .classify(&PathBuf::from("/ws/.yoi/memory/_staging/abc.json")) diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 933ad2bb..12ba8d8a 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -171,7 +171,7 @@ impl WorkerEvent { /// `Method::Run` and `Event::UserMessage` carry `Vec`. Dumb /// clients (CLI piping, scripts) only need to produce a single /// `Segment::Text`; richer clients (TUI / GUI) construct typed atoms -/// (paste chips, file refs, knowledge refs, knowledge refs) and +/// (paste chips, file refs) and /// send them through directly so the Worker side never has to re-parse a /// flattened string. /// @@ -201,8 +201,6 @@ pub enum Segment { /// `[Dir: ]` listings; the flattened user text keeps the literal /// `@` placeholder either way. FileRef { path: String }, - /// `#` Knowledge reference (see `docs/plan/memory.md`). - KnowledgeRef { slug: String }, /// Unknown variant from a newer client. Worker treats this as an /// unresolved input — surfaces an alert and inserts a placeholder. /// Round-trip is lossy: re-serializing yields `{"kind":"unknown"}`. @@ -221,9 +219,8 @@ impl Segment { /// to surface user-visible alerts for unresolved refs should do so /// alongside this call (Worker does so at submit time). /// - /// Sigil-prefixed variants (`FileRef` / `KnowledgeRef`) - /// flatten back to their literal sigil form (`@`, `#`, - /// ) — matching what the user originally typed. Resolved + /// Sigil-prefixed variants (`FileRef`) flatten back to their literal + /// sigil form (`@`) when converted to text. /// content (e.g. file body or shallow directory listing for `FileRef`) is /// delivered as separate `Item::system_message`s adjacent to the user /// message; the resolution itself is the caller's job. `Unknown` falls back to @@ -238,13 +235,7 @@ impl Segment { out.push('@'); out.push_str(path); } - Segment::KnowledgeRef { slug } => { - out.push('#'); - out.push_str(slug); - } - Segment::Unknown => { - out.push_str("[unknown input segment]"); - } + Segment::Unknown => {} } } out @@ -287,8 +278,7 @@ pub enum Event { /// /// Carries the JSON form of `session_store::SystemItem`. Covers /// `Method::Notify` echoes, child-Worker lifecycle events from - /// `Method::WorkerEvent`, `@` / `#` / - /// resolution payloads, and any future agent-side injection kind. + /// `Method::WorkerEvent`, `@` resolution payloads, and any future /// Clients dispatch on the `kind` tag for typed rendering instead /// of parsing free-text prefixes like `[Notification] …` or /// `[File: …]`. @@ -599,21 +589,17 @@ pub enum AlertSource { /// Kind of completion requested by `Method::ListCompletions`. /// -/// Mirrors the completion prefix sigils: `@` → `File`, `#` → `Knowledge`. +/// Mirrors the completion prefix sigil: `@` → `File`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum CompletionKind { File, - Knowledge, } -/// One candidate returned in `Event::Completions::entries`. +/// One completion candidate for a prefix query. /// -/// `value` is a path (file kind) or a Knowledge slug. -/// `is_dir` is meaningful only for the file kind — it lets the TUI -/// keep a trailing `/` after a directory selection so the user can -/// drill in without re-typing the prefix. +/// `value` is a path (file kind). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct CompletionEntry { @@ -1177,16 +1163,16 @@ mod tests { #[test] fn event_completions_format_and_default_is_dir() { let event = Event::Completions { - kind: CompletionKind::Knowledge, + kind: CompletionKind::File, entries: vec![CompletionEntry { - value: "clear".into(), + value: "src/main.rs".into(), is_dir: false, }], }; let json = serde_json::to_string(&event).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(parsed["event"], "completions"); - assert_eq!(parsed["data"]["kind"], "knowledge"); + assert_eq!(parsed["data"]["kind"], "file"); assert_eq!(parsed["data"]["entries"][0]["value"], "clear"); // is_dir defaults to false on inbound payloads that omit it. diff --git a/crates/session-store/src/segment_log.rs b/crates/session-store/src/segment_log.rs index 69efde29..ace151a3 100644 --- a/crates/session-store/src/segment_log.rs +++ b/crates/session-store/src/segment_log.rs @@ -74,7 +74,7 @@ pub enum LogEntry { /// User input accepted at submit time. Carries the original typed /// `Vec` so clients can re-render typed atoms (paste chips, - /// file/knowledge refs) on segment restore. + /// file refs) on segment restore. /// Replay flattens these into a `Item::user_message` for the worker /// history; the worker layer never sees segments directly. UserInput { ts: u64, segments: Vec }, @@ -88,8 +88,8 @@ pub enum LogEntry { ToolResult { ts: u64, item: LoggedItem }, /// One typed agent-injected system item: notification, child-Worker - /// lifecycle event, `@` / `#` / `/` resolution - /// payload. Each `SystemItem` carries kind metadata that the LLM + /// lifecycle event, `@` / `/` resolution payload. Each + /// `SystemItem` carries kind metadata that the LLM /// itself never sees (the LLM gets `Item::system_message` with the /// item's denormalised `body`), but live clients and replay paths /// dispatch on `kind` for typed rendering. diff --git a/crates/session-store/src/system_item.rs b/crates/session-store/src/system_item.rs index ff1728f4..7b406785 100644 --- a/crates/session-store/src/system_item.rs +++ b/crates/session-store/src/system_item.rs @@ -2,7 +2,7 @@ //! //! Items in worker history with `role:system` are never produced by the //! LLM — they are always inserted by the Worker itself (notifications, -//! file/knowledge ref resolutions, child-worker lifecycle events, +//! file ref resolutions, child-worker lifecycle events, //! future `` tags, …). [`SystemItem`] carries the //! typed shape of each such injection so clients can dispatch on //! `kind` instead of parsing text prefixes like `[Notification] …` or @@ -106,7 +106,7 @@ fn render_system_reminder(body: &str) -> String { /// /// Each variant carries the kind-specific raw data clients use for /// typed rendering (`Notification.message`, `WorkerEvent.event`, file -/// path / knowledge slug / etc.), plus a pre-rendered +/// path / identifier / etc.), plus a pre-rendered /// `body` (where applicable) that is the exact `role:system` text the /// LLM actually saw at commit time. `body` is denormalised so that /// segment log replay reconstructs worker history byte-identical to @@ -139,10 +139,11 @@ pub enum SystemItem { /// byte-identical to what was sent. FileAttachment { path: String, body: String }, - /// `#` Knowledge reference resolution. `body` is the - /// rendered text the LLM saw (Worker composes the `[Knowledge: …]` - /// header + body). - Knowledge { slug: String, body: String }, + /// Historical persisted Knowledge reference resolution. Knowledge is no + /// longer active, so restored sessions ignore this item instead of replaying + /// archived record text into model context. + #[serde(rename = "knowledge")] + LegacyKnowledgeIgnored { slug: String, body: String }, /// Compatibility sink for pre-removal persisted `kind: "workflow"` /// system items. These entries are intentionally not replayed as @@ -173,7 +174,7 @@ impl SystemItem { SystemItem::Notification { body, .. } => body.clone(), SystemItem::WorkerEvent { body, .. } => body.clone(), SystemItem::FileAttachment { body, .. } => body.clone(), - SystemItem::Knowledge { body, .. } => body.clone(), + SystemItem::LegacyKnowledgeIgnored { .. } => String::new(), SystemItem::LegacyIgnored { slug } => { format!("Ignored legacy procedure item: /{slug}") } @@ -195,7 +196,7 @@ impl SystemItem { SystemItem::Notification { .. } => "notification", SystemItem::WorkerEvent { .. } => "worker_event", SystemItem::FileAttachment { .. } => "file_attachment", - SystemItem::Knowledge { .. } => "knowledge", + SystemItem::LegacyKnowledgeIgnored { .. } => "legacy_knowledge_ignored", SystemItem::LegacyIgnored { .. } => "legacy_ignored", SystemItem::TaskReminder { .. } => "task_reminder", SystemItem::Interrupt { .. } => "interrupt", diff --git a/crates/ticket/src/tool.rs b/crates/ticket/src/tool.rs index 2d6d1b7c..c25540f3 100644 --- a/crates/ticket/src/tool.rs +++ b/crates/ticket/src/tool.rs @@ -154,7 +154,7 @@ fn base_tool_description(name: &str) -> &'static str { /// Build the model-visible Ticket tool description for a configured Ticket backend. /// /// `record_language` is the durable Ticket record/tool-body language, distinct from -/// worker response language and Memory/Knowledge language. Keeping this on the tool +/// worker response language and Memory language. Keeping this on the tool /// surface ensures every Ticket-capable Worker sees the policy without hidden context /// injection or role-launch-only prose. pub fn ticket_tool_description(name: &str, record_language: Option<&str>) -> String { @@ -162,7 +162,7 @@ pub fn ticket_tool_description(name: &str, record_language: Option<&str>) -> Str if let Some(language) = record_language.filter(|language| !language.trim().is_empty()) { description.push_str("\n\nTicket record language: "); description.push_str(language.trim()); - description.push_str(". Use this language for durable Ticket record and Ticket tool body text, including Ticket item bodies, thread comments/plans/decisions/implementation reports, reviews, resolutions, intake summaries, and orchestration plan notes. This policy is distinct from worker.language for normal prose and memory.language for Memory/Knowledge. Preserve protocol literals, file paths, commands, logs, identifiers, and quoted external text when translation would reduce fidelity."); + description.push_str(". Use this language for durable Ticket record and Ticket tool body text, including Ticket item bodies, thread comments/plans/decisions/implementation reports, reviews, resolutions, intake summaries, and orchestration plan notes. This policy is distinct from worker.language for normal prose and memory.language for Memory. Preserve protocol literals, file paths, commands, logs, identifiers, and quoted external text when translation would reduce fidelity."); } description } diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index c46c42ac..70e12dd7 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -595,7 +595,6 @@ impl App { self.input_history.cancel_browse(); match kind { CompletionKind::File => self.input.replace_with_file_ref(start, value), - CompletionKind::Knowledge => self.input.replace_with_knowledge_ref(start, value), } self.completion = None; true @@ -607,7 +606,6 @@ impl App { /// suggestion" — partial typing like `@README.` followed by /// Enter should chip when the popup is on `README.md`. /// - /// Files and Knowledge entries chipify here. Directory file entries return `false` /// so the caller can fall through to `apply_completion_text` /// for drill-in — chip-ifying a directory on Enter would strand /// the user with no way to inspect children. @@ -628,7 +626,6 @@ impl App { self.input_history.cancel_browse(); match kind { CompletionKind::File => self.input.replace_with_file_ref(start, value), - CompletionKind::Knowledge => self.input.replace_with_knowledge_ref(start, value), } self.completion = None; true @@ -2166,13 +2163,13 @@ impl App { self.blocks.push(Block::WorkerEvent { event }); } session_store::SystemItem::FileAttachment { body, .. } - | session_store::SystemItem::Knowledge { body, .. } | session_store::SystemItem::TaskReminder { body, .. } | session_store::SystemItem::Interrupt { body } => { self.task_store.apply_system_message_text(&body); self.blocks.push(Block::SystemMessage { text: body }); } session_store::SystemItem::LegacyIgnored { .. } => {} + session_store::SystemItem::LegacyKnowledgeIgnored { .. } => {} } } @@ -2969,24 +2966,6 @@ mod completion_flow_tests { assert!(app.completion.is_some()); } - #[test] - fn outdated_completions_event_is_dropped() { - let mut app = App::new("test".into()); - for c in "@x".chars() { - app.insert_char(c); - } - let _ = app.refresh_completion(); - // Reply for a different kind shouldn't overwrite state. - app.handle_worker_event(Event::Completions { - kind: CompletionKind::Knowledge, - entries: vec![CompletionEntry { - value: "stale".into(), - is_dir: false, - }], - }); - assert!(app.completion.as_ref().unwrap().entries.is_empty()); - } - #[test] fn committed_user_message_survives_fresh_segment_rotation() { let mut app = App::new("test".into()); @@ -3697,9 +3676,6 @@ mod completion_flow_tests { Segment::Text { content: " and ".into(), }, - Segment::KnowledgeRef { - slug: "design-note".into(), - }, Segment::Paste { id: 1, chars: 13, diff --git a/crates/tui/src/input.rs b/crates/tui/src/input.rs index 10c84aff..1d059a1b 100644 --- a/crates/tui/src/input.rs +++ b/crates/tui/src/input.rs @@ -46,24 +46,11 @@ impl FileRefAtom { } } -/// `#` chip — confirmed completion of a Knowledge reference. -#[derive(Debug, Clone)] -pub struct KnowledgeRefAtom { - pub slug: String, -} - -impl KnowledgeRefAtom { - pub fn label(&self) -> String { - format!("#{}", self.slug) - } -} - #[derive(Debug, Clone)] pub enum Atom { Char(char), Paste(PasteRef), FileRef(FileRefAtom), - KnowledgeRef(KnowledgeRefAtom), } impl Atom { @@ -74,7 +61,6 @@ impl Atom { Atom::Char(_) => None, Atom::Paste(p) => Some((Style::default().fg(Color::Magenta), p.label())), Atom::FileRef(r) => Some((Style::default().fg(Color::Cyan), r.label())), - Atom::KnowledgeRef(r) => Some((Style::default().fg(Color::Green), r.label())), } } } @@ -83,7 +69,7 @@ impl Atom { enum AtomClass { Word(WordKind), Sep, - /// Indivisible chip — paste / file ref / knowledge ref. Word motion treats one chip as one unit; deletion + /// Indivisible chip — paste / file ref. Word motion treats one chip as one unit; deletion /// removes the whole atom. Chip, } @@ -103,7 +89,7 @@ enum WordKind { fn atom_class(atom: &Atom) -> AtomClass { match atom { Atom::Char(c) => char_class(*c), - Atom::Paste(_) | Atom::FileRef(_) | Atom::KnowledgeRef(_) => AtomClass::Chip, + Atom::Paste(_) | Atom::FileRef(_) => AtomClass::Chip, } } @@ -195,10 +181,6 @@ impl InputBuffer { self.atoms .push(Atom::FileRef(FileRefAtom { path: path.clone() })); } - protocol::Segment::KnowledgeRef { slug } => { - self.atoms - .push(Atom::KnowledgeRef(KnowledgeRefAtom { slug: slug.clone() })); - } protocol::Segment::Unknown => { self.atoms .extend("[unknown input segment]".chars().map(Atom::Char)); @@ -226,7 +208,6 @@ impl InputBuffer { Atom::Char(c) => text.push(*c), Atom::Paste(paste) => text.push_str(&paste.content), Atom::FileRef(file) => text.push_str(&file.path), - Atom::KnowledgeRef(knowledge) => text.push_str(&knowledge.slug), } } text @@ -254,7 +235,7 @@ impl InputBuffer { } /// Replace `atoms[start..self.cursor]` (the in-flight `@` / - /// `#` token) with the corresponding chip atom + /// active `@` file token) with the corresponding chip atom /// and place the cursor right after the chip. Used by the completion /// confirm path. pub fn replace_with_file_ref(&mut self, start: usize, path: String) { @@ -264,13 +245,6 @@ impl InputBuffer { self.cursor = start + 1; } - pub fn replace_with_knowledge_ref(&mut self, start: usize, slug: String) { - self.atoms.drain(start..self.cursor); - self.atoms - .insert(start, Atom::KnowledgeRef(KnowledgeRefAtom { slug })); - self.cursor = start + 1; - } - /// Replace `atoms[start..self.cursor]` with the chars of `text`, /// leaving cursor at the end of the inserted run. Used by the Tab /// completion path: the popup-selected entry is inserted as raw @@ -286,7 +260,7 @@ impl InputBuffer { self.cursor = idx; } - /// If the cursor is currently inside a `@` / `#` / + /// If the cursor is currently inside a `@` / /// `/` token that satisfies the trigger rules, return the /// kind, the index of the leading sigil atom, and the typed text /// after the sigil (sigil itself excluded). @@ -311,7 +285,7 @@ impl InputBuffer { } let kind = match c { '@' => Some(protocol::CompletionKind::File), - '#' => Some(protocol::CompletionKind::Knowledge), + _ => None, }; if let Some(k) = kind { @@ -480,7 +454,7 @@ impl InputBuffer { /// Build the typed `Vec` sent over the protocol. Adjacent /// `Atom::Char`s are concatenated into a single `Segment::Text`; each - /// chip atom (`Paste` / `FileRef` / `KnowledgeRef` ) + /// chip atom (`Paste` / `FileRef`) /// becomes a standalone `Segment` so that clients re-rendering an /// `Event::UserMessage` see the same indivisible chip rather than a /// flattened string. @@ -510,12 +484,6 @@ impl InputBuffer { path: r.path.clone(), }); } - Atom::KnowledgeRef(r) => { - flush_text(&mut buf, &mut out); - out.push(protocol::Segment::KnowledgeRef { - slug: r.slug.clone(), - }); - } } } if !buf.is_empty() { @@ -1028,14 +996,6 @@ mod completion_prefix_tests { assert_eq!(prefix, "sr"); } - #[test] - fn hash_sigil_triggers_knowledge_completion() { - let buf = buf_from("#abc"); - let (kind, _, prefix) = buf.pending_completion_prefix().unwrap(); - assert_eq!(kind, CompletionKind::Knowledge); - assert_eq!(prefix, "abc"); - } - #[test] fn newline_before_cursor_invalidates_trigger() { let buf = buf_from("@a\nbc"); @@ -1234,7 +1194,7 @@ mod word_motion_tests { for a in &buf.atoms { match a { Atom::Char(c) => out.push(*c), - Atom::Paste(_) | Atom::FileRef(_) | Atom::KnowledgeRef(_) => out.push_str("

"), + Atom::Paste(_) | Atom::FileRef(_) => out.push_str("

"), } } out diff --git a/crates/tui/src/ui.rs b/crates/tui/src/ui.rs index 23d0c7f9..e90f14eb 100644 --- a/crates/tui/src/ui.rs +++ b/crates/tui/src/ui.rs @@ -965,7 +965,7 @@ fn push_padded_lines(lines: &mut Vec>, text: &str, kind: MessageKi /// Render `Block::UserMessage` from typed segments. Each non-text /// segment renders as a one-piece chip whose colour matches the input /// area's chip presentation (paste = magenta, `@` file = cyan, -/// `#` knowledge = green, `/` workflow = yellow), so the user +/// `/` workflow = yellow), so the user /// recognises their own typed atoms in the scrollback. fn render_user_message( lines: &mut Vec>, @@ -1097,7 +1097,6 @@ fn chip_span_for(seg: &Segment, fallback: Style) -> (Style, String) { format!("[Clipboard #{id} | {chars} chars, {line_count} lines]"), ), Segment::FileRef { path } => (Style::default().fg(Color::Cyan), format!("@{path}")), - Segment::KnowledgeRef { slug } => (Style::default().fg(Color::Green), format!("#{slug}")), Segment::Unknown => (fallback, "[unknown segment]".to_owned()), } } @@ -1112,7 +1111,6 @@ fn segment_display_text(seg: &Segment) -> String { id, chars, lines, .. } => format!("[Clipboard #{id} | {chars} chars, {lines} lines]"), Segment::FileRef { path } => format!("@{path}"), - Segment::KnowledgeRef { slug } => format!("#{slug}"), Segment::Unknown => "[unknown segment]".to_owned(), } } diff --git a/crates/worker/src/controller.rs b/crates/worker/src/controller.rs index b3c8ba76..fb8bf7dc 100644 --- a/crates/worker/src/controller.rs +++ b/crates/worker/src/controller.rs @@ -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 diff --git a/crates/worker/src/prompt/catalog.rs b/crates/worker/src/prompt/catalog.rs index e4c50316..6635b72c 100644 --- a/crates/worker/src/prompt/catalog.rs +++ b/crates/worker/src/prompt/catalog.rs @@ -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 { - 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 { 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")); } diff --git a/crates/worker/src/prompt/system.rs b/crates/worker/src/prompt/system.rs index 2e91894d..955f197e 100644 --- a/crates/worker/src/prompt/system.rs +++ b/crates/worker/src/prompt/system.rs @@ -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 `/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 { 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) } -/// `- : ` 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) -> 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 { [ "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")); - } } diff --git a/crates/worker/src/shared_state.rs b/crates/worker/src/shared_state.rs index d2b611b8..ebcfefc0 100644 --- a/crates/worker/src/shared_state.rs +++ b/crates/worker/src/shared_state.rs @@ -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, - knowledge: OnceLock>, } 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) { - let _ = self.knowledge.set(knowledge); - } - - pub fn list_knowledge_completions(&self, prefix: &str) -> Vec { - 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!["alpha", "alphabet"] - ); - assert!(state.list_knowledge_completions("zzz").is_empty()); - } } diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index 8007d51b..22fae41b 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -502,16 +502,15 @@ pub struct Worker { /// [`Self::from_manifest`], or defaults to the builtin pack when a /// Worker is constructed through lower-level paths that have no loader. prompts: Arc, - /// Memory workspace layout used for Memory/Knowledge record operations. + /// Memory workspace layout used for Memory record operations. memory_layout: Option, /// 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` so @@ -617,7 +616,6 @@ impl Worker 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 Worker { 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 Worker { /// /// 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 Worker { 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 { - &self.prompts + pub fn prompts(&self) -> Arc { + Arc::clone(&self.prompts) } /// The current segment ID. Read lock-free from the shared session @@ -1450,9 +1439,6 @@ impl Worker { let alerter = self.alerter.clone(); let tool_names: Vec = { 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 Worker { } } } - // 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 Worker { } else { None }; - let inject_resident_knowledge = self.inject_resident_knowledge && memory_layout.is_some(); - let resident: Vec = 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 Worker { 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 Worker { .as_mut() .expect("worker present") .set_system_prompt(rendered); - self.append_resident_exposure_event(resident_exposure_snapshots); Ok(()) } @@ -1700,11 +1664,10 @@ impl Worker { })?; self.user_segments.push(input.clone()); - // Resolve `@` file refs and `#` 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 `@` 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 Worker { out } - fn resolve_knowledge_refs(&self, segments: &[Segment]) -> Vec { - 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 { - 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, - ) { - 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) { - 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 Worker { Ok(()) } - pub fn knowledge_completions(&self) -> Vec { - 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 Worker { 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 Worker { // 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 Worker { 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, 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 { diff --git a/crates/yoi/src/memory_lint.rs b/crates/yoi/src/memory_lint.rs index 4b9bed88..e7d23f41 100644 --- a/crates/yoi/src/memory_lint.rs +++ b/crates/yoi/src/memory_lint.rs @@ -217,7 +217,6 @@ fn collect_record_paths(layout: &WorkspaceLayout) -> Result, LintCl collect_md_files(&layout.decisions_dir(), &mut paths)?; collect_md_files(&layout.requests_dir(), &mut paths)?; - collect_md_files(&layout.knowledge_dir(), &mut paths)?; Ok(paths) } @@ -413,7 +412,7 @@ mod tests { } #[test] - fn lints_only_workspace_memory_and_knowledge_records() { + fn lints_only_workspace_memory_records() { let dir = TempDir::new().unwrap(); let root = dir.path(); write(&root.join(".yoi/memory/summary.md"), valid_summary()); diff --git a/docs/README.md b/docs/README.md index 863e1ce4..6e974a4e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ It is not a dumping ground for external research, old plans, API inventories, or 5. [`design/tool-permissions-scope.md`](design/tool-permissions-scope.md) — tool policy and filesystem scope. 6. [`design/plugin-packages.md`](design/plugin-packages.md) — plugin package distribution, discovery, and enablement boundaries. 7. [`development/plugin-development.md`](development/plugin-development.md) — how to build, package, enable, and inspect Yoi Plugins. -8. [`design/memory-knowledge.md`](design/memory-knowledge.md) — generated memory, Knowledge, and audit records. +8. [`design/memory-knowledge.md`](design/memory-knowledge.md) — generated memory and audit records. 9. [`design/workspace-kanban-orchestrator-runtime.md`](design/workspace-kanban-orchestrator-runtime.md) — how Kanban operations become durable orchestration events and backend-internal routing decisions. 10. [`development/work-items.md`](development/work-items.md) — how project work is recorded and reviewed. 11. [`development/validation.md`](development/validation.md) — how to check changes. diff --git a/docs/design/memory-knowledge.md b/docs/design/memory-knowledge.md index 9e8020df..629c2b99 100644 --- a/docs/design/memory-knowledge.md +++ b/docs/design/memory-knowledge.md @@ -1,38 +1,15 @@ -# Memory and Knowledge +# Generated memory records -Yoi memory is generated context, not project authority. +Yoi keeps generated memory under `.yoi/memory/` for durable, low-volume context: -The authoritative record for work is still code, git history, work item files, tickets, session logs, and explicit user instruction. Memory helps the agent retrieve durable preferences and prior rationale, but it must not replace the records that made those facts true. +- `summary.md` is optional resident context. +- `decisions/*.md` capture durable decisions and rationale. +- `requests/*.md` capture durable user preferences or standing requests. -## Record types +Memory records are not workspace record authority for exact implementation state. Use tickets, objectives, repository files, git history, and session logs for exact current facts. -- `summary.md` is resident background context for normal Workers. -- `decisions/` stores durable decisions that are useful across turns. -- `requests/` stores durable user requests and preferences. -- `.yoi/knowledge/` stores curated Knowledge records when available. -- `_logs/` stores append-only audit observations. -- `_staging/` is generated candidate state before consolidation. +## Historical Knowledge records -Generated `.yoi/memory` is personal/generated state in this repository. Curated workflow/Knowledge assets may be tracked separately when intended. +Older workspaces may contain `.yoi/knowledge/`. Knowledge is no longer an active supported feature or workspace record authority. Current memory tooling ignores it; archive or inspect those files manually if needed. -## What memory should not do - -Memory should not duplicate authoritative project records. Do not copy ticket threads, TODO lists, implementation reports, or full docs into memory merely to make them resident. - -The useful memory is the small part that changes future behavior: a policy, a rationale, a user preference, or a durable conclusion that would otherwise be hard to find. - -## Lookup policy - -Agents should use memory and Knowledge when the request depends on prior decisions, historical rationale, project workflow, or durable preferences. - -Agents should not query memory every turn. Local repository files, current user instructions, command output, and tickets are more authoritative for exact current state. - -## Audit and mutation - -Memory extraction/consolidation writes append-only observations under `_logs`. No-op and idle notices belong there rather than in user-facing UI. - -Memory mutation should be explicit work or part of the configured memory maintenance path. Casual edits during unrelated tasks make memory harder to trust. - -## Language - -Memory follows configured memory language policy. Conversation prose follows the user's language unless configured otherwise. +Future Agent Skills are intentionally separate and are not implemented by this design note. diff --git a/docs/design/overview.md b/docs/design/overview.md index 78e3f6b2..3902dc9a 100644 --- a/docs/design/overview.md +++ b/docs/design/overview.md @@ -15,7 +15,7 @@ That rule shapes the crate split. The runtime can restart, attach, compact, or d - `client` contains reusable one-shot socket/runtime-command mechanics so lower crates do not depend on the product CLI. - `manifest` resolves Profiles, Manifests, model/provider references, scopes, prompts, and tool permission policy into a runtime contract. - `tools` implements built-in tools with bounded output and policy-aware execution. -- `memory` owns generated memory, Knowledge records, linting, staging, and audit observations. +- `memory` owns generated memory summary/decision/request records, linting, staging, and audit observations. - `workspace-server` is the local Workspace control-plane seam. It can project Tickets, Workers, lifecycle, usage, and orchestration events, but browser/API operations must stay on opaque backend identities instead of raw local paths, sockets, Worker names, or session files. - `tui` is a UI over Worker authority; it should not invent durable state. diff --git a/docs/manifest.toml b/docs/manifest.toml index 16de2c7e..14d1a465 100644 --- a/docs/manifest.toml +++ b/docs/manifest.toml @@ -232,7 +232,7 @@ permission = "write" # ===== [memory] ============================================================= # Memory subsystem の opt-in。 # - セクションが *ある* … memory tools (MemoryRead/Write/Edit) を登録、 -# `/memory/` と `/knowledge/` +# `/memory/` と `/` # の通常 write を Worker 自体に対して deny する。 # - セクションが *無い* … 何も起きない (legacy 動作)。 # `[memory]` だけ書いて中身を省略するのも有効 (全フィールド既定値で有効化)。 @@ -243,7 +243,7 @@ permission = "write" # workspace_root = "/abs/path/to/workspace" # # # 任意。デフォルト: tool 側既定 = 20。 -# # MemoryQuery / KnowledgeQuery が 1 回に返す最大件数。 +# # MemoryQuery / MemoryQuery が 1 回に返す最大件数。 # query_result_limit = 20 # # # 任意。デフォルト: tool 側既定 = 3。 diff --git a/docs/report/test-validity-20260612/memory.md b/docs/report/test-validity-20260612/memory.md index 9db2eec9..097a5ecb 100644 --- a/docs/report/test-validity-20260612/memory.md +++ b/docs/report/test-validity-20260612/memory.md @@ -19,12 +19,12 @@ ## 現在のテストがよくカバーしていること - crate の責務に沿った主要な pure / filesystem-local invariants はかなり広く押さえられている。 - - `.yoi/memory` / `.yoi/knowledge` の path classification、opaque subtree (`_staging`, `_usage`, `_logs`) の除外、invalid slug / nested path reject。 + - `.yoi/memory` / `.yoi` の path classification、opaque subtree (`_staging`, `_usage`, `_logs`) の除外、invalid slug / nested path reject。 - workspace root resolution で `.yoi` project records だけを memory marker と見なさない挙動。 - - linter の frontmatter 必須 field、`replaced_by` の存在確認・self reference、body size、Knowledge `model_invokation` description cap、same slug create reject、similar slug warning。 - - `MemoryRead` / `MemoryWrite` / `MemoryEdit` / `MemoryDelete` / `MemoryQuery` / `KnowledgeQuery` の基本成功・基本失敗・slug rule・workflow kind 非公開。 - - query の list/search、case-insensitive search、excerpt context、result limit、Knowledge kind filter、frontmatter search、query が usage event を増やさないこと。 - - resident summary / resident knowledge collection の missing/malformed/empty/並び順/`model_invokation` filter。 + - linter の frontmatter 必須 field、`replaced_by` の存在確認・self reference、body size、`resident-injection` description cap、same slug create reject、similar slug warning。 + - `MemoryRead` / `MemoryWrite` / `MemoryEdit` / `MemoryDelete` / `MemoryQuery` / `MemoryQuery` の基本成功・基本失敗・slug rule・workflow kind 非公開。 + - query の list/search、case-insensitive search、excerpt context、result limit、kind filter、frontmatter search、query が usage event を増やさないこと。 + - resident summary / の missing/malformed/empty/並び順/`resident-injection` filter。 - extract staging, extract pointer fold, extract input rendering で tool result content や reasoning を落とすこと。 - consolidation staging list, invalid staging count, lock acquire/release/stale handling, tidy hints, consolidate prompt sections。 - usage event aggregation で explicit use と resident exposure を分けること。 @@ -37,13 +37,13 @@ - extract payload → staging → consolidation input → memory tools write/edit/delete → audit/usage までの一連の流れは、個別部品ごとにはあるが、run-level の不変条件としては検証されていない。 - Pod / Worker 経由の real tool registry や permission scope との接続はこの crate 単体ではほぼ未検証。 - linter の網羅性に穴がある。 - - `InvalidStatus`、unknown/extra frontmatter fields、malformed date、`sources` / `last_sources` の shape、request / knowledge / summary の必須 field failure が体系的には確認されていない。 + - `InvalidStatus`、unknown/extra frontmatter fields、malformed date、`sources` / `last_sources` の shape、request / summary の必須 field failure が体系的には確認されていない。 - `replaced_by` cycle detection の実シナリオは弱い。`references.rs` の test は unknown reference で 1 error になる smoke に近く、既存 A→B / B→A のような cycle report を明確には固定していない。 - `LowImportanceLargeRecord` と `SourcesOverflow` warning は tidy 側では一部見ているが、linter warning と tool output/audit への伝播としては薄い。 - tool の edge case が不足している。 - `MemoryRead` の `offset` / `limit` / truncation summary、limit=0 の `.max(1)` 挙動、空ファイル・末尾改行なしの line numbering が未検証。 - - `MemoryEdit` の `replace_all`, duplicate old_string reject, old_string empty, identical replacement, non-UTF-8 file, summary/knowledge edit path が未検証。 - - `MemoryWrite` の invalid slug、summary with slug、Knowledge/Request 作成、write success audit contents、warning summary/audit reason が未検証。 + - `MemoryEdit` の `replace_all`, duplicate old_string reject, old_string empty, identical replacement, non-UTF-8 file, summary edit path が未検証。 + - `MemoryWrite` の invalid slug、summary with slug、Request 作成、write success audit contents、warning summary/audit reason が未検証。 - `MemoryDelete` は成功 path のみで、missing file、summary slug forbidden、invalid slug、workflow kind reject、audit failure record が未検証。 - 誤解を招く / 弱い test がある。 - `write_aggregates_multiple_errors` は名前に反して、実際の assertion は `status` / `missing` の substring だけで、body too long など複数 error aggregation を確認していない。現実の linter は frontmatter parse failure で早期 return するため、この test は「複数 error が集約される」保証になっていない。 @@ -51,7 +51,7 @@ - filesystem failure / concurrency はほぼ未検証。 - write/edit/delete の permission error、directory/file collision、partial write、外部同時変更は現状未カバー。 - consolidation lock は live pid / stale pid / cleanup は見ているが、corrupt lock overwrite や `release_only` は明示テストがない。 -- query / resident の malformed handling はある程度あるが、KnowledgeQuery の kind filter 時に malformed frontmatter を skip する仕様、malformed でも query だけなら body hit できる仕様は直接の regression test があるとよい。 +- query / resident の malformed handling はある程度あるが、MemoryQuery の kind filter 時に malformed frontmatter を skip する仕様、malformed でも query だけなら body hit できる仕様は直接の regression test があるとよい。 - schema strictness が仕様なら危険。 - `frontmatter::deserialize_strict` という名前だが、schema structs 側に `deny_unknown_fields` が見当たらず、unknown field reject の test もない。extra field を許す設計なら問題ないが、「strict」を期待するならテスト・実装とも不足。 @@ -63,8 +63,8 @@ - `MemoryRead` の `offset` / `limit` / truncation / limit=0 を追加。 - `MemoryDelete` の missing file・summary slug forbidden・invalid slug・workflow kind reject を追加。 - 中優先度: - - linter の invalid status、malformed timestamps、request/knowledge/summary の必須 field、Knowledge `last_sources` malformed、warning propagation を追加。 - - KnowledgeQuery の malformed frontmatter behavior: kind filter では skip、query-only では body match 可能、という仕様を固定。 + - linter の invalid status、malformed timestamps、request/summary の必須 field、`last_sources` malformed、warning propagation を追加。 + - MemoryQuery の malformed frontmatter behavior: kind filter では skip、query-only では body match 可能、という仕様を固定。 - write/edit/delete/read の audit log JSON を success/failure それぞれで軽く確認する。 - consolidation lock の corrupt lock overwrite と `release_only` の staging preservation を追加。 - 低〜中優先度: diff --git a/docs/report/test-validity-20260612/tui.md b/docs/report/test-validity-20260612/tui.md index 55bc57a4..1ff7efca 100644 --- a/docs/report/test-validity-20260612/tui.md +++ b/docs/report/test-validity-20260612/tui.md @@ -40,7 +40,7 @@ - running 中の submit queueing - rollback 時の入力復元 - input history persistence - - file / knowledge completion + - file completion - typed `Segment` の保持 - context usage 表示 - live system item / task snapshot の反映 diff --git a/docs/report/test-validity-20260612/yoi.md b/docs/report/test-validity-20260612/yoi.md index c43b969f..10565841 100644 --- a/docs/report/test-validity-20260612/yoi.md +++ b/docs/report/test-validity-20260612/yoi.md @@ -38,7 +38,7 @@ - `memory_lint.rs` は主要な CLI 固有 invariants をカバーしている: - option parsing と usage error - - 意図した memory/Knowledge record path だけが lint されること + - 意図した memory record path だけが lint されること - invalid record が lint failure になること - `--warnings-as-errors` が status を変えること - JSON output が parse 可能で、期待される counts を含むこと @@ -97,7 +97,7 @@ - `show` における path traversal / invalid id rejection - paused/done/archived/all に対する list filtering -- `memory_lint.rs` には妥当な focused tests があるが、decisions と Knowledge records を明示的にはカバーしておらず、symlink/directory の特殊性、読めない file、summary fragments を超えた stdout human-output details もカバーしていない。 +- `memory_lint.rs` には妥当な focused tests があるが、decisions を明示的にはカバーしておらず、symlink/directory の特殊性、読めない file、summary fragments を超えた stdout human-output details もカバーしていない。 - `session_cli.rs` は意図的に薄いが、ほとんどは最小 fixture を通じて delegated analytics output shape を検証している。より広い analytics correctness は `session-analytics` に属する。`yoi` crate には、unknown subcommands/options と duplicate path handling の test がまだない。 diff --git a/resources/prompts/common/tool-usage.md b/resources/prompts/common/tool-usage.md index e7489f4f..92ff3982 100644 --- a/resources/prompts/common/tool-usage.md +++ b/resources/prompts/common/tool-usage.md @@ -7,18 +7,16 @@ You can run multiple tools simultaneously by calling them within a single respon 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 -{% 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. +Use memory proactively 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 %}Prefer a small targeted `MemoryQuery` 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. +Resident memory is 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 %} diff --git a/resources/prompts/internal.toml b/resources/prompts/internal.toml index 221ab563..5b3bd2a5 100644 --- a/resources/prompts/internal.toml +++ b/resources/prompts/internal.toml @@ -48,14 +48,6 @@ The following is the current durable session/workspace summary. Treat it as back {{ summary }}\ """ -resident_knowledge_section = """\ ---- -## Resident knowledge - -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 }}\ -""" worker_orchestration_guidance_section = "{% include \"$yoi/common/worker-orchestration\" %}" diff --git a/resources/prompts/internal/memory_consolidation_system.md b/resources/prompts/internal/memory_consolidation_system.md index 875fe510..c6293049 100644 --- a/resources/prompts/internal/memory_consolidation_system.md +++ b/resources/prompts/internal/memory_consolidation_system.md @@ -1,72 +1,42 @@ -You are the consolidation worker for a Yoi memory subsystem. +# Memory consolidation worker -Your job is to take extract activity-log staging entries together with the workspace's current `memory/*` / `knowledge/*` records, then run two steps back-to-back in this single session: +Your job is to take extract activity-log staging entries together with the workspace's current `memory/*` records, then run two steps back-to-back in this single session: -1. **Integration step** — fold staging into memory and knowledge. -2. **Tidy step** — clean up the existing records that the integration step didn't already touch. +1. **Integration step** — fold staging into memory. +2. **Tidy step** — produce a compact tidy report for stale / redundant / protected memory records. -You have: -- `MemoryRead`, `MemoryWrite`, `MemoryEdit`, `MemoryDelete` for memory and knowledge records. -- `MemoryQuery` for memory-side records (summary / decisions / requests). -- `KnowledgeQuery` for knowledge records — use it to find existing slugs before creating new ones. +You may use: -Your initial user message contains the staging entries, the full memory records, the knowledge candidate report, and the tidy hints. Existing knowledge bodies are NOT in the prompt; pull them through `KnowledgeQuery` + `MemoryRead` when relevant. +- `MemoryQuery` to find existing memory records. +- `MemoryRead`, `MemoryWrite`, `MemoryEdit`, `MemoryDelete` for memory records. -# Memory language +Your initial user message contains the staging entries, the full memory records, and the tidy hints. -- `language`: `{{ language }}`. -- Write durable memory and knowledge prose in this language, including frontmatter descriptions and record bodies. -- Existing records in another language may be rewritten into this language when you touch them for integration or tidy work; do not rewrite untouched records only for language normalization. -- Preserve code identifiers, paths, command names, quoted user text, logs, and external proper nouns when translation would reduce fidelity. +## Language -# Common rules (both steps) +- `language`: `{{language}}` +- Write durable memory prose in this language, including frontmatter descriptions and record bodies. +- Preserve literal identifiers, paths, commands, branch names, issue IDs, tool names, model names, and quoted user/system text as-is. +- If the configured language is unclear, use English. -- **Do not invent provenance.** Decisions / Requests `sources` arrays MUST be copied from the staging `source` field for the originating activity log entries. Do not synthesise `session_id` or entry ranges. Do not fabricate `last_sources` for Knowledge. -- **Rewrite is allowed and often preferred over append.** When integrating new information, restructure existing records to raise information density. Preserve the existing claims, rationale, and `sources` while you compress. -- **Update over create.** If an existing slug fits, edit it. Only create a new slug when no existing record fits and you can articulate why. -- **`replaced` over delete.** When a Decision is superseded by a different one, mark the old one `status: replaced` with `replaced_by: `. Do not silently drop it. -- **Don't duplicate static docs.** Skip content that already lives in `AGENTS.md`, `docs/plan/*`, or other fixed project documents. -- **Respect authoritative project records.** Issue trackers, task boards, planning documents, changelogs, version-control history, and generated reports are authoritative for their exact contents. Do not mirror them verbatim, preserve raw status churn, or maintain a parallel ledger in memory. Durable project-management facts and abstractions are valid when they help future work: recurring workflow constraints, prioritisation rationale, long-lived ownership / process decisions, and stable lessons that cut across individual items. If a candidate write only makes sense as an exact status mirror or when paired with a transient identifier, drop it. -- **Empty output is fine.** If a staging entry doesn't justify a memory write, skip it. -- **Slug rules.** Slugs are kebab-case, short, recognisable, and must be unique within their kind. Same-slug create is a linter error — use Edit instead. -- **Linter errors come back as tool errors.** When the memory linter rejects a write, read the error, fix the issue (missing frontmatter field, oversized body, unknown reference, etc.), and try again. Do not work around the rule. +## Integration step -# Integration step +The generated memory principle is: do not mirror tickets, task boards, reports, changelogs, git history, or generated artifacts verbatim. Keep durable abstractions, policy, rationale, recurring constraints, and user preferences. -Walk every staging entry in the input. For each one: +- Summary (`summary.md`) is resident body context. Keep it concise and strategic. +- Decisions (`memory/decisions/*.md`) capture durable accepted direction/rationale. +- Requests (`memory/requests/*.md`) capture durable user preferences or standing instructions. +- Preserve and update sources for decisions/requests when staging entries support them. +- **Do not invent provenance.** Decisions / Requests `sources` arrays MUST be copied from the staging `source` field for the originating activity log entries. Do not synthesise `session_id` or entry ranges. +- Keep records useful as durable context. Delete or merge stale duplicates only when safe and supported by the supplied evidence. -- **Routing by staging field:** - - `decisions` (staging) → `memory/decisions/.md`, but only when the entry is a real **design / policy / approach** judgement. "We did X in this session" is not a decision — it's a session log; drop it. The rationale must outlive the session. - - `requests` (staging) → `memory/requests/.md`. Copy `sources` verbatim. - - `attempts` (staging) → default is **drop**. Memory has no `attempts/` folder by design; do not invent one and do not stash attempts under `decisions/`. The only exception is when several attempts together form a durable trend worth a one-line summary in `memory/summary.md` (e.g. "X reliably fails on Y"). - - `discussions` (staging) → if the discussion settled on a design / policy direction during the slice, fold the conclusion into a `decisions/` record. If it stayed unresolved but the question itself is durable, fold a one-line note into `summary.md`. Otherwise drop. Never create a `decisions/` record that just records "we discussed X". -- Update existing knowledge records when the staging activity refines them. Use `KnowledgeQuery` to find candidates before creating anything new. -- **Knowledge creation is gated.** Only create a new `knowledge/.md` when the originating source appears in the supplied "Knowledge candidate report". When the report is empty (the metrics pipeline is still being built), do not create new knowledge — fold the activity into decisions / requests / summary or update existing knowledge instead. -- Rewrite `memory/summary.md` only when needed. Aim for 1–5k tokens. Preserve the high-level shape (current focus, recent decisions, stable facts) while pruning stale items. +## Tidy step -# Tidy step +Once the integration step is done, evaluate every existing memory record against four categories: -Once the integration step is done, evaluate every existing memory and knowledge record against four categories: +- `obsolete`: contradicted or no longer useful. +- `superseded`: replaced by a newer/more accurate record; include the replacement slug if obvious. +- `duplicate`: overlaps enough to merge/delete; include the canonical slug if obvious. +- `protected`: keep despite low recent use because it is foundational, policy-like, safety-critical, or currently relevant. -- `outdated`: was correct, no longer matches the current implementation / policy / operation. -- `superseded`: another record is now the de-facto authoritative one; this one is mostly redundant. -- `unused`: not wrong, but rarely referenced — noise rather than signal. -- `noisy`: useful content but bad shape (overlap, sources accumulation, fractured slugs that should merge). - -A single record may fall into more than one category. Choose one of `drop / merge / split / trim / rewrite`: - -- Prefer `merge` and `trim` over `drop` for anything you'd flag as `unused` or `noisy` — git can reverse you, but a confidently-wrong drop hurts discovery. -- `drop` is allowed for `outdated` / `superseded` records you can justify in the diff. -- `replaced` markers (`status: replaced`) and chains pointed at by the tidy hints should be collapsed in this step. - -**Protection threshold.** When the tidy hints include explicit-invoke metrics, records with `frequency >= 1.0 invokes/Mtoken` are off-limits to drop / large compression. The metrics pipeline is not always populated; when the input lacks frequency data, behave conservatively and skip drop on long-standing records. - -# Closing the turn - -When both steps are done, write a short final assistant message stating: - -- which staging entries you folded in (by short summary, not by ID), -- which existing records you touched (slug + operation), -- anything you intentionally left alone and why. - -Then end the turn. Do not ask questions — there is no human in the loop for this run. +Emit tidy recommendations in your final response. Do not delete protected records. diff --git a/resources/prompts/internal/memory_extract_system.md b/resources/prompts/internal/memory_extract_system.md index 15585c59..5c4de45e 100644 --- a/resources/prompts/internal/memory_extract_system.md +++ b/resources/prompts/internal/memory_extract_system.md @@ -1,6 +1,6 @@ You are the activity extractor for a Yoi memory subsystem. -Your single job: read the supplied conversation slice and emit a structured JSON record of "what happened" via the `write_extracted` tool. You are not consolidating, summarising, or generating knowledge — that is the consolidation worker's job. +Your single job: read the supplied conversation slice and emit a structured JSON record of "what happened" via the `write_extracted` tool. You are not consolidating, summarising, or generating memory — that is the consolidation worker's job. # Memory language diff --git a/web/workspace/src/lib/generated/protocol.ts b/web/workspace/src/lib/generated/protocol.ts index d59209fd..b3da98cd 100644 --- a/web/workspace/src/lib/generated/protocol.ts +++ b/web/workspace/src/lib/generated/protocol.ts @@ -6,7 +6,7 @@ export type AlertLevel = "warn" | "error"; export type AlertSource = "worker" | "engine" | "compactor" | "agents_md"; -export type CompletionKind = "file" | "knowledge"; +export type CompletionKind = "file"; export type WorkerStatus = "idle" | "running" | "paused"; @@ -79,7 +79,7 @@ message: string, */ timestamp_ms: number, }; -export type Segment = { "kind": "text", content: string, } | { "kind": "paste", id: number, chars: number, lines: number, content: string, } | { "kind": "file_ref", path: string, } | { "kind": "knowledge_ref", slug: string, } | { "kind": "unknown" }; +export type Segment = { "kind": "text", content: string, } | { "kind": "paste", id: number, chars: number, lines: number, content: string, } | { "kind": "file_ref", path: string, } | { "kind": "unknown" }; export type WorkerEvent = { "kind": "turn_ended", worker_name: string, } | { "kind": "errored", worker_name: string, message: string, } | { "kind": "shut_down", worker_name: string, } | { "kind": "scope_sub_delegated", /** diff --git a/web/workspace/src/lib/workspace/console/composer-command.test.ts b/web/workspace/src/lib/workspace/console/composer-command.test.ts index 8f7b05d5..baffa920 100644 --- a/web/workspace/src/lib/workspace/console/composer-command.test.ts +++ b/web/workspace/src/lib/workspace/console/composer-command.test.ts @@ -1,72 +1,20 @@ -import { - buildComposerRequest, - parseSigilSegments, -} from "./composer-command.ts"; +import { parseSigilSegments } from "./composer-command.ts"; -declare const Deno: { - test(name: string, fn: () => void): void; -}; - -function assert(condition: unknown, message: string): asserts condition { - if (!condition) { - throw new Error(message); - } -} +declare const Deno: { test(name: string, fn: () => void): void }; function assertEquals(actual: T, expected: T): void { - const actualJson = JSON.stringify(actual); - const expectedJson = JSON.stringify(expected); - if (actualJson !== expectedJson) { - throw new Error(`Expected ${expectedJson}, got ${actualJson}`); - } + const a = JSON.stringify(actual); + const e = JSON.stringify(expected); + if (a !== e) throw new Error(`Expected ${e}, got ${a}`); } -Deno.test("parseSigilSegments converts TUI-style references", () => { - assertEquals( - parseSigilSegments("read @src/main.rs then #memory and /literal"), - [ - { kind: "text", content: "read " }, - { kind: "file_ref", path: "src/main.rs" }, - { kind: "text", content: " then " }, - { kind: "knowledge_ref", slug: "memory" }, - { kind: "text", content: " and /literal" }, - ], - ); -}); - -Deno.test("buildComposerRequest sends user segments when sigils are present", () => { - const result = buildComposerRequest("inspect @README.md"); - assert(result.ok, "request should be accepted"); - assertEquals(result.request?.kind, "user"); - assertEquals(result.request?.content, "inspect @README.md"); - assertEquals(result.request?.segments, [ - { kind: "text", content: "inspect " }, - { kind: "file_ref", path: "README.md" }, +Deno.test("parseSigilSegments turns file sigils into file refs", () => { + assertEquals(parseSigilSegments("read @src/main.rs"), [ + { kind: "text", content: "read " }, + { kind: "file_ref", path: "src/main.rs" }, ]); }); -Deno.test("buildComposerRequest parses colon commands", () => { - const compact = buildComposerRequest(":compact"); - assert(compact.ok, "compact should be accepted"); - assertEquals(compact.request, { kind: "compact", content: "" }); - - const peer = buildComposerRequest(":peer companion"); - assert(peer.ok, "peer should be accepted"); - assertEquals(peer.request, { kind: "register_peer", content: "companion" }); - - const help = buildComposerRequest(":help compact"); - assert(help.ok, "help should be accepted"); - assertEquals(help.request, undefined); - assert( - help.notice?.includes(":compact"), - "help should return a local notice", - ); -}); - -Deno.test("buildComposerRequest rejects invalid colon commands", () => { - const unknown = buildComposerRequest(":does-not-exist"); - assert(!unknown.ok, "unknown command should be rejected"); - - const invalidPeer = buildComposerRequest(":peer"); - assert(!invalidPeer.ok, "invalid peer command should be rejected"); +Deno.test("parseSigilSegments leaves hash sigils as plain text", () => { + assertEquals(parseSigilSegments("ask #memory"), [{ kind: "text", content: "ask #memory" }]); }); diff --git a/web/workspace/src/lib/workspace/console/composer-command.ts b/web/workspace/src/lib/workspace/console/composer-command.ts index d8f40c5b..34681b3a 100644 --- a/web/workspace/src/lib/workspace/console/composer-command.ts +++ b/web/workspace/src/lib/workspace/console/composer-command.ts @@ -161,7 +161,7 @@ function invalidUsage(name: string): ComposerCommandResult { export function parseSigilSegments(input: string): Segment[] { const segments: Segment[] = []; - const pattern = /(^|\s)([@#])([^\s]+)/g; + const pattern = /(^|\s)([@])([^\s]+)/g; let cursor = 0; let match: RegExpExecArray | null; while ((match = pattern.exec(input)) !== null) { @@ -185,8 +185,6 @@ function sigilSegment(sigil: string, value: string): Segment { switch (sigil) { case "@": return { kind: "file_ref", path: value }; - case "#": - return { kind: "knowledge_ref", slug: value }; default: return { kind: "text", content: `${sigil}${value}` }; } diff --git a/web/workspace/src/lib/workspace/console/composer-completion.test.ts b/web/workspace/src/lib/workspace/console/composer-completion.test.ts index a340b86c..0f62ed03 100644 --- a/web/workspace/src/lib/workspace/console/composer-completion.test.ts +++ b/web/workspace/src/lib/workspace/console/composer-completion.test.ts @@ -22,7 +22,7 @@ function assertEquals(actual: T, expected: T): void { } } -Deno.test("completionTokenAt detects TUI-style sigils before the cursor", () => { +Deno.test("completionTokenAt detects command and file sigils before the cursor", () => { assertEquals(completionTokenAt("open @src/ma", "open @src/ma".length), { sigil: "@", kind: "file", @@ -31,8 +31,8 @@ Deno.test("completionTokenAt detects TUI-style sigils before the cursor", () => prefix: "src/ma", }); assertEquals(completionTokenAt(":comp", 5)?.kind, "command"); - assertEquals(completionTokenAt("ask #mem", 8)?.kind, "knowledge"); assertEquals(completionTokenAt("run /work", 9), null); + assertEquals(completionTokenAt("ask #plain", "ask #plain".length), null); }); Deno.test("applyCompletion replaces the active token and advances the cursor", () => { diff --git a/web/workspace/src/lib/workspace/console/composer-completion.ts b/web/workspace/src/lib/workspace/console/composer-completion.ts index 1eeaf143..41cc3d52 100644 --- a/web/workspace/src/lib/workspace/console/composer-completion.ts +++ b/web/workspace/src/lib/workspace/console/composer-completion.ts @@ -1,7 +1,7 @@ -export type ComposerCompletionKind = "command" | "file" | "knowledge"; +export type ComposerCompletionKind = "command" | "file"; export type ComposerCompletionToken = { - sigil: ":" | "@" | "#"; + sigil: ":" | "@"; kind: ComposerCompletionKind; start: number; end: number; @@ -35,7 +35,7 @@ export function completionTokenAt( ): ComposerCompletionToken | null { const boundedCursor = Math.max(0, Math.min(cursor, value.length)); const before = value.slice(0, boundedCursor); - const match = /(^|\s)([:@#])([^\s]*)$/.exec(before); + const match = /(^|\s)([:@])([^\s]*)$/.exec(before); if (!match) { return null; } @@ -85,7 +85,5 @@ function completionKindForSigil( return "command"; case "@": return "file"; - case "#": - return "knowledge"; } } diff --git a/web/workspace/src/lib/workspace/console/model.test.ts b/web/workspace/src/lib/workspace/console/model.test.ts index eed464a0..c15d48f8 100644 --- a/web/workspace/src/lib/workspace/console/model.test.ts +++ b/web/workspace/src/lib/workspace/console/model.test.ts @@ -35,23 +35,7 @@ Deno.test("workerConsoleHref encodes runtime and worker target authority", () => ); }); -Deno.test("segmentsToText preserves protocol segment semantics", () => { - const text = segmentsToText([ - { kind: "text", content: "hello" }, - { kind: "file_ref", path: "/tmp/example.md" }, - { kind: "knowledge_ref", slug: "design-note" }, - ]); - assert(text.includes("hello"), "text segment should render content"); - assert( - text.includes("@file /tmp/example.md"), - "file ref should render as a file reference", - ); - assert( - text.includes("@knowledge design-note"), - "knowledge ref should render as a knowledge reference", - ); -}); Deno.test("projectConsole projects visible protocol rows", () => { const projection = projectConsole([ diff --git a/web/workspace/src/lib/workspace/console/model.ts b/web/workspace/src/lib/workspace/console/model.ts index e1a5278a..8e685550 100644 --- a/web/workspace/src/lib/workspace/console/model.ts +++ b/web/workspace/src/lib/workspace/console/model.ts @@ -317,8 +317,6 @@ export function segmentsToText(segments: Segment[]): string { `[paste ${segment.id}: ${segment.chars} chars / ${segment.lines} lines]`; case "file_ref": return `@file ${segment.path}`; - case "knowledge_ref": - return `@knowledge ${segment.slug}`; case "unknown": return "[unknown segment]"; } diff --git a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte index 9fc73632..c18c0415 100644 --- a/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte @@ -46,7 +46,7 @@ } type WorkerCompletionsResult = { - kind: "file" | "knowledge"; + kind: "file"; prefix: string; entries: ComposerCompletionEntry[]; diagnostics: Diagnostic[];