docs(agent): Add doc-comments to all public items in agent-core

This commit is contained in:
2025-12-26 18:35:46 +01:00
parent f1f1f88181
commit b555256d21
5 changed files with 158 additions and 99 deletions

View File

@@ -1,4 +1,4 @@
//! Context compaction for long conversations
//! Context compaction for long conversations.
//!
//! When the conversation context grows too large, this module compacts
//! earlier messages into a summary while preserving recent context.
@@ -6,16 +6,16 @@
use color_eyre::eyre::Result;
use llm_core::{ChatMessage, ChatOptions, LlmProvider};
/// Token limit threshold for triggering compaction
/// Token limit threshold for triggering compaction.
const CONTEXT_LIMIT: usize = 180_000;
/// Threshold ratio at which to trigger compaction (90% of limit)
/// Threshold ratio at which to trigger compaction (90% of limit).
const COMPACTION_THRESHOLD: f64 = 0.9;
/// Number of recent messages to preserve during compaction
/// Number of recent messages to preserve during compaction.
const PRESERVE_RECENT: usize = 10;
/// Token counter for estimating context size
/// Token counter for estimating the size of conversation history in tokens.
pub struct TokenCounter {
chars_per_token: f64,
}
@@ -27,12 +27,13 @@ impl Default for TokenCounter {
}
impl TokenCounter {
/// Creates a new `TokenCounter` with default settings.
pub fn new() -> Self {
// Rough estimate: ~4 chars per token for English text
Self { chars_per_token: 4.0 }
}
/// Estimate token count for a message
/// Estimates the token count for a single message.
pub fn count_message(&self, message: &ChatMessage) -> usize {
let content_len = message.content.as_ref().map(|c| c.len()).unwrap_or(0);
// Add overhead for role, metadata
@@ -40,19 +41,19 @@ impl TokenCounter {
((content_len as f64 / self.chars_per_token) as usize) + overhead
}
/// Estimate total token count for all messages
/// Estimates the total token count for a list of messages.
pub fn count_messages(&self, messages: &[ChatMessage]) -> usize {
messages.iter().map(|m| self.count_message(m)).sum()
}
/// Check if context should be compacted
/// Determines if the conversation history should be compacted based on token limits.
pub fn should_compact(&self, messages: &[ChatMessage]) -> bool {
let count = self.count_messages(messages);
count > (CONTEXT_LIMIT as f64 * COMPACTION_THRESHOLD) as usize
}
}
/// Context compactor that summarizes conversation history
/// Context compactor that uses an LLM to summarize conversation history.
pub struct Compactor {
token_counter: TokenCounter,
}
@@ -64,22 +65,22 @@ impl Default for Compactor {
}
impl Compactor {
/// Creates a new `Compactor`.
pub fn new() -> Self {
Self {
token_counter: TokenCounter::new(),
}
}
/// Check if messages need compaction
/// Returns `true` if the provided messages exceed the compaction threshold.
pub fn needs_compaction(&self, messages: &[ChatMessage]) -> bool {
self.token_counter.should_compact(messages)
}
/// Compact messages by summarizing earlier conversation
/// Compacts conversation history by summarizing earlier messages.
///
/// Returns compacted messages with:
/// - A system message containing the summary of earlier context
/// - The most recent N messages preserved in full
/// The resulting message list will contain a summary system message followed
/// by the most recent conversation context.
pub async fn compact<P: LlmProvider>(
&self,
provider: &P,
@@ -169,7 +170,7 @@ impl Compactor {
Ok(summary.trim().to_string())
}
/// Get token counter for external use
/// Returns a reference to the internal `TokenCounter`.
pub fn token_counter(&self) -> &TokenCounter {
&self.token_counter
}

View File

@@ -9,23 +9,25 @@ use color_eyre::eyre::Result;
use std::path::Path;
use std::process::Command;
/// Status of a file in the git working tree
/// Represents the git status of a specific file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GitFileStatus {
/// File has been modified
/// File has been modified.
Modified { path: String },
/// File has been added (staged)
/// File has been added to the index (staged).
Added { path: String },
/// File has been deleted
/// File has been deleted.
Deleted { path: String },
/// File has been renamed
/// File has been renamed.
Renamed { from: String, to: String },
/// File is untracked
/// File is not tracked by git.
Untracked { path: String },
}
impl GitFileStatus {
/// Get the primary path associated with this status
/// Returns the primary path associated with this status entry.
///
/// For renames, this returns the new path.
pub fn path(&self) -> &str {
match self {
Self::Modified { path } => path,
@@ -37,25 +39,25 @@ impl GitFileStatus {
}
}
/// Complete state of a git repository
/// Represents the captured state of a git repository.
#[derive(Debug, Clone)]
pub struct GitState {
/// Whether the current directory is in a git repository
/// `true` if the directory is a git repository.
pub is_git_repo: bool,
/// Current branch name (None if not in a repo or detached HEAD)
/// The name of the current branch, if any.
pub current_branch: Option<String>,
/// Main branch name (main/master, None if not detected)
/// The name of the detected main/master branch.
pub main_branch: Option<String>,
/// Status of files in the working tree
/// List of file status entries for the working tree.
pub status: Vec<GitFileStatus>,
/// Whether there are any uncommitted changes
/// `true` if there are any staged or unstaged changes.
pub has_uncommitted_changes: bool,
/// Remote URL for the repository (None if no remote configured)
/// The URL of the 'origin' remote, if configured.
pub remote_url: Option<String>,
}
impl GitState {
/// Create a default GitState for non-git directories
/// Creates a `GitState` representing a non-repository directory.
pub fn not_a_repo() -> Self {
Self {
is_git_repo: false,
@@ -68,10 +70,11 @@ impl GitState {
}
}
/// Detect the current git repository state
/// Detects the git state of the specified working directory.
///
/// This function runs various git commands to gather information about the repository.
/// If git is not available or the directory is not a git repo, returns a default state.
/// This function executes git commands to inspect the repository. It handles cases
/// where git is missing or the directory is not a repository by returning a
/// "not a repo" state rather than an error.
pub fn detect_git_state(working_dir: &Path) -> Result<GitState> {
// Check if this is a git repository
let is_repo = Command::new("git")
@@ -252,13 +255,10 @@ fn get_remote_url(working_dir: &Path) -> Result<Option<String>> {
}
}
/// Check if a git command is safe (read-only)
/// Heuristically determines if a git command string is "safe" (read-only).
///
/// Safe commands include:
/// - status, log, show, diff, branch (without -D)
/// - remote (without add/remove)
/// - config --get
/// - rev-parse, ls-files, ls-tree
/// Safe commands (like `status` or `diff`) are generally allowed without
/// explicit confirmation in certain modes.
pub fn is_safe_git_command(command: &str) -> bool {
let parts: Vec<&str> = command.split_whitespace().collect();
@@ -287,13 +287,11 @@ pub fn is_safe_git_command(command: &str) -> bool {
}
}
/// Check if a git command is destructive
/// Heuristically determines if a git command is "destructive" or potentially dangerous.
///
/// Returns (is_destructive, warning_message) tuple.
/// Destructive commands include:
/// - push --force, reset --hard, clean -fd
/// - rebase, amend, filter-branch
/// - branch -D, tag -d
/// Returns a tuple of `(is_destructive, warning_message)`. Destructive commands
/// (like `reset --hard` or `push --force`) should always trigger a warning or
/// require explicit approval.
pub fn is_destructive_git_command(command: &str) -> (bool, &'static str) {
let cmd_lower = command.to_lowercase();
@@ -347,16 +345,7 @@ pub fn is_destructive_git_command(command: &str) -> (bool, &'static str) {
(false, "")
}
/// Format git state for human-readable display
///
/// Example output:
/// ```text
/// Git Repository: yes
/// Current branch: feature-branch
/// Main branch: main
/// Status: 3 modified, 1 untracked
/// Remote: https://github.com/user/repo.git
/// ```
/// Formats a `GitState` into a human-readable summary string.
pub fn format_git_status(state: &GitState) -> String {
if !state.is_git_repo {
return "Not a git repository".to_string();

View File

@@ -1,3 +1,8 @@
//! Core agent orchestration for the Owlen AI agent.
//!
//! This crate provides the central engine that coordinates Large Language Models (LLMs),
//! a rich set of system tools, and user interaction through an event-driven loop.
pub mod session;
pub mod system_prompt;
pub mod git;
@@ -71,27 +76,27 @@ pub enum AgentEvent {
pub type AgentEventSender = mpsc::Sender<AgentEvent>;
pub type AgentEventReceiver = mpsc::Receiver<AgentEvent>;
/// Create channel for agent events
/// Creates a channel for agent events with a buffer size of 100.
pub fn create_event_channel() -> (AgentEventSender, AgentEventReceiver) {
mpsc::channel(100)
}
/// Optional context for tools that need external dependencies
/// Optional context for tools that need external dependencies or shared state.
#[derive(Clone)]
pub struct ToolContext {
/// Todo list for TodoWrite tool
/// Todo list for the `todo_write` tool.
pub todo_list: Option<TodoList>,
/// Channel for asking user questions
/// Channel for asking user questions via the `ask_user` tool.
pub ask_sender: Option<AskSender>,
/// Shell manager for background shells
/// Shell manager for handling background shells.
pub shell_manager: Option<ShellManager>,
/// Plan manager for planning mode
/// Plan manager for managing implementation plans in planning mode.
pub plan_manager: Option<Arc<PlanManager>>,
/// Current agent mode (normal or planning)
/// Current agent mode (e.g., Normal or Planning).
pub agent_mode: Arc<RwLock<AgentMode>>,
}
@@ -108,52 +113,58 @@ impl Default for ToolContext {
}
impl ToolContext {
/// Creates a new, empty `ToolContext`.
pub fn new() -> Self {
Self::default()
}
/// Adds a `TodoList` to the context.
pub fn with_todo_list(mut self, list: TodoList) -> Self {
self.todo_list = Some(list);
self
}
/// Adds an `AskSender` to the context for user interaction.
pub fn with_ask_sender(mut self, sender: AskSender) -> Self {
self.ask_sender = Some(sender);
self
}
/// Adds a `ShellManager` to the context for managing bash sessions.
pub fn with_shell_manager(mut self, manager: ShellManager) -> Self {
self.shell_manager = Some(manager);
self
}
/// Adds a `PlanManager` to the context.
pub fn with_plan_manager(mut self, manager: PlanManager) -> Self {
self.plan_manager = Some(Arc::new(manager));
self
}
/// Initializes a `PlanManager` with the given project root and adds it to the context.
pub fn with_project_root(mut self, project_root: PathBuf) -> Self {
self.plan_manager = Some(Arc::new(PlanManager::new(project_root)));
self
}
/// Check if agent is in planning mode
/// Checks if the agent is currently in planning mode.
pub async fn is_planning(&self) -> bool {
self.agent_mode.read().await.is_planning()
}
/// Get current agent mode
/// Returns the current `AgentMode`.
pub async fn get_mode(&self) -> AgentMode {
self.agent_mode.read().await.clone()
}
/// Set agent mode
/// Sets the current `AgentMode`.
pub async fn set_mode(&self, mode: AgentMode) {
*self.agent_mode.write().await = mode;
}
}
/// Define all available tools for the LLM
/// Returns definitions for all available tools that the agent can use.
pub fn get_tool_definitions() -> Vec<Tool> {
vec![
Tool::function(
@@ -466,7 +477,10 @@ impl ToolCallsBuilder {
}
}
/// Execute a tool call and return the result
/// Executes a single tool call and returns its result as a string.
///
/// This function handles permission checking and interacts with various tool-specific
/// backends (filesystem, shell, etc.) using the provided `ToolContext`.
pub async fn execute_tool(
tool_name: &str,
arguments: &Value,
@@ -858,7 +872,11 @@ pub async fn execute_tool(
}
}
/// Run the agent loop with tool calling
/// Runs the core agent loop for a given user prompt.
///
/// This function iteratively calls the LLM provider, processes tool execution requests,
/// and feeds the results back to the model until a final text response is generated
/// or the iteration limit is reached.
pub async fn run_agent_loop<P: LlmProvider>(
provider: &P,
user_prompt: &str,
@@ -988,7 +1006,10 @@ pub async fn run_agent_loop<P: LlmProvider>(
}
}
/// Run agent loop with event streaming
/// Runs the agent loop and streams events back through the provided channel.
///
/// This allows UIs to provide real-time feedback as the LLM generates text and
/// as tools are being executed.
pub async fn run_agent_loop_streaming<P: LlmProvider>(
provider: &P,
user_prompt: &str,

View File

@@ -1,3 +1,8 @@
//! Session state and history management.
//!
//! This module provides tools for tracking conversation history, capturing
//! usage statistics, and managing session checkpoints for persistence and rewind.
use color_eyre::eyre::{Result, eyre};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -5,16 +10,23 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
/// Statistics for a single chat session.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionStats {
/// The time when the session started.
pub start_time: SystemTime,
/// Total number of messages exchanged.
pub total_messages: usize,
/// Total number of tools executed by the agent.
pub total_tool_calls: usize,
/// Total wall-clock time spent in the session.
pub total_duration: Duration,
/// Rough estimate of the total tokens used.
pub estimated_tokens: usize,
}
impl SessionStats {
/// Creates a new `SessionStats` instance with zeroed values.
pub fn new() -> Self {
Self {
start_time: SystemTime::now(),
@@ -25,16 +37,19 @@ impl SessionStats {
}
}
/// Records a new message in the statistics.
pub fn record_message(&mut self, tokens: usize, duration: Duration) {
self.total_messages += 1;
self.estimated_tokens += tokens;
self.total_duration += duration;
}
/// Increments the tool call counter.
pub fn record_tool_call(&mut self) {
self.total_tool_calls += 1;
}
/// Formats a duration into a human-readable string.
pub fn format_duration(d: Duration) -> String {
let secs = d.as_secs();
if secs < 60 {
@@ -53,22 +68,32 @@ impl Default for SessionStats {
}
}
/// In-memory history of the current session.
#[derive(Debug, Clone)]
pub struct SessionHistory {
/// List of prompts provided by the user.
pub user_prompts: Vec<String>,
/// List of responses generated by the assistant.
pub assistant_responses: Vec<String>,
/// Chronological log of all tool calls made.
pub tool_calls: Vec<ToolCallRecord>,
}
/// Record of a single tool execution.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallRecord {
/// Name of the tool that was called.
pub tool_name: String,
/// JSON-encoded arguments provided to the tool.
pub arguments: String,
/// Output produced by the tool.
pub result: String,
/// Whether the tool execution was successful.
pub success: bool,
}
impl SessionHistory {
/// Creates a new, empty `SessionHistory`.
pub fn new() -> Self {
Self {
user_prompts: Vec::new(),
@@ -77,18 +102,22 @@ impl SessionHistory {
}
}
/// Appends a user message to history.
pub fn add_user_message(&mut self, message: String) {
self.user_prompts.push(message);
}
/// Appends an assistant response to history.
pub fn add_assistant_message(&mut self, message: String) {
self.assistant_responses.push(message);
}
/// Appends a tool call record to history.
pub fn add_tool_call(&mut self, record: ToolCallRecord) {
self.tool_calls.push(record);
}
/// Clears all stored history.
pub fn clear(&mut self) {
self.user_prompts.clear();
self.assistant_responses.clear();
@@ -102,17 +131,21 @@ impl Default for SessionHistory {
}
}
/// Represents a file modification with before/after content
/// Represents a file modification with before/after content.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileDiff {
/// Absolute path to the file.
pub path: PathBuf,
/// Content of the file before modification.
pub before: String,
/// Content of the file after modification.
pub after: String,
/// When the modification occurred.
pub timestamp: SystemTime,
}
impl FileDiff {
/// Create a new file diff
/// Creates a new `FileDiff`.
pub fn new(path: PathBuf, before: String, after: String) -> Self {
Self {
path,
@@ -123,20 +156,27 @@ impl FileDiff {
}
}
/// A checkpoint captures the state of a session at a point in time
/// A checkpoint captures the full state of a session at a point in time.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Checkpoint {
/// Unique identifier for the checkpoint.
pub id: String,
/// When the checkpoint was created.
pub timestamp: SystemTime,
/// Session statistics at the time of checkpoint.
pub stats: SessionStats,
/// History of user prompts.
pub user_prompts: Vec<String>,
/// History of assistant responses.
pub assistant_responses: Vec<String>,
/// History of tool calls.
pub tool_calls: Vec<ToolCallRecord>,
/// List of file modifications made during the session.
pub file_diffs: Vec<FileDiff>,
}
impl Checkpoint {
/// Create a new checkpoint from current session state
/// Creates a new checkpoint from the current session state.
pub fn new(
id: String,
stats: SessionStats,
@@ -154,7 +194,7 @@ impl Checkpoint {
}
}
/// Save checkpoint to disk
/// Saves the checkpoint to a JSON file on disk.
pub fn save(&self, checkpoint_dir: &Path) -> Result<()> {
fs::create_dir_all(checkpoint_dir)?;
let path = checkpoint_dir.join(format!("{}.json", self.id));
@@ -163,7 +203,7 @@ impl Checkpoint {
Ok(())
}
/// Load checkpoint from disk
/// Loads a checkpoint from disk by ID.
pub fn load(checkpoint_dir: &Path, id: &str) -> Result<Self> {
let path = checkpoint_dir.join(format!("{}.json", id));
let content = fs::read_to_string(&path)
@@ -173,7 +213,7 @@ impl Checkpoint {
Ok(checkpoint)
}
/// List all available checkpoints in a directory
/// Lists all available checkpoint IDs in the given directory.
pub fn list(checkpoint_dir: &Path) -> Result<Vec<String>> {
if !checkpoint_dir.exists() {
return Ok(Vec::new());
@@ -196,14 +236,14 @@ impl Checkpoint {
}
}
/// Session checkpoint manager
/// Manages the creation and restoration of session checkpoints.
pub struct CheckpointManager {
checkpoint_dir: PathBuf,
file_snapshots: HashMap<PathBuf, String>,
}
impl CheckpointManager {
/// Create a new checkpoint manager
/// Creates a new `CheckpointManager` pointing to the specified directory.
pub fn new(checkpoint_dir: PathBuf) -> Self {
Self {
checkpoint_dir,
@@ -211,7 +251,7 @@ impl CheckpointManager {
}
}
/// Snapshot a file's current content before modification
/// Snapshots a file's current content before modification to track changes.
pub fn snapshot_file(&mut self, path: &Path) -> Result<()> {
if !self.file_snapshots.contains_key(path) {
let content = fs::read_to_string(path).unwrap_or_default();
@@ -220,7 +260,7 @@ impl CheckpointManager {
Ok(())
}
/// Create a file diff after modification
/// Creates a `FileDiff` if the file has been modified since it was snapshotted.
pub fn create_diff(&self, path: &Path) -> Result<Option<FileDiff>> {
if let Some(before) = self.file_snapshots.get(path) {
let after = fs::read_to_string(path).unwrap_or_default();
@@ -238,7 +278,7 @@ impl CheckpointManager {
}
}
/// Get all file diffs since last checkpoint
/// Returns all file modifications tracked since the last checkpoint.
pub fn get_all_diffs(&self) -> Result<Vec<FileDiff>> {
let mut diffs = Vec::new();
for (path, before) in &self.file_snapshots {
@@ -250,12 +290,12 @@ impl CheckpointManager {
Ok(diffs)
}
/// Clear file snapshots
/// Clears all internal file snapshots.
pub fn clear_snapshots(&mut self) {
self.file_snapshots.clear();
}
/// Save a checkpoint
/// Saves the current session state as a new checkpoint.
pub fn save_checkpoint(
&mut self,
id: String,
@@ -269,17 +309,19 @@ impl CheckpointManager {
Ok(checkpoint)
}
/// Load a checkpoint
/// Loads a checkpoint by ID.
pub fn load_checkpoint(&self, id: &str) -> Result<Checkpoint> {
Checkpoint::load(&self.checkpoint_dir, id)
}
/// List all checkpoints
/// Lists all available checkpoints.
pub fn list_checkpoints(&self) -> Result<Vec<String>> {
Checkpoint::list(&self.checkpoint_dir)
}
/// Rewind to a checkpoint by restoring file contents
/// Rewinds the local filesystem to the state captured in the specified checkpoint.
///
/// Returns a list of paths that were restored.
pub fn rewind_to(&self, checkpoint_id: &str) -> Result<Vec<PathBuf>> {
let checkpoint = self.load_checkpoint(checkpoint_id)?;
let mut restored_files = Vec::new();

View File

@@ -1,10 +1,12 @@
//! System Prompt Management
//! System Prompt Management.
//!
//! Composes system prompts from multiple sources for agent sessions.
//! This module is responsible for composing the complex system prompts sent to the LLM.
//! It merges base instructions, tool definitions, project-specific context, and
//! dynamically injected content from skills and hooks.
use std::path::Path;
/// Builder for composing system prompts
/// Builder for incrementally composing a system prompt from various sources.
#[derive(Debug, Clone, Default)]
pub struct SystemPromptBuilder {
sections: Vec<PromptSection>,
@@ -19,11 +21,12 @@ struct PromptSection {
}
impl SystemPromptBuilder {
/// Creates a new, empty `SystemPromptBuilder`.
pub fn new() -> Self {
Self::default()
}
/// Add the base agent prompt
/// Adds the base agent identity and instructions section.
pub fn with_base_prompt(mut self, content: impl Into<String>) -> Self {
self.sections.push(PromptSection {
name: "base".to_string(),
@@ -33,7 +36,7 @@ impl SystemPromptBuilder {
self
}
/// Add tool usage instructions
/// Adds tool usage instructions.
pub fn with_tool_instructions(mut self, content: impl Into<String>) -> Self {
self.sections.push(PromptSection {
name: "tools".to_string(),
@@ -43,7 +46,8 @@ impl SystemPromptBuilder {
self
}
/// Load and add project instructions from CLAUDE.md or .owlen.md
/// Attempts to load project-specific instructions from `CLAUDE.md` or `.owlen.md`
/// located in the provided project root.
pub fn with_project_instructions(mut self, project_root: &Path) -> Self {
// Try CLAUDE.md first (Claude Code compatibility)
let claude_md = project_root.join("CLAUDE.md");
@@ -73,7 +77,7 @@ impl SystemPromptBuilder {
self
}
/// Add skill content
/// Adds domain-specific knowledge or instructions as a "skill".
pub fn with_skill(mut self, skill_name: &str, content: impl Into<String>) -> Self {
self.sections.push(PromptSection {
name: format!("skill:{}", skill_name),
@@ -83,7 +87,7 @@ impl SystemPromptBuilder {
self
}
/// Add hook-injected content (from SessionStart hooks)
/// Injects context dynamically from hooks (e.g., during session initialization).
pub fn with_hook_injection(mut self, content: impl Into<String>) -> Self {
self.sections.push(PromptSection {
name: "hook".to_string(),
@@ -93,7 +97,7 @@ impl SystemPromptBuilder {
self
}
/// Add custom section
/// Adds a generic custom section with a specific name and priority.
pub fn with_section(mut self, name: impl Into<String>, content: impl Into<String>, priority: i32) -> Self {
self.sections.push(PromptSection {
name: name.into(),
@@ -103,7 +107,9 @@ impl SystemPromptBuilder {
self
}
/// Build the final system prompt
/// Finalizes the build process and returns the combined system prompt string.
///
/// Sections are sorted by priority and separated by a horizontal rule (`---`).
pub fn build(mut self) -> String {
// Sort by priority
self.sections.sort_by_key(|s| s.priority);
@@ -116,13 +122,13 @@ impl SystemPromptBuilder {
.join("\n\n---\n\n")
}
/// Check if any content has been added
/// Returns `true` if no prompt sections have been added yet.
pub fn is_empty(&self) -> bool {
self.sections.is_empty()
}
}
/// Default base prompt for Owlen agent
/// Returns the standard default identity and guideline prompt for the Owlen agent.
pub fn default_base_prompt() -> &'static str {
r#"You are Owlen, an AI assistant that helps with software engineering tasks.
@@ -145,7 +151,7 @@ You have access to tools for reading files, writing code, running commands, and
- Use `web_search` for current information"#
}
/// Generate tool instructions based on available tools
/// Dynamically generates a summary of available tools to be included in the system prompt.
pub fn generate_tool_instructions(tool_names: &[&str]) -> String {
let mut instructions = String::from("## Available Tools\n\n");