Condor Tools Registry — the Descriptor-Driven Clinical Tool Catalog
Audiences: developer, internal, investor
tools-registry/is the clinical surface the model plans over. It lives atddx-api/src/ai/condor/tools-registry/(194 files) and holds the descriptor-driven business tools — patient, appointment, medication, note, billing, plus the card/list read tools that render into the Condor UI. Every tool has a declared identity indescriptors/, is constructed through one factory (create-condor-tool.ts), and is exposed to a run through exactly one immutable snapshot resolved by one singleton service. The runtime that consumes it is documented in Condor Engine; the ratified subsystem contract isddx-api/src/ai/condor/TOOLS.md.
Purpose
An agentic clinical assistant is only as safe as its tool surface. A tool here is not just a function the LLM may call — it is a declared capability carrying its control plane, its policy class, its side-effect class, its retry safety, its human-in-the-loop tier, and its owning team. Those declarations are what let the system answer, mechanically: may the planner select this? does it mutate? may it be retried? must a human approve it? which team is on call?
tools-registry/ is the home of that declaration layer, and the counterpart to
the generic Mastra-native toolkit in mastra/tools/ (filesystem, exec, search,
vision, PDF, session-entity primitives). The split between the two is a ratified,
mechanically-guarded contract — see the two-registry contract.
The catalog seam — one singleton, one snapshot per run
condor-tool-catalog.service.ts is the single application-scoped
(@Injectable() singleton) owner of the derived descriptor catalog. It exposes
read accessors over the descriptor array and exactly one resolution entry point.
| Member | Role |
|---|---|
descriptors | the full, unfiltered descriptor catalog (CONDOR_TOOL_DESCRIPTORS) |
toolIds | every declared tool id, derived from the descriptor array |
descriptor(id) | one descriptor by id, via a prebuilt DESCRIPTOR_BY_ID map |
activeBusinessToolIds() | the plan-eligible business-plane ids for the running profile + env |
isPlanEligible(toolId) | the per-id plan-eligibility predicate |
resolveSnapshot(input) | the ONE per-run candidate resolution (condor-tool-catalog.service.ts:126) |
The architectural rule this service exists to enforce: the candidate set is
resolved once and then passed as data. resolveSnapshot delegates to
CondorToolRagService.resolveCandidateSet, registry-verifies and de-dupes the
result, and returns an immutable ToolSelectionSnapshot
(tool-selection-snapshot.ts:26) carrying toolIds, source, candidateSetId,
ragDescriptions and activationReasons. Prompt assembly, plan validation,
workflow compilation, observability and persistence all read that one value
object — none of them re-resolve. The service is the singleton; the per-run state
is a value object, never a per-request service (that shape is deliberate: a
per-request Nest provider here is a barrel-DI-cycle hazard).
Two details in resolveSnapshot are load-bearing and easy to break:
- Forced tools lead the ordered set. Policy additions (attachment-reader
tools when files are present; the clinical intake-candidate floor on a
multi-entity intake) are prepended ahead of the RAG hits, not appended
(
condor-tool-catalog.service.ts:149-167). The planner's tool-block degradation ladder sheds candidates from the tail (lowest RAG rank) first, so a tail-appended floor would be the first thing shed — re-introducing exactly the dropped-vitals defect the floor exists to prevent. Leading the set also gives those tools description and param-synopsis priority. - RAG provenance survives key normalization. A candidate id is remapped to
its canonical registry key (
visitRecordingCard→visit.recording.card, and underscore/kebab variants), and theactivationReasonsentry is remapped with it. Without that remap the tool stays surfaced but silently loses itsrag-retrievedreason, disabling downstream intent/retrieval guards.
Boot is a hard gate, not a warning: the constructor resolves the active catalog
profile once and calls assertNoDemoOnlyInProduction — under the
clinical-production profile, a non-production descriptor being active is a
boot failure, not a logged anomaly.
The descriptor-driven tool model
descriptors/tool-descriptor.ts defines one ToolDescriptor shape whose fields
are each an independent, queryable axis. Keeping them separate is the point —
collapsing any two of them has, historically, produced a real defect.
| Axis | Values | What it governs |
|---|---|---|
plane | business · planner-control · agent-control · internal | plan eligibility — only business descriptors may be planner workflow steps |
profiles | clinical-production · workspace · demo · test | which environments the tool is active in |
policyClass | read · mutate · … | approval authority — the single input the tool-policy engine consults |
sideEffect | none · idempotent · mutation · external-send · exec | observability + idempotency classification, finer-grained than policyClass |
idempotency | not-needed · durable-required | whether the operation needs a durable idempotency key |
retryClass | none (default) · transient-idempotent | transport retry opt-in |
hitlTier | high · low | the second axis of the mutate approval gate |
owner | condor-clinical · condor-docgen · condor-workspace · condor-demo · condor-core | accountable team / on-call |
metadataRef | factory · rag-metadata · display-registry | provenance of the tool's physical RAG/display metadata |
Three of these deserve emphasis:
hitlTier is compile-time mandatory for mutating tools. HITL_TIERS is
['high','low'] (tool-descriptor.ts:123): high always suspends for
Approve/Reject — even in an autonomous operator chat, reserved for the
highest-stakes clinical writes (prescription, medication, signed document,
billing charge); low is autonomous-allow / supervised-suspend, where the human
who typed the request is the approval, but an ambient or supervised dispatch
still suspends. That requirement is enforced by the type system, not by review:
MutateRequiresHitlTier (tool-descriptor.ts:229) is a conditional type
distributed per-element by defineDescriptors, so a policyClass: 'mutate' row
that omits hitlTier is a tsc error. A plain array-level satisfies cannot
do this — the conditional would collapse to the non-mutate branch for the whole
array — which is why the helper threads each row through its own generic.
retryClass and idempotency are deliberately different axes.
idempotency: 'durable-required' marks a tool that needs a durable key because
it mutates; keying retry off it would retry precisely the dangerous set. Retry
safety is declared separately, and even then the declaration cannot win:
resolveRetryPolicy returns none unconditionally for any mutating tool
(tool-retry-policy.ts). The reasoning is a clinical-safety failure mode — a
mutating call whose write succeeded but whose response timed out is
indistinguishable at the client from one that never landed, so retrying it writes
twice: a duplicate prescription, a duplicate billing entry. 28 tool files declare
mutating: true.
owner is derived, not hand-typed. deriveToolOwner computes the owning
team from each tool's plane + module, so ownership has one auditable derivation
rather than a hundred hand-written strings that drift out of date.
A parallel derived view, CondorToolMetadataV2 via deriveToolMetadataV2, is a
pure projection of those same descriptor fields — it introduces no second source
of truth, and metadataRef is retained as provenance so the eventual physical
relocation of the RAG-metadata blocks stays traceable.
The factory — one construction path, one policy authority
Every business tool is built through createCondorTool
(create-condor-tool.ts:212). The factory is where declaration becomes behavior,
and it composes two distinct layers around the raw tool body:
- Inner layer — inside the
run:callback handed towithToolLogging. This is the single choke point that owns the emit lifecycle (emitToolStarted/emitToolCompleted/emitToolFailed) and where the observer chain composes, so an observer sees exactly ONE invoke and ONE result per logical tool call. - Outer layer — around the function returned by
wrapExecuteWithExecutionContract. This is where retry sits. The placement is the whole reason a retried call emitstool.invoked/tool.completedexactly once; retrying at the inner layer would emit once per attempt and corrupt every downstream consumer, including the health endpoint's failure counters.
The factory also resolves one approval authority. When a descriptor declares
a policyClass, the tool-policy engine is the sole approval decision-maker and
the per-tool requireApproval callback is bypassed
(create-condor-tool.ts:232-248) — a stale callback can no longer force a
suspend on a read tool or silently downgrade a mutation. The factory then stamps
policyClass, policyDomain and hitlTier onto the constructed tool object
(create-condor-tool.ts:498-531), falling back to descriptor coverage when the
tool declares mutating: true without an explicit class. That stamping closes a
real security hole: a tool whose policyClass never reached the tool object read
as 'read' at the suspend gate, so 28 mutating tools were ungated.
Two guarantees the factory preserves, and which must never be collapsed into each other:
| Failure shape | Behavior |
|---|---|
Soft failure — the tool returns {ok:false} | reported as ok:false, returned as-is, never converted into a throw. Model-repairable. |
| Hard failure — the tool throws | reported to onError and re-thrown unchanged, never swallowed. Hard-fails the run by design. |
Observer failures are a third, separate class: every observer hook is individually try/caught, a throwing observer is swallowed, counted, and warned once — instrumentation may never reach the tool path.
Grouping and gating — what the planner is even allowed to see
Four independent filters narrow the declared catalog down to what a given run may plan over.
- Plane filter. Only
businessdescriptors are plan-eligible. The planner-control tools (tool search, referent resolution) and the agent-control meta-tools are structurally excluded from business candidate disclosure — indexing them as business candidates was a named audit finding. - Profile filter.
resolveActiveCatalogProfileresolves the process's profile once at boot;activeDescriptors/activeBusinessToolIds/isPlanEligibleToolId(descriptors/catalog-profile.ts) apply it per id. A demo-only tool simply does not exist underclinical-production— and if one is active there, boot fails. - RAG candidate resolution. Of the active set, only what the tool-RAG
retrieves for this prompt reaches the planner. This is a genuine gate, not a
ranking nicety: a tool absent from the candidate set can never be selected, no
matter how well the plan is written. Hardening a tool's
ragKeywords/ragDescriptionis therefore the fix when the right tool is never picked. - Forced floors and policy additions. Where retrieval is not trustworthy enough (multi-entity clinical intake; a turn carrying attachments), an explicit floor forces ids into scope and, per the ordering rule above, places them where the degradation ladder cannot shed them.
How tools surface to the LLM
Tools are disclosed progressively, under a hard byte budget — the planner never
sees the full business-tool schema set at once. (The live business-tool count is
authoritative in ddx-api/scripts/audit/condor-tools-registry-direction-audit.sh,
not a literal here — it drifts on every tool add/remove; as of this writing the
guard baseline reads 76 while the tree holds 78, a known repo inconsistency
flagged for the condor-tools-uplift owner, NOT a docs error.)
- Candidate manifests.
meta-tools.tsrenders one compact line per candidate viabuildCandidateManifest, bounded byMANIFEST_MAX_BYTES = 300andMANIFEST_MIN_BYTES = 120(meta-tools.ts:117), withtruncateBytesas the single truncation routine the whole subsystem shares. The manifest's reportedbytesincludes any appended fragment, so budget accounting stays honest instead of under-reporting. - Control-plane meta-tools.
search_toolsandload_tool(META_TOOL_IDS,meta-tools.ts:43) let an agent-loop widen its own surface on demand.load_toolis hash-gated: it fails on a manifest that was never surfaced, so an agent cannot conjure a tool it was not shown.findMetaToolPlanViolationsrejects a plan that tries to route through them — they are agent-control, not workflow steps. - Injectable fragments. Two optional declarative slots inject at two
different phases:
instructionFragment(behavioral — how to use the tool) is appended to the candidate manifest at planner phase and costs bytes for candidates only;stylingFragment(presentational — how the result should be presented) is appended at formatter phase and costs the planner nothing. Both are capped at 120 bytes, and a styling fragment carrying a raw colour literal throws rather than being quietly truncated into something that merely looks fine. - Card rendering + localization. The read/card tools emit UI-block card specs
rather than prose. All rendered chrome (titles, kv keys, empty states, FHIR
status labels, severity enums) routes through one localization seam split across
card-i18n-base.ts,card-i18n-status.ts,card-i18n-extras.ts,card-i18n-hitl.tsandcard-i18n.ts. Clinical data — drug names, ICD codes, patient names, dates — is never localized, and LLM-facing text (ragDescription,ragKeywords, output instructions) stays English.
The two-registry contract
ddx-api/src/ai/condor/TOOLS.md is the ratified contract (2026-07-25) and the
authority for anything in this tree. Its core rule is a one-directional
dependency:
tools-registry/MAY import frommastra/tools/.mastra/tools/MUST NOT import fromtools-registry/.
mastra/tools/ sits below tools-registry/ and knows nothing about
descriptors, planes, profiles or the business catalog. The _-prefixed modules
under mastra/tools/ (_with-tool-logging.ts, _rag-metadata.ts) are shared
infrastructure, not tools — and they are the sanctioned crossing point.
The rule is measured, not aspirational. condor-tools-registry-direction-audit.sh
treats the forward count as a baseline (drift reported, new forward imports
legal) and the reverse count as a guard (any hit fails). Its check (d) is a
clinical-safety gate of its own: tool-retry-policy.ts keeps a local mirror of
the mutating predicate (importing the canonical one would cycle, since the factory
imports the retry module), and the audit asserts the predicate expression appears
in both files — a one-sided edit would silently enable retry for a tool that
actually mutates.
Where a new tool belongs follows from what it is, not who calls it: a clinical
or HMS domain operation a clinician would recognise as a task goes in
tools-registry/ with a descriptor; a generic agent capability with no HMS
semantics goes in mastra/tools/ as a plain Mastra tool; and a new cross-folder
seam belongs in mastra/tools/ with a _ prefix — authoring it under
tools-registry/ and importing it upward is exactly the inversion the guard
forbids.
Adding, removing, moving or renaming a file under tools-registry/tools/ changes
the audited tool count and fails the audit by design. That is intentional
friction: the clinical tool surface should not grow by accident.
Audience notes
Developer. Start at TOOLS.md — it is the contract, and §5–§8 cover the
observer/retry seam, post-tool processors, injectable fragments, and the operator
surface plus failure taxonomy. Then read descriptors/tool-descriptor.ts for the
axes and create-condor-tool.ts for how a declaration becomes behavior. Two gate
blind spots to know before you trust a green run: the build tsconfig excludes spec
files (report both a default npx tsc --noEmit and the -p tsconfig.build.json
run), and the test runner exits 0 on zero path matches (pass the
fail-on-no-tests flag). Run
ddx-api/scripts/audit/condor-tools-registry-direction-audit.sh after any edit in
either folder. For anything that renders — a card, a new UI-block-emitting tool —
type-check green is not sufficient evidence; the render path has to be verified
in the browser, because arg-binding bugs and render-gate omissions are both
invisible to tsc.
Internal. The registry is where clinical safety is declared rather than remembered. Three properties carry most of that weight: mutating tools can never be retried (a timed-out write that already landed must not be repeated); a mutating descriptor cannot exist without a declared HITL tier, because the compiler refuses it; and a non-production tool active under the clinical profile fails boot rather than logging a warning. Ownership is derived per tool, so on-call routing for a misbehaving capability is a lookup, not an investigation. The recurring lesson from this subsystem's defect history is that the dangerous failures are the silent ones — a policy class that never reached the tool object, a floor tool shed by the degradation ladder, an activation reason lost to key normalization. Each of those now has a mechanical guard, and that is the pattern to extend rather than to work around.
Investor. The clinical tool catalog is the productized surface of the platform: each tool is a discrete clinical capability — book an appointment, record a note, prescribe, list allergies, produce a document — that the assistant can select and execute against the live hospital system. Two things make that commercially defensible. First, the surface is declared and audited rather than ad hoc: every capability states whether it changes patient data, whether a human must approve it, and which team owns it, and violations of those declarations fail the build or fail boot rather than reaching a clinician. Second, the surface is extensible without re-architecture — adding a capability means adding a declaration and a tool, not rewiring the engine, and the deliberate friction on that count is what keeps a clinical catalog from growing carelessly.
Related Topics
- Condor Engine — the runtime that resolves a snapshot and compiles a plan over these tools.
- Condor Sandbox & Builder — the workspace
sandbox the
condor-workspace-owned tools operate in, plus the builder's operator console. - TUCAN RAG Tools — the legacy engine's tool-retrieval approach, the ancestor of Condor's candidate-set gate.
- AI Medical Tools — the clinical tool surface from the product/capability side.