Skip to content

refactor(triage): move haiku calls up to a signed, observable headless actor #502

Description

@bdelanghe

Problem

The headless LLM (haiku) call is buried inside two triage verb delegates as a "separate call", not modeled as a first-class actor:

  • triage/prioritize-bulk.tsdefaultClaudeRunnerbuildTriageHaikuClassifierRuntimeProfile + executeAgentProfile(...)
  • triage/type-pass.ts → same pattern

We've moved to always-headless, so any headless action should be a dedicated headless actor at the machine level — consistent with prx's grain (signed spawn@<role> = ocap, "actors own writes", the pilot/SDK executor pattern). Burying it in the delegate also blocks hermetic testing of prioritizeBulkActor/typePassActor (the wrapper can't inject around an inline executeAgentProfile).

Proposed change

  1. Extract a headlessClassifierActor — one fromPromise over executeAgentProfile + the haiku runtime profile + the --output-format json envelope parse. The single headless-invocation actor (SDK-executor pattern).
  2. Restructure the triage machine so the prioritize-bulk / type-pass states invoke the headless actor; its output flows through machine context to the apply step. The delegates drop their inline runClaude and become pure transforms (consume a classifier result).
  3. Make it a signed + observable step (this issue's scope): wire the headless actor into the signed-spawn so each call is attested as spawn@classifier (ocap), and emit a leg-event so the headless LLM invocation is observable (telemetry seam). One attestation/telemetry site instead of two inline call sites.

Why now

  • Aligns headless LLM calls with the signed/observable actor model from the start (no unsigned/unobserved headless node).
  • Makes the triage actors fully testable: the machine injects a fake headless actor; the delegates become pure (the testability wall hit in refactor(triage): break actors↔machine import cycle; make per-run actors testable #499, where prioritizeBulkActor/typePassActor couldn't be driven hermetically because the haiku call is inline).

Depends on / sequencing

Builds on #499 (triage actors↔machine cycle-break + the per-actor deps seam — the seam is what lets the machine inject the headless actor). Land after #499.

Acceptance criteria

  • headlessClassifierActor exists as a machine-level fromPromise actor wrapping executeAgentProfile + envelope parse.
  • prioritize-bulk / type-pass delegates no longer call executeAgentProfile inline; they consume the actor's result and are pure (no headless IO).
  • The triage machine invokes the headless actor in the classification states; result flows via context.
  • Each headless call is attested as spawn@classifier (signed) and emits a leg-event (observable).
  • prioritizeBulkActor / typePassActor wrappers are hermetically testable (machine-injected fake headless actor) and covered.

🤖 Filed with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions