Merge branch 'tui-knowledge-completion' into develop
This commit is contained in:
commit
59bf20f2cd
|
|
@ -21,7 +21,7 @@ pub mod workspace;
|
||||||
pub use error::{LintError, LintWarning, MemoryError};
|
pub use error::{LintError, LintWarning, MemoryError};
|
||||||
pub use extract::ExtractPointerPayload;
|
pub use extract::ExtractPointerPayload;
|
||||||
pub use linter::{LintReport, Linter};
|
pub use linter::{LintReport, Linter};
|
||||||
pub use resident::{ResidentKnowledgeEntry, collect_resident_knowledge};
|
pub use resident::{ResidentKnowledgeEntry, collect_resident_knowledge, list_knowledge_slugs};
|
||||||
pub use scope::deny_write_rules;
|
pub use scope::deny_write_rules;
|
||||||
pub use slug::Slug;
|
pub use slug::Slug;
|
||||||
pub use usage::{
|
pub use usage::{
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
//! Collect resident-injection candidates from the workspace.
|
//! Workspace knowledge enumeration helpers.
|
||||||
//!
|
//!
|
||||||
//! Walks `<workspace>/knowledge/*.md`, returns the records whose
|
//! Two surfaces, both walking `<workspace>/.insomnia/knowledge/*.md`:
|
||||||
//! frontmatter has `model_invokation: true` as `(slug, description)`
|
//!
|
||||||
//! pairs sorted by slug. The Pod system-prompt assembler appends them
|
//! - [`collect_resident_knowledge`] — resident-injection candidates
|
||||||
//! into the trailing section so descriptions sit next to the scope
|
//! (`model_invokation: true`) returned as `(slug, description)` pairs
|
||||||
//! summary and AGENTS.md.
|
//! for the Pod system-prompt assembler.
|
||||||
|
//! - [`list_knowledge_slugs`] — every slug whose file parses, regardless
|
||||||
|
//! of `model_invokation`. Used by the Pod 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
|
//! Files that fail to read or parse are skipped silently — the Linter
|
||||||
//! enforces shape on write, so a malformed file here means external
|
//! enforces shape on write, so a malformed file here means external
|
||||||
|
|
@ -19,17 +23,40 @@ pub struct ResidentKnowledgeEntry {
|
||||||
pub description: String,
|
pub description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Walk `<workspace>/knowledge/*.md` and return entries whose
|
/// Walk `<workspace>/.insomnia/knowledge/*.md` and return entries whose
|
||||||
/// frontmatter has `model_invokation: true`, sorted by slug. A missing
|
/// frontmatter has `model_invokation: true`, sorted by slug. A missing
|
||||||
/// `knowledge/` directory yields an empty vec.
|
/// directory yields an empty vec.
|
||||||
pub fn collect_resident_knowledge(layout: &WorkspaceLayout) -> Vec<ResidentKnowledgeEntry> {
|
pub fn collect_resident_knowledge(layout: &WorkspaceLayout) -> Vec<ResidentKnowledgeEntry> {
|
||||||
|
let mut out: Vec<ResidentKnowledgeEntry> = 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk `<workspace>/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<String> {
|
||||||
|
let mut out: Vec<String> = 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 dir = layout.knowledge_dir();
|
||||||
let entries = match std::fs::read_dir(&dir) {
|
let entries = match std::fs::read_dir(&dir) {
|
||||||
Ok(it) => it,
|
Ok(it) => it,
|
||||||
Err(_) => return Vec::new(),
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut out: Vec<ResidentKnowledgeEntry> = Vec::new();
|
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if !path.is_file() {
|
if !path.is_file() {
|
||||||
|
|
@ -55,15 +82,8 @@ pub fn collect_resident_knowledge(layout: &WorkspaceLayout) -> Vec<ResidentKnowl
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
if fm.model_invokation {
|
visit(slug, fm);
|
||||||
out.push(ResidentKnowledgeEntry {
|
|
||||||
slug,
|
|
||||||
description: fm.description,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
out.sort_by(|a, b| a.slug.cmp(&b.slug));
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -164,4 +184,41 @@ mod tests {
|
||||||
let got = collect_resident_knowledge(&layout);
|
let got = collect_resident_knowledge(&layout);
|
||||||
assert_eq!(got.len(), 1);
|
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(".insomnia/knowledge/bad.md"),
|
||||||
|
"---\nthis is not yaml: : :\n---\nbody\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
dir.path().join(".insomnia/knowledge/note.txt"),
|
||||||
|
"not markdown\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let got = list_knowledge_slugs(&layout);
|
||||||
|
assert_eq!(got, vec!["good"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -388,6 +388,12 @@ impl PodController {
|
||||||
.map(|slug| crate::shared_state::WorkflowCandidate { slug })
|
.map(|slug| crate::shared_state::WorkflowCandidate { slug })
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
|
shared_state.set_knowledge(
|
||||||
|
pod.knowledge_completions()
|
||||||
|
.into_iter()
|
||||||
|
.map(|slug| crate::shared_state::KnowledgeCandidate { slug })
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
runtime_dir.write_manifest(&manifest_toml).await?;
|
runtime_dir.write_manifest(&manifest_toml).await?;
|
||||||
runtime_dir.write_status(&shared_state).await?;
|
runtime_dir.write_status(&shared_state).await?;
|
||||||
runtime_dir.write_history(&shared_state).await?;
|
runtime_dir.write_history(&shared_state).await?;
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,15 @@ async fn handle_connection(stream: tokio::net::UnixStream, handle: PodHandle) {
|
||||||
is_dir: c.is_dir,
|
is_dir: c.is_dir,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
protocol::CompletionKind::Knowledge => Vec::new(),
|
protocol::CompletionKind::Knowledge => handle
|
||||||
|
.shared_state
|
||||||
|
.list_knowledge_completions(&prefix)
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| protocol::CompletionEntry {
|
||||||
|
value: c.slug,
|
||||||
|
is_dir: false,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
protocol::CompletionKind::Workflow => handle
|
protocol::CompletionKind::Workflow => handle
|
||||||
.shared_state
|
.shared_state
|
||||||
.list_workflow_completions(&prefix)
|
.list_workflow_completions(&prefix)
|
||||||
|
|
|
||||||
|
|
@ -1237,6 +1237,13 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
self.workflow_registry.list_user_invocable("")
|
self.workflow_registry.list_user_invocable("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn knowledge_completions(&self) -> Vec<String> {
|
||||||
|
self.memory_layout
|
||||||
|
.as_ref()
|
||||||
|
.map(memory::list_knowledge_slugs)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Flatten a typed segment list into the single string the Worker
|
/// Flatten a typed segment list into the single string the Worker
|
||||||
/// receives as the user message, and emit user-facing alerts for
|
/// receives as the user message, and emit user-facing alerts for
|
||||||
/// segments that fall through to placeholder (knowledge / workflow
|
/// segments that fall through to placeholder (knowledge / workflow
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,11 @@ pub struct WorkflowCandidate {
|
||||||
pub slug: String,
|
pub slug: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct KnowledgeCandidate {
|
||||||
|
pub slug: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Shared state between PodController and runtime directory.
|
/// Shared state between PodController and runtime directory.
|
||||||
///
|
///
|
||||||
/// Controller updates this in-memory; RuntimeDir writes it to disk.
|
/// Controller updates this in-memory; RuntimeDir writes it to disk.
|
||||||
|
|
@ -37,6 +42,7 @@ pub struct PodSharedState {
|
||||||
/// directly without spinning up a controller).
|
/// directly without spinning up a controller).
|
||||||
fs_view: OnceLock<PodFsView>,
|
fs_view: OnceLock<PodFsView>,
|
||||||
workflows: OnceLock<Vec<WorkflowCandidate>>,
|
workflows: OnceLock<Vec<WorkflowCandidate>>,
|
||||||
|
knowledge: OnceLock<Vec<KnowledgeCandidate>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PodSharedState {
|
impl PodSharedState {
|
||||||
|
|
@ -56,6 +62,7 @@ impl PodSharedState {
|
||||||
user_segments: RwLock::new(Vec::new()),
|
user_segments: RwLock::new(Vec::new()),
|
||||||
fs_view: OnceLock::new(),
|
fs_view: OnceLock::new(),
|
||||||
workflows: OnceLock::new(),
|
workflows: OnceLock::new(),
|
||||||
|
knowledge: OnceLock::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,6 +95,23 @@ impl PodSharedState {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_knowledge(&self, knowledge: Vec<KnowledgeCandidate>) {
|
||||||
|
let _ = self.knowledge.set(knowledge);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_knowledge_completions(&self, prefix: &str) -> Vec<KnowledgeCandidate> {
|
||||||
|
self.knowledge
|
||||||
|
.get()
|
||||||
|
.map(|items| {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.filter(|candidate| candidate.slug.starts_with(prefix))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn user_segments(&self) -> Vec<Vec<Segment>> {
|
pub fn user_segments(&self) -> Vec<Vec<Segment>> {
|
||||||
self.user_segments
|
self.user_segments
|
||||||
.read()
|
.read()
|
||||||
|
|
@ -230,4 +254,38 @@ mod tests {
|
||||||
assert!(parsed.is_array());
|
assert!(parsed.is_array());
|
||||||
assert_eq!(parsed[0]["role"], "assistant");
|
assert_eq!(parsed[0]["role"], "assistant");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn knowledge_completions_empty_when_unset() {
|
||||||
|
let state = test_state();
|
||||||
|
assert!(state.list_knowledge_completions("").is_empty());
|
||||||
|
assert!(state.list_knowledge_completions("foo").is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn knowledge_completions_filter_by_prefix() {
|
||||||
|
let state = test_state();
|
||||||
|
state.set_knowledge(vec![
|
||||||
|
KnowledgeCandidate {
|
||||||
|
slug: "alpha".into(),
|
||||||
|
},
|
||||||
|
KnowledgeCandidate {
|
||||||
|
slug: "alphabet".into(),
|
||||||
|
},
|
||||||
|
KnowledgeCandidate {
|
||||||
|
slug: "beta".into(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
let all = state.list_knowledge_completions("");
|
||||||
|
assert_eq!(all.len(), 3);
|
||||||
|
let alpha = state.list_knowledge_completions("alpha");
|
||||||
|
assert_eq!(
|
||||||
|
alpha
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.slug.as_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec!["alpha", "alphabet"]
|
||||||
|
);
|
||||||
|
assert!(state.list_knowledge_completions("zzz").is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user