Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/320.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added host-owned, root- and session-scoped Lua plugin state with bounded snapshots, lifecycle restoration, and cleanup.
20 changes: 11 additions & 9 deletions n00n-agent/src/agent/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ use crate::cancel::{CancelMap, CancelToken, PreDispatchGate};
use crate::mcp::McpSession;
use crate::permissions::{PermissionAnswer, PermissionManager};
use crate::tools::{
ActiveTools, Deadline, FileReadTracker, LocalTools, ToolAudience, ToolContext, ToolFilter,
ToolRegistry,
ActiveTools, Deadline, FileReadTracker, LocalTools, SessionIdentity, ToolAudience, ToolContext,
ToolFilter, ToolRegistry,
};
use crate::{
AgentConfig, AgentError, AgentEvent, AgentInput, AgentMode, EventSender, ExtractedCommand,
FusionContinuation, FusionPhase, FusionRequestDecision, FusionState, InterruptPoint,
InterruptSource, ToolDoneEvent, TurnCompleteEvent,
};
use n00n_config::{ToolKey, ToolOutputLines};
#[cfg(test)]
use n00n_storage::id::SessionRef;

use crate::tokenize::{
Expand Down Expand Up @@ -167,7 +168,7 @@ pub struct AgentParams {
pub config: Arc<AgentConfig>,
pub tool_output_lines: ToolOutputLines,
pub permissions: Arc<PermissionManager>,
pub session_id: Option<SessionRef>,
pub identity: Option<SessionIdentity>,
pub timeouts: n00n_providers::Timeouts,
pub openai_options: OpenAiOptions,
pub file_tracker: Arc<FileReadTracker>,
Expand Down Expand Up @@ -214,7 +215,7 @@ pub struct Agent<'h> {
thinking_empty_retried: bool,
permissions: Arc<PermissionManager>,
opts: RequestOptions,
session_id: Option<SessionRef>,
identity: Option<SessionIdentity>,
timeouts: n00n_providers::Timeouts,
openai_options: OpenAiOptions,
file_tracker: Arc<FileReadTracker>,
Expand Down Expand Up @@ -273,7 +274,7 @@ impl<'h> Agent<'h> {
post_tool_empty_retried: false,
thinking_empty_retried: false,
opts: RequestOptions::default(),
session_id: params.session_id,
identity: params.identity,
file_tracker: params.file_tracker,
prompt_slots: params.prompt_slots,
subagent_cancels: params.subagent_cancels,
Expand Down Expand Up @@ -502,7 +503,7 @@ impl<'h> Agent<'h> {
event_tx: &self.event_tx,
cancel: &self.cancel,
opts,
session_id: self.session_id.as_ref(),
session_id: self.identity.as_ref().map(SessionIdentity::session_id),
})
.await
}
Expand Down Expand Up @@ -886,6 +887,7 @@ impl<'h> Agent<'h> {
prompt_slots: Arc::clone(&self.prompt_slots),
opts: self.opts.clone(),
subagent_cancels: Arc::clone(&self.subagent_cancels),
identity: self.identity.clone(),
registry: Arc::clone(&self.registry),
workflow: self.workflow,
audience: self.audience,
Expand Down Expand Up @@ -1144,7 +1146,7 @@ impl<'h> Agent<'h> {
&self.event_tx,
&self.cancel,
CompactionTrigger::Auto,
self.session_id.as_ref(),
self.identity.as_ref().map(SessionIdentity::session_id),
&cwd,
None,
)
Expand Down Expand Up @@ -2141,7 +2143,7 @@ mod tests {
},
std::path::PathBuf::from("/tmp"),
)),
session_id: None,
identity: None,
timeouts: n00n_providers::Timeouts::default(),
openai_options: OpenAiOptions::default(),
file_tracker: FileReadTracker::fresh(),
Expand Down Expand Up @@ -3354,7 +3356,7 @@ mod tests {
},
std::path::PathBuf::from("/tmp"),
)),
session_id: None,
identity: None,
timeouts: n00n_providers::Timeouts::default(),
openai_options: OpenAiOptions::default(),
file_tracker: FileReadTracker::fresh(),
Expand Down
8 changes: 5 additions & 3 deletions n00n-agent/src/headless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ use crate::cancel::{CancelMap, CancelToken};
use crate::permissions::PermissionManager;
use crate::prompt::ResolvedSlots;
use crate::template;
use crate::tools::{DescriptionContext, FileReadTracker, ToolAudience, ToolFilter, ToolRegistry};
use crate::tools::{
DescriptionContext, FileReadTracker, SessionIdentity, ToolAudience, ToolFilter, ToolRegistry,
};
use crate::{
Agent, AgentConfig, AgentEvent, AgentInput, AgentMode, AgentParams, AgentRunParams, Envelope,
EventSender, ImageSource, McpHandle, McpSession, PermissionsConfig, ToolOutput,
Expand Down Expand Up @@ -263,7 +265,7 @@ pub fn spawn(params: HeadlessParams) -> HeadlessHandle {
params.permissions_config,
working_dir_path,
)),
session_id: Some(session_ref_clone.clone()),
identity: Some(SessionIdentity::root(session_ref_clone.clone())),
timeouts: params.timeouts,
openai_options: params.openai_options,
file_tracker: FileReadTracker::fresh(),
Expand Down Expand Up @@ -518,7 +520,7 @@ pub fn spawn_interactive(params: InteractiveParams) -> InteractiveHandle {
config: Arc::clone(&params.config),
tool_output_lines: ToolOutputLines::default(),
permissions: Arc::clone(&permissions),
session_id: Some(session_ref_clone.clone()),
identity: Some(SessionIdentity::root(session_ref_clone.clone())),
timeouts: params.timeouts,
openai_options: params.openai_options,
file_tracker: Arc::clone(&file_tracker),
Expand Down
73 changes: 72 additions & 1 deletion n00n-agent/src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,43 @@ pub fn timeout_annotation(secs: u64) -> String {
pub type LocalToolFn = Arc<dyn Fn(&Value) -> Result<String, String> + Send + Sync>;
pub type LocalTools = Arc<HashMap<String, LocalToolFn>>;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionIdentity {
session_id: n00n_storage::id::SessionRef,
root_session_id: n00n_storage::id::SessionRef,
}

impl SessionIdentity {
#[must_use]
pub fn root(session_id: n00n_storage::id::SessionRef) -> Self {
Self {
root_session_id: session_id.clone(),
session_id,
}
}

#[must_use]
pub fn child(
session_id: n00n_storage::id::SessionRef,
root_session_id: n00n_storage::id::SessionRef,
) -> Self {
Self {
session_id,
root_session_id,
}
}

#[must_use]
pub fn session_id(&self) -> &n00n_storage::id::SessionRef {
&self.session_id
}

#[must_use]
pub fn root_session_id(&self) -> &n00n_storage::id::SessionRef {
&self.root_session_id
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#[derive(Clone)]
pub struct ToolContext {
pub provider: Arc<dyn Provider>,
Expand All @@ -321,6 +358,8 @@ pub struct ToolContext {
pub prompt_slots: Arc<crate::prompt::ResolvedSlots>,
pub opts: RequestOptions,
pub subagent_cancels: Arc<CancelMap<String>>,
/// Immutable session and root identity of the agent executing this tool.
pub identity: Option<SessionIdentity>,
pub registry: Arc<ToolRegistry>,
pub tool_filter: ToolFilter,
pub workflow: bool,
Expand Down Expand Up @@ -588,6 +627,7 @@ pub fn interpreter_ctx(
prompt_slots: Arc::new(crate::prompt::ResolvedSlots::default()),
opts: RequestOptions::default(),
subagent_cancels: Arc::new(CancelMap::new()),
identity: None,
registry,
tool_filter: ToolFilter::All,
workflow: false,
Expand Down Expand Up @@ -629,7 +669,8 @@ pub mod test_support {

use super::{
AgentMode, Arc, CancelToken, DescriptionContext, FileReadTracker, LazyLock,
PermissionManager, ToolContext, ToolRegistry, Value, interpreter_ctx, registry,
PermissionManager, SessionIdentity, ToolContext, ToolRegistry, Value, interpreter_ctx,
registry,
};

pub const GUARDED_TOOL_NAME: &str = "guarded_mock";
Expand Down Expand Up @@ -705,6 +746,9 @@ pub mod test_support {
Arc::new(ToolRegistry::new()),
);
ctx.tool_use_id = tool_use_id.map(String::from);
ctx.identity = Some(SessionIdentity::root(
n00n_storage::id::SessionRef::generate(),
));
ctx
}

Expand Down Expand Up @@ -745,6 +789,33 @@ mod tests {

const LINE_LIMIT: usize = 500;

#[test]
fn root_session_identity_uses_one_id() {
let session_id = n00n_storage::id::SessionRef::generate();
let identity = SessionIdentity::root(session_id.clone());

assert_eq!(identity.session_id(), &session_id);
assert_eq!(identity.root_session_id(), &session_id);
}

#[test]
fn descendant_session_identities_inherit_root_and_remain_distinct() {
let root = SessionIdentity::root(n00n_storage::id::SessionRef::generate());
let child = SessionIdentity::child(
n00n_storage::id::SessionRef::generate(),
root.root_session_id().clone(),
);
let grandchild = SessionIdentity::child(
n00n_storage::id::SessionRef::generate(),
child.root_session_id().clone(),
);

assert_ne!(child.session_id(), root.session_id());
assert_ne!(grandchild.session_id(), child.session_id());
assert_eq!(child.root_session_id(), root.root_session_id());
assert_eq!(grandchild.root_session_id(), root.root_session_id());
}

#[test_case(true ; "vision_model_keeps_view_image")]
#[test_case(false ; "text_only_model_loses_view_image")]
fn from_config_gates_view_image_on_vision(vision: bool) {
Expand Down
26 changes: 19 additions & 7 deletions n00n-lua/src/api/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ use n00n_agent::cancel::CancelMap;
use n00n_agent::tools::interpreter_bridge;
use n00n_agent::tools::registry::ToolRegistry;
use n00n_agent::tools::{
Deadline, DescriptionContext, FileReadTracker, LocalToolFn, LocalTools, ToolAudience,
ToolContext, ToolFilter, ToolLive,
Deadline, DescriptionContext, FileReadTracker, LocalToolFn, LocalTools, SessionIdentity,
ToolAudience, ToolContext, ToolFilter, ToolLive,
};
use n00n_agent::{
Agent, AgentEvent, AgentInput, AgentMode, AgentParams, AgentRunParams, Envelope, EventSender,
Expand All @@ -36,6 +36,7 @@ use tracing::info;
use crate::api::ui::buf::BufHandle;
use crate::api::util::convert::{JSON_ARRAY_META_FIELD, json_to_lua, lua_to_json, lua_tool_result};
use crate::api::util::ctx::{AgentContext, LuaCtx};
use crate::state::PluginStateStore;

const SESSION_CLOSED_ERR: &str = "session closed";
const DEFAULT_SESSION_AUDIENCE: ToolAudience = ToolAudience::GENERAL_SUB;
Expand Down Expand Up @@ -93,8 +94,7 @@ fn model_to_lua_table(lua: &Lua, model: &Model) -> LuaResult<Table> {
}

fn dispatch_ctx<'a>(ctx: &'a LuaCtx, method: &str) -> Result<&'a AgentContext, String> {
ctx.agent()
.ok_or_else(|| ctx.cap_err(&format!("n00n.agent.{method}")))
ctx.dispatch_agent(method)
}

fn parse_session_mode(
Expand Down Expand Up @@ -642,6 +642,10 @@ async fn session(
opts: Table,
) -> LuaResult<Pair<mlua::AnyUserData>> {
let agent_ctx = try_pair!(dispatch_ctx(&ctx, "session")).clone();
let Some(parent_identity) = agent_ctx.identity.clone() else {
return Ok(err_pair("session identity is unavailable"));
};
let plugin_state_store = try_pair!(ctx.plugin_state_store());
drop(ctx);
let model_spec: Option<String> = opts.get("model_spec")?;
let system: Option<String> = opts.get("system")?;
Expand Down Expand Up @@ -716,7 +720,7 @@ async fn session(
audience,
workflow: false,
};
let tools = n00n_agent::tools::ToolRegistry::global().definitions_active(
let tools = agent_ctx.registry.definitions_active(
&vars,
&ctx,
model.supports_tool_examples(),
Expand Down Expand Up @@ -848,13 +852,16 @@ async fn session(
config: session_config(&agent_ctx.config, excluded_tools.clone()),
tool_output_lines: n00n_config::ToolOutputLines::default(),
permissions: Arc::clone(&agent_ctx.permissions),
session_id: Some(session_id.into()),
identity: Some(SessionIdentity::child(
session_id.into(),
parent_identity.root_session_id().clone(),
)),
timeouts: agent_ctx.timeouts,
openai_options: agent_ctx.openai_options,
file_tracker: FileReadTracker::fresh(),
prompt_slots: Arc::clone(&agent_ctx.prompt_slots),
subagent_cancels: Arc::new(CancelMap::new()),
registry: Arc::clone(n00n_agent::tools::ToolRegistry::global_arc()),
registry: Arc::clone(&agent_ctx.registry),
audience,
},
system: system.unwrap_or_else(String::new),
Expand All @@ -880,6 +887,8 @@ async fn session(
prompt_rx,
prompt_tx: Some(prompt_tx),
parent_cancels: Arc::clone(&agent_ctx.subagent_cancels),
plugin_state_store,
child_state_owner: session_id,
child_id,
parent_tool_use_id,
parent_event_tx: parent_tx,
Expand Down Expand Up @@ -1381,6 +1390,8 @@ struct SessionState {
prompt_rx: flume::Receiver<SubagentPrompt>,
prompt_tx: Option<flume::Sender<SubagentPrompt>>,
parent_cancels: Arc<CancelMap<String>>,
plugin_state_store: Arc<PluginStateStore>,
child_state_owner: n00nId,
child_id: String,
parent_tool_use_id: String,
parent_event_tx: EventSender,
Expand All @@ -1404,6 +1415,7 @@ impl SessionState {
self.closed = true;
self.progress.set_current_done();
self.parent_cancels.remove(&self.child_id);
self.plugin_state_store.drop_owner(self.child_state_owner);
let messages = std::mem::replace(&mut self.history, History::new(Vec::new())).into_vec();
let _ = self.parent_event_tx.send(AgentEvent::SubagentHistory {
tool_use_id: self.child_id.clone(),
Expand Down
Loading
Loading