agent-runtime-kernel 0.1.1

Internal model, context, and Tool execution Kernel for Agent Runtime
Documentation
use runtime_extension_api::ContextItem;
use runtime_kernel_api::KernelFailure;
use runtime_ports::{ConversationMessage, ModelMessage, ModelRole};

/// Deterministic initial context assembly. Full request budgeting (including
/// Tool schemas and output reservation) is enforced immediately before every
/// model invocation by the compaction pipeline.
pub struct ContextManager {
    max_messages: usize,
}

impl ContextManager {
    pub fn new(max_messages: usize) -> Result<Self, KernelFailure> {
        if max_messages < 2 {
            return Err(KernelFailure::new(
                "CONTEXT_LIMIT_INVALID",
                "context requires at least 2 messages",
            ));
        }
        Ok(Self { max_messages })
    }

    pub fn assemble(
        &self,
        system_prompt: &str,
        mut context_items: Vec<ContextItem>,
        history: Vec<ConversationMessage>,
        request_messages: &[ConversationMessage],
    ) -> Result<Vec<ModelMessage>, KernelFailure> {
        let system = self.render_system(system_prompt, &mut context_items)?;

        if request_messages.is_empty() || request_messages.len() >= self.max_messages {
            return Err(KernelFailure::new(
                "REQUEST_TRANSCRIPT_INVALID",
                "request messages must fit within the configured context message limit",
            ));
        }

        let mut selected = Vec::new();
        let history_slots = self.max_messages.saturating_sub(1 + request_messages.len());
        for message in history.iter().rev().take(history_slots) {
            selected.push(message.clone());
        }
        selected.reverse();

        let dropped = history.len().saturating_sub(selected.len());
        if dropped > 0 && history_slots > 0 {
            let checkpoint = structured_checkpoint(dropped, &history);
            if selected.len() >= history_slots && !selected.is_empty() {
                selected.remove(0);
            }
            selected.insert(
                0,
                ConversationMessage {
                    role: ModelRole::System,
                    text: checkpoint,
                },
            );
        }

        let mut messages = Vec::with_capacity(selected.len() + request_messages.len() + 1);
        messages.push(ModelMessage::text(ModelRole::System, system));
        messages.extend(
            selected
                .into_iter()
                .map(|message| ModelMessage::text(message.role, message.text)),
        );
        messages.extend(
            request_messages
                .iter()
                .map(|message| ModelMessage::text(message.role, message.text.clone())),
        );
        Ok(messages)
    }

    pub fn refresh_system(
        &self,
        messages: &mut [ModelMessage],
        system_prompt: &str,
        mut context_items: Vec<ContextItem>,
    ) -> Result<(), KernelFailure> {
        let system = self.render_system(system_prompt, &mut context_items)?;
        let first = messages.first_mut().ok_or_else(|| {
            KernelFailure::new("CONTEXT_TRANSCRIPT_INVALID", "model transcript is empty")
        })?;
        if first.role != ModelRole::System {
            return Err(KernelFailure::new(
                "CONTEXT_TRANSCRIPT_INVALID",
                "model transcript must begin with system context",
            ));
        }
        *first = ModelMessage::text(ModelRole::System, system);
        Ok(())
    }

    fn render_system(
        &self,
        system_prompt: &str,
        context_items: &mut [ContextItem],
    ) -> Result<String, KernelFailure> {
        context_items.sort_by(|left, right| {
            right
                .required
                .cmp(&left.required)
                .then_with(|| right.priority.cmp(&left.priority))
                .then_with(|| left.source.cmp(&right.source))
        });

        let mut system = system_prompt.trim().to_string();
        for item in context_items {
            if !item.content.trim().is_empty() {
                system.push_str("\n\n<context source=\"");
                system.push_str(&item.source);
                system.push_str("\">\n");
                system.push_str(item.content.trim());
                system.push_str("\n</context>");
            }
        }

        Ok(system)
    }
}

fn structured_checkpoint(dropped: usize, history: &[ConversationMessage]) -> String {
    let goal = history
        .iter()
        .find(|message| message.role == ModelRole::User)
        .map(|message| truncate(&message.text, 512))
        .unwrap_or_else(|| "unknown".to_string());
    format!(
        "<checkpoint>\ngoal: {goal}\ndone: {dropped} earlier conversation messages compacted\nstate: later messages remain authoritative\nwatch: verify assumptions before destructive actions\nnext: continue from the most recent user request\n</checkpoint>"
    )
}

fn truncate(value: &str, max: usize) -> String {
    if value.len() <= max {
        return value.to_string();
    }
    let mut boundary = max;
    while !value.is_char_boundary(boundary) {
        boundary -= 1;
    }
    format!("{}…", &value[..boundary])
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn keeps_required_edges_and_compacts_old_history() {
        let manager = ContextManager::new(4).unwrap();
        let history = (0..10)
            .map(|index| ConversationMessage {
                role: if index % 2 == 0 {
                    ModelRole::User
                } else {
                    ModelRole::Assistant
                },
                text: format!("message-{index}"),
            })
            .collect();
        let messages = manager
            .assemble(
                "system",
                Vec::new(),
                history,
                &[ConversationMessage {
                    role: ModelRole::User,
                    text: "current".into(),
                }],
            )
            .unwrap();
        assert_eq!(messages.first().unwrap().role, ModelRole::System);
        assert_eq!(messages.last().unwrap().role, ModelRole::User);
        assert!(messages.len() <= 5);
        assert!(format!("{:?}", messages).contains("checkpoint"));
    }
}