6-Python: Phases as Tools (Anthropic Tool-Use API)

6-Python: Phases as Tools (Anthropic Tool-Use API)

The character of a question is the intensity of the not-knowing within it.

Context

S5 wired the cycle into a LangGraph state graph. The graph's topology — explicit edges from S to G to Q to P to V — was the mechanism that enforced cycle order. An agent walking the graph could not call P before Q because the only way to reach P was through the Q edge.

This article ports the cycle into a different idiom: each phase is a tool the agent can call autonomously. The agent decides what to call next; there is no graph topology to constrain it. The cycle order is enforced by tool input schemas and runtime checks rather than by graph edges. P's tool function checks for the presence of pattern and resonance on the cycle state; if either is missing, the call returns an error and the agent must call the upstream tools first. The agent learns the order by being told (in the system prompt) and by being corrected (by tool errors) — not by being unable to express the call.

Two things motivate this surface. First: autonomous agents — the kind built with any contemporary tool-use API — are organized around tool palettes, not graphs. If 5QLN is to live natively in that ecosystem, phases need to be tools. Second: the schema enforcement is a different shape of constraint than the graph constraint. A graph forbids out-of-order traversal; a schema forbids out-of-order parameters. Both arrive at the same destination (cycle order preserved); each is more natural in different deployment contexts.

The article uses the Anthropic Client SDK (the anthropic Python package) and its tool-use API directly. The higher-level Claude Agent SDK (claude_agent_sdk) provides autonomous loop orchestration on top, with custom tools typically registered via MCP — that path is S7's territory. The Client SDK pattern shown here is the foundation; everything S7 does extends it.


What the agent loop looks like

The standard tool-use loop with the Anthropic Python SDK:

import anthropic

client = anthropic.Anthropic()
messages = [{"role": "user", "content": "Start a cycle..."}]

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=4096,
    tools=[...],   # tool definitions
    messages=messages,
)

while response.stop_reason == "tool_use":
    # Execute each tool the model called this turn, append results, re-invoke
    ...

# response.stop_reason == "end_turn" — the agent is done

This is the loop the rest of the article fills in. The agent's autonomy lives in messages.create. The application's authority lives in tool execution, the validator interleaving, and the receptive routing to humans. The asymmetry from S1 lives in which tools call out to the human and which do not.


Architecture

fivqln/agent_sdk/
├── __init__.py
├── tools.py              # Tool input schemas + tool definitions
├── handlers.py           # Tool functions (the cycle-order enforcement)
├── system_prompt.py      # Constitutional Block + role/asymmetry instructions
├── runtime.py            # The agent loop (validator-interleaved)
└── human.py              # Default ask_human implementation (CLI)

The cycle state is application-managed. Tools receive the current Cycle (passed via closure from the runtime), validate prior outputs, perform their work, and return an updated Cycle. The runtime calls validate() after every tool result and halts on DEFINITE violations. ATTESTATION_REQUIRED findings are forwarded to the agent and to the final report.


Tool input schemas

Pydantic v2 exports JSON Schema via model_json_schema(), which the Anthropic API accepts directly as input_schema. The schemas hold what the AGENT supplies — not the prior cycle state. The runtime reads prior state from the Cycle and the tool function asserts it.

# fivqln/agent_sdk/tools.py
"""
Tool input schemas. Each schema captures what the agent provides to the
tool — typically just the new symbolic content. Prior cycle state is read
from the application-managed Cycle object, not from the agent's input.

This split is deliberate. The agent should never have to "carry" prior
outputs in its tool calls — that would create paraphrase pressure and
risk silent drift between what the agent re-types and what was actually
validated. Prior outputs live in one place: the Cycle.
"""

from typing import Optional
from pydantic import BaseModel, Field


# === Receptive tools ===
# These call out to the human. Their input schemas carry no symbolic
# content — only context the agent provides for the audit trail.

