Cognitive Router¶
The cognitive router compiles natural-language tasks onto proven
orchestration shapes. The LLM fills one typed GoalFrame; everything
after that — protocol selection, policy gating, compilation — is
rule-based.
Dispatch¶
Router ¶
Router(*, extractor: Agent, compiler: CognitiveCompiler, on_frame: Callable[[GoalFrame], None] | None = None)
One-shot dispatcher: text in, :class:RunnableResult out.
Parameters¶
extractor:
:class:~locus.Agent configured with
output_schema=GoalFrame. Its parsed result drives selection.
compiler:
:class:CognitiveCompiler set up with protocols, capabilities,
policy, and model.
on_frame:
Optional callback fired right after a frame is extracted. Useful
for telemetry / workbench display.
Source code in src/locus/router/runtime.py
extract
async
¶
Run the extractor and return the parsed :class:GoalFrame.
Emits router.frame.extracted on success or
router.frame.failed on schema rejection, scoped to
run_id when supplied. Raises :class:FrameExtractionError
on failure — the compiler can't recover from that.
Source code in src/locus/router/runtime.py
dispatch
async
¶
Extract a frame, compile a runnable, execute it.
run_id scopes every emitted :class:StreamEvent to one
cognitive dispatch. Defaults to a fresh uuid4 if omitted —
callers that want their own correlation id (a request id, a
Slack thread, etc.) should pass one in. The id is also
attached to the :class:RunnableResult raw payload so
downstream callers can correlate without re-parsing the bus.
Source code in src/locus/router/runtime.py
FrameExtractionError ¶
Bases: RuntimeError
Raised when the extractor agent failed to produce a valid GoalFrame.
RunnableResult ¶
Bases: BaseModel
Normalized result envelope for any compiled router execution.
Goal Frame¶
The typed contract the LLM fills. Picker / compiler read this structure deterministically; the LLM never names a protocol directly.
GoalFrame ¶
Bases: BaseModel
The single typed object the LLM produces.
Everything downstream of GoalFrame — protocol selection,
capability binding, policy verdict, builder dispatch — is
deterministic. The LLM never authors graph topology, only this
schema.
TaskType ¶
Bases: StrEnum
The kind of cognitive work the user is asking for.
Every protocol declares which TaskType values it can handle, so
the registry can filter candidates by frame.primary_goal alone.
Risk ¶
Bases: _OrderedStrEnum
Complexity ¶
Bases: _OrderedStrEnum
Protocol registry¶
A protocol is a named, parameterized orchestration shape (e.g.
"specialist_fanout", "deep_research", "human_review"). Built-ins are
in builtin_protocols(); users register custom shapes via
ProtocolRegistry.register().
Protocol ¶
Bases: BaseModel
Declarative description of an orchestration shape.
ProtocolRegistry ¶
Deterministic mapping from :class:GoalFrame to :class:Protocol.
Source code in src/locus/router/protocol.py
filter_candidates ¶
filter_candidates(frame: GoalFrame, *, available_capabilities: set[str] | None = None) -> list[Protocol]
Return every protocol that passes the three gates.
The shared filter for both select() (rule-based ranker) and
any opt-in selector that wants to choose among already-qualified
candidates. Gates:
frame.primary_goal in p.handlesp.risk_max >= frame.riskset(p.requires_capabilities).issubset(available_capabilities)
Returns the survivors in registration order. Empty list is a
valid result — callers raise :class:NoMatchingProtocolError
when appropriate (this method is filter-only by design).
Source code in src/locus/router/protocol.py
select ¶
Pick the best protocol for frame.
Filters by handles ∋ primary_goal ∧ risk_max ≥ frame.risk
∧ required_capabilities ⊆ available_capabilities, then ranks
candidates by complexity-fit + cost.
Source code in src/locus/router/protocol.py
builtin_protocols ¶
The full eight-protocol catalogue. Compose your own
:class:ProtocolRegistry from these (or a subset).
Source code in src/locus/router/protocol.py
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 | |
BuilderContext ¶
Bases: BaseModel
Bundle of inputs passed to every protocol builder.
Builders need the model, the tool-resolution view, and the skill catalogue; passing a typed context keeps the builder signature stable as we add follow-up protocols.
NoMatchingProtocolError ¶
Bases: LookupError
Raised by :meth:ProtocolRegistry.select when no protocol fits the frame.
Picker¶
The picker decides which protocol best matches a GoalFrame.
LLMProtocolPicker is the default; PickedProtocol is the decision
record returned to the compiler.
LLMProtocolPicker ¶
Picks a protocol from a pre-filtered candidate set using an LLM.
Construct with a locus model (any provider) and an optional
system_prompt override. The picker reuses the same Agent +
output_schema machinery the router's extractor uses — one
structured-output call per pick.
Parameters¶
model:
Any locus model instance (an :class:~locus.Agent-compatible
model object or model string).
system_prompt:
Optional override for the picker's system prompt. The default
instructs the model on the rule hierarchy (canonical match,
cost-fit, conservative fallback).
Example::
picker = LLMProtocolPicker(model=my_model)
compiler = CognitiveCompiler(
protocols=registry,
capabilities=capabilities,
policy=PolicyGate(),
model=my_model,
protocol_picker=picker,
)
Source code in src/locus/router/picker.py
pick
async
¶
Pick one protocol from candidates for frame.
Returns a (protocol, rationale) tuple. The compiler threads
the rationale into the router.protocol.selected event so
the LLM's reasoning is observable.
Raises :class:PickerError when the model output is unparseable
or names an id not in candidates — the compiler catches this
and falls back to the rule-based ranker.
The caller is responsible for the candidate set being non-empty and already filtered; this method does not re-validate handles / risk / capabilities.
Source code in src/locus/router/picker.py
PickedProtocol ¶
Bases: BaseModel
Typed output schema for :class:LLMProtocolPicker.
Used as output_schema= on the picker agent so the model is
forced to return a structured pick rather than free-form text.
PickerError ¶
Bases: RuntimeError
Raised by :class:LLMProtocolPicker when the model output is
unusable (parse failure or returned id not in the candidate set).
The compiler catches this and falls back to the rule-based ranker
after emitting a router.protocol.picker_fallback event. Users
do not normally see this exception.
Policy gate¶
Pre-execution policy checks (cost, risk, capability requirements,
optional human approval). PolicyVerdict either allows compilation
to proceed or rejects with a structured reason.
PolicyGate ¶
Pre-flight risk check for a (frame, protocol) pair.
Two thresholds:
max_risk— anything strictly above is denied.require_approval_above— anything strictly above (and withinmax_risk) is allowed but flagged for an approval interrupt.
Defaults: max_risk=Risk.HIGH (nothing denied by default) and
require_approval_above=Risk.MEDIUM (HIGH-risk frames need
explicit approval).
Source code in src/locus/router/policy.py
PolicyVerdict ¶
Bases: BaseModel
One of three outcomes from :meth:PolicyGate.check.
PolicyDeniedError ¶
Bases: RuntimeError
Raised when the policy gate refuses to compile a runnable.
Compiler¶
Turns a PickedProtocol + GoalFrame into an executable Runnable.
Approval callbacks (HITL gates) plug in here.
CognitiveCompiler ¶
CognitiveCompiler(*, protocols: ProtocolRegistry, capabilities: CapabilityIndex, policy: PolicyGate, model: Any, skills: SkillIndex | None = None, a2a_endpoint: str | None = None, on_approval: ApprovalCallback | None = None, protocol_picker: LLMProtocolPicker | None = None)
Glue between protocols, capabilities, policy, and the model.
Parameters¶
protocols:
:class:ProtocolRegistry populated with built-in or custom
protocols.
capabilities:
:class:CapabilityIndex over the surrounding ToolRegistry.
The index resolves capability ids to real tools at compile time.
policy:
:class:PolicyGate that runs between selection and build.
model:
A locus model instance (or model string) injected into every
builder. Builders pass it to :class:~locus.Agent / specialist
constructors.
skills:
Optional :class:SkillIndex. When provided, every emitted
:class:Agent is configured with a
:class:~locus.skills.SkillsPlugin containing the skills tagged
for frame.domain (plus any globally-tagged skills). The
agent loop's L1 / L2 / L3 progressive disclosure surfaces them
at runtime.
on_approval:
Optional async callback fired when the verdict requires
approval. Defaults to denying — wire your workbench / CLI
approval flow here.
protocol_picker:
Optional :class:LLMProtocolPicker. When present, the compiler
delegates the last-mile protocol pick to the model whenever
the filter leaves more than one candidate. When absent (the
default), selection is the deterministic
:func:_rank_key-based ranker. The picker is the only part
of the routing pipeline that uses the model; everything else
— filtering, policy gating, capability binding, builder dispatch
— remains rule-based.
Source code in src/locus/router/compiler.py
compile
async
¶
Pick a protocol, run the gate, build the runnable.
run_id (when provided) scopes every emitted
:class:StreamEvent so the workbench's SSE consumer can
correlate selection / verdict / compile events with one
cognitive dispatch.
Source code in src/locus/router/compiler.py
ApprovalCallback
module-attribute
¶
Async callback used when a verdict requires approval.
Returning True lets the compiled runnable execute; returning
False raises :class:PolicyDeniedError. Defaults deny.
Runnables¶
The compiled execution shape. Runnable is the base Protocol — every
concrete runnable below produces a RunnableResult when invoked.
Runnable ¶
Bases: Protocol
Anything the router compiler can return.
The contract is intentionally minimal: a single async execute
that yields a normalized :class:RunnableResult. Builders wrap
each emitted primitive in the matching adapter below — call sites
never branch on the concrete primitive type.
AgentRunnable ¶
Bases: BaseModel
Adapter from :class:locus.Agent to :class:Runnable.
Agent.invoke is sync but already handles being called from a
running event loop (it spawns a worker thread internally). We wrap
it in :func:asyncio.to_thread so the surrounding coroutine yields
properly while the agent runs.
PipelineRunnable ¶
Bases: BaseModel
Adapter from any composition primitive to :class:Runnable.
Works for :class:SequentialPipeline, :class:ParallelPipeline,
and :class:LoopAgent — they all share the
async run(task) -> PipelineResult shape.
OrchestratorRunnable ¶
Bases: BaseModel
Adapter from :class:locus.Orchestrator to :class:Runnable.
DebateRunnable ¶
Bases: BaseModel
Adapter for the debate protocol.
Runs N debater agents (each a :class:Agent) in parallel against
the same task, then feeds the joined transcript to a single judge
:class:Agent
that picks the strongest argument. The judge's verdict is the
surface text. There is no native locus "debate" primitive — this
builder composes :class:ParallelPipeline + an extra
:class:Agent step, which is small enough to live in this adapter.
A2ARunnable ¶
Bases: BaseModel
Adapter for the a2a_delegate protocol.
Wraps an :class:A2AClient so a remote, separately-deployed agent
can serve a request. The remote agent is responsible for any tool
calls / orchestration on its end; the router simply forwards the
user prompt and packages the response.
Capabilities¶
The skill / tool index the compiler binds protocol slots to. Custom
capabilities register themselves via CapabilityIndex.annotate(...)
or by exposing a SkillIndex.
Capability ¶
Bases: BaseModel
A tool (or human action) tagged with router-relevant metadata.
CapabilityIndex ¶
View over a :class:ToolRegistry that adds router metadata.
Not a replacement for ToolRegistry — the underlying Tool
instances live there. CapabilityIndex only holds the metadata
overlay (domain, risk) and resolves capabilities back to tools on
demand.
Source code in src/locus/router/capability.py
annotate ¶
annotate(cap_id: str, *, tool_name: str, description: str, domain: str, risk: Risk = Risk.LOW) -> Capability
Register router metadata for an existing tool (or a human step).
tool_name must already exist in the underlying
:class:ToolRegistry, except for the $human sentinel which is
treated as a non-tool capability.
Source code in src/locus/router/capability.py
lookup ¶
Resolve a list of capability ids; raises if any are missing.
Source code in src/locus/router/capability.py
for_domain ¶
all ¶
resolve_tool ¶
Return the underlying :class:Tool. Raises for human capabilities.
Source code in src/locus/router/capability.py
HUMAN_SENTINEL
module-attribute
¶
Sentinel value for tool_name when the capability is human-only.
SkillIndex ¶
A domain-tagged view over a list of :class:Skill instances.
Source code in src/locus/router/skill_index.py
register ¶
Add a skill to the index with an optional domain tag.
domain="" (the default) means "applies to every domain";
:meth:for_domain always returns these alongside domain-specific
matches.
Source code in src/locus/router/skill_index.py
register_many ¶
get ¶
Return a skill by name. Raises :class:KeyError if missing.
Source code in src/locus/router/skill_index.py
for_domain ¶
Skills tagged with domain, plus every globally-tagged skill.
The router calls this with :attr:GoalFrame.domain to pick the
catalogue handed to each compiled :class:Agent. Globally-tagged
skills (registered with domain="") are always included so a
common catalogue (e.g. "communication tone", "safety checks") is
available everywhere.