Part 2 of 18 · 8 min read
From language to domain actions
Typed tool adapters, intent uncertainty, service contracts and bounded planning.
Arunank Sharan · Kuzushi Labs ·
In this article
Consider a staff member asking: “Find the patient and arrange a follow-up next Tuesday.” Before anything can be booked, we need to resolve the person, interpret the date, identify the appropriate practitioner and obtain a usable slot.
We split that work across an intent classifier, domain agents and a tool registry. I find that separation useful because each layer has a narrower question to answer.
Route the request before planning the operation
The backend begins by loading or creating a session, recording the user's message and assembling context. The classifier returns structured information: intent, category, confidence, extracted entities and any clarification requirement. It can also request an ordered chain of agents.
The orchestration code asks for clarification when the returned confidence is below 0.7, or when clarification is explicitly requested. This is a configured routing threshold. It is not a demonstrated 70% probability that the action will be correct.
After routing, a domain agent performs more detailed parameter extraction. Patient work and appointment work use different plans, even when the user expresses them in one sentence. An ordinary greeting can take a simpler response path without invoking a business operation.
Keep planning specific to the domain
The patient agent prepares search, registration, update and duplicate-management operations. The appointment agent prepares availability checks, booking, changes and cancellation. Journey, ticket and communication agents extend the same pattern to the rest of the relationship workflow.
The backend registry contains 18 agent classes, including later extensions for billing, intake, reports and other domains. That describes the catalogue in the code. It does not prove that all 18 have a completed path from user request to verified business outcome.
An agent's plan contains named tools, parameters and a confirmation flag. The shared executor runs the steps sequentially and stops when a step raises an error. Results retain the tool name, output, success state and execution time, so response formatting has something more concrete than the model's original intention.
The registry is an adapter boundary
The registry connects a tool name to a service and method. It also declares risk, confirmation requirements and permission scopes. This makes the intended interface inspectable.
The important engineering detail is the conversion between that interface and the domain service. A model may extract flat fields, while the service expects a validated request object. An appointment tool may use one method name while the implemented scheduling service exposes another. The checked-out code contains examples of both kinds of mismatch.
I would therefore avoid counting registry entries as delivered capabilities. Each adapter needs a contract test that demonstrates the accepted arguments, the actual service call and the returned shape. The same check should reject unresolved identifiers before a write is attempted.
Declared permission scopes also need enforcement at execution time. Putting a scope name in a tool description documents intent; the server still needs to establish who is acting and whether that operation is allowed.
Turn the registry into an executable contract
The inspected executor resolves a service and method from a registry, then calls it with the plan's parameters. This shortened excerpt preserves that mechanism:
tool_info = TOOL_REGISTRY.get(tool_name)
if not tool_info:
raise ValueError(f"Unknown tool: {tool_name}")
service = await self._get_service(tool_info["service"])
method = getattr(service, tool_info["method"])
result = await method(**parameters)
That is a useful extension point, but it moves correctness into the adapter. In the inspected appointment path, several registered method names do not match the available service methods. Patient creation has a related mismatch: a service expects a typed patient object while an agent may produce flat fields. A syntactically valid tool call can therefore still be unexecutable.
I would replace implicit parameter forwarding with an explicit adapter per write operation. The following is a proposed interface, not an additional method already present in the repository:
from dataclasses import dataclass
from typing import Literal, Protocol
@dataclass(frozen=True)
class PrepareBooking:
patient_id: str
slot_id: str
@dataclass(frozen=True)
class ToolResult:
status: Literal["succeeded", "needs_input", "conflict", "failed"]
data: dict
error_code: str | None = None
class BookingPort(Protocol):
async def prepare(self, actor, request: PrepareBooking) -> ToolResult: ...
The adapter validates fields, checks permission, calls the real service and converts its result into one stable shape. The model-facing name can remain appointment.prepare while a backend refactor changes the underlying method. I would also reject extra input fields, bound string lengths and validate identifiers before entering the adapter.
Separate uncertainty from missing information
A single confidence score collapses too many questions. We may be certain the request is about booking while still lacking a patient, time zone or location. I would represent those separately:
{
"intent": "appointment.prepare",
"interpretation_confidence": 0.91,
"entities": {"patient_id": "p-001"},
"missing": ["location_id", "preferred_date"],
"ambiguities": [],
"next_action": "ask_for_required_fields"
}
Here 0.91 is an illustrative model output, not measured accuracy. The deterministic rule is that required fields are missing. There is no reason to invoke the booking service yet.
For ambiguous identities, the response should contain a candidate selection task. For ambiguous language, it should contain a clarification question. For denied access, it should contain neither patient candidates nor a model-generated workaround. These branches deserve separate test cases and separate UI states.
Choose the smallest planner that fits the task
An administrative workflow usually has a known order. Find patient → select slot → prepare booking → confirm → record receipt does not need an open-ended planner to rediscover its dependencies every turn. I would use deterministic transitions for those dependencies and let the language model interpret the user's wording inside the current step.
ReAct provides a useful research lens for interleaving reasoning and actions. My application of that idea is narrower: let tool observations revise the next permitted step, while keeping write authority in the application. The paper does not establish the safety or reliability of this PRM implementation.
For a read-only investigation such as “Why is this follow-up still pending?”, a bounded tool loop is more useful. It may inspect a journey, its communication record and an outstanding ticket. I would cap it at six tool calls and one clarification in an initial reference profile, then return the evidence gathered if the budget is exhausted. Those limits are design defaults to tune with traces, not historical production settings.
Test the adapter without the model
My first adapter test would assert that the exact typed service request reaches the service. My second would make the service return a business conflict without raising an exception. The wrapper must preserve conflict; it must not report success simply because the Python call returned normally.
A third test would request an unknown operation. The registry should reject it before any service lookup. A fourth would use a valid operation under a principal whose permission has been revoked. That must fail even if a previous conversation cached the tool description.
Once these tests pass, model evaluation becomes easier to interpret: a wrong tool selection is an interpretation failure; a correct selection with an incompatible adapter is an engineering failure. Combining them into one “agent success” number hides the fix.
Multiple agents need a result contract
The orchestrator can run agents in sequence, carry returned data into the next agent's context and stop on failure or a pending confirmation. That is the beginning of a useful coordinated workflow.
It is also where loose data shapes become expensive. A patient search result must produce an unambiguous selected patient identifier before the appointment agent uses it. A list of matches is not the same thing as a resolved person. Generic dictionary merging cannot settle that choice.
The current chain stops when an agent asks for confirmation. Confirming that saved plan does not, by itself, resume the remaining chain. I treat durable continuation as a separate implementation step, with an explicit record of which operation completed and what remains.
The design lesson is to make the output of one stage a trustworthy input to the next. Better language understanding helps, but reliable action depends on those contracts being precise.
The implementation kit includes the articles, diagrams and an offline reference lab. Code identified as a reference design is a new teaching implementation; it is not a claim that the original project already shipped that behavior.