Skip to content

The action organ

Path: hot · Status: named and sketched, no spec and no code — but the channel into it ships and is live · Code: none; markers.py is the channel

Alto's conversational model has no tools, and under the target design it never will. When Alto wants something done in the world it says so, in its own words, inside a commitment pair (¤…¤) — and a separate organ, the only thing in the system that knows Home Assistant exists, turns that stated intent into a validated tool call. That organ is the missing half of device control, and the interesting part is how far the wiring goes without it: the LoRA emits action spans, the runtime parses and captures them, and they go nowhere by design.

Where it sits

Between Integration and the guard (below). Its input is an action intent in natural language — not a structured call — and its output is call_ha_service / get_device_state arguments that validate_tool_call accepts or rejects. It owns the tool schemas; nothing above it does. Everything below it is the guard, described at the end of this page, and none of that changes when this organ lands.

Where the action path attaches is not settled by the corpus. alto-conscious-locus.md draws intent rendered down into [machinery: bundle, steering, renderer, action] — action inside the block below Integration. The overview diagram draws the opposite: alto-organs-overview.mmd has IN -.->|⟦act⟧ intent| ACT, straight from Integration to the Action organ. Neither states the rule, so the corpus is silent on this. What it does fix is the part pillar 8 depends on: the action path descends below the conscious line, and Integration never holds a tool. The shipped runtime is a third answer and a temporary one — the LoRA marks its own output, a channel split where a transducer would go (integration.md).

The intended design

The design statement is Integration-MVP (2026-06-30), written out in SPEC-integration-mvp.md (lines 5 and 123); the code keeps only its consequence, as the comment above the reply paths (alto.py:231-235). A marking LoRA splits Integration's output into three channels: internal thought, filtered before anything routes; ⟦say⟧-marked words, verbatim to voice; and ⟦act⟧-marked actions, routed to a separate action-LLM "whose job is exclusively to turn natural-language action commitments into validated tool calls." The ownership line is the sharp one: that organ is the only thing that ever knows about HA, the project store or web search — Integration never does. It inverts the pre-MVP model, where the conversational LLM invoked tools itself.

Two reasons keep tools off the conversational model, and only one of them expires. Reason one is Integration-MVP's accepted regression. Reason two is SPEC-writer-build_14 §1.7, the section the code comment cites (alto.py:231-235): the route from a surfaced memory to call_ha_service is otherwise complete, so registering one tool there lets a fabricated memory become a device call. When this organ lands and reason one expires, reason two does not — the organ gets the tools, the conversational path never does.

The internal shape is drafted in SPEC-two-stage-executor.md over two-stage-action-executor.md: a full-context orchestrator (no tools, resolves referents like "turn it back on", emits a directive) plus a fresh-context executor (tools only, no history), because tool-call reliability decays as history grows. Carry its framing — "the executor proposes, the registry disposes" — and read its status honestly: a 2026-06-12 sweep spec, adversarially reviewed, not owner-interviewed, and a gated build-if-needed item (BUILD-ORDER.md item 10) whose gate is a measured silent-failure counter plus a latency arm. Its evidence is three single deterministic trajectories (1/6 → 4/6 → 5/6) the spec itself calls directional — not a result to quote.

Consent is already decided. tool-capability-model.md maps every future tool onto the existing Three-Tier Action Model — autonomous / act-and-notify / confirm-first (Alto-Design-Spec.md, from line 518) — rather than inventing a second consent system. Reads are Tier 1; writes that act under your identity are Tier 2/3.

What exists today

The channel, in full, on the live path. markers.py is a three-state streaming machine over the raw delta stream: ¦…¦ is speech and routes to TTS, ¤…¤ is an action span, anything outside a pair is thought and is discarded. Spans are captured whole rather than per-delta — _close_action (markers.py:52) appends the stripped span to self.actions — because the organ, when it lands, needs a complete action string to dispatch, not fragments; finalize (markers.py:111) captures a span left open by a truncated stream. None of this is dormant: integration.marker_filter is true as shipped (config.yaml:240), ollama.model is the LoRA (alto-integration-v6, config.yaml:85), and 300 of that adapter's 716-row training corpus are pure_action or mixed_marker rows (SPEC-integration-retrain-v2.md:31-36).

