polyc-tools 2026.9.0

The in-process tool core for polychrome agents: local executors (coding, web fetch, wallet, ...), the tool registry, and MCP composition. The networked connectors live in polyc-connectors.
#![allow(clippy::unwrap_used)] // test/example/bench: panics are acceptable
//! Callability at the executor seam (#635): a call to a DEAD connector — one
//! whose specs came from a shipped catalog but whose endpoint is unreachable —
//! returns a typed temporarily-unavailable TOOL RESULT in-transcript, never a
//! turn failure, and never removes the tool from the composed surface.
//!
//! Existence, callability, and advertisement are three separately-managed
//! properties: the composed registry keeps owning and listing the tool before
//! AND after the failed call, so nothing vanishes under the model mid-turn.

#![allow(clippy::pedantic, clippy::nursery, missing_docs)]

use std::sync::Arc;

use polyc_agent::ToolExecutor;
use polyc_llm::ToolSpec;
use polyc_tools::{CompositeRegistry, ConnectOptions, ConnectionPool, McpToolSource, SpecSource};

/// Compose a registry over a shipped-catalog connector whose endpoint nothing
/// listens on. Composition needs no network (the catalog ships the specs), so
/// it succeeds; only a call dials — and fails.
async fn dead_registry() -> CompositeRegistry {
    let source = McpToolSource::pooled(
        ConnectionPool::new(),
        "conv-dead",
        // Port 1 is reserved and unbound: the lazy dial is refused immediately.
        "http://127.0.0.1:1/mcp",
        ConnectOptions {
            label: Some("notes".to_owned()),
            source: SpecSource::Shipped(vec![ToolSpec::new(
                "echo",
                "Echo the input text back.",
                serde_json::json!({ "type": "object" }),
            )]),
            ..ConnectOptions::default()
        },
    )
    .await
    .expect("a shipped catalog composes with zero network");
    CompositeRegistry::new().with(Arc::new(source))
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dead_connector_call_is_a_typed_unavailable_result_not_a_vanished_tool() {
    let registry = dead_registry().await;

    // Existence: the composed registry owns and advertises the tool even
    // though its connector is unreachable — the catalog is the source of
    // existence, not the connector's reachability.
    assert!(registry.owns("notes__echo"), "the registry owns the tool");
    assert!(
        registry.specs().iter().any(|s| s.name == "notes__echo"),
        "the tool is listed before the call"
    );

    // Callability: the call comes back as an ORDINARY tool result (a JSON
    // string, not an Err/panic) carrying the stable transport kind and a
    // message that says the tool is temporarily unavailable and recovers.
    let out = registry.execute("notes__echo", "{}").await;
    let v: serde_json::Value = serde_json::from_str(&out).expect("a JSON tool result");
    assert_eq!(
        v["kind"], "transport_unreachable",
        "a dead connector must surface the typed transport kind: {out}"
    );
    let msg = v["error"].as_str().expect("a human-readable message");
    assert!(
        msg.contains("temporarily unavailable"),
        "the message must say the tool is temporarily unavailable: {msg}"
    );
    assert!(
        msg.contains("Try again"),
        "the message must say the tool recovers (try again): {msg}"
    );

    // Existence is untouched by the failed call: nothing vanished.
    assert!(registry.owns("notes__echo"), "still owned after the call");
    assert!(
        registry.specs().iter().any(|s| s.name == "notes__echo"),
        "still listed after the call"
    );
}