class ReceiveSparkInput(BaseModel):
    """Input for receive_spark. The agent supplies nothing the human's
    question depends on — the tool prompts the human and returns what
    arrived from ∞0."""
    reason_for_calling: str = Field(
        description=(
            "Brief explanation of why you are calling this tool now. "
            "For audit trail only. Does not affect what the human provides."
        ),
    )


class HoldPhiInput(BaseModel):
    """Input for hold_phi (the receptive part of Q). Same pattern as
    receive_spark — the human supplies φ; the agent's input is audit-only."""
    reason_for_calling: str = Field(description="Brief explanation. Audit only.")


class ConfirmEnrichedReturnInput(BaseModel):
    """Input for confirm_enriched_return (V's receptive moment). The agent
    provides a PROPOSED ∞0' question; the human confirms or revises."""
    proposed_question: str = Field(
        description=(
            "Your proposed ∞0' — the question this cycle reveals that "
            "could not have been asked before the cycle. The human will "
            "review and either confirm or revise."
        ),
    )


# === LLM-driven tools ===
# These carry the agent's actual symbolic work in their schemas.

class GrowPatternInput(BaseModel):
    """G = α ≡ {α'}. The agent supplies α and {α'}; the tool reads X
    from the cycle."""
    alpha_description: str = Field(
        description=(
            "The irreducible α found within X. Removing α should make X "
            "collapse. Per D1 §2.2."
        ),
    )
    alpha_expressions: list[str] = Field(
        min_length=1,
        description=(
            "Self-similar {α'} at other scales or in other domains. Each "
            "must be self-similar to α (same essence at a different scale), "
            "not merely topically related."
        ),
    )
    pattern_description: str = Field(description="The validated pattern Y.")
    lens: Optional[str] = Field(
        default=None,
        description=(
            "Optional: one of the 25 sub-phase lenses (SS through VV). "
            "If applied, the pattern recognition was refined through the "
            "borrowed quality."
        ),
    )


class FindResonanceInput(BaseModel):
    """The LLM step within Q (after the human has held φ). The agent
    supplies Ω and names the ⋂ landing."""
    omega_context: str = Field(
        description=(
            "What the larger context (Ω) makes possible — the field "
            "around the inquiry, beyond the individual."
        ),
    )
    landing: str = Field(
        description=(
            "What φ and Ω together revealed that neither alone contained. "
            "What turned the lock at ⋂. Per D1 §2.3, ⋂ cannot be "
            "manufactured — you can name what landed; only the human can "
            "attest the landing was real (S4 surfaces this attestation)."
        ),
    )
    key_description: str = Field(description="The validated Resonant Key Z.")


class PowerInput(BaseModel):
    """P = δE/δV → ∇. The agent maps the energy/value ratio and names ∇."""
    energy_value_observation: str = Field(
        description=(
            "The δE/δV mapping: where is energy spent? Where does value "
            "appear? The ratio reveals (does not compute) ∇."
        ),
    )
    gradient_direction: str = Field(
        description=(
            "The Natural Gradient ∇ — the path of least resistance leading "
            "toward α. Direction already present in the situation."
        ),
    )
    flow_description: str = Field(description="The validated Flow A.")


class ComposeSeedInput(BaseModel):
    """V composition (R7 — two passes). The agent reads the formation
    trail and composes the artifact."""
    artifact: str = Field(
        description=(
            "The Fractal Seed B'' — composed from the formation trail. "
            "Must carry α faithfully (R7). The validator will check that "
            "the α you carry matches the α validated in G."
        ),
    )
    fulfillment: str = Field(
        description="What this cycle produced for the inquiry's aim (B's first dimension)."
    )
    propagation: str = Field(
        description="What this cycle gives beyond itself (B's second dimension)."
    )


# === Tool definition list for the Anthropic API ===

