From fc44f30aea1150f72a502a7ca6e7be69c36abd67 Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 13 Jul 2026 14:31:22 +0100 Subject: [PATCH 1/8] feat(tools-core): add Unadvertised source wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dispatch-only catalogs: specs() empty, get() passes through, inner catalog events drained but not forwarded. Fills the gap between Filtered (hides dispatch too) and fully advertised sources — needed for compose children the model should invoke through scripts without seeing individually. --- crates/agentkit-tools-core/src/lib.rs | 64 +++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/crates/agentkit-tools-core/src/lib.rs b/crates/agentkit-tools-core/src/lib.rs index f19b08b..501cec3 100644 --- a/crates/agentkit-tools-core/src/lib.rs +++ b/crates/agentkit-tools-core/src/lib.rs @@ -2453,6 +2453,23 @@ pub trait ToolSource: Send + Sync { { Renamed::new(self, mapping) } + + /// Wraps this source so its tools stay resolvable by name but are never + /// advertised: `specs()` is empty and catalog events are swallowed, while + /// `get()` passes through. Use this for dispatch-only catalogs — e.g. + /// children of a composition tool that the model should invoke through a + /// script rather than see individually. + /// + /// Contrast with [`filtered`](Self::filtered), which hides tools from + /// both advertisement *and* dispatch. + /// + /// To wrap an `Arc` instead, use [`Unadvertised::new`]. + fn unadvertised(self) -> Unadvertised + where + Self: Sized, + { + Unadvertised::new(self) + } } impl ToolSource for ToolRegistry { @@ -2594,6 +2611,43 @@ where } } +/// A [`ToolSource`] wrapper that dispatches tools without advertising them. +/// `specs()` is always empty and the inner source's catalog events are +/// drained but not forwarded; `get()` delegates unchanged, so every inner +/// tool remains invocable by exact name. +/// +/// Constructed via [`ToolSource::unadvertised`] or directly. +pub struct Unadvertised { + inner: S, +} + +impl Unadvertised { + /// Creates a new dispatch-only wrapper. + pub fn new(inner: S) -> Self { + Self { inner } + } +} + +impl ToolSource for Unadvertised +where + S: ToolSource, +{ + fn specs(&self) -> Vec { + Vec::new() + } + + fn get(&self, name: &ToolName) -> Option> { + self.inner.get(name) + } + + fn drain_catalog_events(&self) -> Vec { + // Drain the inner source so dynamic catalogs don't accumulate + // unread events, but advertise nothing downstream. + let _ = self.inner.drain_catalog_events(); + Vec::new() + } +} + /// A [`ToolSource`] wrapper that renames specific tools. Tools whose /// original name appears in the forward mapping are advertised under the /// new name and resolved from the new name back to the original. @@ -4573,6 +4627,16 @@ mod tests { assert!(source.get(&ToolName::new("danger_drop")).is_none()); } + #[test] + fn unadvertised_dispatches_without_advertising() { + let source = registry_with(&["hidden_a", "hidden_b"]).unadvertised(); + assert!(source.specs().is_empty()); + assert!(source.get(&ToolName::new("hidden_a")).is_some()); + assert!(source.get(&ToolName::new("hidden_b")).is_some()); + assert!(source.get(&ToolName::new("missing")).is_none()); + assert!(source.drain_catalog_events().is_empty()); + } + #[test] fn renamed_remaps_specs_and_lookups() { let source = registry_with(&["legacy_name", "passthrough"]) From 50ace21471214c49f7e9c5de528fc5b6d35ccc61 Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 13 Jul 2026 14:31:37 +0100 Subject: [PATCH 2/8] fix(mcp): harden server lifecycle state on error paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - disconnect_server: unregister tools and emit ServerDisconnected even when close() fails; error is advisory. Previously a close failure left stale, still-callable adapters in the catalog with no retry path (UnknownServer on reattempt). - unregister_server: new API to fully remove a server (config, options, auth) so detached servers stop accreting and cannot be resurrected by bulk connects. - connect_servers_settled: settle a chosen subset in parallel, idempotent per server — building block for lazy on-demand connection strategies. connect_all_settled now delegates to it and no longer re-handshakes live servers. - close replaced connections on overwrite instead of dropping them silently (leaked Streamable HTTP sessions). - map IO/transport/timeout call failures to ToolError::Unavailable so callers can trigger reconnects without string-matching. - McpServerHandle::tool_names(): list namespaced names without building adapters. --- .../tests/mcp_manager.rs | 91 ++++++++++ crates/agentkit-mcp/src/lib.rs | 158 +++++++++++++++--- 2 files changed, 224 insertions(+), 25 deletions(-) diff --git a/crates/agentkit-integration-tests/tests/mcp_manager.rs b/crates/agentkit-integration-tests/tests/mcp_manager.rs index 9d6c54c..1cc8e78 100644 --- a/crates/agentkit-integration-tests/tests/mcp_manager.rs +++ b/crates/agentkit-integration-tests/tests/mcp_manager.rs @@ -147,6 +147,97 @@ async fn connect_all_settled_keeps_successes_and_reports_each_failure() { assert_tool_names(&source, &["mcp_alpha_only_alpha", "mcp_beta_only_beta"]); } +#[tokio::test] +async fn connect_servers_settled_targets_requested_servers_idempotently() { + let alpha = spawn_http_mcp(vec![simple_tool("only_alpha", "alpha-only tool.")]).await; + let beta = spawn_http_mcp(vec![simple_tool("only_beta", "beta-only tool.")]).await; + + let mut manager = McpServerManager::new() + .with_server(McpServerConfig::streamable_http("alpha", &alpha.url)) + .with_server(McpServerConfig::streamable_http("beta", &beta.url)); + let source = manager.source(); + + // Only requested servers are attempted; unknown ids settle as failures. + let settled = manager.connect_servers_settled(["alpha", "missing"]).await; + assert_eq!(settled.connected().len(), 1); + assert_eq!(settled.failed().len(), 1); + assert_eq!(settled.failed()[0].server_id, McpServerId::new("missing")); + assert!(matches!( + settled.failed()[0].error, + McpError::UnknownServer(_) + )); + assert!( + manager + .connected_server(&McpServerId::new("beta")) + .is_none() + ); + assert_tool_names(&source, &["mcp_alpha_only_alpha"]); + + let alpha_handle = manager + .connected_server(&McpServerId::new("alpha")) + .expect("alpha connected"); + assert_eq!( + alpha_handle.tool_names(), + vec![agentkit_tools_core::ToolName::new("mcp_alpha_only_alpha")] + ); + let first_alpha = alpha_handle.connection(); + + // Re-requesting a connected server is idempotent (same live connection, + // no re-handshake); duplicates are attempted once. + let settled = manager + .connect_servers_settled(["alpha", "alpha", "beta"]) + .await; + assert_eq!(settled.connected().len(), 2); + assert!(!settled.has_failures()); + let second_alpha = manager + .connected_server(&McpServerId::new("alpha")) + .expect("alpha connected") + .connection(); + assert!( + std::sync::Arc::ptr_eq(&first_alpha, &second_alpha), + "already-connected server must keep its live connection" + ); + assert_tool_names(&source, &["mcp_alpha_only_alpha", "mcp_beta_only_beta"]); +} + +#[tokio::test] +async fn unregister_server_removes_registration_and_tools() { + let alpha = spawn_http_mcp(vec![simple_tool("only_alpha", "alpha-only tool.")]).await; + + let mut manager = + McpServerManager::new().with_server(McpServerConfig::streamable_http("alpha", &alpha.url)); + let source = manager.source(); + manager.connect_all().await.expect("connect_all succeeds"); + assert_tool_names(&source, &["mcp_alpha_only_alpha"]); + + manager + .unregister_server(&McpServerId::new("alpha")) + .await + .expect("unregister succeeds"); + assert!( + manager + .connected_server(&McpServerId::new("alpha")) + .is_none() + ); + assert_tool_names(&source, &[]); + + // Gone for good: direct reconnects error and bulk connects no longer + // resurrect the config. + assert!(matches!( + manager.connect_server(&McpServerId::new("alpha")).await, + Err(McpError::UnknownServer(_)) + )); + let settled = manager.connect_all_settled().await; + assert!(settled.connected().is_empty()); + assert!(!settled.has_failures()); + + // Unregistering an id the manager has never heard of reports it. + assert!(matches!( + manager.unregister_server(&McpServerId::new("alpha")).await, + Err(McpError::UnknownServer(_)) + )); +} + #[tokio::test] async fn connect_all_settled_times_out_slow_discovery_per_server() { let fast = spawn_http_mcp(vec![simple_tool("fast_tool", "Fast tool.")]).await; diff --git a/crates/agentkit-mcp/src/lib.rs b/crates/agentkit-mcp/src/lib.rs index 1e310d3..f1b3acb 100644 --- a/crates/agentkit-mcp/src/lib.rs +++ b/crates/agentkit-mcp/src/lib.rs @@ -2114,6 +2114,18 @@ impl McpServerHandle { &self.namespace } + /// Returns the agentkit-namespaced name of every tool this server + /// advertised at discovery time, without building adapters or cloning + /// schemas. Cheaper than [`Self::tool_registry`] when only the names + /// are needed. + pub fn tool_names(&self) -> Vec { + self.snapshot + .tools + .iter() + .map(|tool| ToolName::new(self.namespace.apply(self.server_id(), &tool.name))) + .collect() + } + /// Builds a [`ToolRegistry`] containing an [`McpToolAdapter`] for each tool. pub fn tool_registry(&self) -> ToolRegistry { self.snapshot @@ -2423,7 +2435,8 @@ impl McpServerManager { snapshot, namespace: self.namespace.clone(), }; - self.connections.insert(server_id.clone(), handle.clone()); + self.install_connection(server_id.clone(), handle.clone()) + .await; self.register_server_tools(server_id, &handle.snapshot); self.emit_catalog_event(McpCatalogEvent::ServerConnected { server_id: server_id.clone(), @@ -2431,6 +2444,16 @@ impl McpServerManager { Ok(handle) } + /// Installs a freshly connected handle, gracefully closing any previous + /// connection it replaces so transports (and, for Streamable HTTP, + /// server-side sessions) are not leaked. Close failures on the replaced + /// connection are ignored — it is being discarded either way. + async fn install_connection(&mut self, server_id: McpServerId, handle: McpServerHandle) { + if let Some(previous) = self.connections.insert(server_id, handle) { + let _ = previous.connection.close().await; + } + } + /// Connects all registered servers concurrently. pub async fn connect_all(&mut self) -> Result, McpError> { let plans: Vec<( @@ -2478,7 +2501,7 @@ impl McpServerManager { Vec::with_capacity(results.len()); for (server_id, handle) in results { connected.push((server_id.clone(), handle.snapshot.clone())); - self.connections.insert(server_id, handle.clone()); + self.install_connection(server_id, handle.clone()).await; handles.push(handle); } for (server_id, snapshot) in &connected { @@ -2494,27 +2517,71 @@ impl McpServerManager { /// connection attempt to settle. /// /// Unlike [`Self::connect_all`], this method does not fail fast. Every - /// server is attempted in parallel; successful connections are installed - /// into the manager and tool catalog, while each failed connection is - /// returned with its [`McpServerId`] and [`McpError`]. + /// unconnected server is attempted in parallel; successful connections + /// are installed into the manager and tool catalog, while each failed + /// connection is returned with its [`McpServerId`] and [`McpError`]. + /// + /// Already-connected servers are not re-handshaken: their existing + /// handles are left untouched and returned as connected. pub async fn connect_all_settled(&mut self) -> McpConnectAllSettled { - let plans: Vec<( + let server_ids: Vec = self.configs.keys().cloned().collect(); + self.connect_servers_settled(server_ids).await + } + + /// Connects the given registered servers concurrently and waits for + /// every connection attempt to settle. + /// + /// The settled semantics match [`Self::connect_all_settled`], scoped to + /// `server_ids`. The call is idempotent per server: + /// + /// - already-connected servers are not re-handshaken; their existing + /// handles are returned as connected, + /// - unregistered identifiers settle as failures with + /// [`McpError::UnknownServer`], + /// - duplicate identifiers are attempted once. + /// + /// This is the building block for lazy, on-demand connection strategies: + /// connect exactly the servers a turn needs, in parallel, without + /// touching (or resurrecting) anything else the manager knows about. + pub async fn connect_servers_settled(&mut self, server_ids: I) -> McpConnectAllSettled + where + I: IntoIterator, + T: Into, + { + let mut connected = Vec::new(); + let mut failures = Vec::new(); + let mut seen = BTreeSet::new(); + let mut plans: Vec<( McpServerId, McpServerConfig, McpServerOptions, Option, - )> = self - .configs - .iter() - .map(|(id, cfg)| { - ( - id.clone(), - cfg.clone(), - self.options.get(id).cloned().unwrap_or_default(), - self.auth.get(id).cloned(), - ) - }) - .collect(); + )> = Vec::new(); + + for server_id in server_ids { + let server_id: McpServerId = server_id.into(); + if !seen.insert(server_id.clone()) { + continue; + } + if let Some(existing) = self.connections.get(&server_id) { + connected.push(existing.clone()); + continue; + } + let Some(config) = self.configs.get(&server_id).cloned() else { + failures.push(McpServerConnectionError { + error: McpError::UnknownServer(server_id.to_string()), + server_id, + }); + continue; + }; + plans.push(( + server_id.clone(), + config, + self.options.get(&server_id).cloned().unwrap_or_default(), + self.auth.get(&server_id).cloned(), + )); + } + let handler_config = self.handler_config.clone(); let namespace = self.namespace.clone(); @@ -2543,15 +2610,13 @@ impl McpServerManager { }); let results = join_all(futures).await; - let mut connected = Vec::new(); - let mut failures = Vec::new(); let mut connected_snapshots = Vec::new(); for (server_id, result) in results { match result { Ok(handle) => { connected_snapshots.push((server_id.clone(), handle.snapshot.clone())); - self.connections.insert(server_id, handle.clone()); + self.install_connection(server_id, handle.clone()).await; connected.push(handle); } Err(error) => { @@ -2656,16 +2721,43 @@ impl McpServerManager { } /// Disconnects a server and removes it from active connections. + /// + /// The server's tools are always removed from the catalog and + /// [`McpCatalogEvent::ServerDisconnected`] is always emitted, even when + /// closing the underlying connection fails — the returned error is + /// advisory. After this method returns the manager no longer tracks the + /// connection, and the server (whose config stays registered) can be + /// reconnected via [`Self::connect_server`]. pub async fn disconnect_server(&mut self, server_id: &McpServerId) -> Result<(), McpError> { let Some(handle) = self.connections.remove(server_id) else { return Err(McpError::UnknownServer(server_id.to_string())); }; - handle.connection.close().await?; + let close_result = handle.connection.close().await; self.unregister_server_tools(server_id); self.emit_catalog_event(McpCatalogEvent::ServerDisconnected { server_id: server_id.clone(), }); - Ok(()) + close_result + } + + /// Removes a server's registration entirely: its config, options, and + /// stored credentials. If the server is currently connected it is + /// disconnected first (tools unregistered, + /// [`McpCatalogEvent::ServerDisconnected`] emitted), and any close error + /// is returned as advisory after the removal has completed. + /// + /// Unlike [`Self::disconnect_server`], the server cannot be reconnected + /// afterwards without registering it again, and bulk operations such as + /// [`Self::connect_all_settled`] no longer attempt it. + pub async fn unregister_server(&mut self, server_id: &McpServerId) -> Result<(), McpError> { + let was_registered = self.configs.remove(server_id).is_some(); + self.options.remove(server_id); + self.auth.remove(server_id); + match self.disconnect_server(server_id).await { + // Registered but not connected: nothing to close. + Err(McpError::UnknownServer(_)) if was_registered => Ok(()), + result => result, + } } /// Stores or clears authentication credentials for a server. @@ -3008,6 +3100,22 @@ impl McpToolAdapter { } } +/// Maps a non-invocation [`McpError`] onto the [`ToolError`] vocabulary. +/// +/// I/O, transport, and timeout failures mean the connection itself is +/// unhealthy rather than that the call was invalid, so they surface as +/// [`ToolError::Unavailable`] — callers can match on the variant to trigger +/// reconnects without string-inspection. JSON-RPC application errors and +/// everything else remain [`ToolError::ExecutionFailed`]. +fn tool_error_from_mcp(error: McpError) -> ToolError { + match &error { + McpError::Io(_) | McpError::Transport(_) | McpError::Timeout { .. } => { + ToolError::Unavailable(error.to_string()) + } + _ => ToolError::ExecutionFailed(error.to_string()), + } +} + #[async_trait] impl Tool for McpToolAdapter { fn spec(&self) -> &ToolSpec { @@ -3072,11 +3180,11 @@ impl Tool for McpToolAdapter { Err(McpError::Invocation(err)) => { self.handle_invocation_error(err, &input).await? } - Err(other) => return Err(ToolError::ExecutionFailed(other.to_string())), + Err(other) => return Err(tool_error_from_mcp(other)), } } Err(McpError::Invocation(err)) => self.handle_invocation_error(err, &input).await?, - Err(other) => return Err(ToolError::ExecutionFailed(other.to_string())), + Err(other) => return Err(tool_error_from_mcp(other)), }; let is_error = result.is_error.unwrap_or(false); From 4f828dab2d4f31b016dceefc8c00a1ad36bee5ab Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 13 Jul 2026 14:31:46 +0100 Subject: [PATCH 3/8] perf(compose): cache rendered spec between catalog events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit specs() re-rendered every child's output schema into the compose description on each call — once per model step — even for frozen catalogs. Cache the rendered spec and invalidate on drain_catalog_events, the same signal the loop uses to refresh the model-visible catalog, so the cache is never staler than the model's view. Document Unadvertised as the supported way to keep children dispatchable without enumerating them. --- crates/agentkit-tool-compose/src/lib.rs | 169 +++++++++++++++++++++++- 1 file changed, 163 insertions(+), 6 deletions(-) diff --git a/crates/agentkit-tool-compose/src/lib.rs b/crates/agentkit-tool-compose/src/lib.rs index e0ef81a..1b6203e 100644 --- a/crates/agentkit-tool-compose/src/lib.rs +++ b/crates/agentkit-tool-compose/src/lib.rs @@ -7,8 +7,8 @@ use std::collections::{BTreeMap, BTreeSet}; use std::error::Error; use std::fmt; -use std::sync::Arc; -use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex as StdMutex}; use agentkit_core::{MetadataMap, ToolCallId, ToolOutput, ToolResultPart, TurnCancellation}; use agentkit_tools_core::{ @@ -117,6 +117,29 @@ pub struct ComposeTool { config: ComposeConfig, states: Arc>>, sources: Vec>, + spec_cache: Arc, +} + +/// Memoizes the rendered compose spec between catalog changes. +/// +/// The compose description enumerates every child's output schema, so +/// rebuilding it on each `specs()` call (once per model step) is wasted work +/// for static catalogs. The cache is invalidated when a child source reports +/// catalog events through [`ToolSource::drain_catalog_events`] — the same +/// signal the agent loop uses to refresh the model-visible catalog, so the +/// cached spec can never be staler than what the model already sees. +struct ComposeSpecCache { + dirty: AtomicBool, + spec: StdMutex>, +} + +impl ComposeSpecCache { + fn empty() -> Self { + Self { + dirty: AtomicBool::new(false), + spec: StdMutex::new(None), + } + } } impl ComposeTool { @@ -169,9 +192,17 @@ impl ComposeTool { } /// Adds another child source to this compose source. + /// + /// To make a source's tools dispatchable through `tool(name, input)` + /// without advertising them individually to the model (and without + /// enumerating their schemas in the compose description), wrap it with + /// [`ToolSource::unadvertised`] first. pub fn with_source(mut self, source: impl ToolSource + 'static) -> Self { self.sources.push(Arc::new(source)); - self.spec = self.compose_spec(); + // Fresh cache: clones of the pre-`with_source` tool must not share + // a slot with the extended catalog. + self.spec_cache = Arc::new(ComposeSpecCache::empty()); + self.spec = self.cached_compose_spec(); self } @@ -187,8 +218,9 @@ impl ComposeTool { config, states: Arc::new(Mutex::new(BTreeMap::new())), sources, + spec_cache: Arc::new(ComposeSpecCache::empty()), }; - tool.spec = tool.compose_spec(); + tool.spec = tool.cached_compose_spec(); tool } @@ -225,6 +257,21 @@ impl ComposeTool { Self::base_spec(&self.config, Some(&catalog)) } + /// Returns the compose spec, recomputing it only when a child source + /// has reported catalog changes since the last call (see + /// [`ComposeSpecCache`]). + fn cached_compose_spec(&self) -> ToolSpec { + let mut slot = self + .spec_cache + .spec + .lock() + .expect("compose spec cache poisoned"); + if self.spec_cache.dirty.swap(false, Ordering::AcqRel) || slot.is_none() { + *slot = Some(self.compose_spec()); + } + slot.clone().expect("compose spec cache filled above") + } + fn child_specs(&self) -> Vec { let mut seen = BTreeSet::new(); let mut specs = Vec::new(); @@ -299,7 +346,7 @@ impl ToolSource for ComposeTool { fn specs(&self) -> Vec { let mut seen = BTreeSet::new(); let mut specs = Vec::new(); - let compose_spec = self.compose_spec(); + let compose_spec = self.cached_compose_spec(); seen.insert(compose_spec.name.clone()); specs.push(compose_spec); for spec in self.child_specs() { @@ -324,6 +371,7 @@ impl ToolSource for ComposeTool { .flat_map(|source| source.drain_catalog_events()) .collect(); if !events.is_empty() { + self.spec_cache.dirty.store(true, Ordering::Release); let mut event = ToolCatalogEvent::new(COMPOSE_TOOL_NAME); event.changed.push(COMPOSE_TOOL_NAME.into()); events.push(event); @@ -389,7 +437,7 @@ impl Tool for ComposeTool { } fn current_spec(&self) -> Option { - Some(self.compose_spec()) + Some(self.cached_compose_spec()) } async fn invoke( @@ -1252,4 +1300,113 @@ mod tests { // Two compose runs, each making two nested calls. assert_eq!(child_calls.load(Ordering::SeqCst), 4); } + + #[tokio::test] + async fn unadvertised_children_dispatch_without_advertisement() { + let child = EchoTool::new(); + let child_calls = child.calls.clone(); + let compose = ComposeTool::new(ComposeConfig::default()) + .with_source(ToolRegistry::new().with(child).unadvertised()); + + let specs = ToolSource::specs(&compose); + let names: Vec<_> = specs.iter().map(|s| s.name.0.as_str()).collect(); + assert_eq!(names, vec![COMPOSE_TOOL_NAME]); + assert!( + !specs[0].description.contains("echo"), + "hidden child must not be enumerated in the compose description" + ); + assert!( + ToolSource::get(&compose, &ToolName::new("echo")).is_some(), + "hidden child stays resolvable for dispatch" + ); + + let executor: Arc = Arc::new(BasicToolExecutor::new([ + Arc::new(compose) as Arc + ])); + let owned = owned_context(executor.clone(), Arc::new(AllowAllPermissions)); + let mut ctx = owned.borrowed(); + let outcome = executor + .execute( + request("return tool('echo', { value = 7 })", Value::Null), + &mut ctx, + ) + .await; + match outcome { + ToolExecutionOutcome::Completed(result) => assert_eq!( + result.result.output, + ToolOutput::structured(json!({ "value": 7 })) + ), + other => panic!("unexpected outcome: {other:?}"), + } + assert_eq!(child_calls.load(Ordering::SeqCst), 1); + } + + /// A dynamic source whose advertised description can change at runtime + /// and whose pending catalog events are surfaced through + /// `drain_catalog_events`. + struct MutableSource { + spec: StdMutex, + pending_event: AtomicBool, + specs_calls: Arc, + } + + impl ToolSource for MutableSource { + fn specs(&self) -> Vec { + self.specs_calls.fetch_add(1, Ordering::SeqCst); + vec![self.spec.lock().expect("spec lock").clone()] + } + + fn get(&self, _name: &ToolName) -> Option> { + None + } + + fn drain_catalog_events(&self) -> Vec { + if self.pending_event.swap(false, Ordering::AcqRel) { + let mut event = ToolCatalogEvent::new("mutable"); + event.changed.push("echo".into()); + vec![event] + } else { + Vec::new() + } + } + } + + #[test] + fn compose_spec_is_cached_until_catalog_events() { + let specs_calls = Arc::new(AtomicUsize::new(0)); + let source = Arc::new(MutableSource { + spec: StdMutex::new( + ToolSpec::new("echo", "echo input", json!({"type": "object"})) + .with_output_schema(json!({"type": "string"})), + ), + pending_event: AtomicBool::new(false), + specs_calls: specs_calls.clone(), + }); + let compose = ComposeTool::new(ComposeConfig::default()).with_source(source.clone()); + + let baseline = specs_calls.load(Ordering::SeqCst); + let first = ToolSource::specs(&compose); + // One walk for the live child list only — the compose description is + // served from cache, not re-rendered from another source.specs() pass. + assert_eq!(specs_calls.load(Ordering::SeqCst), baseline + 1); + let second = ToolSource::specs(&compose); + assert_eq!(specs_calls.load(Ordering::SeqCst), baseline + 2); + assert_eq!(first[0].description, second[0].description); + + // Change the child's output schema without an event: the cached + // compose description intentionally stays as-is (the model's catalog + // would not refresh either). + *source.spec.lock().expect("spec lock") = + ToolSpec::new("echo", "echo input", json!({"type": "object"})) + .with_output_schema(json!({"type": "number"})); + let stale = ToolSource::specs(&compose); + assert!(stale[0].description.contains("\"string\"")); + + // After the source reports a catalog event, the description refreshes. + source.pending_event.store(true, Ordering::Release); + let events = ToolSource::drain_catalog_events(&compose); + assert!(!events.is_empty()); + let fresh = ToolSource::specs(&compose); + assert!(fresh[0].description.contains("\"number\"")); + } } From 15a4aad890d82d00b44f789591d10cada19c1a90 Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 13 Jul 2026 14:41:46 +0100 Subject: [PATCH 4/8] test(snapshots): canonicalise JSON order in recordings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agent-client-protocol enables serde_json preserve_order for the whole workspace, so map serialization order depends on the build graph — snapshots recorded pre-ACP mismatched deterministically (permission_deny) or flaked by binary (after_tool_result). Sort object keys during normalisation and refresh recordings, which also picks up the failure_kind metadata and explicit None fields stale snapshots predated. --- .../src/snapshot.rs | 100 ++++++++++++++++-- .../snapshots/after_tool_result_submit.ron | 4 + .../tests/snapshots/approval_flow_approve.ron | 4 + .../tests/snapshots/approval_flow_deny.ron | 4 + .../tests/snapshots/bg_detach_post_turn.ron | 6 ++ .../tests/snapshots/bg_detach_quick.ron | 4 + .../tests/snapshots/bg_foreground.ron | 4 + .../tests/snapshots/bg_mixed.ron | 6 ++ .../tests/snapshots/bg_pure_background.ron | 4 + .../tests/snapshots/bg_two_in_order.ron | 8 ++ .../tests/snapshots/bg_two_out_of_order.ron | 8 ++ .../tests/snapshots/collision_first_wins.ron | 4 + .../tests/snapshots/collision_last_wins.ron | 4 + .../tests/snapshots/dynamic_catalog.ron | 6 ++ .../tests/snapshots/federation.ron | 6 ++ .../tests/snapshots/mcp_connect.ron | 4 + .../tests/snapshots/mcp_disconnect.ron | 4 + .../tests/snapshots/mcp_list_changed.ron | 4 + .../tests/snapshots/mcp_progress.ron | 4 + .../tests/snapshots/mcp_refresh_added.ron | 4 + .../tests/snapshots/mcp_refresh_removed.ron | 4 + .../tests/snapshots/permission_deny.ron | 12 ++- .../tests/snapshots/tool_execution_failed.ron | 4 + 23 files changed, 199 insertions(+), 13 deletions(-) diff --git a/crates/agentkit-integration-tests/src/snapshot.rs b/crates/agentkit-integration-tests/src/snapshot.rs index b9fc901..5ab39ea 100644 --- a/crates/agentkit-integration-tests/src/snapshot.rs +++ b/crates/agentkit-integration-tests/src/snapshot.rs @@ -23,7 +23,7 @@ use std::collections::VecDeque; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; -use agentkit_core::{Item, TurnCancellation}; +use agentkit_core::{Item, MetadataMap, Part, ToolOutput, TurnCancellation}; use agentkit_loop::{ LoopError, ModelAdapter, ModelSession, ModelTurn, ModelTurnEvent, SessionConfig, TurnRequest, }; @@ -288,22 +288,100 @@ pub fn assert_recording(observed: &SessionRecording, path: impl AsRef) { /// Strips non-deterministic fields the loop populates so snapshot comparisons /// stay stable. Currently zeroes [`Item::created_at`] across every Item. fn normalise_recording(recording: &mut SessionRecording) { - for item in &mut recording.initial_items { - item.created_at = None; - } + recording.initial_items.iter_mut().for_each(normalise_item); for turn in &mut recording.turns { - for item in &mut turn.input { - item.created_at = None; + turn.input.iter_mut().for_each(normalise_item); + for spec in &mut turn.tools { + canonicalise_json(&mut spec.input_schema); + if let Some(schema) = &mut spec.output_schema { + canonicalise_json(schema); + } + canonicalise_metadata(&mut spec.metadata); } for event in &mut turn.events { - if let ModelTurnEvent::Finished(result) = event { - for item in &mut result.output_items { - item.created_at = None; + match event { + ModelTurnEvent::ToolCall(part) => { + canonicalise_json(&mut part.input); + canonicalise_metadata(&mut part.metadata); + } + ModelTurnEvent::Finished(result) => { + result.output_items.iter_mut().for_each(normalise_item); + canonicalise_metadata(&mut result.metadata); } + ModelTurnEvent::Delta(_) | ModelTurnEvent::Usage(_) => {} } } } - for item in &mut recording.final_transcript { - item.created_at = None; + recording.final_transcript.iter_mut().for_each(normalise_item); +} + +fn normalise_item(item: &mut Item) { + item.created_at = None; + item.parts.iter_mut().for_each(canonicalise_part); + canonicalise_metadata(&mut item.metadata); +} + +fn canonicalise_part(part: &mut Part) { + match part { + Part::Text(part) => canonicalise_metadata(&mut part.metadata), + Part::Media(part) => canonicalise_metadata(&mut part.metadata), + Part::File(part) => canonicalise_metadata(&mut part.metadata), + Part::Reasoning(part) => canonicalise_metadata(&mut part.metadata), + Part::Structured(part) => { + canonicalise_json(&mut part.value); + if let Some(schema) = &mut part.schema { + canonicalise_json(schema); + } + canonicalise_metadata(&mut part.metadata); + } + Part::ToolCall(part) => { + canonicalise_json(&mut part.input); + canonicalise_metadata(&mut part.metadata); + } + Part::ToolResult(part) => { + canonicalise_output(&mut part.output); + canonicalise_metadata(&mut part.metadata); + } + Part::Custom(part) => { + if let Some(value) = &mut part.value { + canonicalise_json(value); + } + canonicalise_metadata(&mut part.metadata); + } + } +} + +fn canonicalise_output(output: &mut ToolOutput) { + match output { + ToolOutput::Text(_) => {} + ToolOutput::Structured(value) => canonicalise_json(value), + ToolOutput::Parts(parts) => parts.iter_mut().for_each(canonicalise_part), + ToolOutput::Files(files) => files + .iter_mut() + .for_each(|file| canonicalise_metadata(&mut file.metadata)), + } +} + +fn canonicalise_metadata(metadata: &mut MetadataMap) { + metadata.values_mut().for_each(canonicalise_json); +} + +/// Recursively sorts JSON object keys so recordings compare identically +/// whether or not some crate in the build graph enables serde_json's +/// `preserve_order` feature (agent-client-protocol does, so workspace-wide +/// builds serialize maps in insertion order while narrower builds sort them). +fn canonicalise_json(value: &mut serde_json::Value) { + match value { + serde_json::Value::Object(map) => { + let mut entries: Vec<(String, serde_json::Value)> = + std::mem::take(map).into_iter().collect(); + entries.sort_by(|left, right| left.0.cmp(&right.0)); + for (_, nested) in &mut entries { + canonicalise_json(nested); + } + map.extend(entries); + } + serde_json::Value::Array(values) => values.iter_mut().for_each(canonicalise_json), + _ => {} } } diff --git a/crates/agentkit-integration-tests/tests/snapshots/after_tool_result_submit.ron b/crates/agentkit-integration-tests/tests/snapshots/after_tool_result_submit.ron index 3db08a5..95a01d7 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/after_tool_result_submit.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/after_tool_result_submit.ron @@ -81,6 +81,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -186,6 +188,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/approval_flow_approve.ron b/crates/agentkit-integration-tests/tests/snapshots/approval_flow_approve.ron index 7806bf3..01620f9 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/approval_flow_approve.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/approval_flow_approve.ron @@ -109,6 +109,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -214,6 +216,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/approval_flow_deny.ron b/crates/agentkit-integration-tests/tests/snapshots/approval_flow_deny.ron index ed7a556..d147a3b 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/approval_flow_deny.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/approval_flow_deny.ron @@ -109,6 +109,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -214,6 +216,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/bg_detach_post_turn.ron b/crates/agentkit-integration-tests/tests/snapshots/bg_detach_post_turn.ron index f8cb912..dd8f1c0 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/bg_detach_post_turn.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/bg_detach_post_turn.ron @@ -109,6 +109,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -214,6 +216,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -347,6 +351,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/bg_detach_quick.ron b/crates/agentkit-integration-tests/tests/snapshots/bg_detach_quick.ron index 8ce766c..11a0463 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/bg_detach_quick.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/bg_detach_quick.ron @@ -109,6 +109,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -214,6 +216,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/bg_foreground.ron b/crates/agentkit-integration-tests/tests/snapshots/bg_foreground.ron index 46183b8..3e03012 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/bg_foreground.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/bg_foreground.ron @@ -109,6 +109,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -214,6 +216,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/bg_mixed.ron b/crates/agentkit-integration-tests/tests/snapshots/bg_mixed.ron index dd672c9..7033370 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/bg_mixed.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/bg_mixed.ron @@ -137,6 +137,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -280,6 +282,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -451,6 +455,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/bg_pure_background.ron b/crates/agentkit-integration-tests/tests/snapshots/bg_pure_background.ron index 655d535..debdbf5 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/bg_pure_background.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/bg_pure_background.ron @@ -109,6 +109,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -214,6 +216,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/bg_two_in_order.ron b/crates/agentkit-integration-tests/tests/snapshots/bg_two_in_order.ron index d8b69e5..d22c9a2 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/bg_two_in_order.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/bg_two_in_order.ron @@ -137,6 +137,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -280,6 +282,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -451,6 +455,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -650,6 +656,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/bg_two_out_of_order.ron b/crates/agentkit-integration-tests/tests/snapshots/bg_two_out_of_order.ron index 42a3f92..5c9ad56 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/bg_two_out_of_order.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/bg_two_out_of_order.ron @@ -137,6 +137,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -280,6 +282,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -451,6 +455,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -650,6 +656,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/collision_first_wins.ron b/crates/agentkit-integration-tests/tests/snapshots/collision_first_wins.ron index 41eecfc..fd6016b 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/collision_first_wins.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/collision_first_wins.ron @@ -109,6 +109,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -214,6 +216,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/collision_last_wins.ron b/crates/agentkit-integration-tests/tests/snapshots/collision_last_wins.ron index e87ff9e..47bac87 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/collision_last_wins.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/collision_last_wins.ron @@ -109,6 +109,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -214,6 +216,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/dynamic_catalog.ron b/crates/agentkit-integration-tests/tests/snapshots/dynamic_catalog.ron index 4202461..262f540 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/dynamic_catalog.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/dynamic_catalog.ron @@ -101,6 +101,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -218,6 +220,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -347,6 +351,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/federation.ron b/crates/agentkit-integration-tests/tests/snapshots/federation.ron index da84147..b7c733f 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/federation.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/federation.ron @@ -177,6 +177,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -362,6 +364,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -568,6 +572,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/mcp_connect.ron b/crates/agentkit-integration-tests/tests/snapshots/mcp_connect.ron index 50c562d..7717e6a 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/mcp_connect.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/mcp_connect.ron @@ -133,6 +133,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -259,6 +261,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/mcp_disconnect.ron b/crates/agentkit-integration-tests/tests/snapshots/mcp_disconnect.ron index 02cd84e..b376a5a 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/mcp_disconnect.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/mcp_disconnect.ron @@ -119,6 +119,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -221,6 +223,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/mcp_list_changed.ron b/crates/agentkit-integration-tests/tests/snapshots/mcp_list_changed.ron index 01cd719..53e02bb 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/mcp_list_changed.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/mcp_list_changed.ron @@ -102,6 +102,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -221,6 +223,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/mcp_progress.ron b/crates/agentkit-integration-tests/tests/snapshots/mcp_progress.ron index cfe4c45..d9578cb 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/mcp_progress.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/mcp_progress.ron @@ -127,6 +127,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -261,6 +263,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/mcp_refresh_added.ron b/crates/agentkit-integration-tests/tests/snapshots/mcp_refresh_added.ron index dbfabdf..917ac62 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/mcp_refresh_added.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/mcp_refresh_added.ron @@ -102,6 +102,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -221,6 +223,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/mcp_refresh_removed.ron b/crates/agentkit-integration-tests/tests/snapshots/mcp_refresh_removed.ron index 8aaf76b..1064f38 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/mcp_refresh_removed.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/mcp_refresh_removed.ron @@ -119,6 +119,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -221,6 +223,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), diff --git a/crates/agentkit-integration-tests/tests/snapshots/permission_deny.ron b/crates/agentkit-integration-tests/tests/snapshots/permission_deny.ron index e9c0f76..814e8cb 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/permission_deny.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/permission_deny.ron @@ -81,6 +81,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -124,7 +126,9 @@ SessionRecording( call_id: ToolCallId("call-deny"), output: Text("tool permission denied: PermissionDenial { code: CustomPolicyDenied, message: \"policy says no\", metadata: {} }"), is_error: true, - metadata: {}, + metadata: { + "agentkit.tool.failure_kind": "permission_denied", + }, )), ], metadata: {}, @@ -172,6 +176,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -215,7 +221,9 @@ SessionRecording( call_id: ToolCallId("call-deny"), output: Text("tool permission denied: PermissionDenial { code: CustomPolicyDenied, message: \"policy says no\", metadata: {} }"), is_error: true, - metadata: {}, + metadata: { + "agentkit.tool.failure_kind": "permission_denied", + }, )), ], metadata: {}, diff --git a/crates/agentkit-integration-tests/tests/snapshots/tool_execution_failed.ron b/crates/agentkit-integration-tests/tests/snapshots/tool_execution_failed.ron index 70b04d3..734ba7a 100644 --- a/crates/agentkit-integration-tests/tests/snapshots/tool_execution_failed.ron +++ b/crates/agentkit-integration-tests/tests/snapshots/tool_execution_failed.ron @@ -81,6 +81,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), @@ -172,6 +174,8 @@ SessionRecording( ], usage: None, metadata: {}, + model: None, + response_id: None, )), ], ), From 4037424fed3bd379eda6c63a787a95abcf7e033a Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 13 Jul 2026 14:47:51 +0100 Subject: [PATCH 5/8] fix(mcp): surface request-side JSON-RPC errors as InvalidInput MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InvalidParams/InvalidRequest/ParseError mean the caller sent a bad request — map them to ToolError::InvalidInput so the model is told to fix its arguments instead of treating the tool as broken. Only applies after the error responder declines (PassThrough) or when none is installed; server-side and custom codes stay ExecutionFailed. --- crates/agentkit-mcp/src/lib.rs | 104 ++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/crates/agentkit-mcp/src/lib.rs b/crates/agentkit-mcp/src/lib.rs index f1b3acb..1109f42 100644 --- a/crates/agentkit-mcp/src/lib.rs +++ b/crates/agentkit-mcp/src/lib.rs @@ -3082,7 +3082,7 @@ impl McpToolAdapter { input: &Value, ) -> Result { let Some(responder) = self.connection.handler_config().error_responder.clone() else { - return Err(ToolError::ExecutionFailed(err.to_string())); + return Err(tool_error_from_invocation(err)); }; let method = McpMethod::ToolsCall { name: self.tool_name.clone(), @@ -3095,11 +3095,26 @@ impl McpToolAdapter { }; match responder.handle(&err, ctx).await { ErrorResponderOutcome::SynthesizeResult(result) => Ok(result), - ErrorResponderOutcome::PassThrough => Err(ToolError::ExecutionFailed(err.to_string())), + ErrorResponderOutcome::PassThrough => Err(tool_error_from_invocation(err)), } } } +/// Maps a JSON-RPC invocation error that no [`McpErrorResponder`] +/// synthesized into the [`ToolError`] vocabulary. Request-side faults — +/// invalid params, invalid request, parse errors — surface as +/// [`ToolError::InvalidInput`] so the model is told to fix its arguments +/// rather than that the tool broke; server-side and unrecognized codes stay +/// [`ToolError::ExecutionFailed`]. +fn tool_error_from_invocation(error: McpInvocationError) -> ToolError { + match &error { + McpInvocationError::InvalidParams { .. } + | McpInvocationError::InvalidRequest { .. } + | McpInvocationError::ParseError { .. } => ToolError::InvalidInput(error.to_string()), + _ => ToolError::ExecutionFailed(error.to_string()), + } +} + /// Maps a non-invocation [`McpError`] onto the [`ToolError`] vocabulary. /// /// I/O, transport, and timeout failures mean the connection itself is @@ -3751,3 +3766,88 @@ impl From for McpServerId { Self::new(value) } } + +#[cfg(test)] +mod error_mapping_tests { + use super::*; + + #[test] + fn transport_class_errors_map_to_unavailable() { + for error in [ + McpError::Transport("connection reset".into()), + McpError::Timeout { + operation: "tools/call", + duration: Duration::from_secs(5), + }, + McpError::Io(std::io::Error::other("broken pipe")), + ] { + assert!( + matches!(tool_error_from_mcp(error), ToolError::Unavailable(_)), + "transport-class errors must surface as Unavailable" + ); + } + } + + #[test] + fn non_transport_errors_stay_execution_failed() { + for error in [ + McpError::Protocol("bad frame".into()), + McpError::Invocation(McpInvocationError::InternalError { + message: "server exploded".into(), + data: None, + }), + ] { + assert!(matches!( + tool_error_from_mcp(error), + ToolError::ExecutionFailed(_) + )); + } + } + + #[test] + fn request_side_invocation_errors_map_to_invalid_input() { + for error in [ + McpInvocationError::InvalidParams { + message: "missing field `id`".into(), + data: None, + }, + McpInvocationError::InvalidRequest { + message: "not a request".into(), + data: None, + }, + McpInvocationError::ParseError { + message: "bad json".into(), + data: None, + }, + ] { + assert!(matches!( + tool_error_from_invocation(error), + ToolError::InvalidInput(_) + )); + } + } + + #[test] + fn server_side_invocation_errors_stay_execution_failed() { + for error in [ + McpInvocationError::InternalError { + message: "boom".into(), + data: None, + }, + McpInvocationError::MethodNotFound { + message: "tools/call".into(), + data: None, + }, + McpInvocationError::Other { + code: -32099, + message: "custom".into(), + data: None, + }, + ] { + assert!(matches!( + tool_error_from_invocation(error), + ToolError::ExecutionFailed(_) + )); + } + } +} From 3744202ecdca73cbdc45b705fd33198d093b0b6e Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 13 Jul 2026 14:47:51 +0100 Subject: [PATCH 6/8] test(mcp): cover disconnect against a dead server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds HttpServerHandle::shutdown so tests can kill the server midway and pin that disconnect_server leaves clean state — connection untracked, tools unregistered, ServerDisconnected emitted, config retryable — regardless of the close outcome. --- .../src/http_mcp_server.rs | 13 ++++++ .../src/snapshot.rs | 5 +- .../tests/mcp_manager.rs | 46 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/crates/agentkit-integration-tests/src/http_mcp_server.rs b/crates/agentkit-integration-tests/src/http_mcp_server.rs index 52af3bb..5435540 100644 --- a/crates/agentkit-integration-tests/src/http_mcp_server.rs +++ b/crates/agentkit-integration-tests/src/http_mcp_server.rs @@ -177,6 +177,19 @@ impl HttpServerHandle { tools.len() != before } + /// Kills the server immediately, freeing the port. Subsequent client + /// requests — including the session `DELETE` a graceful close issues — + /// fail at the transport level. Useful for exercising close/disconnect + /// error paths against a server that has gone away. + pub fn shutdown(&mut self) { + if let Some(tx) = self.shutdown_tx.take() { + let _ = tx.send(()); + } + if let Some(handle) = self.join.take() { + handle.abort(); + } + } + /// Emits `notifications/tools/list_changed` on the live MCP session. /// Returns an error if the client hasn't completed the handshake yet /// (no peer has been stashed) or the underlying notify fails. diff --git a/crates/agentkit-integration-tests/src/snapshot.rs b/crates/agentkit-integration-tests/src/snapshot.rs index 5ab39ea..d3a3d07 100644 --- a/crates/agentkit-integration-tests/src/snapshot.rs +++ b/crates/agentkit-integration-tests/src/snapshot.rs @@ -312,7 +312,10 @@ fn normalise_recording(recording: &mut SessionRecording) { } } } - recording.final_transcript.iter_mut().for_each(normalise_item); + recording + .final_transcript + .iter_mut() + .for_each(normalise_item); } fn normalise_item(item: &mut Item) { diff --git a/crates/agentkit-integration-tests/tests/mcp_manager.rs b/crates/agentkit-integration-tests/tests/mcp_manager.rs index 1cc8e78..273d528 100644 --- a/crates/agentkit-integration-tests/tests/mcp_manager.rs +++ b/crates/agentkit-integration-tests/tests/mcp_manager.rs @@ -238,6 +238,52 @@ async fn unregister_server_removes_registration_and_tools() { )); } +#[tokio::test] +async fn disconnect_against_dead_server_cleans_catalog() { + let mut alpha = spawn_http_mcp(vec![simple_tool("only_alpha", "alpha-only tool.")]).await; + + let mut manager = + McpServerManager::new().with_server(McpServerConfig::streamable_http("alpha", &alpha.url)); + let source = manager.source(); + manager.connect_all().await.expect("connect_all succeeds"); + assert_tool_names(&source, &["mcp_alpha_only_alpha"]); + + let mut events = manager.subscribe_catalog_events(); + + // Kill the server before disconnecting. rmcp swallows the failed + // session DELETE (close only errors when its service task panicked), + // so `result` may be Ok or Err — the contract under test is that + // manager state, catalog, and lifecycle events are identical to a + // clean disconnect either way. + alpha.shutdown(); + let _advisory = manager.disconnect_server(&McpServerId::new("alpha")).await; + assert!( + manager + .connected_server(&McpServerId::new("alpha")) + .is_none() + ); + assert_tool_names(&source, &[]); + let mut saw_disconnected = false; + while let Ok(event) = events.try_recv() { + if matches!(&event, McpCatalogEvent::ServerDisconnected { server_id } + if *server_id == McpServerId::new("alpha")) + { + saw_disconnected = true; + } + } + assert!( + saw_disconnected, + "ServerDisconnected must be emitted even when close fails" + ); + + // The config survives, so a retry is a reconnect attempt (which fails + // against the dead server) — not an UnknownServer contract violation. + assert!(!matches!( + manager.connect_server(&McpServerId::new("alpha")).await, + Err(McpError::UnknownServer(_)) + )); +} + #[tokio::test] async fn connect_all_settled_times_out_slow_discovery_per_server() { let fast = spawn_http_mcp(vec![simple_tool("fast_tool", "Fast tool.")]).await; From 5daa392e2eb11bf91ac7168968c4c22185c24e74 Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 13 Jul 2026 14:53:59 +0100 Subject: [PATCH 7/8] chore(release): 0.10.1 --- Cargo.lock | 94 +++++++++---------- Cargo.toml | 2 +- book/src/acp.md | 2 +- book/src/feature-flags.md | 16 ++-- book/src/installation.md | 6 +- crates/agentkit-acp/Cargo.toml | 6 +- .../agentkit-adapter-completions/Cargo.toml | 8 +- crates/agentkit-capabilities/Cargo.toml | 2 +- crates/agentkit-compaction/Cargo.toml | 4 +- crates/agentkit-context/Cargo.toml | 2 +- crates/agentkit-http/README.md | 2 +- crates/agentkit-integration-tests/Cargo.toml | 16 ++-- crates/agentkit-loop/Cargo.toml | 8 +- crates/agentkit-mcp/Cargo.toml | 6 +- crates/agentkit-provider-anthropic/Cargo.toml | 8 +- crates/agentkit-provider-cerebras/Cargo.toml | 8 +- crates/agentkit-provider-groq/Cargo.toml | 8 +- crates/agentkit-provider-mistral/Cargo.toml | 8 +- crates/agentkit-provider-ollama/Cargo.toml | 8 +- crates/agentkit-provider-openai/Cargo.toml | 8 +- .../agentkit-provider-openrouter/Cargo.toml | 8 +- crates/agentkit-provider-vllm/Cargo.toml | 8 +- crates/agentkit-reporting/Cargo.toml | 4 +- crates/agentkit-reporting/README.md | 2 +- crates/agentkit-task-manager/Cargo.toml | 4 +- crates/agentkit-tool-compose/Cargo.toml | 4 +- crates/agentkit-tool-fs/Cargo.toml | 6 +- crates/agentkit-tool-shell/Cargo.toml | 6 +- crates/agentkit-tool-skills/Cargo.toml | 6 +- crates/agentkit-tools-core/Cargo.toml | 4 +- crates/agentkit/Cargo.toml | 46 ++++----- crates/agentkit/README.md | 4 +- docs/acp.md | 6 +- projects/how/Cargo.toml | 8 +- 34 files changed, 169 insertions(+), 169 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 434e690..337b9af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,7 +123,7 @@ dependencies = [ [[package]] name = "agentkit" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-acp", "agentkit-adapter-completions", @@ -152,7 +152,7 @@ dependencies = [ [[package]] name = "agentkit-acp" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agent-client-protocol 1.0.1", "agent-client-protocol-tokio", @@ -171,7 +171,7 @@ dependencies = [ [[package]] name = "agentkit-adapter-completions" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-http", @@ -188,7 +188,7 @@ dependencies = [ [[package]] name = "agentkit-capabilities" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "async-trait", @@ -199,7 +199,7 @@ dependencies = [ [[package]] name = "agentkit-compaction" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -212,7 +212,7 @@ dependencies = [ [[package]] name = "agentkit-context" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "async-fs", @@ -224,7 +224,7 @@ dependencies = [ [[package]] name = "agentkit-core" -version = "0.10.0" +version = "0.10.1" dependencies = [ "futures-timer", "serde", @@ -234,7 +234,7 @@ dependencies = [ [[package]] name = "agentkit-http" -version = "0.10.0" +version = "0.10.1" dependencies = [ "async-trait", "bytes", @@ -250,7 +250,7 @@ dependencies = [ [[package]] name = "agentkit-integration-tests" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-capabilities", "agentkit-core", @@ -273,7 +273,7 @@ dependencies = [ [[package]] name = "agentkit-loop" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-capabilities", "agentkit-core", @@ -289,7 +289,7 @@ dependencies = [ [[package]] name = "agentkit-mcp" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-capabilities", "agentkit-core", @@ -310,7 +310,7 @@ dependencies = [ [[package]] name = "agentkit-provider-anthropic" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-http", @@ -329,7 +329,7 @@ dependencies = [ [[package]] name = "agentkit-provider-cerebras" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-http", @@ -350,7 +350,7 @@ dependencies = [ [[package]] name = "agentkit-provider-groq" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-adapter-completions", "agentkit-core", @@ -364,7 +364,7 @@ dependencies = [ [[package]] name = "agentkit-provider-mistral" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-adapter-completions", "agentkit-core", @@ -378,7 +378,7 @@ dependencies = [ [[package]] name = "agentkit-provider-ollama" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-adapter-completions", "agentkit-core", @@ -392,7 +392,7 @@ dependencies = [ [[package]] name = "agentkit-provider-openai" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-adapter-completions", "agentkit-core", @@ -406,7 +406,7 @@ dependencies = [ [[package]] name = "agentkit-provider-openrouter" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-adapter-completions", "agentkit-core", @@ -421,7 +421,7 @@ dependencies = [ [[package]] name = "agentkit-provider-vllm" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-adapter-completions", "agentkit-core", @@ -435,7 +435,7 @@ dependencies = [ [[package]] name = "agentkit-reporting" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -447,7 +447,7 @@ dependencies = [ [[package]] name = "agentkit-task-manager" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-tools-core", @@ -459,7 +459,7 @@ dependencies = [ [[package]] name = "agentkit-tool-compose" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-tools-core", @@ -474,7 +474,7 @@ dependencies = [ [[package]] name = "agentkit-tool-fs" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-capabilities", "agentkit-core", @@ -490,7 +490,7 @@ dependencies = [ [[package]] name = "agentkit-tool-shell" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-capabilities", "agentkit-core", @@ -504,7 +504,7 @@ dependencies = [ [[package]] name = "agentkit-tool-skills" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-capabilities", "agentkit-core", @@ -521,7 +521,7 @@ dependencies = [ [[package]] name = "agentkit-tools-core" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-capabilities", "agentkit-core", @@ -535,7 +535,7 @@ dependencies = [ [[package]] name = "agentkit-tools-derive" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-tools-core", @@ -598,7 +598,7 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anthropic-chat" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -898,7 +898,7 @@ dependencies = [ [[package]] name = "cerebras-batch" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -911,7 +911,7 @@ dependencies = [ [[package]] name = "cerebras-chat" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -987,7 +987,7 @@ dependencies = [ [[package]] name = "compose-bench" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -1590,7 +1590,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "how-cli" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -2057,7 +2057,7 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "mcp-dynamic-auth" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-mcp", "async-trait", @@ -2070,7 +2070,7 @@ dependencies = [ [[package]] name = "mcp-reference-interop" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-mcp", @@ -2212,7 +2212,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openrouter-acp-trio" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agent-client-protocol 1.0.1", "agentkit-acp", @@ -2230,7 +2230,7 @@ dependencies = [ [[package]] name = "openrouter-agent-cli" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-compaction", "agentkit-context", @@ -2251,7 +2251,7 @@ dependencies = [ [[package]] name = "openrouter-chat" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -2262,7 +2262,7 @@ dependencies = [ [[package]] name = "openrouter-codemod" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -2278,7 +2278,7 @@ dependencies = [ [[package]] name = "openrouter-coding-agent" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-compaction", "agentkit-core", @@ -2294,7 +2294,7 @@ dependencies = [ [[package]] name = "openrouter-compaction-agent" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-compaction", "agentkit-core", @@ -2310,7 +2310,7 @@ dependencies = [ [[package]] name = "openrouter-context-agent" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-context", "agentkit-core", @@ -2325,7 +2325,7 @@ dependencies = [ [[package]] name = "openrouter-context-window-compaction" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-compaction", "agentkit-core", @@ -2342,7 +2342,7 @@ dependencies = [ [[package]] name = "openrouter-macro-tool" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -2358,7 +2358,7 @@ dependencies = [ [[package]] name = "openrouter-mcp-tool" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -2371,7 +2371,7 @@ dependencies = [ [[package]] name = "openrouter-parallel-agent" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -2387,7 +2387,7 @@ dependencies = [ [[package]] name = "openrouter-session-persistence" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", @@ -2401,7 +2401,7 @@ dependencies = [ [[package]] name = "openrouter-subagent-tool" -version = "0.10.0" +version = "0.10.1" dependencies = [ "agentkit-core", "agentkit-loop", diff --git a/Cargo.toml b/Cargo.toml index 685dcc3..1270f33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ homepage = "https://github.com/danielkov/agentkit" license = "MIT" repository = "https://github.com/danielkov/agentkit" rust-version = "1.92" -version = "0.10.0" +version = "0.10.1" [workspace.dependencies] async-trait = "0.1.89" diff --git a/book/src/acp.md b/book/src/acp.md index 4c37744..f5a4d8b 100644 --- a/book/src/acp.md +++ b/book/src/acp.md @@ -170,7 +170,7 @@ On the umbrella crate, ACP is behind the `acp` feature (implies `loop`): ```toml [dependencies] -agentkit = { version = "0.10.0", features = ["acp"] } +agentkit = { version = "0.10.1", features = ["acp"] } ``` The `agentkit-acp` crate itself has a default `stdio` feature (pulls in `agent-client-protocol-tokio` for `serve_stdio`) and an `unstable-acp` feature that forwards to the upstream SDK's unstable protocol surface. diff --git a/book/src/feature-flags.md b/book/src/feature-flags.md index e021cd3..c30d81b 100644 --- a/book/src/feature-flags.md +++ b/book/src/feature-flags.md @@ -36,13 +36,13 @@ The umbrella crate `agentkit` re-exports subcrates behind feature flags. **Minimal orchestration:** ```toml -agentkit = { version = "0.10.0", features = ["core", "capabilities", "tools", "loop"] } +agentkit = { version = "0.10.1", features = ["core", "capabilities", "tools", "loop"] } ``` **Coding agent:** ```toml -agentkit = { version = "0.10.0", features = [ +agentkit = { version = "0.10.1", features = [ "core", "capabilities", "context", "tools", "loop", "tool-fs", "tool-shell", "reporting", ] } @@ -51,7 +51,7 @@ agentkit = { version = "0.10.0", features = [ **MCP-enabled agent:** ```toml -agentkit = { version = "0.10.0", features = [ +agentkit = { version = "0.10.1", features = [ "core", "capabilities", "context", "tools", "loop", "tool-fs", "tool-shell", "reporting", "mcp", ] } @@ -60,7 +60,7 @@ agentkit = { version = "0.10.0", features = [ **ACP-exposed agent (editor-addressable over the Agent Client Protocol):** ```toml -agentkit = { version = "0.10.0", features = [ +agentkit = { version = "0.10.1", features = [ "core", "capabilities", "context", "tools", "loop", "tool-fs", "tool-shell", "reporting", "acp", ] } @@ -69,7 +69,7 @@ agentkit = { version = "0.10.0", features = [ **OpenRouter-backed example host (streaming, prompt caching):** ```toml -agentkit = { version = "0.10.0", features = [ +agentkit = { version = "0.10.1", features = [ "core", "capabilities", "tools", "loop", "reporting", "provider-openrouter", ] } @@ -78,7 +78,7 @@ agentkit = { version = "0.10.0", features = [ **OpenAI-compatible provider host (streaming):** ```toml -agentkit = { version = "0.10.0", features = [ +agentkit = { version = "0.10.1", features = [ "core", "capabilities", "tools", "loop", "reporting", "provider-groq", ] } @@ -90,7 +90,7 @@ or `provider-openai` as needed. **Anthropic Messages API host (streaming, extended thinking, server tools):** ```toml -agentkit = { version = "0.10.0", features = [ +agentkit = { version = "0.10.1", features = [ "core", "capabilities", "tools", "loop", "reporting", "provider-anthropic", ] } @@ -99,7 +99,7 @@ agentkit = { version = "0.10.0", features = [ **Cerebras Inference host (streaming, reasoning, rate-limit snapshot):** ```toml -agentkit = { version = "0.10.0", features = [ +agentkit = { version = "0.10.1", features = [ "core", "capabilities", "tools", "loop", "reporting", "provider-cerebras", ] } diff --git a/book/src/installation.md b/book/src/installation.md index 7123b34..96a0056 100644 --- a/book/src/installation.md +++ b/book/src/installation.md @@ -14,7 +14,7 @@ Or add it to your `Cargo.toml`: ```toml [dependencies] -agentkit = "0.10.0" +agentkit = "0.10.1" ``` ## Minimal dependency set @@ -25,14 +25,14 @@ To keep your build lean, disable defaults and pick only what you need: ```toml [dependencies] -agentkit = { version = "0.10.0", default-features = false, features = ["core", "loop"] } +agentkit = { version = "0.10.1", default-features = false, features = ["core", "loop"] } ``` Provider adapters and MCP integration are opt-in features: ```toml [dependencies] -agentkit = { version = "0.10.0", features = ["provider-anthropic", "mcp", "tool-fs", "tool-shell"] } +agentkit = { version = "0.10.1", features = ["provider-anthropic", "mcp", "tool-fs", "tool-shell"] } ``` See the [Feature flags reference](./feature-flags.md) for the full list. diff --git a/crates/agentkit-acp/Cargo.toml b/crates/agentkit-acp/Cargo.toml index f3712ce..f0b6a36 100644 --- a/crates/agentkit-acp/Cargo.toml +++ b/crates/agentkit-acp/Cargo.toml @@ -17,9 +17,9 @@ unstable-acp = ["agent-client-protocol/unstable"] [dependencies] agent-client-protocol = "1.0.1" agent-client-protocol-tokio = { version = "0.11.1", optional = true } -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core" } async-trait.workspace = true base64.workspace = true serde.workspace = true diff --git a/crates/agentkit-adapter-completions/Cargo.toml b/crates/agentkit-adapter-completions/Cargo.toml index d9eaee2..b5f5d62 100644 --- a/crates/agentkit-adapter-completions/Cargo.toml +++ b/crates/agentkit-adapter-completions/Cargo.toml @@ -10,10 +10,10 @@ rust-version.workspace = true version.workspace = true [dependencies] -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-http = { version = "0.10.0", path = "../agentkit-http" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-http = { version = "0.10.1", path = "../agentkit-http" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core" } async-trait.workspace = true base64.workspace = true futures-util.workspace = true diff --git a/crates/agentkit-capabilities/Cargo.toml b/crates/agentkit-capabilities/Cargo.toml index f52c886..c469dca 100644 --- a/crates/agentkit-capabilities/Cargo.toml +++ b/crates/agentkit-capabilities/Cargo.toml @@ -10,7 +10,7 @@ license.workspace = true rust-version.workspace = true [dependencies] -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } async-trait.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/agentkit-compaction/Cargo.toml b/crates/agentkit-compaction/Cargo.toml index ca235a0..0cd9ec8 100644 --- a/crates/agentkit-compaction/Cargo.toml +++ b/crates/agentkit-compaction/Cargo.toml @@ -10,8 +10,8 @@ license.workspace = true rust-version.workspace = true [dependencies] -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } async-trait.workspace = true serde.workspace = true thiserror.workspace = true diff --git a/crates/agentkit-context/Cargo.toml b/crates/agentkit-context/Cargo.toml index 233dfe6..aa4c8f2 100644 --- a/crates/agentkit-context/Cargo.toml +++ b/crates/agentkit-context/Cargo.toml @@ -10,7 +10,7 @@ license.workspace = true rust-version.workspace = true [dependencies] -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } async-fs = "2.2.0" async-trait.workspace = true serde_json.workspace = true diff --git a/crates/agentkit-http/README.md b/crates/agentkit-http/README.md index 86138b7..f59256e 100644 --- a/crates/agentkit-http/README.md +++ b/crates/agentkit-http/README.md @@ -18,7 +18,7 @@ feature, which is **enabled by default**. Disable it to compile trait-only when you want to bring your own backend: ```toml -agentkit-http = { version = "0.10.0", default-features = false } +agentkit-http = { version = "0.10.1", default-features = false } ``` A second optional feature, `reqwest-middleware-client`, layers diff --git a/crates/agentkit-integration-tests/Cargo.toml b/crates/agentkit-integration-tests/Cargo.toml index 41a3786..c83a8cc 100644 --- a/crates/agentkit-integration-tests/Cargo.toml +++ b/crates/agentkit-integration-tests/Cargo.toml @@ -12,14 +12,14 @@ path = "src/lib.rs" doctest = false [dependencies] -agentkit-capabilities = { version = "0.10.0", path = "../agentkit-capabilities" } -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } -agentkit-mcp = { version = "0.10.0", path = "../agentkit-mcp" } -agentkit-task-manager = { version = "0.10.0", path = "../agentkit-task-manager" } -agentkit-tool-compose = { version = "0.10.0", path = "../agentkit-tool-compose" } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core", features = ["schemars"] } -agentkit-tools-derive = { version = "0.10.0", path = "../agentkit-tools-derive" } +agentkit-capabilities = { version = "0.10.1", path = "../agentkit-capabilities" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } +agentkit-mcp = { version = "0.10.1", path = "../agentkit-mcp" } +agentkit-task-manager = { version = "0.10.1", path = "../agentkit-task-manager" } +agentkit-tool-compose = { version = "0.10.1", path = "../agentkit-tool-compose" } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core", features = ["schemars"] } +agentkit-tools-derive = { version = "0.10.1", path = "../agentkit-tools-derive" } async-trait.workspace = true schemars = { workspace = true, features = ["derive"] } axum = { version = "0.8", default-features = false, features = ["http1", "tokio"] } diff --git a/crates/agentkit-loop/Cargo.toml b/crates/agentkit-loop/Cargo.toml index 1b1a8cc..c2fa450 100644 --- a/crates/agentkit-loop/Cargo.toml +++ b/crates/agentkit-loop/Cargo.toml @@ -10,10 +10,10 @@ license.workspace = true rust-version.workspace = true [dependencies] -agentkit-capabilities = { version = "0.10.0", path = "../agentkit-capabilities" } -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-task-manager = { version = "0.10.0", path = "../agentkit-task-manager" } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core" } +agentkit-capabilities = { version = "0.10.1", path = "../agentkit-capabilities" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-task-manager = { version = "0.10.1", path = "../agentkit-task-manager" } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core" } async-trait.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/agentkit-mcp/Cargo.toml b/crates/agentkit-mcp/Cargo.toml index 7dd9b83..58d6a4f 100644 --- a/crates/agentkit-mcp/Cargo.toml +++ b/crates/agentkit-mcp/Cargo.toml @@ -13,9 +13,9 @@ rust-version.workspace = true default = [] [dependencies] -agentkit-capabilities = { version = "0.10.0", path = "../agentkit-capabilities" } -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core" } +agentkit-capabilities = { version = "0.10.1", path = "../agentkit-capabilities" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core" } async-trait.workspace = true futures-util.workspace = true http = "1" diff --git a/crates/agentkit-provider-anthropic/Cargo.toml b/crates/agentkit-provider-anthropic/Cargo.toml index 77f1375..48925c7 100644 --- a/crates/agentkit-provider-anthropic/Cargo.toml +++ b/crates/agentkit-provider-anthropic/Cargo.toml @@ -10,10 +10,10 @@ rust-version.workspace = true version.workspace = true [dependencies] -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-http = { version = "0.10.0", path = "../agentkit-http" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-http = { version = "0.10.1", path = "../agentkit-http" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core" } async-trait.workspace = true base64.workspace = true futures-util.workspace = true diff --git a/crates/agentkit-provider-cerebras/Cargo.toml b/crates/agentkit-provider-cerebras/Cargo.toml index 8850489..384b28a 100644 --- a/crates/agentkit-provider-cerebras/Cargo.toml +++ b/crates/agentkit-provider-cerebras/Cargo.toml @@ -19,10 +19,10 @@ batch = [] experimental = ["service-tiers", "predicted-outputs", "batch"] [dependencies] -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-http = { version = "0.10.0", path = "../agentkit-http" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-http = { version = "0.10.1", path = "../agentkit-http" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core" } async-trait.workspace = true futures-util.workspace = true futures-timer.workspace = true diff --git a/crates/agentkit-provider-groq/Cargo.toml b/crates/agentkit-provider-groq/Cargo.toml index 310c240..ccb822d 100644 --- a/crates/agentkit-provider-groq/Cargo.toml +++ b/crates/agentkit-provider-groq/Cargo.toml @@ -10,10 +10,10 @@ rust-version.workspace = true version.workspace = true [dependencies] -agentkit-adapter-completions = { version = "0.10.0", path = "../agentkit-adapter-completions" } -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-http = { version = "0.10.0", path = "../agentkit-http" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } +agentkit-adapter-completions = { version = "0.10.1", path = "../agentkit-adapter-completions" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-http = { version = "0.10.1", path = "../agentkit-http" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } async-trait.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/agentkit-provider-mistral/Cargo.toml b/crates/agentkit-provider-mistral/Cargo.toml index f84aa21..00b0b26 100644 --- a/crates/agentkit-provider-mistral/Cargo.toml +++ b/crates/agentkit-provider-mistral/Cargo.toml @@ -10,10 +10,10 @@ rust-version.workspace = true version.workspace = true [dependencies] -agentkit-adapter-completions = { version = "0.10.0", path = "../agentkit-adapter-completions" } -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-http = { version = "0.10.0", path = "../agentkit-http" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } +agentkit-adapter-completions = { version = "0.10.1", path = "../agentkit-adapter-completions" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-http = { version = "0.10.1", path = "../agentkit-http" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } async-trait.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/agentkit-provider-ollama/Cargo.toml b/crates/agentkit-provider-ollama/Cargo.toml index 52cd666..29a0553 100644 --- a/crates/agentkit-provider-ollama/Cargo.toml +++ b/crates/agentkit-provider-ollama/Cargo.toml @@ -10,10 +10,10 @@ rust-version.workspace = true version.workspace = true [dependencies] -agentkit-adapter-completions = { version = "0.10.0", path = "../agentkit-adapter-completions" } -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-http = { version = "0.10.0", path = "../agentkit-http" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } +agentkit-adapter-completions = { version = "0.10.1", path = "../agentkit-adapter-completions" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-http = { version = "0.10.1", path = "../agentkit-http" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } async-trait.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/agentkit-provider-openai/Cargo.toml b/crates/agentkit-provider-openai/Cargo.toml index 0a75132..78caf67 100644 --- a/crates/agentkit-provider-openai/Cargo.toml +++ b/crates/agentkit-provider-openai/Cargo.toml @@ -10,10 +10,10 @@ rust-version.workspace = true version.workspace = true [dependencies] -agentkit-adapter-completions = { version = "0.10.0", path = "../agentkit-adapter-completions" } -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-http = { version = "0.10.0", path = "../agentkit-http" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } +agentkit-adapter-completions = { version = "0.10.1", path = "../agentkit-adapter-completions" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-http = { version = "0.10.1", path = "../agentkit-http" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } async-trait.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/agentkit-provider-openrouter/Cargo.toml b/crates/agentkit-provider-openrouter/Cargo.toml index b0cc330..5b0afb8 100644 --- a/crates/agentkit-provider-openrouter/Cargo.toml +++ b/crates/agentkit-provider-openrouter/Cargo.toml @@ -10,10 +10,10 @@ rust-version.workspace = true version.workspace = true [dependencies] -agentkit-adapter-completions = { version = "0.10.0", path = "../agentkit-adapter-completions" } -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-http = { version = "0.10.0", path = "../agentkit-http" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } +agentkit-adapter-completions = { version = "0.10.1", path = "../agentkit-adapter-completions" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-http = { version = "0.10.1", path = "../agentkit-http" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } async-trait.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/agentkit-provider-vllm/Cargo.toml b/crates/agentkit-provider-vllm/Cargo.toml index 5e804fb..8d4c5fc 100644 --- a/crates/agentkit-provider-vllm/Cargo.toml +++ b/crates/agentkit-provider-vllm/Cargo.toml @@ -10,10 +10,10 @@ rust-version.workspace = true version.workspace = true [dependencies] -agentkit-adapter-completions = { version = "0.10.0", path = "../agentkit-adapter-completions" } -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-http = { version = "0.10.0", path = "../agentkit-http" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } +agentkit-adapter-completions = { version = "0.10.1", path = "../agentkit-adapter-completions" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-http = { version = "0.10.1", path = "../agentkit-http" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } async-trait.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/agentkit-reporting/Cargo.toml b/crates/agentkit-reporting/Cargo.toml index bdd412c..4ff10f4 100644 --- a/crates/agentkit-reporting/Cargo.toml +++ b/crates/agentkit-reporting/Cargo.toml @@ -13,8 +13,8 @@ rust-version.workspace = true tracing = ["dep:tracing"] [dependencies] -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop" } serde.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/crates/agentkit-reporting/README.md b/crates/agentkit-reporting/README.md index e356ebc..bbbf2ea 100644 --- a/crates/agentkit-reporting/README.md +++ b/crates/agentkit-reporting/README.md @@ -134,7 +134,7 @@ std::thread::spawn(move || { is gated behind the `tracing` feature to keep the dependency opt-in: ```toml -agentkit-reporting = { version = "0.10.0", features = ["tracing"] } +agentkit-reporting = { version = "0.10.1", features = ["tracing"] } ``` ```rust,ignore diff --git a/crates/agentkit-task-manager/Cargo.toml b/crates/agentkit-task-manager/Cargo.toml index 142040b..afdd95b 100644 --- a/crates/agentkit-task-manager/Cargo.toml +++ b/crates/agentkit-task-manager/Cargo.toml @@ -10,8 +10,8 @@ license.workspace = true rust-version.workspace = true [dependencies] -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core" } async-trait.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["sync", "time"] } diff --git a/crates/agentkit-tool-compose/Cargo.toml b/crates/agentkit-tool-compose/Cargo.toml index 6cedf4e..3b6dcf8 100644 --- a/crates/agentkit-tool-compose/Cargo.toml +++ b/crates/agentkit-tool-compose/Cargo.toml @@ -10,8 +10,8 @@ license.workspace = true rust-version.workspace = true [dependencies] -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core" } async-trait.workspace = true mlua.workspace = true serde = { workspace = true, features = ["derive"] } diff --git a/crates/agentkit-tool-fs/Cargo.toml b/crates/agentkit-tool-fs/Cargo.toml index b2d100f..51b8e30 100644 --- a/crates/agentkit-tool-fs/Cargo.toml +++ b/crates/agentkit-tool-fs/Cargo.toml @@ -10,8 +10,8 @@ license.workspace = true rust-version.workspace = true [dependencies] -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core" } async-fs = "2.2.0" async-trait.workspace = true futures-lite = "2.6.1" @@ -20,5 +20,5 @@ serde_json.workspace = true thiserror.workspace = true [dev-dependencies] -agentkit-capabilities = { version = "0.10.0", path = "../agentkit-capabilities" } +agentkit-capabilities = { version = "0.10.1", path = "../agentkit-capabilities" } tokio.workspace = true diff --git a/crates/agentkit-tool-shell/Cargo.toml b/crates/agentkit-tool-shell/Cargo.toml index 5a46d5d..eb00a44 100644 --- a/crates/agentkit-tool-shell/Cargo.toml +++ b/crates/agentkit-tool-shell/Cargo.toml @@ -10,8 +10,8 @@ license.workspace = true rust-version.workspace = true [dependencies] -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core" } async-trait.workspace = true serde.workspace = true serde_json.workspace = true @@ -19,4 +19,4 @@ thiserror.workspace = true tokio = { workspace = true, features = ["process", "time"] } [dev-dependencies] -agentkit-capabilities = { version = "0.10.0", path = "../agentkit-capabilities" } +agentkit-capabilities = { version = "0.10.1", path = "../agentkit-capabilities" } diff --git a/crates/agentkit-tool-skills/Cargo.toml b/crates/agentkit-tool-skills/Cargo.toml index 04bebc3..c031da8 100644 --- a/crates/agentkit-tool-skills/Cargo.toml +++ b/crates/agentkit-tool-skills/Cargo.toml @@ -10,8 +10,8 @@ license.workspace = true rust-version.workspace = true [dependencies] -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core" } async-fs = "2.2.0" async-trait.workspace = true futures-lite = "2.6.1" @@ -21,5 +21,5 @@ serde-saphyr = "0.0.23" thiserror.workspace = true [dev-dependencies] -agentkit-capabilities = { version = "0.10.0", path = "../agentkit-capabilities" } +agentkit-capabilities = { version = "0.10.1", path = "../agentkit-capabilities" } tokio.workspace = true diff --git a/crates/agentkit-tools-core/Cargo.toml b/crates/agentkit-tools-core/Cargo.toml index 5fa5ab8..a02eca3 100644 --- a/crates/agentkit-tools-core/Cargo.toml +++ b/crates/agentkit-tools-core/Cargo.toml @@ -14,8 +14,8 @@ default = [] schemars = ["dep:schemars"] [dependencies] -agentkit-capabilities = { version = "0.10.0", path = "../agentkit-capabilities" } -agentkit-core = { version = "0.10.0", path = "../agentkit-core" } +agentkit-capabilities = { version = "0.10.1", path = "../agentkit-capabilities" } +agentkit-core = { version = "0.10.1", path = "../agentkit-core" } async-trait.workspace = true schemars = { workspace = true, optional = true } serde.workspace = true diff --git a/crates/agentkit/Cargo.toml b/crates/agentkit/Cargo.toml index 1a2a043..080efa2 100644 --- a/crates/agentkit/Cargo.toml +++ b/crates/agentkit/Cargo.toml @@ -10,29 +10,29 @@ license.workspace = true rust-version.workspace = true [dependencies] -agentkit-capabilities = { version = "0.10.0", path = "../agentkit-capabilities", optional = true } -agentkit-acp = { version = "0.10.0", path = "../agentkit-acp", optional = true } -agentkit-compaction = { version = "0.10.0", path = "../agentkit-compaction", optional = true } -agentkit-context = { version = "0.10.0", path = "../agentkit-context", optional = true } -agentkit-core = { version = "0.10.0", path = "../agentkit-core", optional = true } -agentkit-loop = { version = "0.10.0", path = "../agentkit-loop", optional = true } -agentkit-mcp = { version = "0.10.0", path = "../agentkit-mcp", optional = true } -agentkit-adapter-completions = { version = "0.10.0", path = "../agentkit-adapter-completions", optional = true } -agentkit-provider-anthropic = { version = "0.10.0", path = "../agentkit-provider-anthropic", optional = true } -agentkit-provider-cerebras = { version = "0.10.0", path = "../agentkit-provider-cerebras", optional = true } -agentkit-provider-groq = { version = "0.10.0", path = "../agentkit-provider-groq", optional = true } -agentkit-provider-mistral = { version = "0.10.0", path = "../agentkit-provider-mistral", optional = true } -agentkit-provider-ollama = { version = "0.10.0", path = "../agentkit-provider-ollama", optional = true } -agentkit-provider-openai = { version = "0.10.0", path = "../agentkit-provider-openai", optional = true } -agentkit-provider-openrouter = { version = "0.10.0", path = "../agentkit-provider-openrouter", optional = true } -agentkit-provider-vllm = { version = "0.10.0", path = "../agentkit-provider-vllm", optional = true } -agentkit-reporting = { version = "0.10.0", path = "../agentkit-reporting", optional = true } -agentkit-task-manager = { version = "0.10.0", path = "../agentkit-task-manager", optional = true } -agentkit-tool-compose = { version = "0.10.0", path = "../agentkit-tool-compose", optional = true } -agentkit-tool-fs = { version = "0.10.0", path = "../agentkit-tool-fs", optional = true } -agentkit-tool-shell = { version = "0.10.0", path = "../agentkit-tool-shell", optional = true } -agentkit-tool-skills = { version = "0.10.0", path = "../agentkit-tool-skills", optional = true } -agentkit-tools-core = { version = "0.10.0", path = "../agentkit-tools-core", optional = true } +agentkit-capabilities = { version = "0.10.1", path = "../agentkit-capabilities", optional = true } +agentkit-acp = { version = "0.10.1", path = "../agentkit-acp", optional = true } +agentkit-compaction = { version = "0.10.1", path = "../agentkit-compaction", optional = true } +agentkit-context = { version = "0.10.1", path = "../agentkit-context", optional = true } +agentkit-core = { version = "0.10.1", path = "../agentkit-core", optional = true } +agentkit-loop = { version = "0.10.1", path = "../agentkit-loop", optional = true } +agentkit-mcp = { version = "0.10.1", path = "../agentkit-mcp", optional = true } +agentkit-adapter-completions = { version = "0.10.1", path = "../agentkit-adapter-completions", optional = true } +agentkit-provider-anthropic = { version = "0.10.1", path = "../agentkit-provider-anthropic", optional = true } +agentkit-provider-cerebras = { version = "0.10.1", path = "../agentkit-provider-cerebras", optional = true } +agentkit-provider-groq = { version = "0.10.1", path = "../agentkit-provider-groq", optional = true } +agentkit-provider-mistral = { version = "0.10.1", path = "../agentkit-provider-mistral", optional = true } +agentkit-provider-ollama = { version = "0.10.1", path = "../agentkit-provider-ollama", optional = true } +agentkit-provider-openai = { version = "0.10.1", path = "../agentkit-provider-openai", optional = true } +agentkit-provider-openrouter = { version = "0.10.1", path = "../agentkit-provider-openrouter", optional = true } +agentkit-provider-vllm = { version = "0.10.1", path = "../agentkit-provider-vllm", optional = true } +agentkit-reporting = { version = "0.10.1", path = "../agentkit-reporting", optional = true } +agentkit-task-manager = { version = "0.10.1", path = "../agentkit-task-manager", optional = true } +agentkit-tool-compose = { version = "0.10.1", path = "../agentkit-tool-compose", optional = true } +agentkit-tool-fs = { version = "0.10.1", path = "../agentkit-tool-fs", optional = true } +agentkit-tool-shell = { version = "0.10.1", path = "../agentkit-tool-shell", optional = true } +agentkit-tool-skills = { version = "0.10.1", path = "../agentkit-tool-skills", optional = true } +agentkit-tools-core = { version = "0.10.1", path = "../agentkit-tools-core", optional = true } [features] default = ["core", "capabilities", "tools", "loop", "reporting"] diff --git a/crates/agentkit/README.md b/crates/agentkit/README.md index 81f2d91..3242393 100644 --- a/crates/agentkit/README.md +++ b/crates/agentkit/README.md @@ -51,7 +51,7 @@ Add agentkit with the features you need: ```toml [dependencies] -agentkit = { version = "0.10.0", features = ["provider-openrouter", "tool-fs", "tool-shell"] } +agentkit = { version = "0.10.1", features = ["provider-openrouter", "tool-fs", "tool-shell"] } tokio = { version = "1", features = ["full"] } ``` @@ -161,7 +161,7 @@ When implementing a custom adapter, only the default features are needed: ```toml [dependencies] -agentkit = "0.10.0" +agentkit = "0.10.1" ``` ```rust,ignore diff --git a/docs/acp.md b/docs/acp.md index fc43777..6c366dd 100644 --- a/docs/acp.md +++ b/docs/acp.md @@ -57,9 +57,9 @@ Recommended initial crate: [dependencies] agent-client-protocol = "1.0.0" agent-client-protocol-tokio = { version = "0.11.1", optional = true } -agentkit-core = { path = "../agentkit-core", version = "0.10.0" } -agentkit-loop = { path = "../agentkit-loop", version = "0.10.0" } -agentkit-tools-core = { path = "../agentkit-tools-core", version = "0.10.0" } +agentkit-core = { path = "../agentkit-core", version = "0.10.1" } +agentkit-loop = { path = "../agentkit-loop", version = "0.10.1" } +agentkit-tools-core = { path = "../agentkit-tools-core", version = "0.10.1" } async-trait = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/projects/how/Cargo.toml b/projects/how/Cargo.toml index 33000bf..ffbaeba 100644 --- a/projects/how/Cargo.toml +++ b/projects/how/Cargo.toml @@ -14,10 +14,10 @@ name = "how" path = "src/main.rs" [dependencies] -agentkit-core = { version = "0.10.0", path = "../../crates/agentkit-core" } -agentkit-loop = { version = "0.10.0", path = "../../crates/agentkit-loop" } -agentkit-tools-core = { version = "0.10.0", path = "../../crates/agentkit-tools-core" } -agentkit-provider-openrouter = { version = "0.10.0", path = "../../crates/agentkit-provider-openrouter" } +agentkit-core = { version = "0.10.1", path = "../../crates/agentkit-core" } +agentkit-loop = { version = "0.10.1", path = "../../crates/agentkit-loop" } +agentkit-tools-core = { version = "0.10.1", path = "../../crates/agentkit-tools-core" } +agentkit-provider-openrouter = { version = "0.10.1", path = "../../crates/agentkit-provider-openrouter" } async-trait.workspace = true crossterm = "0.29" dotenvy.workspace = true From dee4ca38c0d27066201843760e4ead1c53a5ece7 Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 13 Jul 2026 15:02:06 +0100 Subject: [PATCH 8/8] test(tool-fs): expect Failed for in-tool permission denial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The read-before-write policy is enforced inside the tool body (it needs resource state), so its denial surfaces as Failed — FailedBeforeInvocation is reserved for checker denials that stop the tool from ever starting. The assertion predated that split. --- crates/agentkit-tool-fs/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/agentkit-tool-fs/src/lib.rs b/crates/agentkit-tool-fs/src/lib.rs index 651b1d1..c905fde 100644 --- a/crates/agentkit-tool-fs/src/lib.rs +++ b/crates/agentkit-tool-fs/src/lib.rs @@ -1678,9 +1678,13 @@ mod tests { &mut ctx, ) .await; + // The read-before-write policy is enforced inside the tool body (it + // needs resource state), so a denial is a plain `Failed`, not + // `FailedBeforeInvocation` — that classification is reserved for + // checker denials that stop the tool from ever starting. assert!(matches!( denied_edit, - ToolExecutionOutcome::FailedBeforeInvocation(ToolError::PermissionDenied(_)) + ToolExecutionOutcome::Failed(ToolError::PermissionDenied(_)) )); let read = executor