v0.8.0 — compose Lua tool, OTel GenAI spans, MCP server manager
v0.8.0 — compose Lua tool, OTel GenAI spans, MCP server manager
Two big themes this release: a Lua tool-composition surface so the model can chain N tool calls inside one round-trip, and a workspace-wide telemetry pass that aligns spans with the OpenTelemetry GenAI semantic conventions. Plus first-class lifecycle management for MCP servers.
What's Changed
compose Lua tool + ToolSpec::output_schema
agentkit-tool-compose ships a single compose tool: the model supplies a Lua 5.4 script and optional JSON input, the script runs sandboxed (no io/os/require/load, instruction and nested-call budgets) and can invoke any visible tool synchronously via tool(name, input) or list them with tools(). Only the script's return value enters the transcript, so N tool round-trips collapse to one.
use agentkit_tool_compose::registry;
let agent = Agent::builder()
.model(adapter)
.add_tool_source(registry()) // exposes `compose`
.build()?;Compose is opt-in. ToolSpec gains a new output_schema field so tools can advertise the JSON shape they return; compose reads those schemas back when it lists available tools, so the model can write tool("fs_read", ...) knowing the exact return shape without a discovery round-trip. Native tools (fs_read, fs_search, shell, skills, MCP) populate output schemas where the shape is known.
The accompanying openrouter-codemod example shows the canonical use: a refactor that touches 40 files becomes one compose call instead of 80 read/write round-trips.
OTel GenAI semantic conventions
agent.turn and agent.execute_tool spans now carry gen_ai.operation.name, gen_ai.conversation.id, gen_ai.provider.name, and token usage attributes. The loop wraps every model request in a chat {model} span that stays open until the turn finishes — including over SSE — and records request/response model, response id, finish reason, and token usage from the wire response. otel.name carries the dynamic semconv span name (invoke_agent, execute_tool {tool}, chat {model}) for OTel bridges while tracing span names stay stable for log filtering.
// Contract additions
trait ModelAdapter {
fn provider_name(&self) -> Option<&str> { None } // new default hook
}
trait ModelSession {
fn model_name(&self) -> Option<&str> { None } // new default hook
}
struct ModelTurnResult {
// ...
pub model: Option<String>, // new
pub response_id: Option<String>, // new
}agent.execute_tool spans record error.type=tool_error when a tool result is an error, and McpConnection::call_tool gets a child mcp.call_tool span carrying mcp.server.id and mcp.tool.name so MCP round-trip latency is separable from dispatch overhead.
McpServerManager + connect timeouts
agentkit-mcp gains McpServerManager for declarative lifecycle: register servers up front, call connect_all_settled() to bring them up in parallel and get a settled result per server (one server's transport error no longer fails the batch), and tear them down together. Per-server McpServerOptions exposes a connect_timeout that bounds transport setup, the initialize handshake, and the initial tools/list / resources/list / prompts/list discovery as a single duration.
use std::time::Duration;
use agentkit_mcp::{McpServerManager, McpServerOptions};
let mut manager = McpServerManager::new();
manager.register_with_options(
git_server_config,
McpServerOptions::new().with_timeout(Duration::from_secs(10)),
);
manager.register(fs_server_config);
let outcomes = manager.connect_all_settled().await;
for (server_id, result) in outcomes {
match result {
Ok(handle) => tracing::info!(%server_id, tools = handle.snapshot.tools.len(), "mcp ready"),
Err(err) => tracing::warn!(%server_id, %err, "mcp connect failed"),
}
}compose-bench + case study
benchmarks/compose-bench is a deterministic harness comparing granular tool-calling against the same tools plus compose across six life-like scenarios (helpdesk triage, revenue aggregation, incident investigation, CRM cleanup, calendar scheduling, file-based config migration). docs/compose-case-study.md writes up the multi-model results: on claude-sonnet-4.5, compose cuts cost by 38–77%, model round-trips by 25–43%, and wall time by 6–57% while raising accuracy on scenarios where the granular arm mis-transcribed values under load. The sweep also flags where composition is an anti-pattern (exploratory investigation on weaker models) — the case study is the headline result, not the tool itself.
Other
ToolSourcecombinators (Prefixed,Filtered,Renamedfrom 0.5) compose cleanly withComposeTool::wrap(source)— wrapped child sources stay live, so MCP catalogs feed the compose description without a discovery cache.- Docs sweep: book chapters on tool composition, output schemas, and nested execution scope; MCP chapter picks up
McpServerManagerand timeout options.
Migration notes
ModelAdapter::provider_nameandModelSession::model_namehave default no-op impls — existing adapters compile unchanged. Implement them to populategen_ai.provider.name/gen_ai.request.modelon spans.ModelTurnResultgainsmodelandresponse_idfields; if you construct it directly (test doubles, custom adapters), fill them in or set toNone.- Per-adapter chat spans are deleted in favour of the loop-level span. If you grep traces for an adapter-specific span name, switch to the
chatspan filtered bygen_ai.provider.name. McpServerOptions::discovery_timeoutis the newconnect_timeoutfield in 0.8.1 — pin to 0.8.0 if you've started using the field name.
Commits
- feat: add compose Lua tool and ToolSpec output_schema (#6) (1263f6a)
- feat: add McpServerManager::connect_all_settled (2955ec4)
- feat(mcp): server options for connect timeout (a081a25)
- feat(compose): state when to use compose in its tool description (2326d58)
- feat: add compose-bench, a compose-vs-granular tool benchmark (d971223)
- docs: compose effectiveness case study with multi-model results (30b1966)
- docs: surface tool-compose, ToolSpec output_schema, and nested execution scope (7a70168)
- feat(telemetry): align spans with OTel GenAI semantic conventions (#7) (9966594)
- docs(telemetry): document ModelAdapter::provider_name and OTel GenAI spans (13cd37b)
- feat(telemetry): provider_name and chat span on Anthropic and Cerebras adapters (6f385c2)
- feat(telemetry): emit GenAI inference spans from the loop for all adapters (6bd736f)
- docs(telemetry): reflect loop-level chat span and mcp.call_tool (66691a8)
- chore: format (1115325)
- chore(release): 0.8.0 (0bd5695)
Full Changelog: v0.7.0...v0.8.0