Aielia Alpha
BUILD A HARNESS · SYSTEM ANATOMY

Inside the harness.

A harness is a governance and reliability control plane around an autonomous agent: the agent has intelligence, the harness has authority. The agent proposes; the harness decides what's allowed to happen next; evidence decides whether the result is accepted. Every non-trivial turn walks the same eleven layers of it, in the same order, whether it's Aielia (Build A Harness's own reference chat agent) or a flow drawn on the canvas. This is that loop — World Model through Reviewer Pass — read out of the harness source (packages/harness, with its Python twin in adapter/harness/). Click any layer below to open its internal flowchart.

11control layers walked every non-trivial turn
9verification checks defined — in a chat turn only 2 normally return a result, the rest report SKIPPED
6named recovery strategies, fixed escalating order
10diagnostic health dimensions feeding Control State

Eleven layers, in the order they run.

The chart follows one non-trivial turn in the order the harness actually executes it. Before the loop the assistant's adaptive layer control decides how much of the harness the turn needs, and the harness initializes its state. Then every iteration thinks (contradiction check, hypotheses, diagnostics, control state, planning), acts (the execution gate, the tool call, then the evidence and world model are updated from what came back) and checks (diagnostics and control state re-scored, verification, and recovery if the task failed), and repeats until every task is complete. The reviewer pass runs once after the loop, and again if it reopens a task; then the answer is validated. Layers keep the numbers used in the code and docs, drawn in execution order. Click any layer, or any 🤖 hook, for its own flowchart.

Harness core — generic, adapter/harness/ · packages/harness Added by Aielia — packages/aielia/src/*.ts Grayed — in the harness, but not expected to change anything in Aielia's agent (evidence in each popup)
The eleven harness layers in the order one turn runs them A swim-lane timeline. Before the loop, the assistant's adaptive layer control decides how much of the harness the turn needs and the harness initializes its state. Each loop iteration thinks (contradiction check, hypotheses, diagnostics, control state, planning), acts (execution gate, the tool call, then evidence and the world model are updated from the result) and checks (diagnostics and control state re-scored, verification, and recovery if it failed), then repeats until every task is complete. After the loop the reviewer pass runs, and can reopen a task for a second pass, then the answer is validated and returned. Before the loop once per turn Think every iteration Act every iteration Check every iteration After the loop once (twice if reopened) Around it all Adaptive layer controlpicks how much harnessthis turn needs Initialize the runbeliefs from your message + factstask graph · control state trivial one-liner → reply, harness skipped poll forupdates 4Contradictionnew belief vs. the record 3Hypothesisbookkeeping only(opt-in: readings) 5Diagnosticsten health dimensionsneutral < 3 tasks 6Control Statesets the run's mode+5 tiers 7Planningtask graph · 6 taskstates, dependencies + semantic backstop each iteration starts here ↓ 8ExecutionVOI · review · action gate+5 dims run the toolthe one real action 2Evidence& reasoning, cappedby tool reliability 1World Modelupdated after everyaction + pending-action approval + tool-policy gate + trust-tagging 5·6Re-scoreDiagnostics → ControlState after the action 9Verificationnine independent checks+9 checks 10Recoveryrollback + replan on failure+6 strategies + failure-mode backstop + Trajectory Supervisor stalled? one LLM call: redirect, reframe, gather, ask, stop task left → next iteration ↑ all tasks complete → leave the loop 11Reviewer Passself-review · may reopen a task+3 lenses + semantic review-checker Output validationcontract, if set · your statedconstraints judged (one re-ask) replystreamed (final answer) reopened task → second pass through the loop ↑ Compression Plan persistence Fact memory Spend cap Undo log Goal graph Next steps
harness core (packages/harness) — numbered as in the code added by Aielia (packages/aielia) reopen path
Skipped on trivial turns

A single consolidated call up front (adaptive layer control) (risk, triviality, decomposition, plan-abandonment, plan-template matching) flags a genuinely self-contained factual one-liner as trivial — that reply returns directly and none of the eleven layers above run. Everything else walks the full loop. See Aielia for how this applies turn by turn in the reference chat agent.

One turn, everything on.

The eleven layers above run inside a single proposer loop (ASSISTANT_ONE_LOOP, on by default): the harness drives one tool-calling iteration at a time instead of reviewing a finished reply. Four mechanisms wrap that loop. The diagram shows one turn with all of them enabled: the goal graph decides which thread has focus, the steering channel is polled at the start of every iteration, the Supervisor is consulted only if an iteration stalls, and once the reply is out, up to three next-step options appear under it. Click any card, or any 🤖 hook in the chain above, for its flow.

One turn with the one-loop proposer, mid-turn steering, the Trajectory Supervisor and the goal graph all enabled A swim-lane timeline. The identity matcher attaches the message to the open goal it continues, or starts a new thread, and the Scheduler keeps one thread in focus. Then the harness loop runs one iteration at a time, polling the steering channel at the start of each. A message sent mid-turn enters the steering channel and is classified at the next poll: corrections and urgent additions become notes for the next LLM call; anything that can wait, a new goal or a cancel becomes a follow-up turn. If an iteration stalls, the Supervisor is consulted at each stall and its directive decides what happens next. After a full turn, up to three next-step options appear under the reply; when a thread's last task completes its suggestions are saved for /goals. You CLI · chat-ui Steering LiveSteeringChannel Goal graph across turns Harness loop one-loop proposer Supervisor stall edge only 1 · message mid-turn message reply next-step options queue absorbs it — no waiting runs as next turn deferred asks new goal · cancel · deferred task match turn → threador start a new one Scheduler → ACTIVE thread DONE → suggestions saved/goals shows the graph + next steps iteration 1poll · act · verify iteration 2stalls iteration 3poll · act · verify steering note → next LLM call next poll: classify scope × urgency (1 LLM call) consulted per stalldigest → 1 LLM call → directive cannot_make_progress() directive: continue · redirect · reframe gather evidence · ask you · abort → failed task is re-queued
harness core (packages/harness) added by Aielia (packages/aielia) stalled iteration
What is on by default

All four are on by default in Aielia's CLI and chat-ui (the assistant library itself leaves steering, the goal graph and next-step options off): the one-loop proposer (ASSISTANT_ONE_LOOP), the Trajectory Supervisor (HARNESS_TRAJECTORY_SUPERVISOR), and mid-turn steering with the goal graph (ASSISTANT_GOAL_GRAPH), and the next-step options (ASSISTANT_GOAL_GRAPH_SUGGEST). Each has an explicit disabled (or 0) escape hatch. With steering and the goal graph disabled, a message sent during a turn waits in line exactly as it did before, and no goal graph is kept.

Also by default: the lexical checks under the semantic layers are off (ASSISTANT_LEXICAL_MODE=disabled since 2026-09-28, so contradiction, failure matching and injection detection run on the LLM check alone), the grounding check behind a verified label is on, and so is the semantic judge for caller constraints. Also on: the single retry of a system-error failure (AUDIT_RETRY_SYSTEM_ERRORS), semantic failure matching, resuming a stuck plan only when the message continues it, and the budgeted, priority-ranked facts render with keyed supersession (AUDIT_MEMORY_BUDGETED_RENDER; =0 restores the old 20-fact slice). Off by default, behind AUDIT_* flags: semantic hypotheses, reviewer revision, semantic transcript compaction, experience learning, source reliability, the memory write gate, memory audit log, episodic digests with the recall tool, the memory reviewer and memory consolidation (AUDIT_MEMORY_WRITE_GATE, AUDIT_MEMORY_AUDIT_LOG, AUDIT_EPISODIC_DIGEST, AUDIT_RECALL_TOOL, AUDIT_MEMORY_REVIEWER, AUDIT_MEMORY_CONSOLIDATION), the per-step task-completion check, contradiction severity, code plan templates, retrying every failed task, and ask mode (ASSISTANT_ASK_MODE, which lets the Supervisor and the harness put a structured question to you). The adaptive layer policy is also off: the static policy is the default.

What it remembers, and who may write it.

Memory is separate from the World Model, which is rebuilt empty every turn. Everything that reaches it goes through one service, so no writer can choose its own route: where a candidate lands depends on where it came from, how confident it is, and the configured write mode. By default only the budgeted facts block with keyed corrections is new; the write gate and the cross-turn writers are opt-in and grayed below. Click any box for its details.

How Aielia's memory worksFacts reach memory from the in-turn extractor, optionally through a write gate, and are routed to durable facts, a pending queue or session facts by source, confidence and write mode. Three opt-in writers (memory reviewer, consolidation, episodic digest) add cross-turn memories, staged for approval by default. Each turn a budgeted block of facts goes into the system prompt and durable facts are seeded into the World Model; an opt-in recall tool reads digests. Slash commands let the user inspect, confirm, forget and undo.Writewho proposes a factCheck and routeone serviceStoreswhat is keptReadwhat the model seesYou control itslash commandsIn-turn extractorclassifier + keyword passyou stated · model inferredMemory reviewerafter 5 turns with no memorywrite, and again at /newConsolidationproposes merges and archivesrun with /memory consolidateEpisodic digesthandoff summary at /newand before compactionWrite gateredacts secrets, flagsinstruction-like textRoute by write modedurable · pending · session, chosen bysource and confidence, then by write mode:staged (default) · auto · user_onlyAudit logevery write kept with itspre-image: history, undoDurable factskept across sessions;a keyed fact replaces the oldPending queuewaits for /memoryconfirm or rejectSession factsthis conversation onlycleared by /newRetired and archivereplaced or archived;kept out of the promptDigest storeone rolling digest perconversation, kept 90 days1World Modeldurable facts seededas beliefs for the turnBudgeted facts block → system promptmost important first, within 4,000 characters;durable and session facts share the budgetrecall_memory toollists or opens a digest;result tagged untrusted/memory status/memory forget/memory off · on/memory confirm · reject/memory history · undo/memory consolidate accept · dismiss/memory forget digest
added by Aielia, on by default opt-in, off by default harness core (packages/harness)
on by defaultWho gets inA fact you state yourself is kept if it is durable. A model-inferred fact is kept only at high confidence, waits in the pending queue at medium (/memory confirm), and stays session-only at low. With memoryWriteMode=user_only even a high-confidence inference waits for you.memoryWriteMode · AUDIT_MODEL_INFERRED_FACTS
on by defaultWhat the model readsEach turn the facts are rendered into the system prompt by kind (identity and preference first, then the rest), newest first, until a 4,000-character budget is spent. Facts that do not fit are left out of that turn and counted, not deleted.AUDIT_MEMORY_BUDGETED_RENDER · memoryBudgetChars
on by defaultCorrectionsA fact carries a key for the attribute it describes; a new fact with the same key replaces the old one, which moves to a retired store. Shown on one corrections scenario (3 of 3 seeds, control failed). The classifier chooses the keys and they are not stable, so a replacement can be missed.AUDIT_MEMORY_BUDGETED_RENDER
partly opt-inSafety rails/memory off stops all memory writes. Cross-turn writers are staged for your approval by default and nothing is deleted automatically. The write gate redacts secrets and flags instruction-like text, failing closed when its check cannot run; it is off by default and showed no outcome gain, because the base model already resisted.memoryWriteMode · AUDIT_MEMORY_WRITE_GATE
opt-in, off by defaultCross-turn writersThree opt-in writers add what one turn cannot see: a reviewer that proposes memories from repeated behaviour, an episodic digest written at the session edge, and consolidation that proposes merges and archives, applied only when you accept. In auto mode the reviewer passed 2 of 2 seeds; the digest, 2 seeds on one scenario; consolidation is unmeasured as an outcome.AUDIT_MEMORY_REVIEWER · AUDIT_EPISODIC_DIGEST · AUDIT_MEMORY_CONSOLIDATION
opt-in, off by defaultRecalling past sessionsDigests are never put into the prompt. With the recall tool on, the model can list or open one through the read-only recall_memory tool, and the result is wrapped as untrusted data. Digests are kept 90 days by default.AUDIT_RECALL_TOOL · AUDIT_EPISODIC_RETENTION_DAYS
What is and is not shown

The corrections behaviour is the only memory change with a demonstrated outcome difference. The write gate, audit log, digest and recall, reviewer and consolidation are mechanisms that work in testing; their value on real use is unmeasured or small-sample, and a plain-language forget request is only staged for your approval (a known gap).

1 World Model

Fresh, typed beliefs formed for this turn — what the message says and the memory facts carried in. The World Model is scratch state, rebuilt empty every turn.

Files: world_model.py · world_model_ops.py · staleness.py · generation-id.ts · update-world-model.ts (TS twin)
Message + transcript
→
Form typed beliefsstated facts and memory facts; tool results stay observations
→
Stamp generation_idmonotonic per-step counter, pinned by Control State (the Python twin also pins Plan, Execution and Verification version objects)
→
Published to World Modelread by Layer 4, 6
pinned generation_id still current the downstream read proceeds normally
world model has advanced past the pinned generation_id the gate re-resolves Control State once and raises StalenessError only if it is still stale (both twins), instead of silently returning an outdated belief. The Python twin can run the same check on Plan, Execution and Verification objects; the TypeScript twin Aielia runs pins only Control State
2 Evidence & Reasoning

The evidence store backing the reply. Each result's reliability is capped at the ceiling set for the tool that produced it.

Files: evidence.py · tool_reliability.py · tool_manifest.py · gather-evidence.ts · apply-tool-reliability.ts (TS twin)
Tool call result
→
Check tool availability
→
Cap reliability at the tool's ceiling
→
Evidence storefeeds Layer 3
tool available (tool_manifest) — call proceeds as planned
tool unavailable no evidence is collected and a warning is logged. The Python manifest can name a fallback tool; in Aielia every enabled tool is registered with no fallback. This is an availability check, not the security allowlist (that one guards fn_ref values at /compile, /flows and /run)
3 Hypothesis

Grayed in Aielia by default this layer is bookkeeping: nothing reads its output while the reply is written, and the opt-in version showed no measurable benefit at about 4× the cost (see Evaluation below).

Candidate explanations kept alongside the beliefs, seeded from four sources, regenerated until the sources are diverse, then pruned by confidence. By default in Aielia this layer is bookkeeping: the seeds are fixed templates with no predicted observations, and nothing reads them while the reply is written (Control State reads only their source diversity, through Diagnostics).

Files: hypothesis.py · generate-update-hypotheses.ts · semantic-hypotheses.ts
Evidence + beliefs
→
Seed hypothesessymptom · counterfactual · failure library · analogy
→
Eliminate weak oneslow confidence or contradicted; keep the top 5 if over 10
→
Active setfeeds the coverage dimensions (Layer 5)
default template seeds only; their empty predicted_observations mean the review gate's hypothesis_compatibility check has nothing to compare against
opt-in AUDIT_SEMANTIC_HYPOTHESES=1: for an underdetermined request, one LLM call proposes 2–4 competing explanations with a separating check, the model sees them while drafting, and one call per execution rules out the ones new evidence contradicts

Evaluation: suggestive, not proven — 9/10 vs 8/10 on a matched 10-seed run, at about 4× the cost. It stays off.

4 Contradiction

Checks a new belief against ones already on record — "the build is passing" against a "failing" belief from three turns ago.

Files: contradiction.py · belief_graph.py · detect-contradictions.ts (TS twin)
New belief
→
Compare to beliefs on record
no conflict belief added, nothing else happens
conflict found logged. LOW or MEDIUM severity cuts the affected beliefs' confidence by 10% or 25%; HIGH puts them on the invalidation frontier instead, and Verification and the Reviewer count it as unresolved. The keyword detector grades a pair HIGH when both beliefs are at least 0.8 confident and MEDIUM otherwise (a pair with both under 0.5 is ignored); LOW comes only from the advisory granularity check. In Aielia that detector is off by default, so conflicts come from the semantic backstop (below), which records them as MEDIUM
severity = SYSTEM_BREAKING a HIGH conflict whose belief pair an active hypothesis predicted as a conflict. Control State Tier 1 then forces BLOCKED on the next resolve. Only the keyword detector's HIGH pairs can be upgraded, so in Aielia by default (detector off, and the default hypothesis seeds predict nothing) this does not occur
5 Diagnostics

Grayed in Aielia the ratio dimensions (progress_rate, oscillation_score) stay neutral until 3 tasks have been attempted, and Aielia's ordinary turns end at two or fewer attempted tasks (per the eval notes; a long multi-step plan can reach three), so on most turns this layer produces no signal that changes a decision. It is the input to the harness-level Control State resolver, which is redundant here for the same reason (Layer 6).

Ten health dimensions, each normalised to 0–1, computed from the turn so far and fed straight into Control State.

Files: diagnostics.py · update-diagnostics.ts (TS twin)
normalise() → assert_normalised()
Diagnostics
↓
Control State (Layer 6)

Click any dimension for its exact type and definition. Each one below 0.2 maps to a specific recovery action class — that mapping is what Control State's Tier 2 and Recovery act on. The definitions shown are what both twins compute; progress_rate and oscillation_score stay neutral until 3 tasks have been attempted. failure_recurrence and oscillation_score are stored with 0 meaning healthy and are inverted (1 minus the value) before the 0.2 test.

6 Control State

Where it bites in Aielia the harness-level resolver is shown to work at the harness level (4 or fewer executions against 43 without it), but in the assistant its ratio inputs need 3 or more attempted tasks, which the eval notes say Aielia's graphs do not reach. Its effect is delivered instead by a turn-local copy behind the tool-policy gate (a retry loop is blocked once 8 failures are counted, so the 9th attempt is refused) and by the stall rule and Trajectory Supervisor for executor failures.

The sole control input for action selection (INV-06) — resolve_control_state() is the only function allowed to write it. Five tiers run in strict order; the first one that fires wins.

Files: control_state.py · resolve-control-state.ts (TS twin)
1 SYSTEM_BREAKING contradiction on record? yes → BLOCKED (permission DENY, mode RECOVERY), stop
↓ no
2 any diagnostic dimension < 0.2? yes → BLOCKED (+ recovery tag; cycle → HUMAN_REQUIRED)
↓ no
3 coverage dimension 0.2–0.4? yes → CAUTIOUS, continues
↓
4 proportional elevation factor (blended 80/20 with a matched failure pattern's confidence) > 0.05? yes → CAUTIOUS (only raises NORMAL; a pending MEDIUM/HIGH reviewer finding does the same)
↓
5 nothing above fired → NORMAL
7 Planning

A task graph with six task states (PENDING, RUNNING, COMPLETE, FAILED, BLOCKED, HUMAN_REQUIRED) and dependency tracking. Ready tasks are picked highest-risk first. Two ready tasks run side by side when neither is HIGH risk and their write domains do not overlap (or overlap with a recorded conflict probability of 0.5 or less). Aielia's tasks declare no write domains, so any two independent ready non-HIGH tasks may run in parallel; a dependency chain or a HIGH-risk task runs serially.

Files: task_graph.py · parallel_merge.py · task-graph.ts · select-task.ts (TS twin)

Live part task states, dependency order, cycle detection, the risk rating and two-at-once dispatch of independent non-HIGH tasks. Inert in Aielia the write-domain conflict-probability check, since its tasks declare no write domains.

PENDING
→
RUNNING
→
COMPLETE
off-ramp a task can instead move to FAILED (set only by Execution; Recovery may re-queue it to PENDING), BLOCKED (a caller scope change or a cancelled goal) or HUMAN_REQUIRED (declared, not set by current code); COMPLETE is terminal
risk rating each task also carries LOW / MEDIUM / HIGH, read by Execution's review gate before it runs
8 Execution

Value-of-information estimated, a five-dimension review gate runs, then the action gate decides whether the action fires.

Files: execution.py · voi.py · risk.py · review_gate.py · review-proposed-change.ts (TS twin)
Proposed change
task_alignment
world_model_consistency
output_contract_precheck
code_quality
hypothesis_compatibility
all five checked in one pass
2 review failures in a row on this task?
no the action gate runs: PASS executes; BLOCK (permission denied) or ESCALATE (human required) re-queues the task, and the run halts only if it is stalled
yes the first failure re-queues the task; the second in a row halts with an escalation (a failure on any dimension counts, a pass resets the count)

Grayed in Aielia: with the lexical checks off (the default) the four text-matching dimensions cannot fail. hypothesis_compatibility also has nothing to compare (the default hypotheses predict nothing), output_contract_precheck has an empty contract, and code_quality has no linter. task_alignment only fails on an empty task description. Real conflicts are flagged, as a heads-up only, by the semantic review-checker; it never fails the gate. The risk estimate is discarded; the value-of-information result only decides whether extra evidence cross-checking is registered, never the reply.

9 Verification

Nine independent checks the draft has to clear before it's trusted — not passed on the model's word.

Files: verification.py · verify.ts (TS twin)
Draft output
a check that can't run → SKIPPED, never counted as pass
has_critical_failure?
no draft trusted, turn continues
yes sent to Recovery (Layer 10)
in a chat turn only evidence_sufficiency and output_contract_partial normally return a pass or fail. consistency is SKIPPED because the runtime marks its checker unavailable (contradictions are handled by Layer 4); requirements and assumptions can only fail on an empty output; syntax, unit, integration and goal_correctness are SKIPPED
grounding check (Aielia) separate from the nine checks and run after the reply: before a tool-backed reply is labelled verified, one LLM call compares it against the raw tool output (AUDIT_SEMANTIC_GROUNDING, on by default). An ungrounded verdict, an unchecked reply (no tool text, or the call errored) or no passing layer means unverified, never verified

Grayed checks report SKIPPED in a chat turn (no linter, test runner or integration runner to call). Click any check for what it specifically catches.

10 Recovery

Six named strategies in a fixed, escalating order — a rollback-and-replan path instead of the run crashing out. In Aielia the strategy picked is mostly bookkeeping (a one-sentence hint reaches the model); what changes the outcome is whether a failed task is retried at all.

Files: recovery.py · replanning.py · memory.py
1 · DIRECT_EDITsmallest targeted fix
→
2 · TRACE_EXECpin down where it broke
→
3 · BROADER_SEARCHwiden context search
→
4 · REIMPLEMENTrebuild the approach
→
5 · MINIMAL_FIXaccept a smaller fix
→
6 · ESCALATEhand off to a human

Past experience can bias which strategy is tried first, but only when experience learning is switched on (AUDIT_EXPERIENCE_LEARNING=1, off by default). Otherwise nothing writes the strategy weights and this fixed order is always used, as it is whenever the experience store is unavailable (INV-10). A confident (0.7 or higher) failure-mode match that names a preferred strategy jumps straight to it. It ranks below the Supervisor's redirect and, when experience learning is on, below the learned ranking.

system-error retry AUDIT_RETRY_SYSTEM_ERRORS, on by default. Each failed execution is tagged system_error (the executor threw, or the tool or model call broke) or exhausted (it ran out of its own iteration budget). After the ladder switches strategy, a system_error task is re-queued once. Exhausted loops, rejected completion checks and failed verification are never retried, and the once-per-task cap means a persistent outage still ends in the honest "could not complete" reply. Demonstrated: with the matcher off, a synthetic 503 passes with the rule and fails without it.
retry every failed task AUDIT_RETRY_FAILED_TASK, off: flipping it changed 8 existing tests (runaway loops end escalated, rejected plan steps get retried).
learned ranking AUDIT_EXPERIENCE_LEARNING, off. Wired and unit-tested, but not demonstrable as an outcome benefit in Aielia — the chosen strategy does not change what the model sees.
stall rule a run that stalls (completion velocity, a strategy loop, a recurring failure or risk oscillation) gets a global rebuild of the task graph, otherwise a local replan. The Trajectory Supervisor, if enabled, is consulted only at that stall.
11 Reviewer Pass

A final pass reviews the run before the answer leaves the harness. Its verdict feeds the next Control State resolve; in a narrow case it sends a finished task back through the loop.

Files: reviewer.py · reviewer-pass.ts (TS twin)
implementerdid it do what it set out to do
reviewersecond read for gaps, regressions
adversarialtries to break it — ephemeral, INV-09; only on a non-LOW-risk or 3+ task turn
findings merged
Propagation drain
↓
reviewer-lens finding names a finished task?
no output released; the verdict is stored in the checkpoint and only used if a task is later reopened (the second pass's Control State resolve)
yes reopens that COMPLETE task → loop back to Planning (never writes control_state directly — INV-06). Implementer findings quote a success criterion and never reopen a task; the generic "respond helpfully" default criterion is never checked, on every turn.
opt-in AUDIT_REVIEWER_REVISION=1: at the end of a run, a HIGH verdict (or a MEDIUM implementer finding that a real success criterion is uncovered) sends the last answer back once, with the finding, to be answered again (only when every task is complete and nothing else was reopened). It fires and acts on a real model but showed no outcome difference (a matched re-run passes with or without it), so it stays off.
A Adaptive layer control

One consolidated LLM call, made before the harness runs, that classifies the turn (in the code, the turn-intent classifier). Its judgments decide whether the harness runs at all, how the turn is planned, whether it needs approval, and whether the Reviewer Pass's adversarial lens runs. A greeting needs none of it; a risky multi-step request needs all of it.

Files: turn-intent-classifier.ts · turn-policy.ts · turn-interpreter.ts
riskLevel LOW / MEDIUM / HIGH, or UNKNOWN when the classifier call fails or can't be parsed (treated as HIGH and requires approval), with a stated riskReason. Drives the approval gate and Control State, and the depth of the Reviewer Pass: its adversarial lens only runs on a non-LOW-risk turn or one with three or more tasks.
isTrivial only meaningful when riskLevel is LOW — true turns every harness layer off: the reply goes straight back and HarnessRuntime.run() never runs (and no next-step options are proposed).
decomposedTasks a multi-step request broken into an ordered task list, or null. Shapes Planning (Layer 7): one task per node instead of a single task.
plan flags isAbandonRequest, matchedPlanTemplate and needsMultiStepPlan: abandon judgment is only honoured when a plan is already active; template matching and the durable-plan check only when none is. Together they decide whether the turn runs under a tracked, user-approved plan.
isBulkReminderRequest (only true when isReminderRequest is too, i.e. more than one reminder in a turn) forces approval, like HIGH or UNKNOWN risk. isReminderRequest alone does not.
isPlanQuestion a question about an active plan's state is answered from the recorded plan instead of re-running it (by default, only while a step has failed)
continuesPlan with a plan active, whether the message tells the assistant to carry on (a go-ahead, "retry", details for a failed step). Only an explicit false sets a stuck plan aside (AUDIT_STUCK_PLAN_RESUME).
facts and constraints statesDurableFacts (each with a confidence and a durable bit) feed user-fact memory; statedConstraints, lastingConstraints and liftedConstraints feed the constraint judge.
isUnderdetermined read only when the opt-in semantic hypotheses are on (Layer 3)
layer-policy signals needsGrounding, ambiguity, userPosture, pushbackOnPriorTurn and statesConstraint ride the same call and are read only by the layer policy (pushbackOnPriorTurn also tags the previous turn's telemetry row).
layer policy layerPolicyMode is static: every layer runs as configured. An adaptive mode exists that would switch layers off per turn, but all eight of its rules are still unproven hypotheses, so it stays off; shadow telemetry records what it would have done, with the real LLM calls and tokens the four escalation hooks spend.
🤖 Trust-tagging

Untrusted content is wrapped before it ever has a chance to become a belief in World Model (Layer 1).

Files: trust-tagging.ts · recall-tool.ts
web_search / fetch_url results wrapped in an untrusted_external_content tag; the system prompt tells the model that tag is data, never instructions
read_file / list_directory results left untouched — file content is user-owned, not adversarial-by-default
detectInjectionLikelyWithLLM() an LLM check on tool output for content that looks like it's trying to issue instructions. Since the lexical checks were switched off by default (2026-09-28) it is the only check in the assistant itself, and it runs on every web result and approved shell output, not only long ones (AUDIT_LLM_INJECTION_DETECT=0 removes it); read_file, list_directory and reminder results are not checked.
recall_memory digests (opt-in, AUDIT_RECALL_TOOL) are wrapped in the same tag
run_shell_command output gets the same injection check and tag when an approved command runs; the tag is kept in the saved transcript, not shown in the chat bubble
🤖 Semantic contradiction backstop

An LLM check that compares new beliefs against the ones already on record, by meaning rather than wording. Since 2026-09-28 it is the contradiction detector Aielia uses by default: the harness's lexical negation-pair detector is switched off in the CLI and desktop app.

Files: contradiction-checker.ts · lexical/lexical-mode.ts
semantic pass (default) an LLM call when new check-worthy beliefs are added, in the harness and again on memory facts (AUDIT_SEMANTIC_CONTRADICTION=0 turns it off); task-completion trail beliefs are never checked
lexical pass (opt back in) ASSISTANT_LEXICAL_MODE=enabled restores the keyword detector (build passed/failed, file exists/missing…) and the skip that avoids the LLM call when every new belief is already lexically structured
severity a found contradiction is recorded as MEDIUM; with AUDIT_SEMANTIC_CONTRADICTION_SEVERITY=1 (off) the same call can grade it HIGH

Evaluation: verified — on its layer conversation the turn passes with the check on and fails with it off, in 3 of 3 runs, and the check's own LLM call appears only in the ON arm.

🤖 Pending-action approval

A proposed file write, shell command, email or undo is staged, not executed — real user approval is a separate, later step, on top of Execution (Layer 8)'s own review gate.

Files: file-tools.ts · action-approval-service.ts · assistant-session.ts
Proposed action
→
stagePendingAction()recorded, not run
→
user decides
approved applyPendingAction() actually runs it
rejected discardPendingAction() drops it, nothing happens
never answered sweepAbandonedPendingActions() removes it at startup once it is over 24 hours old (skipped while any session is still resumable)
🤖 Tool-policy gate

A deterministic, harness-state-informed check before each read-only tool call runs — not just advisory classification checked after the fact. Complements pending-action approval above, which already gates write_file/run_shell_command unconditionally regardless of this check.

Files: tool-policy.ts · tool-control-plane.ts
Tool call proposed
→
evaluateToolPolicy()checked against live ControlState
→
ALLOW / DENY / REQUIRE_APPROVAL
ControlState built fresh each turn and updated after every tool-call outcome (success or failure) via gatherEvidence → applyToolReliability → updateWorldModel from the harness core, then only failure_recurrence is recomputed before resolveControlState (the full diagnostics pass is skipped; a missing-file answer is not a failure, only real errors and the third-plus repeat of an identical empty call count) — so a developing failure pattern within one turn can trip a real DENY, not just get logged after the fact
ALLOW the default — ControlState permits, or nothing's gone wrong yet this turn
DENY the call is blocked outright, reported back to the model as a tool result
REQUIRE_APPROVAL also given for an UNKNOWN risk classification and, if a control state carries a HUMAN_REQUIRED or SYSTEM_BREAKING escalation without DENY, for that too (the resolver normally sets DENY alongside them, and DENY is checked first); surfaced as an escalation — a read-only call has no concrete pending mutation to stage the way write/shell do
🤖 Semantic failure-mode backstop

Matches observed symptoms against the curated failure-pattern library by meaning, not exact wording — e.g. "the request took too long and timed out" matches a curated "request timed out" symptom.

Files: failure-mode-matcher.ts

Layered on Recovery (Layer 10)'s pattern library — one LLM call, asked about a task only after it has failed, and not again once it has produced a match for that task (it sees the task's first failure). By default it is the only matcher: the exact-substring match is switched off with the other lexical checks (ASSISTANT_LEXICAL_MODE=enabled restores it). A match with confidence 0.7 or higher and a curated preferred strategy re-queues the failed task. Its escalation hook is now gated per call, so an adaptive layer policy can turn it on mid-turn rather than only at the start.

Evaluation: demonstrated on every backend for one pattern — a transient model-call error (a synthetic 503) becomes a failed task, the match re-queues it, and the turn passes where the control fails. One seed each. The other three curated patterns have no demonstrated trigger.

🤖 Semantic review-checker

Checks whether a proposed action genuinely conflicts with a high-confidence belief or an active hypothesis's predicted observation — a real logical conflict, not just topical overlap.

Files: review-checker.ts

Runs in Execution (Layer 8)'s review gate after the lexical dimensions pass (those are off by default in Aielia), and checks against high-confidence beliefs, known facts and hypothesis predictions. Advisory: a conflict is surfaced as a heads-up, not a block. AUDIT_SEMANTIC_CHANGE_REVIEW, on by default.

Evaluation: fires and is wired end to end, but no benefit shown — across six pilots the model caught the conflict without it.

🤖 Context compression

Keeps a long conversation inside the model's context window. Once the transcript passes 40 messages or 20,000 characters, everything but the last 10 messages is folded into one message. By default that message keeps only the first 200 characters of each older message, so a detail deep inside an early long message is lost. Not one of the 11 counted layers.

Files: transcript-compaction.ts · semantic-compaction.ts
opt-in AUDIT_SEMANTIC_COMPACTION=1: one LLM call summarizes the older part instead, keeping names, numbers, dates, decisions and constraints; any failure falls back to the truncated form. On one matched pair it kept a detail the default lost; cost and quality on messier sessions are unmeasured, so it stays off.
🤖 Plan persistence

A durable, user-approved multi-task plan (drafting, awaiting approval, active, then done or abandoned) that survives across turns and crashes and is cleared by /new — separate from the harness's own per-turn task graph (Layer 7), which starts fresh each turn.

Files: plan-store.ts · plan-builder.ts
🤖 User-fact memory

Facts stated in one conversation are extracted and carried into future ones. Each turn the model is shown a block of facts that fits a character budget (memoryBudgetChars, default 4000), most important first, instead of only the 20 most recent. This budgeted render is on by default (AUDIT_MEMORY_BUDGETED_RENDER=0 restores the old path). Everything marked opt-in below is off by default.

Files: fact-extraction.ts · memory-service.ts · memory-governance.ts · episodic-digest.ts · memory-reviewer.ts · memory-consolidation.ts · recall-tool.ts
confidence a fact you assert directly is kept if durable. A model-inferred one is kept only at high confidence, queued for /memory confirm at medium, and session-only at low (shown as unconfirmed).
corrections (default) a fact with a key replaces the earlier fact for the same attribute, and the old one goes to a retired store. Shown on one corrections scenario, 3 of 3 seeds. Not measured: key stability, so a changed attribute can be stored under a different key and miss the replacement.
controls (default) /memory status shows store size against budget; /memory forget removes a fact; /memory off stops all memory writes and leaves existing facts readable. A forget request made in plain language is only staged for your approval.
write gate AUDIT_MEMORY_WRITE_GATE, opt-in. A model check redacts secrets and flags instruction-like text before a fact is stored, and refuses to store when the check cannot run. The base model already resisted in the one scenario run, so no outcome gain is shown.
audit log AUDIT_MEMORY_AUDIT_LOG, opt-in. Enables /memory history and /memory undo. Undo exactness is unit-tested only.
episodic digest and recall AUDIT_EPISODIC_DIGEST and AUDIT_RECALL_TOOL, opt-in. A handoff digest is written at the session edge, and the model can read digests through a read-only recall_memory tool. Shown on one scenario, 2 seeds. Adds a model call per session edge.
reviewer AUDIT_MEMORY_REVIEWER, opt-in. A post-turn pass proposes cross-turn memories. Its writes are staged for /memory confirm unless memoryWriteMode is auto. In auto mode it passed 2 of 2 seeds; the default staged mode failed both arms in its first gate and was then fixed.
consolidation AUDIT_MEMORY_CONSOLIDATION, opt-in; needs the audit log. It proposes merges and archives, applied only on /memory consolidate accept. Not measured as an outcome; one all-flags run cost about 10-40% more.
🤖 Spend cap

checkSpendCap() runs once per new turn, before any LLM call, and refuses it once the session's cumulative cost (sessionCostLimitUsd) or its count of recorded turns and background memory calls (sessionCallLimit) reaches the ceiling. The ledger persists per session across /new and restarts. Neither limit is set by default, and a running turn and an approval resume are never interrupted. A model with no known price counts as $0. No harness-core equivalent — the harness itself doesn't track cost.

Files: spend-cap.ts · model-pricing.ts
🤖 Undo log

Every applied file write or shell command is recorded (the newest 20 are kept), so it can be reverted. A write over a binary file, or a shell run in a workspace too large to snapshot (over 2,000 files), is marked not undoable with the reason. Files skipped (binary, over 1 MB, ignored directories) are listed as unsnapshottable and the revert is flagged incomplete. Emails are not logged. A revert is staged for your approval via /undo-action.

Files: action-snapshot.ts
5belief_freshness

Type: ratio — 1 − stale_flag_ratio

How many beliefs behind the current answer are still fresh vs. flagged stale.

< 0.2 → belief_refresh
5belief_consistency

Type: ratio — 1 − contradiction_density

How much of the current belief set is internally contradiction-free.

< 0.2 → consistency_repair
5belief_support

Type: ratio — mean reliability weight over beliefs

How well-backed the current beliefs are by reliable evidence.

< 0.2 → support_augment
5symptom_coverage

Type: ratio — active hypotheses / observations, capped at 1

Roughly, how many observations have a candidate explanation.

< 0.2 → coverage_expand
5explanation_coverage

Type: entropy — source diversity of the active hypotheses

How varied the sources behind the candidate explanations are.

< 0.2 → explanation_expand
5verification_strength

Type: ratio — 1 − unverified_edge_ratio

How much of the belief dependency graph has been verified.

< 0.2 → verification_pass
5verification_feasibility

Type: composite — tool adequacy + evidence adequacy + abstraction fit (weights 1, 1, 0.3)

Whether the checks that should run even can, given the tools, evidence and task abstraction level.

< 0.2 → feasibility_check
5progress_rate

Type: ratio — tasks completed / tasks attempted (neutral until 3 attempted)

Forward motion across the task graph.

< 0.2 → execution_retry
5failure_recurrence

Type: ratio — min(1, failures recorded / 10)

How many failures have piled up; 0 is healthy.

1 − value < 0.2 → failure_recovery
5oscillation_score

Type: ratio — failed tasks / total tasks (neutral until 3 attempted)

Whether too many tasks in the graph have failed; 0 is healthy.

1 − value < 0.2 → oscillation_stabilise
9syntax

Backing tool: linter

Runs ruff on a given file path in the Python twin; the TypeScript twin never runs it. Without a path it is SKIPPED.

9unit

Backing tool: pytest

Runs pytest on a given path in the Python twin only; the TypeScript twin never runs it.

9integration

Backing tool: integration_runner

Always SKIPPED: no integration runner exists yet.

9consistency

Backing tool: consistency_checker

Fails when the World Model (Layer 1) holds an unresolved HIGH or system-breaking contradiction; it does not read the draft itself. In Aielia the checker is marked unavailable, so it is SKIPPED (the Python twin runs it).

9requirements

Backing tool: requirements_checker

Fails only if success criteria exist but nothing was produced; whether the content satisfies them is not judged here.

9assumptions

Backing tool: assumption_checker

Fails only if assumptions were stated but nothing was produced; confirming them is not implemented.

9goal_correctness

Backing tool: goal_checker

Always SKIPPED: judging whether output is on target needs a model, and no such check is implemented.

9evidence_sufficiency

Backing tool: evidence_checker

Fails when the evidence store (Layer 2) holds too few observations: at least 2 locally, or 5 HIGH/MEDIUM globally. It counts evidence; it does not inspect individual claims.

9output_contract_partial

Backing tool: contract_checker

Checks that a structured output carries the contract's required fields; a plain-text reply always passes.

🤖 Mid-turn steering

A message sent while a turn is still running is absorbed into that turn instead of waiting behind it. Every message ends in exactly one of two places: applied to the answer being written, or run as its own follow-up turn.

Files: live-steering-channel.ts · scope-urgency-classifier.ts · goal-graph-reconcile.ts · agent-loop.ts (steering notes)
Message while a turn runsthe CLI queue is bypassed; the chat-ui composer stays enabled
→
Steering channela FIFO queue, one per session
→
check_caller_updatesone poll per harness iteration
→
Scope × urgency classifierone bounded LLM call; on any doubt: same goal, new task, deferred (INV-41)
SAME_TASK a correction or refinement of what is being written. Becomes a steering note spliced into the proposer's next LLM call as a user turn, so the model applies it. Nothing is matched by keyword.
SAME_GOAL_NEW_TASK · immediate the same: a note for the answer being written now.
SAME_GOAL_NEW_TASK · deferred waits for the current task to finish, then runs as its own turn.
NEW_GOAL pauses the current thread, mints a new READY one in the goal graph, and the ask runs as the next turn.
CANCEL_CURRENT abandons the active thread and blocks the task graph. What you asked for instead (“never mind, just tell me…”) runs as the next turn.
never dropped anything not applied in place, including a note classified after the last LLM call, is handed back to the caller and runs as an ordinary turn, in arrival order.
Status on by default since 2026-09-23 (ASSISTANT_GOAL_GRAPH=disabled restores the old queue-behind-the-turn behaviour). A message is applied to the current answer only if it is classified before the model's next call: on its layer conversation an early correction produced the right answer in one turn, cheaper than queueing. An earlier 11-scenario comparison was graded lexically and has not been reproduced. An earlier design pushed the raw message into the harness's lexically enforced constraint fields; it dropped asks and was replaced by the note-or-follow-up design above.
🤖 Trajectory Supervisor

A slow-loop meta-controller that intervenes only when the loop cannot make progress. One LLM call at the stall edge; it never runs on a healthy iteration (INV-22) and never writes control state (INV-21).

Files: supervisor.ts · trajectory-digest.ts · supervisor-flag.ts · harness-bridge.ts
cannot_make_progress()the stall edge: stalled completion, strategy loop, recurring failure or risk oscillation
→
Trajectory digesta compact summary: failure class, tools tried, recurring records
→
decide() — one LLM calla closed directive set; anything outside it degrades to CONTINUE
→
Directivecarried out by the harness
CONTINUE the fail-safe default — carry on unchanged.
REDIRECT_STRATEGY re-queue the failed task so the proposer tries a different route.
REFRAME_PLAN re-frame the plan around the blocker.
GATHER_EVIDENCE a bounded, read-only workspace investigation (INV-23/24/25); its findings are spliced into the proposer's context and the normal recovery ladder continues. At most three per run, each capped at 20 tool calls (default 5).
ASK_USER asks you one structured question instead of guessing, at most twice per run. With ask mode on (ASSISTANT_ASK_MODE=enabled, off by default) it becomes a resumable question you answer in place. Without it, the question is still shown as the reason the turn stopped, and your answer starts a new turn. Past the cap of two, or with no host wired, it is a plain "could not complete" stop.
ABORT stop and explain why.
Status on by default in Aielia (HARNESS_TRAJECTORY_SUPERVISOR=0 turns it off). The harness library itself still defaults it off; Aielia resolves its own default in supervisor-flag.ts. A turn that never stalls is identical either way.
🤖 Hierarchical goal graph

A persistent goal graph across turns: each root goal is a thread, so concurrent goals, paused work and finished work stay inspectable instead of collapsing into a single plan.

Files: goal-graph-store.ts · goal-thread-identity.ts · goal-identity-matcher.ts · goal-thread-scheduler.ts · next-step-proposer.ts · goal-graph-service.ts
Turn classified as a full turntrivial one-liners and resumed turns (approvals, answers) skip everything below
→
Identity matcher: one LLM calldoes this continue an open thread? only asked when one exists; ambiguous never guesses
→
Attach, or start a threada match brings that thread back into focus (the current one is paused, evidence kept); no match starts a new thread
→
Scheduler keeps one thread in focussole writer of the ACTIVE pointer (INV-39); one ACTIVE thread (INV-38)
→
Turn runs on the ACTIVE threadplan mode and drafting are scoped to that thread
→
Evidence synced back; DONE when every task completesthe thread keeps its task graph and evidence between turns; finishing it triggers the next-step proposer
thread states READY · ACTIVE · BLOCKED · PAUSED · DONE · ABANDONED. PAUSED keeps its partial state and evidence (INV-42); ABANDONED is kept for the audit trail, never deleted.
never selectable a thread still being drafted is never given focus (INV-40); a still-workable ACTIVE thread is never bumped just because others are waiting.
/goals the CLI command and the chat-ui panel show four buckets: freshly computed, carried over, done, and suggested-but-not-committed (drafting).
next steps when a thread reaches DONE, one bounded LLM call proposes follow-ups; high and medium confidence ones are saved on the thread and listed under it in /goals (low confidence is shown for the turn only). They are advice, never tasks.
identity open threads are the candidates (a finished goal is not reopened). A match on a blocked thread keeps it blocked. Ambiguity or a failed call starts a new thread rather than guess, since a missed match costs an extra thread but a wrong one puts work on the wrong goal.
Status on by default since 2026-09-23 (ASSISTANT_GOAL_GRAPH=disabled turns it off; with it off, behaviour is byte-identical to before, INV-43).
🤖 Next-step options

After a full turn, Aielia proposes up to three concrete things you could do next and shows them under the reply, the way Claude Code offers prompt suggestions. They are shortcuts, never a required choice: you can always just type your own message.

Files: next-step-proposer.ts · assistant.ts · cli.ts · NextStepChips.tsx
Full turn completesstatus ok, and not the trivial one-liner fast path
→
Proposer: one bounded LLM callyour request + the reply in, at most three options out, most confident first
→
Shown under the replyCLI: a numbered list · chat-ui: chips under the newest reply
→
You choose, or don'tpick one, or type anything else
CLI type 1, 2 or 3 right after the reply to run that option. Any other input, including a command, drops the options.
chat-ui clicking a chip only fills the composer with that text as an editable draft. It never sends by itself.
typing your own always works, and the composer is never locked to the options.
no options a trivial one-liner, a paused or escalated turn, or a turn with nothing concrete to follow up gets none, and makes no extra call.
cost one extra LLM call per full turn, folded into the turn's usage and spend cap. If it fails, the turn is unaffected and simply shows no options.
Status on by default in the CLI and chat-ui since 2026-09-23 (ASSISTANT_GOAL_GRAPH_SUGGEST=disabled turns it off). The assistant library itself defaults it off, so embedders and benchmarks make no extra call.

Draw it once. Ship it anywhere.

All eleven layers above are drawable, composable node types on the canvas — tested, with architectural invariants permanently enforced as CI gates. Apache 2.0, runs locally via Docker.