What invariants prevent silent skill degradation when tools are composed?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session composes multiple tools or capabilities into a workflow (tool A's output feeds tool B's input), and the composition may silently degrade the effective skill of the workflow below what any individual tool promised. Does not apply to single-tool workflows or compositions with verified equivalence to atomic capabilities.
Core Distinction
The separation between **individual tool quality** (each tool meets its individual accuracy, reliability, and semantics guarantees) and **composed workflow quality** (the chained output meets the workflow's requirements). Relying on individual quality creates the **Composition Skill Decay**: tool A is 90% accurate, tool B is 90% accurate, and the composed A→B workflow is 81% accurate (0.9 × 0.9), not 90%. The workflow silently degrades because errors from A propagate through B, and no one checked the composed accuracy.
What Was Observed
In tool-composition agent architectures, failing to verify composition skill produces three primary failure modes:
- **Accuracy Multiplication Collapse:** A 5-tool pipeline each with 95% accuracy produces 77% workflow accuracy. The user expects 95% because "each tool is accurate," but the workflow quietly operates at 77%.
- **Error Mode Introduction:** Tool A is deterministic and tool B is non-deterministic. Their composition introduces non-determinism into a workflow that was supposed to be deterministic, breaking downstream guarantees.
- **Semantic Contamination:** Tool A returns results in a slightly different format than tool B expects. B's error handling masks the mismatch, and the workflow produces plausible-looking wrong output.
Recorded Elements
- **`per_tool_quality_contract_receipt`:** For each composed tool, its cryptographic quality contract (accuracy, error rate, determinism, semantic guarantees).
- **`composition_quality_computation_signature`:** The computed quality of the composed workflow, including how per-tool qualities combine given the specific composition structure (chained, parallel, conditional).
- **`composition_failure_mode_manifest_receipt`:** The exact failure modes introduced or amplified by the composition, beyond what any single tool exhibits.
- **`composition_semantic_compatibility_signature`:** The cryptographic proof that tool A's output format matches tool B's expected input format (including error handling, edge cases).
Condition of Validity
* **Applies when:** Multiple tools are composed, their quality contracts are non-trivial, and workflow quality matters to downstream consumers. * **Does not apply when:** Single-tool workflows, or compositions where quality is verified as a unit.
Decision Boundary
- **Is the workflow a composition of multiple tools?** If no, single-tool quality applies. If yes, proceed.
- **Is `per_tool_quality_contract_receipt` present for each tool?** If yes, composition quality can be computed. If no, the composition is unquantified.
- **Does `composition_quality_computation_signature` show composed quality meeting the workflow requirement?** If yes, proceed. If no, the composition degrades below requirement.
- **Conversely, when can a composition proceed without quality computation?** Only when the workflow requirement is below the composition's minimum possible quality (conservative bound), or quality is not a requirement (best-effort workflow).
Operational Effect
- **Accuracy Collapse Detection:** Composition quality is computed, not assumed.
- **Error Mode Awareness:** Introduced error modes are surfaced.
- **Semantic Integrity:** Format compatibility is verified before composition.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class CompositionVerdict(Enum):
SINGLE_TOOL = auto()
COMPOSITION_MEETS_QUALITY = auto()
COMPOSITION_DEGRADED = auto()
SEMANTIC_INCOMPATIBLE = auto()
@dataclass(frozen=True)
class ToolQuality:
tool_id: str
accuracy: float
determinism: str # "DETERMINISTIC" | "NON_DETERMINISTIC"
output_schema_hash: str
expected_input_schema_hash: str
@dataclass(frozen=True)
class ToolComposition:
composition_id: str
tools: tuple[ToolQuality, ...]
workflow_quality_requirement: float
introduction_of_nondeterminism_acceptable: bool
@dataclass(frozen=True)
class CompositionPolicy:
require_schema_compatibility: bool
@dataclass(frozen=True)
class CompositionManifest:
verdict: CompositionVerdict
composed_quality: float
directive: str
manifest_digest: str
class ToolCompositionSkillGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_composition(
cls, comp: ToolComposition, policy: CompositionPolicy
) -> CompositionManifest:
# Gate 1: Single tool
if len(comp.tools) <= 1:
q = comp.tools[0].accuracy if comp.tools else 1.0
digest = cls._hash(json.dumps({
"composition": comp.composition_id,
"tools": str(len(comp.tools)),
"quality": f"{q:.3f}"
}, sort_keys=True))
return CompositionManifest(
verdict=CompositionVerdict.SINGLE_TOOL,
composed_quality=q,
directive=f"PROCEED: Single tool workflow, quality {q:.2f}.",
manifest_digest=digest
)
# Gate 2: Schema compatibility
if policy.require_schema_compatibility:
for i in range(len(comp.tools) - 1):
upstream = comp.tools[i]
downstream = comp.tools[i + 1]
if upstream.output_schema_hash != downstream.expected_input_schema_hash:
return CompositionManifest(
verdict=CompositionVerdict.SEMANTIC_INCOMPATIBLE,
composed_quality=0.0,
directive=f"HALT: {upstream.tool_id} output schema != {downstream.tool_id} input schema.",
manifest_digest=cls._hash(f"SCHEMA:{i}")
)
# Gate 3: Non-determinism introduction
has_deterministic = any(t.determinism == "DETERMINISTIC" for t in comp.tools)
has_nondeterministic = any(t.determinism == "NON_DETERMINISTIC" for t in comp.tools)
if has_deterministic and has_nondeterministic and not comp.introduction_of_nondeterminism_acceptable:
return CompositionManifest(
verdict=CompositionVerdict.COMPOSITION_DEGRADED,
composed_quality=0.0,
directive=f"HALT: Mixing deterministic and non-deterministic tools without explicit acceptance.",
manifest_digest=cls._hash("NONDET_INTRODUCED")
)
# Gate 4: Quality composition (chained: multiply accuracies)
composed_quality = 1.0
for tool in comp.tools:
composed_quality *= tool.accuracy
if composed_quality < comp.workflow_quality_requirement:
return CompositionManifest(
verdict=CompositionVerdict.COMPOSITION_DEGRADED,
composed_quality=composed_quality,
directive=f"HALT: Composed quality {composed_quality:.2%} < requirement {comp.workflow_quality_requirement:.2%}.",
manifest_digest=cls._hash(f"DEGRADED:{composed_quality:.3f}")
)
# Gate 5: Meets quality
digest = cls._hash(json.dumps({
"composition": comp.composition_id,
"tools": tuple(t.tool_id for t in comp.tools),
"composed_quality": f"{composed_quality:.3f}",
"requirement": f"{comp.workflow_quality_requirement:.3f}"
}, sort_keys=True))
return CompositionManifest(
verdict=CompositionVerdict.COMPOSITION_MEETS_QUALITY,
composed_quality=composed_quality,
directive=f"PROCEED: {len(comp.tools)}-tool composition, quality {composed_quality:.2%}.",
manifest_digest=digest
)