Skip to content

Tool Contracts

Tool contracts define the formal interface between edge nodes and the gateway in Adaptive Sentience. They provide type-safe, versioned APIs for tool execution with built-in capability requirements and schema validation.


Overview

Problem: In distributed systems, tools running on different nodes need clear interface definitions to ensure correct usage, prevent errors, and enable automated validation.

Solution: Tool contracts provide JSON Schema-based interface definitions that are:

  • Type-safe: Input and output validated against schemas
  • Versioned: Track tool evolution over time
  • Capability-gated: Explicit permission requirements
  • Self-documenting: Machine-readable metadata
  • Deterministic: Optional guarantee of reproducible execution

Core Concepts

What is a Tool Contract?

A tool contract is a formal specification that defines:

  1. Tool Identity: Unique identifier and version
  2. Input Schema: Expected parameters and types
  3. Output Schema: Return value structure
  4. Capability Requirements: Which permissions are needed
  5. Execution Properties: Deterministic vs. non-deterministic
  6. Metadata: Description, documentation, examples

Tool contracts enable the gateway to:

  • Route requests to nodes that have the required tool
  • Validate inputs before sending to edge nodes
  • Verify outputs match expected schema
  • Enforce capability-based authorization
  • Cache results for deterministic tools

Tool Contract Structure

Basic Contract Format

{
  "tool_id": "tool:pii_redact",
  "name": "pii_redact",
  "version": "1.0.0",
  "description": "Redact PII from text using regex patterns",
  "input_schema": {
    "type": "object",
    "properties": {
      "text": {
        "type": "string",
        "description": "Text to redact PII from"
      },
      "redact_emails": {
        "type": "boolean",
        "description": "Whether to redact email addresses",
        "default": true
      },
      "redact_phones": {
        "type": "boolean",
        "description": "Whether to redact phone numbers",
        "default": true
      },
      "replacement": {
        "type": "string",
        "description": "Replacement text for redacted PII",
        "default": "[REDACTED]"
      }
    },
    "required": ["text"]
  },
  "output_schema": {
    "type": "object",
    "properties": {
      "redacted_text": {
        "type": "string",
        "description": "Text with PII redacted"
      },
      "redactions": {
        "type": "array",
        "description": "List of redactions performed",
        "items": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "enum": ["email", "phone"]
            },
            "count": {
              "type": "integer",
              "minimum": 0
            }
          },
          "required": ["type", "count"]
        }
      }
    },
    "required": ["redacted_text", "redactions"]
  },
  "required_capabilities": ["tool:pii_redact"],
  "deterministic": true,
  "tags": ["pii", "privacy", "redaction"]
}

Contract Fields

Field Required Description
tool_id Yes Unique identifier (convention: tool:name)
name Yes Tool name (lowercase, underscores)
version Yes Semantic version (major.minor.patch)
description Yes Human-readable description
input_schema Yes JSON Schema for input validation
output_schema Yes JSON Schema for output validation
required_capabilities Yes List of required capability tokens
deterministic No Whether execution is deterministic (default: false)
tags No Searchable metadata tags
examples No Example input/output pairs

Defining Tool Schemas

JSON Schema Basics

Tool contracts use JSON Schema for input/output validation.

Simple Schema Example:

{
  "input_schema": {
    "type": "object",
    "properties": {
      "url": {
        "type": "string",
        "format": "uri",
        "description": "URL to fetch"
      },
      "timeout": {
        "type": "integer",
        "minimum": 1,
        "maximum": 60,
        "default": 10,
        "description": "Timeout in seconds"
      }
    },
    "required": ["url"]
  }
}

This schema requires: - url: A string in URI format (required) - timeout: An integer between 1-60, defaults to 10 (optional)

Supported JSON Schema Features

Adaptive Sentience supports JSON Schema Draft 7:

Type Validation:

{
  "type": "string",      // string, number, integer, boolean, array, object, null
  "minimum": 0,          // For numbers
  "maximum": 100,
  "pattern": "^[A-Z]",   // Regex for strings
  "format": "email"      // Common formats: email, uri, date-time, ipv4, ipv6
}

Required Fields:

{
  "type": "object",
  "properties": {
    "name": {"type": "string"},
    "age": {"type": "integer"}
  },
  "required": ["name"]    // age is optional
}

Arrays:

{
  "type": "array",
  "items": {
    "type": "string"
  },
  "minItems": 1,
  "maxItems": 10
}

