Skip to content

v0.8.0 — compose Lua tool, OTel GenAI spans, MCP server manager

Choose a tag to compare

@danielkov danielkov released this 23 Jun 23:38

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

  • ToolSource combinators (Prefixed, Filtered, Renamed from 0.5) compose cleanly with ComposeTool::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 McpServerManager and timeout options.

Migration notes

  • ModelAdapter::provider_name and ModelSession::model_name have default no-op impls — existing adapters compile unchanged. Implement them to populate gen_ai.provider.name / gen_ai.request.model on spans.
  • ModelTurnResult gains model and response_id fields; if you construct it directly (test doubles, custom adapters), fill them in or set to None.
  • 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 chat span filtered by gen_ai.provider.name.
  • McpServerOptions::discovery_timeout is the new connect_timeout field 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