def build_tool_definitions() -> list[dict]:
    """Return the list of tool definitions ready for messages.create(tools=...).

    The descriptions name the spec section each tool implements and the
    cycle-order requirement. The agent reads these and learns the protocol
    without us hand-coding a state machine for it."""
    return [
        {
            "name": "receive_spark",
            "description": (
                "S = ∞0 → ?. Receive the question from ∞0. Routes to the "
                "human; you cannot generate ?. Call this first — nothing "
                "else can be called until X is received."
            ),
            "input_schema": ReceiveSparkInput.model_json_schema(),
        },
        {
            "name": "grow_pattern",
            "description": (
                "G = α ≡ {α'}. Find the irreducible α within X and "
                "self-similar {α'}. Requires X (call receive_spark first)."
            ),
            "input_schema": GrowPatternInput.model_json_schema(),
        },
        {
            "name": "hold_phi",
            "description": (
                "Q-step-1: hold φ. Routes to the human; you cannot supply φ. "
                "Requires Y (call grow_pattern first)."
            ),
            "input_schema": HoldPhiInput.model_json_schema(),
        },
        {
            "name": "find_resonance",
            "description": (
                "Q-step-2: hold Ω, watch for ⋂, name the landing. Requires "
                "φ (call hold_phi first)."
            ),
            "input_schema": FindResonanceInput.model_json_schema(),
        },
        {
            "name": "power",
            "description": (
                "P = δE/δV → ∇. Map the energy/value ratio; the ratio "
                "reveals ∇. Requires Z (call find_resonance first)."
            ),
            "input_schema": PowerInput.model_json_schema(),
        },
        {
            "name": "compose_seed",
            "description": (
                "V composition: read the formation trail, compose B'', "
                "name B. Requires A (call power first). After this, call "
                "confirm_enriched_return."
            ),
            "input_schema": ComposeSeedInput.model_json_schema(),
        },
        {
            "name": "confirm_enriched_return",
            "description": (
                "V completion: propose ∞0' and route to the human for "
                "confirmation. Requires B'' (call compose_seed first)."
            ),
            "input_schema": ConfirmEnrichedReturnInput.model_json_schema(),
        },
    ]

Each LLM-driven tool's input schema captures the agent's symbolic work for that phase. Each receptive tool's input schema is minimal — the human supplies what matters. Every tool's description names the phase, the prior outputs required, and what comes next. The agent reads these and learns the cycle protocol.


Tool functions — cycle-order enforcement at runtime

# fivqln/agent_sdk/handlers.py
"""
Tool functions that operate on the Cycle. Each function:
  1. Validates that prior outputs are present (cycle order check).
  2. For receptive tools, calls ask_human.
  3. Constructs the new phase output from input + prior cycle.
  4. Returns the updated Cycle and a message for the agent.
"""

from datetime import datetime, timezone
from typing import Callable

from fivqln.types import (
    Cycle, ValidatedSpark, ValidatedPattern, ResonantKey,
    FormationEntry, FormationTrail, Phase,
)
from fivqln.symbols import (
    CoreEssence, SelfNature, UniversalPotential, NaturalIntersection,
)
from fivqln.agent_sdk.tools import (
    ReceiveSparkInput, GrowPatternInput, HoldPhiInput,
    FindResonanceInput,
)


class ToolError(Exception):
    """Raised when a tool function detects a cycle-order violation or
    missing prior outputs. The runtime catches this and returns it to
    the agent as an error tool result."""


AskHuman = Callable[[dict], dict]


def _append_trail(cycle: Cycle, entry: FormationEntry) -> FormationTrail:
    return FormationTrail(entries=list(cycle.trail.entries) + [entry])