Nested Objects:

{
  "type": "object",
  "properties": {
    "user": {
      "type": "object",
      "properties": {
        "name": {"type": "string"},
        "email": {"type": "string", "format": "email"}
      },
      "required": ["name", "email"]
    }
  }
}

Enums:

{
  "type": "string",
  "enum": ["pending", "running", "completed", "failed"]
}

Complex Schema Example

{
  "input_schema": {
    "type": "object",
    "properties": {
      "document": {
        "type": "object",
        "properties": {
          "content": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100000
          },
          "format": {
            "type": "string",
            "enum": ["text", "markdown", "html"],
            "default": "text"
          }
        },
        "required": ["content"]
      },
      "options": {
        "type": "object",
        "properties": {
          "max_summary_length": {
            "type": "integer",
            "minimum": 50,
            "maximum": 500,
            "default": 200
          },
          "include_keywords": {
            "type": "boolean",
            "default": false
          },
          "language": {
            "type": "string",
            "pattern": "^[a-z]{2}$",
            "default": "en"
          }
        }
      }
    },
    "required": ["document"]
  },
  "output_schema": {
    "type": "object",
    "properties": {
      "summary": {
        "type": "string",
        "minLength": 1
      },
      "keywords": {
        "type": "array",
        "items": {
          "type": "string"
        }
      },
      "confidence": {
        "type": "number",
        "minimum": 0,
        "maximum": 1
      }
    },
    "required": ["summary", "confidence"]
  }
}

Capability Requirements

Single Capability

Most tools require a single capability:

{
  "required_capabilities": ["tool:pii_redact"]
}

Multiple Capabilities

Tools that access sensitive resources may require multiple capabilities:

{
  "required_capabilities": [
    "tool:database_query",
    "resource:customer_db"
  ]
}

The capability token must have all required capabilities for execution to proceed.

Capability Naming Conventions

Pattern Example Use Case
tool:<name> tool:pii_redact Tool execution permission
resource:<name> resource:production_db Resource access
action:<name> action:approve_payment Privileged operation
admin:* admin:user_management Administrative tasks

Versioning

Semantic Versioning

Tool contracts use Semantic Versioning 2.0.0:

  • MAJOR: Incompatible API changes (e.g., removed field, changed behavior)
  • MINOR: Backward-compatible new features (e.g., new optional field)
  • PATCH: Backward-compatible bug fixes

Examples:

1.0.0 → 1.0.1   # Bug fix in redaction logic (patch)
1.0.1 → 1.1.0   # Added optional 'redact_ssn' field (minor)
1.1.0 → 2.0.0   # Changed output format (major - breaking)

Version Selection

When calling a tool, clients can specify version requirements:

{
  "tool_name": "pii_redact",
  "tool_version": "1.x",     // Any 1.x version
  "tool_args": { ... }
}

Version matching strategies:

  • "1.2.3": Exact version
  • "1.2.x": Any patch version of 1.2
  • "1.x": Any minor/patch version of 1.x
  • "latest": Most recent version (default)

Multiple Versions

Edge nodes can register multiple versions of the same tool:

# Edge node supports both v1 and v2
toolhost.register_tool(pii_redact_v1)
toolhost.register_tool(pii_redact_v2)

The gateway routes to the best matching version based on: 1. Client's version requirement 2. Available nodes 3. Node preference (if specified)


Deterministic Execution

What is Deterministic Execution?

A tool is deterministic if it produces the same output for the same input every time.

Deterministic examples: - pii_redact: Regex-based redaction (no randomness) - json_validate: Schema validation (pure function) - unit_convert: Mathematical conversion

Non-deterministic examples: - llm_summarize: Uses AI model (output varies) - fetch_url: Network calls (content changes over time) - current_time: Time-dependent

Declaring Determinism

{
  "tool_id": "tool:unit_convert",
  "deterministic": true
}

Benefits of Deterministic Tools

  1. Caching: Gateway can cache results and skip re-execution
  2. Idempotency: Retries are safe
  3. Reproducibility: Audit trails can be replayed
  4. Testing: Easier to write deterministic tests

Idempotency Caching

For deterministic tools, the gateway computes a cache key:

cache_key = SHA256(tool_id + version + JSON(args))

If a matching cache entry exists, the gateway returns the cached result without executing the tool.

Example:

# First call executes on edge node
result1 = call_tool("unit_convert", {"value": 100, "from": "km", "to": "mi"})

# Second identical call returns cached result
result2 = call_tool("unit_convert", {"value": 100, "from": "km", "to": "mi"})

Registering Tools with Edge Nodes

Python Tool Registration

Step 1: Define the Tool Function

from typing import Dict, Any

def pii_redact(args: Dict[str, Any]) -> Dict[str, Any]:
    """Redact PII from text."""
    text = args["text"]
    redact_emails = args.get("redact_emails", True)
    redact_phones = args.get("redact_phones", True)
    replacement = args.get("replacement", "[REDACTED]")

    redactions = []

    # Email redaction
    if redact_emails:
        import re
        email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
        email_matches = re.findall(email_pattern, text)
        if email_matches:
            text = re.sub(email_pattern, replacement, text)
            redactions.append({"type": "email", "count": len(email_matches)})

    # Phone redaction
    if redact_phones:
        phone_pattern = r'\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b'
        phone_matches = re.findall(phone_pattern, text)
        if phone_matches:
            text = re.sub(phone_pattern, replacement, text)
            redactions.append({"type": "phone", "count": len(phone_matches)})

    return {
        "redacted_text": text,
        "redactions": redactions
    }

Step 2: Create Tool Specification

from edge_node.toolhost import ToolSpec

INPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "text": {"type": "string"},
        "redact_emails": {"type": "boolean", "default": True},
        "redact_phones": {"type": "boolean", "default": True},
        "replacement": {"type": "string", "default": "[REDACTED]"}
    },
    "required": ["text"]
}

OUTPUT_SCHEMA = {
    "type": "object",
    "properties": {
        "redacted_text": {"type": "string"},
        "redactions": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "type": {"type": "string"},
                    "count": {"type": "integer"}
                }
            }
        }
    },
    "required": ["redacted_text", "redactions"]
}

pii_redact_spec = ToolSpec(
    name="pii_redact",
    version="1.0.0",
    description="Redact PII from text using regex patterns",
    required_capability="tool:pii_redact",
    callable=pii_redact,
    input_schema=INPUT_SCHEMA
)

Step 3: Register with ToolHost

from edge_node.toolhost import ToolHost

# Initialize tool host
toolhost = ToolHost(node_id="local:abc123")

# Register tool
toolhost.register_tool(pii_redact_spec)

# Output:
# [TOOLHOST] Registered tool: pii_redact v1.0.0 (requires tool:pii_redact)

Full Edge Node Example

#!/usr/bin/env python3
"""Custom edge node with PII redaction tool."""

import os
from edge_node.toolhost import ToolHost, ToolSpec
from edge_node.node import EdgeNode

# Tool implementation
def pii_redact(args):
    # ... implementation from above ...
    pass

# Tool specification
pii_redact_spec = ToolSpec(
    name="pii_redact",
    version="1.0.0",
    description="Redact PII from text using regex patterns",
    required_capability="tool:pii_redact",
    callable=pii_redact,
    input_schema={
        "type": "object",
        "properties": {
            "text": {"type": "string"}
        },
        "required": ["text"]
    }
)

# Initialize edge node
node_port = int(os.getenv("NODE_PORT", "8000"))
node = EdgeNode(port=node_port)

# Register tool
node.toolhost.register_tool(pii_redact_spec)

# Start node (joins mesh and listens for requests)
node.start()

Run the edge node:

NODE_PORT=8000 python3 my_edge_node.py

Tool Discovery

How Tools are Discovered

  1. Edge Node Registration: When an edge node starts, it broadcasts its available tools via UDP multicast
  2. Gateway Catalog: The gateway builds a catalog of all available tools across the mesh
  3. Client Discovery: Clients query the gateway to discover available tools

Discovery API

Get Available Tools:

curl http://localhost:8787/v1/catalog

Response:

{
  "local": {
    "node_id": "gateway:abc123",
    "tools": []
  },
  "mesh": [
    {
      "node_id": "local:def456",
      "node_type": "macos",
      "http_url": "http://192.168.1.100:8000",
      "tools": [
        {
          "name": "pii_redact",
          "version": "1.0.0",
          "description": "Redact PII from text using regex patterns",
          "required_capability": "tool:pii_redact",
          "deterministic": true
        }
      ]
    }
  ]
}

Tool Metadata