What happens to a captured span today is the whole story of this organ. It is printed to stderr as action (unwired in MVP) and handed to the tracer's record_marker_action (speech_out.py:58-60 blocking, :192-194 streaming; trace.py:151). Then nothing. It is deliberately not recorded as a performed action, and the reason is pillar 3 in one sentence: no effector fires, so recording it would append an [Earlier this session Alto carried out: X] breadcrumb for something Alto did not do, "training the next-turn LLM to trust a lie" (speech_out.py:47-50). The note pre-empts the obvious future edit — when the organ lands, do not record these spans; feed the record from effector success. The dispatcher already has that shape: its one action sink, record_action, is called only from the successful-call branch of ToolDispatcher.execute_tool_calls (tool_dispatch.py:190), after call_service returned. Nothing consumes that sink: Alto builds no dispatcher, the exchange carries no list of actions, and the conversation buffer has no action breadcrumb, so a reconnection rebuilds all three.

The fix cost something, and the code says so rather than hiding it: not recording spans also dropped the user's utterance on every pure-action turn, since turn persistence had been guarding on whether an action note or a reply was present. The repair was to record the user alone (context.py:197-200).

And the conversational model still holds nothing. Neither reply path in alto.py passes a tools argument to the model, and the ToolDispatcher.dispatch call site is gone from both turn handlers. That absence is asserted: test_no_conversational_llm_call_passes_tools (test_alto_v0.py) AST-scans src/alto/ and fails on any .chat( / .chat_stream( call that passes tools, and test_a_blocking_turn_sends_no_tools / test_a_streaming_turn_sends_no_tools run a turn through each path and check the request body has no tools key. An edit that registers a tool on the conversational path has to delete those tests to do it.

The guard

Below this organ is the guard, the hard edge on everything else in the project. Influence over thought is the project; influence over the physical world is gated by code that does not care how the model feels. The guard is a small pure function, validate_tool_call (ha.py:147). It takes a proposed device call, throws away most of what the model said, resolves the entity against a registry it controls, checks the service against a per-domain allowlist, and returns a validated tuple or None. The model's own domain argument is discarded rather than checked (ha.py:153): the registry is trusted over the model. A read-only sibling, validate_state_query (ha.py:168), checks state queries against the same registry.

There is exactly one call_service call site in the code (tool_dispatch.py:185), and it consumes the validated tuple, never the model's arguments. Adding a device capability is three edits and never fewer: the registry (entities: in config.yaml), the allowlist, and a new test. Widening what the model may emit is not one of them, and neither is a second path to call_service.

The guard is complete, and today nothing calls it, because the conversational model has no tools (see above). So it is the one piece of this organ with no gap of its own. The gap is the caller, and the day this organ lands the guard goes hot with no further review, which is the argument for keeping its unit tests. The outbound sibling, which guards what may leave rather than what may act (validate_search_query), belongs to Processing.

The gap

The consumer. All of it. No module, no spec file of its own, no name in code beyond the comments that promise it. It is designed-but-unbuilt at its thinnest: the contract is fixed on both sides — what may reach it (a ¤ span of natural language) and what it must produce (arguments that survive validate_tool_call) — while the organ between them has a sketch and a slot. Three things are genuinely unstated, which is a different category again:

  • How a ¤ span becomes a directive. The two-stage spec's directive contract (ACTION: / CHECK: / REPLY, with a terminal --- sentinel) was authored 2026-06-12, eighteen days before Integration-MVP, against the single-stage tool-calling pipeline. It never mentions commitment spans. Whether the ¤ span is the directive or is parsed into one is unresolved.
  • Confirm-first has no spec. The tier exists and the taxonomy maps tools onto it, but nothing specifies the action-side confirm flow. SPEC-ask-confirmation.md is not it — that spec resolves sub-certain memory facts, and its v1 reversed out of the voice-ask channel.
  • The gate's instrument is attached to dead code. Arm A proposes extending _dispatch_tools (now ToolDispatcher.dispatch) to log silent-failure candidates, and it (tool_dispatch.py:73) has no call site, so nothing is counting. The gate cannot pass on its own terms until something calls the effector path again — which is this organ. Plan that measurement to run somewhere else.

Everything past Home Assistant — calendar, email, autonomous research (V3) — is design direction, and reuses this organ and this guard rather than growing its own.

Pillars this serves

  • Pillar 8 (the deterministic safety boundary stays deterministic) — the organ exists so the thing holding the tools is not the thing with an interior. It proposes; the registry disposes.
  • Pillar 3 (honesty by construction) — twice. Structurally, in keeping a fabricated memory from reaching call_ha_service. And concretely in R18: a captured intent is not a performed act, so it is logged and never written to memory. Alto not claiming to have done what it did not do is the same commitment as not claiming to remember what it never heard.
  • Pillar 7 (composed proof, not mechanisms) — three deterministic n=1 trajectories are a direction, not a distribution; the spec's own criteria demand a multi-turn harness re-run.

Sources