def receive_spark(
    input: ReceiveSparkInput,
    cycle: Cycle,
    ask_human: AskHuman,
) -> tuple[Cycle, str]:
    """Routes to the human. The agent's input is audit-only.

    Returns (updated_cycle, message_for_agent). The message tells the
    agent what was received so it can plan its next call."""
    if cycle.spark is not None:
        raise ToolError("X already received. Cannot receive a new spark mid-cycle.")

    response = ask_human({
        "phase": "S",
        "spec_ref": "§2.1",
        "instruction": (
            "Hold ∞0. Resist closing the space. When something stirs from "
            "the open space, name what arrived as a question."
        ),
        "agent_reason": input.reason_for_calling,
        "fields_required": ["question", "held_by"],
    })

    received_at = datetime.now(timezone.utc)
    spark = ValidatedSpark(
        question=response["question"],
        received_at=received_at,
        held_by=response["held_by"],
    )
    new_trail = _append_trail(cycle, FormationEntry(
        timestamp=received_at,
        phase=Phase.S,
        operation="received question from ∞0 via human attestation",
        output_excerpt=spark.question,
    ))
    new_cycle = cycle.model_copy(update={"spark": spark, "trail": new_trail})

    return new_cycle, (
        f"X received: {spark.question!r}. Now call grow_pattern to find "
        f"α and {{α'}} within X."
    )


def grow_pattern(
    input: GrowPatternInput,
    cycle: Cycle,
    ask_human: AskHuman,  # unused but uniform signature
) -> tuple[Cycle, str]:
    """LLM-driven. The agent supplies α and {α'}; the tool checks X is
    present and constructs the validated pattern Y."""
    if cycle.spark is None:
        raise ToolError(
            "G requires X. The adaptive context chain (§2.6, §3.3) "
            "specifies G decodes with X. Call receive_spark first."
        )

    alpha = CoreEssence(
        description=input.alpha_description,
        expressions=tuple(input.alpha_expressions),
    )
    pattern = ValidatedPattern(
        alpha=alpha,
        pattern_description=input.pattern_description,
    )
    new_trail = _append_trail(cycle, FormationEntry(
        timestamp=datetime.now(timezone.utc),
        phase=Phase.G,
        lens=input.lens,
        operation=f"named α and {{α'}} (lens={input.lens or 'none'})",
        output_excerpt=alpha.description,
    ))
    new_cycle = cycle.model_copy(update={"pattern": pattern, "trail": new_trail})

    return new_cycle, (
        f"Y validated. α = {alpha.description!r}. "
        f"Now call hold_phi to receive the human's direct perception of Y."
    )


# === The split for Q ===

def hold_phi(
    input: HoldPhiInput,
    cycle: Cycle,
    ask_human: AskHuman,
) -> tuple[Cycle, str]:
    """Receptive part of Q. Routes to the human for φ. The result is
    stashed on the Cycle as a private attribute (`_held_phi`) until
    find_resonance combines it with Ω. The Cycle's public schema is
    not contaminated — the stash is consumed and cleared."""
    if cycle.pattern is None:
        raise ToolError("Q-step-1 (φ) requires Y. Call grow_pattern first.")
    if getattr(cycle, "_held_phi", None) is not None:
        raise ToolError(
            "φ already held. Now call find_resonance with Ω and ⋂ landing."
        )

    response = ask_human({
        "phase": "Q",
        "spec_ref": "§2.3 (φ)",
        "instruction": (
            "Look at Y without theory. What do you directly perceive? "
            "Not what you think. Not what data says. What lands."
        ),
        "context": {
            "alpha": cycle.pattern.alpha.description,
            "pattern": cycle.pattern.pattern_description,
        },
        "fields_required": ["perception", "held_by"],
    })

    phi = SelfNature(
        perception=response["perception"],
        held_by=response["held_by"],
    )
    new_trail = _append_trail(cycle, FormationEntry(
        timestamp=datetime.now(timezone.utc),
        phase=Phase.Q,
        operation="held φ via human attestation",
        output_excerpt=phi.perception,
    ))
    new_cycle = cycle.model_copy(update={"trail": new_trail})
    object.__setattr__(new_cycle, "_held_phi", phi)

    return new_cycle, (
        f"φ held: {phi.perception!r}. Now call find_resonance with your Ω "
        f"and the ⋂ landing."
    )