Each discovered tool includes:

  • name: Tool name
  • version: Semantic version
  • description: Human-readable description
  • required_capability: Permission requirements
  • deterministic: Whether execution is deterministic
  • has_schema: Whether input schema is available

Executing Tools

Basic Tool Execution

curl -X POST http://localhost:8787/v1/tool_call \
  -H "Content-Type: application/json" \
  -d '{
    "target": {"kind": "local"},
    "tool_name": "pii_redact",
    "tool_args": {
      "text": "Contact john@example.com at 555-123-4567"
    },
    "token": {
      "capabilities": ["tool:pii_redact"]
    }
  }'

Response:

{
  "ok": true,
  "verified": true,
  "result": {
    "redacted_text": "Contact [REDACTED] at [REDACTED]",
    "redactions": [
      {"type": "email", "count": 1},
      {"type": "phone", "count": 1}
    ]
  },
  "execution_path": ["local:def456"],
  "degraded": false,
  "trace_id": "trace:1a2b3c4d",
  "latency_ms": 15
}

Specifying Tool Version

{
  "tool_name": "pii_redact",
  "tool_version": "1.x",
  "tool_args": { ... }
}

Targeting Specific Nodes

{
  "target": {
    "kind": "node_id",
    "node_id": "local:def456"
  },
  "tool_name": "pii_redact",
  "tool_args": { ... }
}

Schema Validation

Input Validation

Before executing a tool, the edge node validates input arguments against the schema:

def call_tool(tool_name, args, token):
    spec = self.tools[tool_name]

    # Validate against input schema
    valid, error = self._validate_schema(spec, args)
    if not valid:
        return {
            "ok": False,
            "error": f"Schema validation failed: {error}"
        }

    # Execute tool
    result = spec.callable(args)

    return {
        "ok": True,
        "result": result
    }

Example validation error:

{
  "ok": false,
  "error": "Schema validation failed: 'text' is a required property"
}

Output Validation

Output validation ensures tools return the expected structure:

# Tool returns invalid output
result = {"wrong_field": "oops"}

# Validation catches the error
{
  "ok": false,
  "error": "Output validation failed: 'redacted_text' is a required property"
}

Disabling Validation

For development, validation can be disabled:

toolhost = ToolHost(
    node_id="local:abc123",
    validate_schemas=False  # Skip validation (not recommended for production)
)

Advanced Topics

Custom Validation Logic

Beyond JSON Schema, tools can implement custom validation:

def pii_redact(args):
    # Custom validation
    text = args["text"]
    if len(text) > 100000:
        raise ValueError("Text too long (max 100KB)")

    if not text.strip():
        raise ValueError("Text cannot be empty")

    # ... rest of implementation ...

Raised exceptions are caught and returned as errors:

{
  "ok": false,
  "error": "Text too long (max 100KB)"
}

Tool Examples and Documentation

Include example input/output in the contract:

{
  "tool_id": "tool:pii_redact",
  "examples": [
    {
      "input": {
        "text": "Contact john@example.com",
        "redact_emails": true
      },
      "output": {
        "redacted_text": "Contact [REDACTED]",
        "redactions": [{"type": "email", "count": 1}]
      }
    }
  ]
}

These examples are used for: - Auto-generated documentation - Automated testing - Client code generation

Tool Hashing for Auditability

Edge nodes compute a hash of each tool for audit trails:

def _get_tool_hash(spec):
    content = f"{spec.name}:{spec.version}:{spec.required_capability}"
    return hashlib.sha256(content.encode()).hexdigest()[:8]

This hash is included in execution evidence:

{
  "execution_path": ["local:def456"],
  "tool_hash": "a1b2c3d4",
  "tool_version": "1.0.0"
}

Schema Evolution

When evolving schemas:

  1. Adding optional fields: MINOR version bump

    {
      "properties": {
        "text": {"type": "string"},
        "redact_ssn": {"type": "boolean", "default": false}  // NEW
      }
    }
    

  2. Making fields required: MAJOR version bump

    {
      "required": ["text", "format"]  // 'format' is now required
    }
    

  3. Removing fields: MAJOR version bump

  4. Changing types: MAJOR version bump

Best Practices

1. Use Descriptive Names

# Good
ToolSpec(name="pii_redact", ...)

# Bad
ToolSpec(name="redact", ...)  # Too vague
ToolSpec(name="piiRedactionTool", ...)  # Use snake_case

2. Provide Rich Descriptions

