Skip to content

symfonic.capabilities.triage.capability

capability

Pre-hydration intent routing, as something Agent can be handed.

The classifier in :mod:.intent is a port of legacy's deterministic verb+keyword triage. This module hands the verdict to the kernel the way MemoryCapability hands recall: a resolution stage that classifies the turn's prompt and records the verdict on the turn snapshot, where operators and compilation stages read it. Memory retrieval skips hydration on pure action turns through :meth:IntentTriageCapability.classify, not through the snapshot -- STG-7 withholds a sibling's entry from a resolution stage, so the retrieval handler asks the shared instance instead.

The stage claims no effects: classification is a pure function of the prompt (STG-7), and holding no provider handle is what makes "zero model calls" structural rather than measured-after-the-fact.

IntentTriageCapability dataclass

IntentTriageCapability(tool_keywords: Mapping[str, Sequence[str]] = dict(), classifier: IntentClassifier = IntentClassifier())

Classify each turn's prompt as action / knowledge / ambiguous.

Usage::

Agent(provider, capabilities=[
    IntentTriageCapability(tool_keywords={"restart": ("restart",)}),
])

No GrantEffects entry: the stage performs no effect, so there is nothing to authorise.

classify

classify(prompt: str) -> IntentLabel

The verdict for prompt under this instance's configuration.

The reader memory retrieval routes on (CORE-406): the same pure call the stage makes, so a retrieval holding the composed instance can ask what the turn is without reading the snapshot -- which STG-7 does not hand to a resolution stage. Pass the composed instance itself and the recorded verdict and the skip cannot disagree; classification is deterministic, so even two same-configured instances agree.

Source code in src/symfonic/capabilities/triage/capability.py
def classify(self, prompt: str) -> IntentLabel:
    """The verdict for ``prompt`` under this instance's configuration.

    The reader memory retrieval routes on (CORE-406): the same pure call
    the stage makes, so a retrieval holding the composed instance can ask
    what the turn is without reading the snapshot -- which STG-7 does not
    hand to a resolution stage. Pass the composed instance itself and the
    recorded verdict and the skip cannot disagree; classification is
    deterministic, so even two same-configured instances agree.
    """
    return self.classifier.classify(str(prompt), self.tool_keywords).label

contribute

contribute(request: CapabilityRequest) -> CapabilityContribution

Declare the triage stage and the handler that answers it.

Source code in src/symfonic/capabilities/triage/capability.py
def contribute(self, request: CapabilityRequest) -> CapabilityContribution:
    """Declare the triage stage and the handler that answers it."""
    del request  # Pure: classification reads the turn, never the grants.
    classifier = self.classifier
    tool_keywords = self.tool_keywords

    async def handle(context: Any) -> StageResult[Any]:
        """Classify the turn's prompt and record the verdict."""
        turn_request = getattr(context, "request", None)
        if turn_request is None:  # pragma: no cover - defensive
            return no_change("no turn request on the stage context")
        prompt = getattr(turn_request, "prompt", "") or ""
        verdict = classifier.classify(str(prompt), tool_keywords)
        return applied(
            ResolvedInput(
                capability=TRIAGE_CAPABILITY,
                value=(
                    {
                        "label": verdict.label,
                        "confidence": verdict.confidence,
                        "matched_tool_keywords": list(
                            verdict.matched_tool_keywords
                        ),
                        "reasoning": verdict.reasoning,
                        "action_score": verdict.action_score,
                        "knowledge_score": verdict.knowledge_score,
                    },
                ),
                provenance="intent-lexicon-v1",
            ),
            counts={verdict.label: 1},
        )

    return CapabilityContribution(
        capability=TRIAGE_CAPABILITY,
        stages=(
            StageDescriptor(
                stage_id=TRIAGE_STAGE,
                phase=Phase.PROMPT_ASSEMBLY,
                capability=TRIAGE_CAPABILITY,
                priority=_TRIAGE_PRIORITY,
                optional_before=("memory.retrieval",),
                effects=frozenset(),
                kind=StageKind.RESOLUTION,
            ),
        ),
        handlers={TRIAGE_STAGE: handle},
        # No effects: classification is a pure function of the prompt.
        effect_grants=frozenset(),
    )