def find_resonance(
    input: FindResonanceInput,
    cycle: Cycle,
    ask_human: AskHuman,  # unused
) -> tuple[Cycle, str]:
    """LLM step of Q. The agent has held Ω in its context; here it names
    Ω and the ⋂ landing. φ is read from the cycle (stashed by hold_phi)."""
    if cycle.pattern is None:
        raise ToolError("find_resonance requires Y. Call grow_pattern.")
    phi = getattr(cycle, "_held_phi", None)
    if phi is None:
        raise ToolError(
            "find_resonance requires φ. Call hold_phi first; the human "
            "must hold φ before Ω can be witnessed at ⋂."
        )

    omega = UniversalPotential(context=input.omega_context)
    intersection = NaturalIntersection(
        phi=phi, omega=omega, landing=input.landing,
    )
    resonance = ResonantKey(
        intersection=intersection,
        key_description=input.key_description,
    )
    new_trail = _append_trail(cycle, FormationEntry(
        timestamp=datetime.now(timezone.utc),
        phase=Phase.Q,
        operation="LLM held Ω; named ⋂ landing",
        output_excerpt=intersection.landing,
    ))
    new_cycle = cycle.model_copy(update={
        "resonance": resonance,
        "trail": new_trail,
    })
    object.__setattr__(new_cycle, "_held_phi", None)  # consumed

    return new_cycle, "Z validated. Now call power to map δE/δV and reveal ∇."


# power, compose_seed, and confirm_enriched_return follow the same patterns:
# - power is mechanically identical to grow_pattern (LLM-driven, requires Z)
# - compose_seed is also LLM-driven, requires A, also computes the formation
#   trail hash and B'' fingerprint per R11
# - confirm_enriched_return is receptive (like receive_spark), requires B''

The cycle-order enforcement is the line if cycle.spark is None: raise ToolError(...). The agent could try to call any tool in any order. The runtime catches the violation and returns the error to the agent. The agent then re-plans — usually by calling the upstream tool first.

The _held_phi private attribute is the only state the runtime needs beyond what the canonical Cycle from S3 already holds. It is intentionally named with an underscore and never serialized — it exists between hold_phi and find_resonance and disappears once consumed. The Cycle's public schema is unchanged.


The system prompt — Constitutional Block per §3.6

# fivqln/agent_sdk/system_prompt.py
"""
System prompt builder. Per C1 §3.6, every emitted surface carries the
Constitutional Block. The agent's prompt context IS such a surface — it
is what the agent reads to know what 5QLN is.
"""

from fivqln.constitutional_block import CONSTITUTIONAL_BLOCK


SYSTEM_PROMPT_TEMPLATE = """\
You are walking a 5QLN cycle: S → G → Q → P → V.

The cycle's grammar is the Constitutional Block from C1 §3.1:

{constitutional_block}

Your role is to compose the cycle by calling phase tools in order. Each
tool operates on the cycle's accumulated state. The adaptive context
chain (§2.6, §3.3) is enforced at the tool layer: G requires X, Q
requires X+α+Y, P requires X+α+Y+Z, V requires the full trace. Calling
a tool out of order returns an error; you should then call the missing
upstream tool.

CRITICAL — the asymmetry from §1.1 (H = ∞0 | A = K):

  - You CANNOT generate ?. The receive_spark tool prompts the human
    and returns whatever arrived from ∞0.
  - You CANNOT generate φ. The hold_phi tool prompts the human for
    direct perception of Y.
  - You CANNOT certify ⋂ landing. You name what landed; only the
    human can attest the landing was real (the validator surfaces
    this as ATTESTATION_REQUIRED).
  - You CANNOT compose ∞0' unilaterally. The confirm_enriched_return
    tool routes your proposed ∞0' to the human for confirmation or
    revision.

These are not optional disciplines. They are structural — the receptive
tools route to the human regardless of what you say. Trying to bypass
them fills the center (D1 Rule 9): with produced sparks (L2 — Generating)
or with false access to ∞0 (L3 — Claiming). The validator will catch both.

Walk the cycle in order:

  1. receive_spark           → X arrives from ∞0
  2. grow_pattern            → α and {{α'}} found within X (you do this)
  3. hold_phi                → φ held by the human
  4. find_resonance          → you supply Ω; you name what landed at ⋂
  5. power                   → you map δE/δV; ∇ revealed
  6. compose_seed            → you read the trail; compose B''; name B
  7. confirm_enriched_return → you propose ∞0'; the human confirms

After each tool call, the validator from C1 §3.5 runs. DEFINITE
violations halt the loop. HEURISTIC and ATTESTATION_REQUIRED findings
are recorded for the final report. Your job is to walk the cycle
faithfully; the validator's job is to keep us honest about what was
structurally clean and what still needs human attestation.
"""


