pub mod activation;
pub mod assembler;
pub mod entry;
pub mod host;
pub mod lifecycle;
pub mod lorebook;
pub mod plugin;
#[cfg(feature = "stdlib")]
pub mod stdlib;
pub use activation::{ActivationEngine, ActivationReason, ActivationResult, ActivationState};
pub use assembler::{
AssembledBlock, ContextAssembler, GuesstimationTokenizer, Slot, TokenBudget, Tokenizer,
};
pub use entry::{Entry, EntryMeta};
pub use host::{NamespaceAccess, NamespaceConfig, WeaverHost};
pub use lifecycle::{
FnLifecycle, HookError, LifecyclePlugin, PostActivationCtx, PostAssembleCtx, PostEvaluateCtx,
PreActivationCtx, PreEvaluateCtx, TriggerCtx, TurnAdvanceCtx,
};
pub use lorebook::{Lorebook, LorebookConfig};
pub use plugin::Plugin;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use weaver_lang::registry::{CommandSignature, ParamDef, WeaverCommand};
use weaver_lang::{CompiledTemplate, EvalContext, EvalError, Registry, Value};
pub struct ContextWeaver {
lorebook: Lorebook,
registry: Registry,
host: WeaverHost,
activation_state: ActivationState,
config: EngineConfig,
tokenizer: Box<dyn Tokenizer>,
available_slots: HashSet<Slot>,
lifecycle_plugins: Vec<Box<dyn LifecyclePlugin>>,
}
#[derive(Debug, Clone)]
pub struct ChatMessage {
pub role: ChatRole,
pub content: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChatRole {
User,
Assistant,
System,
}
impl ChatMessage {
pub fn user(content: impl Into<String>) -> Self {
Self {
role: ChatRole::User,
content: content.into(),
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: ChatRole::Assistant,
content: content.into(),
}
}
pub fn system(content: impl Into<String>) -> Self {
Self {
role: ChatRole::System,
content: content.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct EngineConfig {
pub max_recursion_depth: usize,
pub max_active_entries: usize,
pub max_trigger_passes: usize,
pub lenient: bool,
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
max_recursion_depth: 10,
max_active_entries: 100,
max_trigger_passes: 3,
lenient: false,
}
}
}
impl ContextWeaver {
pub fn new(lorebook: Lorebook) -> Self {
let mut host = WeaverHost::from_lorebook_config(&lorebook.config);
let mut registry = Registry::new();
register_builtins(&mut registry);
host.set_max_recursion_depth(EngineConfig::default().max_recursion_depth);
Self {
lorebook,
registry,
host,
activation_state: ActivationState::new(),
config: EngineConfig::default(),
tokenizer: Box::new(GuesstimationTokenizer),
available_slots: Slot::standard_slots().into_iter().collect(),
lifecycle_plugins: Vec::new(),
}
}
pub fn with_config(mut self, config: EngineConfig) -> Self {
self.host
.set_max_recursion_depth(config.max_recursion_depth);
self.config = config;
self
}
pub fn set_tokenizer(&mut self, tokenizer: Box<dyn Tokenizer>) {
self.tokenizer = tokenizer;
}
pub fn set_available_slots(&mut self, slots: impl IntoIterator<Item = Slot>) {
self.available_slots = slots.into_iter().collect();
}
pub fn set_variable(&mut self, scope: &str, name: &str, value: impl Into<Value>) {
self.host.set_host_variable(scope, name, value.into());
}
pub fn register_plugin(&mut self, plugin: impl Plugin) -> Result<(), plugin::PluginError> {
plugin.register(&mut self.registry);
plugin.init()
}
pub fn register_lifecycle<P: LifecyclePlugin + 'static>(&mut self, plugin: P) {
self.lifecycle_plugins.push(Box::new(plugin));
}
pub fn activation_state(&self) -> &ActivationState {
&self.activation_state
}
pub fn restore_activation_state(&mut self, state: ActivationState) {
self.activation_state = state;
}
pub fn persistent_state(&self) -> &HashMap<String, Value> {
self.host.persistent_state()
}
pub fn restore_persistent_state(&mut self, state: HashMap<String, Value>) {
self.host.restore_persistent_state(state);
}
pub fn advance_turn(&mut self) -> Result<(), ContextWeaverError> {
self.activation_state.advance_turn();
self.host.clear_transient();
let state = &mut self.activation_state;
for plugin in &mut self.lifecycle_plugins {
let plugin_name = plugin.name().to_string();
let mut ctx = TurnAdvanceCtx { state };
plugin
.on_turn_advance(&mut ctx)
.map_err(|e| ContextWeaverError::PluginHook {
plugin: plugin_name,
hook: "on_turn_advance",
source: e,
})?;
}
Ok(())
}
pub fn assemble(
&mut self,
messages: &[ChatMessage],
) -> Result<Vec<AssembledBlock>, ContextWeaverError> {
let mut messages_owned: Vec<ChatMessage> = messages.to_vec();
let turn = self.activation_state.current_turn();
for plugin in &mut self.lifecycle_plugins {
let plugin_name = plugin.name().to_string();
let mut ctx = PreActivationCtx {
messages: &mut messages_owned,
turn,
};
plugin
.pre_activation(&mut ctx)
.map_err(|e| ContextWeaverError::PluginHook {
plugin: plugin_name,
hook: "pre_activation",
source: e,
})?;
}
let entry_templates = self.build_template_map();
self.host.set_entry_templates(entry_templates);
let mut results = ActivationEngine::scan(
&self.lorebook,
messages,
&mut self.host,
&self.registry,
&self.activation_state,
);
{
let lifecycle_plugins = &mut self.lifecycle_plugins;
let lorebook = &self.lorebook;
for plugin in lifecycle_plugins {
let plugin_name = plugin.name().to_string();
let mut ctx = PostActivationCtx {
results: &mut results,
lorebook,
turn,
};
plugin
.post_activation(&mut ctx)
.map_err(|e| ContextWeaverError::PluginHook {
plugin: plugin_name,
hook: "post_activation",
source: e,
})?;
}
}
if results.len() > self.config.max_active_entries {
results.truncate(self.config.max_active_entries);
}
let mut active_ids: Vec<String> = results.iter().map(|r| r.entry_id.clone()).collect();
self.host
.set_active_entries(active_ids.iter().cloned().collect());
let mut evaluated_cache: HashMap<String, EvaluatedEntry> = HashMap::new();
for entry in self.evaluate_entries(&active_ids)? {
evaluated_cache.insert(entry.id.clone(), entry);
}
for pass_number in 0..self.config.max_trigger_passes {
let mut triggered = self.host.drain_triggered_entries();
if triggered.is_empty() {
break;
}
for plugin in &mut self.lifecycle_plugins {
let plugin_name = plugin.name().to_string();
let mut ctx = TriggerCtx {
triggered_ids: &mut triggered,
pass_number,
};
plugin
.on_trigger_fired(&mut ctx)
.map_err(|e| ContextWeaverError::PluginHook {
plugin: plugin_name,
hook: "on_trigger_fired",
source: e,
})?;
}
let new_results = ActivationEngine::filter_triggered(
&self.lorebook,
&triggered,
&active_ids,
&mut self.host,
&self.registry,
&self.activation_state,
);
if new_results.is_empty() {
break;
}
let new_ids: Vec<String> = new_results
.iter()
.map(|r| r.entry_id.clone())
.filter(|id| !active_ids.contains(id))
.collect();
for id in &new_ids {
active_ids.push(id.clone());
}
results.extend(new_results);
self.host
.set_active_entries(active_ids.iter().cloned().collect());
if !new_ids.is_empty() {
for entry in self.evaluate_entries(&new_ids)? {
evaluated_cache.insert(entry.id.clone(), entry);
}
}
if active_ids.len() > self.config.max_active_entries {
active_ids.truncate(self.config.max_active_entries);
break;
}
}
let evaluated: Vec<EvaluatedEntry> = active_ids
.iter()
.filter_map(|id| evaluated_cache.remove(id))
.collect();
for result in &results {
if matches!(result.reason, ActivationReason::Sticky { .. }) {
continue;
}
if let Some(entry) = self.lorebook.get_entry(&result.entry_id) {
self.activation_state
.record_activation(&result.entry_id, entry.meta.sticky_turns);
}
}
let mut blocks = ContextAssembler::assemble(
evaluated,
&self.lorebook.config,
&*self.tokenizer,
&self.available_slots,
);
{
let lifecycle_plugins = &mut self.lifecycle_plugins;
let lorebook = &self.lorebook;
for plugin in lifecycle_plugins {
let plugin_name = plugin.name().to_string();
let mut ctx = PostAssembleCtx {
blocks: &mut blocks,
lorebook,
};
plugin
.post_assemble(&mut ctx)
.map_err(|e| ContextWeaverError::PluginHook {
plugin: plugin_name,
hook: "post_assemble",
source: e,
})?;
}
}
Ok(blocks)
}
fn build_template_map(&self) -> HashMap<String, Arc<CompiledTemplate>> {
self.lorebook
.entries_in_order()
.map(|e| (e.meta.id.clone(), e.compiled.clone()))
.collect()
}
fn evaluate_entries(
&mut self,
entry_ids: &[String],
) -> Result<Vec<EvaluatedEntry>, ContextWeaverError> {
let mut results = Vec::new();
for id in entry_ids {
if let Some(entry) = self.lorebook.get_entry(id).cloned() {
if let Some(content) = self.evaluate_single_entry(&entry)? {
results.push(EvaluatedEntry {
id: id.clone(),
meta: entry.meta.clone(),
content,
});
}
}
}
Ok(results)
}
fn evaluate_single_entry(
&mut self,
entry: &Entry,
) -> Result<Option<String>, ContextWeaverError> {
let mut skip = false;
for plugin in &mut self.lifecycle_plugins {
let plugin_name = plugin.name().to_string();
let mut ctx = PreEvaluateCtx {
entry,
skip: &mut skip,
};
plugin
.pre_evaluate(&mut ctx)
.map_err(|e| ContextWeaverError::PluginHook {
plugin: plugin_name,
hook: "pre_evaluate",
source: e,
})?;
}
if skip {
return Ok(None);
}
self.host.begin_entry(&entry.meta.id);
let opts = weaver_lang::EvalOptions::new()
.max_node_evaluations(50_000)
.max_iterations(10_000)
.lenient(self.config.lenient);
let result = weaver_lang::evaluate_with_options(
entry.compiled.ast(),
&mut self.host,
&self.registry,
opts,
);
self.host.end_entry();
let mut content = result.map_err(|e| ContextWeaverError::Eval {
entry_id: entry.meta.id.clone(),
source: e,
})?;
for plugin in &mut self.lifecycle_plugins {
let plugin_name = plugin.name().to_string();
let mut ctx = PostEvaluateCtx {
entry,
content: &mut content,
};
plugin
.post_evaluate(&mut ctx)
.map_err(|e| ContextWeaverError::PluginHook {
plugin: plugin_name,
hook: "post_evaluate",
source: e,
})?;
}
Ok(Some(content))
}
}
pub struct EvaluatedEntry {
pub id: String,
pub meta: EntryMeta,
pub content: String,
}
#[derive(Debug)]
pub enum ContextWeaverError {
MetaParse { entry_path: String, message: String },
TemplateParse {
entry_id: String,
errors: Vec<weaver_lang::ParseError>,
},
Eval {
entry_id: String,
source: weaver_lang::EvalError,
},
RecursionLimit { entry_id: String, depth: usize },
Io(std::io::Error),
PluginHook {
plugin: String,
hook: &'static str,
source: HookError,
},
}
impl std::fmt::Display for ContextWeaverError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MetaParse {
entry_path,
message,
} => {
write!(f, "metadata parse error in {entry_path}: {message}")
}
Self::TemplateParse { entry_id, errors } => {
write!(f, "template parse error in entry '{entry_id}':")?;
for e in errors {
write!(f, "\n {e}")?;
}
Ok(())
}
Self::Eval { entry_id, source } => {
write!(f, "evaluation error in entry '{entry_id}': {source}")
}
Self::RecursionLimit { entry_id, depth } => {
write!(f, "recursion limit ({depth}) hit from entry '{entry_id}'")
}
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::PluginHook {
plugin,
hook,
source,
} => {
write!(f, "lifecycle plugin '{plugin}' failed in {hook}: {source}")
}
}
}
}
impl std::error::Error for ContextWeaverError {}
impl From<std::io::Error> for ContextWeaverError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
fn register_builtins(registry: &mut Registry) {
#[cfg(feature = "stdlib")]
{
stdlib::register(registry);
}
#[cfg(not(feature = "stdlib"))]
{
registry.register_processor(weaver_lang::ClosureProcessor::new(
"text",
"upper",
|props| {
let text = props.get("text").and_then(|v| v.as_string()).unwrap_or("");
Ok(Value::String(text.to_uppercase()))
},
));
registry.register_processor(weaver_lang::ClosureProcessor::new(
"text",
"lower",
|props| {
let text = props.get("text").and_then(|v| v.as_string()).unwrap_or("");
Ok(Value::String(text.to_lowercase()))
},
));
}
registry.register_command(IsActiveCommand);
}
struct IsActiveCommand;
impl WeaverCommand for IsActiveCommand {
fn call(
&self,
args: Vec<Value>,
ctx: &mut dyn EvalContext,
_registry: &Registry,
) -> Result<Option<Value>, EvalError> {
let id = args.first().and_then(|v| v.as_string()).ok_or_else(|| {
EvalError::type_error("string", args.first().map_or("none", |v| v.type_name()))
})?;
let is_active = ctx
.resolve_variable("_active", id)?
.is_some_and(|v| v.is_truthy());
Ok(Some(Value::Bool(is_active)))
}
fn signature(&self) -> CommandSignature {
CommandSignature {
name: "is_active".to_string(),
params: vec![ParamDef {
name: "entry_id".to_string(),
expected_type: Some(weaver_lang::registry::ValueType::String),
required: true,
}],
}
}
}