{
  "description": "Redact PII from text using regex patterns. Supports email and phone number detection. NOT production-grade - use specialized PII services for production."
}

3. Use Specific Schemas

{
  // Good - specific constraints
  "properties": {
    "email": {
      "type": "string",
      "format": "email",
      "maxLength": 254
    }
  },

  // Bad - too loose
  "properties": {
    "email": {"type": "string"}
  }
}

4. Include Examples

Always include at least one example input/output pair.

5. Version Deliberately

Don't bump major version unnecessarily. Use minor versions for backward-compatible additions.

6. Test Schema Validation

Write tests that verify schema validation catches invalid inputs:

def test_schema_validation():
    result = toolhost.call_tool(
        "pii_redact",
        {"invalid": "field"},
        token
    )
    assert not result["ok"]
    assert "required property" in result["error"]

7. Document Required Capabilities

Clearly document what each capability grants access to:

# Good
required_capability="tool:pii_redact"  # Grants permission to redact PII

# Bad
required_capability="admin"  # Too broad, unclear what this allows

8. Use Determinism Correctly

Only mark tools as deterministic if they truly are:

# Deterministic - pure function
ToolSpec(name="unit_convert", deterministic=True)

# Non-deterministic - network call
ToolSpec(name="fetch_url", deterministic=False)

Troubleshooting

Tool Not Discovered

Symptom: Gateway doesn't see your tool in the catalog.

Solutions:

  1. Check UDP multicast: Ensure UDP port 9999 is open

    sudo ufw allow 9999/udp  # Linux
    

  2. Verify registration: Check edge node logs for:

    [TOOLHOST] Registered tool: pii_redact v1.0.0
    

  3. Manual refresh: Restart the gateway or trigger a mesh scan:

    curl http://localhost:8787/v1/mesh_scan?duration=5.0
    

Schema Validation Errors

Symptom: Tool execution fails with "Schema validation failed".

Solutions:

  1. Check required fields: Ensure all required fields are provided
  2. Verify types: String vs. number vs. boolean
  3. Test schema separately:

    import jsonschema
    jsonschema.validate(instance=args, schema=INPUT_SCHEMA)
    

  4. Use online validator: jsonschemavalidator.net

Version Mismatch

Symptom: Gateway can't find requested version.

Solutions:

  1. Check available versions:

    curl http://localhost:8787/v1/catalog | jq '.mesh[].tools[] | {name, version}'
    

  2. Use flexible version matching: "1.x" instead of exact version

  3. Update edge node: Deploy newer version to mesh

Capability Token Rejected

Symptom: "Missing required capability: tool:pii_redact"

Solutions:

  1. Add capability to token:

    {
      "token": {
        "capabilities": ["tool:pii_redact"]
      }
    }
    

  2. Check capability name: Must match exactly (case-sensitive)


Reference

ToolSpec Python Class

@dataclass
class ToolSpec:
    name: str                                    # Tool name
    version: str                                 # Semantic version
    description: str                             # Human-readable description
    required_capability: str                     # Required capability token
    callable: Callable[[Dict, Dict], Dict]       # Tool implementation
    input_schema: Optional[Dict] = None          # JSON Schema for input
    output_schema: Optional[Dict] = None         # JSON Schema for output
    deterministic: bool = False                  # Is execution deterministic?
    tags: List[str] = field(default_factory=list)  # Searchable tags
    examples: List[Dict] = field(default_factory=list)  # Example I/O

Tool Call Request Schema

{
  "target": {
    "kind": "local" | "mesh" | "node_id",
    "node_id": "local:abc123"  // Required if kind=node_id
  },
  "tool_name": "pii_redact",
  "tool_version": "1.x",  // Optional
  "tool_args": {
    // Tool-specific arguments
  },
  "token": {
    "capabilities": ["tool:pii_redact"],
    "expires_at": "2024-12-31T23:59:59Z"  // Optional
  },
  "job_id": "job:unique123"  // Optional, for idempotency
}

Tool Call Response Schema

{
  "ok": true,
  "verified": true,
  "result": {
    // Tool-specific output
  },
  "error": null,  // Present if ok=false
  "execution_path": ["local:def456"],
  "degraded": false,
  "trace_id": "trace:1a2b3c4d",
  "latency_ms": 15,
  "tool_hash": "a1b2c3d4",
  "tool_version": "1.0.0"
}

Next Steps


Further Reading