def build_system_prompt() -> str:
    return SYSTEM_PROMPT_TEMPLATE.format(
        constitutional_block=CONSTITUTIONAL_BLOCK,
    )

The Constitutional Block is exactly §3.1, imported from fivqln.constitutional_block, which itself imports from S2's canonical doctest module — the chain of truth is unbroken from the documentation surface to the agent's prompt context. The agent's instructions name §-references throughout so it can quote them when explaining its actions.


The runtime loop

# fivqln/agent_sdk/runtime.py
"""
The agent loop. Standard Anthropic tool-use pattern, with two additions:
  1. The validator from S4 runs after every successful tool call.
  2. Receptive tools route to the human via an injected ask_human callable.
"""

from typing import Callable

import anthropic

from fivqln.types import Cycle
from fivqln.validator import validate, ValidationReport, Severity
from fivqln.agent_sdk.tools import build_tool_definitions
from fivqln.agent_sdk.system_prompt import build_system_prompt
from fivqln.agent_sdk.handlers import (
    receive_spark, grow_pattern, hold_phi, find_resonance,
    power, compose_seed, confirm_enriched_return, ToolError,
    AskHuman,
)


HANDLERS: dict[str, Callable] = {
    "receive_spark": receive_spark,
    "grow_pattern": grow_pattern,
    "hold_phi": hold_phi,
    "find_resonance": find_resonance,
    "power": power,
    "compose_seed": compose_seed,
    "confirm_enriched_return": confirm_enriched_return,
}


def run_cycle(
    *,
    initial_message: str,
    ask_human: AskHuman,
    client: anthropic.Anthropic = None,
    model: str = "claude-opus-4-7",
    max_iterations: int = 50,
) -> tuple[Cycle, ValidationReport]:
    """Run a 5QLN cycle as an autonomous agent.

    The agent decides which tool to call next; the runtime enforces the
    cycle order via tool-function checks and runs the validator after
    every tool call.

    Returns the final Cycle and its full validation report."""
    client = client or anthropic.Anthropic()
    cycle = Cycle()
    messages = [{"role": "user", "content": initial_message}]
    system = build_system_prompt()
    tools = build_tool_definitions()

    for _ in range(max_iterations):
        response = client.messages.create(
            model=model,
            max_tokens=4096,
            system=system,
            tools=tools,
            messages=messages,
        )

        if response.stop_reason != "tool_use":
            # Agent stopped on its own — final assistant message.
            break

        messages.append({"role": "assistant", "content": response.content})

        # Execute every tool_use block in this turn
        tool_results = []
        for block in response.content:
            if block.type != "tool_use":
                continue

            handler = HANDLERS.get(block.name)
            if handler is None:
                tool_results.append(_error_result(
                    block.id, f"Unknown tool: {block.name}",
                ))
                continue

            try:
                cycle, message = handler(block.input, cycle, ask_human)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": message,
                })
            except ToolError as e:
                tool_results.append(_error_result(block.id, str(e)))
                continue

            # Validator runs after every successful tool call.
            report = validate(cycle)
            definite = [
                v for v in report.violations
                if v.severity == Severity.DEFINITE
            ]
            if definite:
                raise RuntimeError(
                    f"C1 validation failed after {block.name}: "
                    f"{[v.message for v in definite]}"
                )

        messages.append({"role": "user", "content": tool_results})

    return cycle, validate(cycle)


