refactor: extract workflow crate

This commit is contained in:
2026-05-11 22:49:07 +09:00
parent 520895f1c9
commit f70975789e
25 changed files with 686 additions and 311 deletions
+1
View File
@@ -27,6 +27,7 @@ fs4 = { workspace = true, features = ["sync"] }
libc = { workspace = true }
schemars = { workspace = true }
memory = { workspace = true }
workflow-crate = { package = "workflow", path = "../workflow" }
uuid = { workspace = true, features = ["v7"] }
session-metrics = { workspace = true }
+25 -22
View File
@@ -150,7 +150,7 @@ pub struct Pod<C: LlmClient, St: Store> {
prompts: Arc<PromptCatalog>,
/// Registry loaded from `<workspace>/.insomnia/workflow/*.md` when
/// memory is enabled. Missing memory config keeps this empty.
workflow_registry: memory::WorkflowRegistry,
workflow_registry: workflow_crate::WorkflowRegistry,
/// Memory workspace layout used by the workflow resolver to load required
/// Knowledge records by exact slug.
memory_layout: Option<memory::WorkspaceLayout>,
@@ -323,7 +323,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
scope_allocation: None,
callback_socket: None,
prompts,
workflow_registry: memory::WorkflowRegistry::empty(),
workflow_registry: workflow_crate::WorkflowRegistry::empty(),
memory_layout: None,
inject_resident_knowledge: true,
pending_scope_snapshot: Arc::new(Mutex::new(None)),
@@ -865,13 +865,13 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
} else {
None
};
let resident_workflows: Vec<memory::ResidentWorkflowEntry> =
let resident_workflows: Vec<workflow_crate::ResidentWorkflowEntry> =
if self.inject_resident_knowledge && self.memory_layout.is_some() {
self.workflow_registry.resident_entries()
} else {
Vec::new()
};
let resident_workflow_slice: Option<&[memory::ResidentWorkflowEntry]> =
let resident_workflow_slice: Option<&[workflow_crate::ResidentWorkflowEntry]> =
if self.inject_resident_knowledge && self.memory_layout.is_some() {
Some(&resident_workflows)
} else {
@@ -1106,7 +1106,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
fn resident_exposure_snapshots(
&self,
knowledge: &[memory::ResidentKnowledgeEntry],
workflows: &[memory::ResidentWorkflowEntry],
workflows: &[workflow_crate::ResidentWorkflowEntry],
) -> Vec<memory::UsageRecordSnapshot> {
let Some(layout) = self.memory_layout.as_ref() else {
return Vec::new();
@@ -1220,8 +1220,8 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
let Segment::WorkflowInvoke { slug } = seg else {
continue;
};
let parsed =
memory::Slug::parse(slug.clone()).map_err(WorkflowResolveError::InvalidSlug)?;
let parsed = workflow_crate::Slug::parse(slug.clone())
.map_err(WorkflowResolveError::InvalidSlug)?;
let record = self
.workflow_registry
.get(&parsed)
@@ -2886,7 +2886,7 @@ pub enum PodError {
ConsolidationLock(#[source] memory::consolidate::LockError),
#[error("workflow load failed: {0}")]
WorkflowLoad(#[source] memory::WorkflowLoadError),
WorkflowLoad(#[source] workflow_crate::WorkflowLoadError),
#[error("workflow invocation failed: {0}")]
WorkflowResolve(#[from] WorkflowResolveError),
@@ -2909,14 +2909,14 @@ struct PodCommon {
scope: Scope,
client: Box<dyn LlmClient>,
prompts: Arc<PromptCatalog>,
workflow_registry: memory::WorkflowRegistry,
workflow_registry: workflow_crate::WorkflowRegistry,
memory_layout: Option<memory::WorkspaceLayout>,
system_prompt_template: Option<SystemPromptTemplate>,
/// SKILL.md shadow events surfaced during workflow-registry build.
/// The Pod constructor drains these into the notify buffer right
/// after the Pod is materialised so the first LLM request observes
/// any skill ↔ workflow collisions.
skill_shadows: Vec<memory::ShadowedSkill>,
skill_shadows: Vec<workflow_crate::ShadowedSkill>,
}
/// Resolve pwd / scope / LLM client / prompt catalog from a validated
@@ -2968,8 +2968,8 @@ fn prepare_pod_common_from_scope(
.as_ref()
.map(|mem| memory::WorkspaceLayout::resolve(mem, &pwd));
let mut workflow_registry = match memory_layout.as_ref() {
Some(layout) => memory::load_workflows(layout).map_err(PodError::WorkflowLoad)?,
None => memory::WorkflowRegistry::empty(),
Some(layout) => workflow_crate::load_workflows(layout).map_err(PodError::WorkflowLoad)?,
None => workflow_crate::WorkflowRegistry::empty(),
};
let skill_shadows = ingest_skills(&mut workflow_registry, manifest);
@@ -2998,21 +2998,21 @@ fn prepare_pod_common_from_scope(
///
/// Skills come exclusively from the manifest's `[skills] directories`
/// list (resolved against the manifest base directory). Internal
/// Workflows already loaded via [`memory::load_workflows`] take priority
/// Workflows already loaded via [`workflow_crate::load_workflows`] take priority
/// over skills sharing the same slug; collisions are surfaced as
/// [`memory::ShadowedSkill`] events that the caller pushes onto the
/// [`workflow_crate::ShadowedSkill`] events that the caller pushes onto the
/// Pod's notification buffer.
fn ingest_skills(
registry: &mut memory::WorkflowRegistry,
registry: &mut workflow_crate::WorkflowRegistry,
manifest: &PodManifest,
) -> Vec<memory::ShadowedSkill> {
) -> Vec<workflow_crate::ShadowedSkill> {
let mut shadows = Vec::new();
let Some(skills_cfg) = manifest.skills.as_ref() else {
return shadows;
};
for dir in &skills_cfg.directories {
for skill in memory::load_skills_from_dir(dir) {
let source = memory::WorkflowSource::Skill { dir: dir.clone() };
for skill in workflow_crate::load_skills_from_dir(dir) {
let source = workflow_crate::WorkflowSource::Skill { dir: dir.clone() };
let record = skill.into_workflow_record(source);
if let Some(shadow) = registry.merge_skill(record) {
shadows.push(shadow);
@@ -3024,7 +3024,7 @@ fn ingest_skills(
/// Drain skill-ingest shadow events into the Pod's notify buffer so the
/// first LLM request renders them as system-message attachments.
fn drain_skill_shadows<C, S>(pod: &Pod<C, S>, shadows: Vec<memory::ShadowedSkill>)
fn drain_skill_shadows<C, S>(pod: &Pod<C, S>, shadows: Vec<workflow_crate::ShadowedSkill>)
where
C: LlmClient,
S: Store,
@@ -3048,6 +3048,9 @@ fn build_scope_with_memory(manifest: &PodManifest, pwd: &Path) -> Result<Scope,
if let Some(mem) = manifest.memory.as_ref() {
let layout = memory::WorkspaceLayout::resolve(mem, pwd);
scope_config.deny.extend(memory::deny_write_rules(&layout));
scope_config
.deny
.extend(workflow_crate::deny_write_rules(&layout));
}
scope_config.allow.extend(skill_dir_read_rules(manifest));
Scope::from_config(&scope_config).map_err(PodError::Scope)
@@ -3215,7 +3218,7 @@ permission = "write"
#[test]
fn ingest_skills_returns_empty_when_skills_section_missing() {
let manifest = minimal_manifest_with_skills(vec![]);
let mut registry = memory::WorkflowRegistry::empty();
let mut registry = workflow_crate::WorkflowRegistry::empty();
let shadows = ingest_skills(&mut registry, &manifest);
assert!(shadows.is_empty());
assert!(registry.is_empty());
@@ -3233,13 +3236,13 @@ permission = "write"
.unwrap();
let manifest = minimal_manifest_with_skills(vec![skills_root.clone()]);
let mut registry = memory::WorkflowRegistry::empty();
let mut registry = workflow_crate::WorkflowRegistry::empty();
let shadows = ingest_skills(&mut registry, &manifest);
// workspace skill `alpha` should be registered (no collision).
assert!(
registry
.get(&memory::Slug::parse("alpha").unwrap())
.get(&workflow_crate::Slug::parse("alpha").unwrap())
.is_some()
);
// No workflow exists to shadow `alpha`, so no shadow event for it.
+2 -1
View File
@@ -18,10 +18,11 @@ use std::sync::Arc;
use chrono::{DateTime, SecondsFormat, Utc};
use manifest::Scope;
use memory::{ResidentKnowledgeEntry, ResidentWorkflowEntry};
use memory::ResidentKnowledgeEntry;
use minijinja::value::Value;
use minijinja::{Environment, ErrorKind, UndefinedBehavior};
use thiserror::Error;
use workflow_crate::ResidentWorkflowEntry;
use crate::prompt::catalog::{CatalogError, PromptCatalog};
use crate::prompt::loader::{LoaderError, PromptLoader, PromptRef};
+7 -6
View File
@@ -9,12 +9,13 @@
use std::fmt;
use llm_worker::Item;
use memory::WorkspaceLayout;
use memory::schema::split_frontmatter;
use memory::{Slug, WorkflowRegistry, WorkspaceLayout};
use workflow_crate::{Slug, WorkflowRegistry};
#[derive(Debug)]
pub enum WorkflowResolveError {
InvalidSlug(memory::LintError),
InvalidSlug(workflow_crate::WorkflowLintError),
NotFound {
slug: String,
},
@@ -90,7 +91,7 @@ pub fn resolve_workflow_invocation(
let mut out = Vec::new();
for req in &record.requires {
let path = layout.knowledge_path(req);
let path = layout.knowledge_dir().join(format!("{req}.md"));
let raw = std::fs::read_to_string(&path).map_err(|source| {
if source.kind() == std::io::ErrorKind::NotFound {
WorkflowResolveError::KnowledgeNotFound {
@@ -150,7 +151,7 @@ mod tests {
&dir.path().join(".insomnia/workflow/run-it.md"),
"---\ndescription: run\nrequires: [policy]\n---\nworkflow body\n",
);
let registry = memory::load_workflows(&layout).unwrap();
let registry = workflow_crate::load_workflows(&layout).unwrap();
(dir, layout, registry)
}
@@ -174,7 +175,7 @@ mod tests {
&dir.path().join(".insomnia/workflow/hidden.md"),
"---\ndescription: hidden\nuser_invocable: false\n---\nbody\n",
);
let registry = memory::load_workflows(&layout).unwrap();
let registry = workflow_crate::load_workflows(&layout).unwrap();
let err = resolve_workflow_invocation(&registry, &layout, "hidden").unwrap_err();
assert!(matches!(err, WorkflowResolveError::NotUserInvocable { .. }));
}
@@ -187,7 +188,7 @@ mod tests {
&dir.path().join(".insomnia/workflow/bad.md"),
"---\ndescription: bad\nrequires: [ghost]\n---\nbody\n",
);
let registry = memory::load_workflows(&layout).unwrap();
let registry = workflow_crate::load_workflows(&layout).unwrap();
let err = resolve_workflow_invocation(&registry, &layout, "bad").unwrap_err();
assert!(matches!(
err,