def _error_result(tool_use_id: str, message: str) -> dict:
    return {
        "type": "tool_result",
        "tool_use_id": tool_use_id,
        "content": message,
        "is_error": True,
    }

Three properties of this loop are worth naming. The validator runs after every successful tool call — the cycle cannot accumulate structural violations. Tool errors (out-of-order calls, missing priors) are returned to the agent as is_error: true results, which the agent learns from and self-corrects. Receptive tools route through ask_human, which the application supplies — the loop has no built-in opinion about how that human is reached.


A real run

import anthropic

from fivqln.agent_sdk import run_cycle


def cli_ask_human(payload: dict) -> dict:
    """Synchronous CLI implementation of ask_human."""
    print(f"\n--- Human input required ({payload['phase']}, "
          f"{payload['spec_ref']}) ---")
    print(payload["instruction"])
    if "context" in payload:
        for key, value in payload["context"].items():
            print(f"  {key}: {value}")

    response = {}
    for field in payload["fields_required"]:
        response[field] = input(f"{field}: ").strip()
    return response


cycle, report = run_cycle(
    initial_message=(
        "Begin a 5QLN cycle. Walk it in order, calling each tool as the "
        "spec requires. I will provide receptive inputs when prompted."
    ),
    ask_human=cli_ask_human,
    client=anthropic.Anthropic(),
)

print(f"\nCycle complete. is_clean: {report.is_clean}, "
      f"is_certified: {report.is_certified}")
for v in report.violations:
    print(f"  [{v.severity}] {v.message}")

What happens at runtime: the agent reads the system prompt, sees the cycle protocol, calls receive_spark. The tool prints to the CLI and waits for input(). The human types the question. The agent receives the spark, calls grow_patternwith its α-finding work. The validator runs after each call. The cycle proceeds through the seven tool steps.

If the agent tries to call power before find_resonance, it gets a ToolError back: "P requires Z. Call find_resonance first." The agent re-plans and calls the upstream tool. The cycle order is enforced by errors, not by topology — but the result is the same.


What this surface enables

S6 establishes the tool-use idiom for 5QLN. Three properties matter for what comes next.

The schema layer is portable. Pydantic's model_json_schema() produces standard JSON Schema. The same schemas that drive the Anthropic tool-use API will drive OpenAI's function-calling, MCP tool definitions (S7), and the TypeScript Zod surface (S8 will mirror them via Zod). The single source of truth in S3 propagates outward.

The handlers are framework-agnostic. receive_sparkgrow_patternhold_phifind_resonance, etc. take (input, cycle, ask_human) and return (updated_cycle, message). They have no Anthropic-specific imports. S7's MCP server will wrap them as MCP tool implementations. A future surface integrating with another agent framework can wrap them too. The handlers are the load-bearing logic; the agent loop is one consumer.

The contrast with S5 makes the architectural choice visible. A team choosing between LangGraph (S5) and tool-use (S6) is choosing between graph-enforced order and schema-enforced order. Same constraint, two enforcement mechanisms. Articles S5 and S6 give the working code for both, with the same type contract and validator underneath. The choice is a deployment-context choice, not a faithfulness choice — both are faithful to the same spec.


Closing

The cycle now runs as autonomous tool calls. The agent walks the protocol; the schemas constrain what it can supply; the handlers enforce the cycle order; the validator runs after every call; the receptive tools route to the human. The asymmetry holds because the receptive tools have no agent-supplied symbolic content in their schemas — only the human's response can fill those slots.

Ahead: S7 — MCP: 5QLN as a Connector. The handlers from this article wrapped as MCP tool implementations. Any MCP-aware client — Claude Code, Claude Desktop, Cursor, Cline, custom — gets 5QLN integration without re-implementing it. The cycle becomes a network-accessible service.


5QLN © 2026 Amihai Loven. Open under the 5QLN Open Source License.

Amihai Loven

Amihai Loven

Jeonju. South Korea