Zum Inhalt springen
Docs

Befehlspalette

Zu einer Seite oder Ansicht springen oder die Dokumentation durchsuchen.

Änderungen

Harness incidents and fallbacks

Runbook for support and operators: every fallback reason with symptom, diagnosis and fix, replay with a chosen engine, the AI kill switch, escalations and audit-chain verification.

Markdown

Every agent run ends with an engine (ai or rules) and, if the AI engine was asked for but did not finish the run, a fallback reason. A fallback is not an outage: the rules engine finishes the run, and every proposed write still waits for a person. This page says how to read a fallback, what caused it and what to change. The design is in ADR-0014; how to change tools, models and budgets is in Extending the harness.

Where to look

PlaceWhat it tells you
Runs list /harness/laeufeEngine badge with "Grund für den Fallback: …", status, cost per run, and "KI-Kosten heute" (today's spend against AI_BUDGET_PER_DAY). Filters surface, engine, status, seite, mandant live in the URL; there is no filter by reason
Run detail /harness/laeufe/<id>Header: engine badge and reason, "Angefordert" ("Automatisch" or "Nur Regeln"), "Engine je Phase" ("KI verworfen → Regeln" on a phase the AI lost). Trace: each step with kind, status and raw JSON. "KI-Verbrauch": every model call with tokens and cost, "Antwort verworfen" on a failed one
Ticket page /support/tickets/<id>The latest run's engine and reason; "Automatisch" disabled with a reason when no key is configured
Databaseagent_run.fallback_reason, requested_engine, engine, engine_detail, usage (JSON), ai_cost (generated from usage); agent_step rows (kind, status, title_de, output); agent_audit_event run.finished with after.fallbackReason

The harness writes no log line for an AI failure: the failure is converted to a value and stored in the run. The run row and its steps are the record. Server logs only carry refused procedure calls (console.debug("[orpc]", code, message, { requestId, data }) for expected 4xx errors, with the harness reason in data.reason) and unexpected errors (console.error). The x-request-id response header matches the requestId in those lines.

Useful queries (run them with a connection that is not the app_tenant role, which only sees one Organization):

Fallback reasons and spend today (Europe/Berlin day, as the budget counts it):

select fallback_reason, engine, count(*), sum(ai_cost)
from agent_run
where started_at >= (date_trunc('day', now() at time zone 'Europe/Berlin') at time zone 'Europe/Berlin')
group by 1, 2;

The step that explains a fallback:

select seq, phase, kind, status, title_de, output
from agent_step
where run_id = '<run id>' and kind = 'guardrail'
order by seq;

A step's input and output over 8 KB are stored as { "truncated": true, "preview": "…" }.

How a fallback is recorded

The ticket engine keeps the first reason of a run (fallbackReason ??= reason in harness.engine.ticket.ts). Where it failed decides how much of the run is rules:

  • Triage fails: the reason is set, no step of its own is recorded (except the budget step below), and the whole run continues on rules. The triage step is a "Regelentscheidung".
  • Diagnose or plan fails: a guardrail step with status blocked, phase diagnose or plan, titled KI-Ausgabe verworfen (<reason>): Regel-Engine übernimmt, output { reason, message }. The rules engine re-runs that phase.
  • Command (practice) fails: a guardrail step with status blocked, phase triage, titled KI-Befehl verworfen (<reason>): Regel-Parser übernimmt.

The fallback reasons are FALLBACK_REASONS in packages/db/src/schema/harness.enums.ts: no_key, timeout, invalid_output, guardrail, error, budget_exceeded, kill_switch. Each has one section below. The UI labels are AGENT_FALLBACK_* in apps/web/src/locales/agent.{de,en,fr}.ts.

no_key

Symptom. "Grund für den Fallback: Kein KI-Schlüssel konfiguriert". The ticket page disables "Automatisch" and starts every run as "Nur Regeln", so those runs carry no reason. no_key appears on runs that were requested as "Automatisch" while no key was configured: a run started before the key was removed and executed after, or an "Automatisch" replay.

Diagnosis. agent_run.requested_engine = 'auto', engine = 'rules', fallback_reason = 'no_key', and no AI step in the trace. harness.engine.info returns ai: null. aiSettings() (packages/api/src/platform/ai/ai.settings.ts) returns null when AI_PROVIDER is unset or the matching key (OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY) is empty.

Fix. Set AI_PROVIDER and its key in the Worker's variables and secrets (locally apps/web/.dev.vars) and restart or redeploy. Check that the ticket page offers "Automatisch" again. Never paste the key into a ticket, a trace or these docs.

timeout

Symptom. "KI hat nicht rechtzeitig geantwortet". In "KI-Verbrauch" the timed-out call is not free: without usage from the provider it is charged at the estimate made before it started (marked as estimated).

Diagnosis. Three causes, told apart by the step's output.message:

  • nach <n> ms: one model call hit its cap, 25 s (CALL_CAP_MS in harness.ai.ts) or less if the run had less time left;
  • Zeitbudget des Laufs erschöpft: under 2 s were left of the run's 45 s deadline (set by aiDeps() in harness.procedures.ts when harness.runs.execute starts) before the next call;
  • abgebrochen: the call was interrupted, for example because the request was cancelled.

AI_TIMEOUT_MS does not apply to the harness. Compare the run's duration_ms and the durationMs of its steps: slow tool calls eat the same 45 s.

Fix. One slow answer: replay with "Automatisch". Repeated timeouts: check the provider's status page and latency; a larger model may need a smaller prompt or a faster model. The caps are constants in code, not env vars.

invalid_output

Symptom. "KI-Ausgabe hat die Prüfungen nicht bestanden".

Diagnosis. The answer did not pass a check. Read output.message of the KI-Ausgabe verworfen (invalid_output) step:

  • the answer does not fit the structured-output schema;
  • diagnose: the root cause does not belong to the ticket's category, or an evidence quote does not appear verbatim in the step it cites (the message names the step it does appear in, if any);
  • plan: the actions are not the ones REQUIRED_ACTIONS demands for the root cause (and not a single escalation), a tool input is not valid JSON for its schema, the reply is longer than 180 words, uses du-form, or names another practice;
  • command: the parameters do not fit the intent ("Parameter passen nicht zum Intent").

Every check except a reply that names another practice (that one is final) and a planned tool outside the skill or scope (guardrail) gets one correction retry. A step "KI-Ausgabe korrigiert (1 Versuch)" with status ok means the retry passed and the run stayed on AI; invalid_output means the retry failed too. In "KI-Verbrauch" a rejected call shows "Antwort verworfen".

Fix. A single case: replay with "Automatisch" and compare. A pattern (same check, many runs): run the AI evals on /harness/evals and see which check fails; adjust the skill's instructions_md (re-seed with db:seed:reset-demo) or the model. Do not loosen the checks to make a model pass.

guardrail

Symptom. "Von einer Schutzregel blockiert".

Diagnosis. Set only by the ticket plan check (harness.engine.ticket-ai.ts): the model planned a tool that is not a write tool of the run's skill (<tool> ist nicht freigegeben), or an action whose input targets another practice or ticket (… zielt auf eine fremde Praxis), or, on a ticket with a suspected prompt injection, any action other than the escalation (Verdacht auf Prompt-Injection: aus diesem Ticket wird keine Korrektur geplant; the engine does not ask the model for a plan on such a ticket, so this only guards other callers of aiPlan). The message is in the KI-Ausgabe verworfen (guardrail) step.

Do not confuse it with guardrail flags on a run (prompt_injection_suspected, tool_not_allowed, tool_disabled, target_out_of_scope, input_invalid, too_many_writes, evidence_unverified). Flags are recorded as guardrail steps of either engine and can lead to an escalation without any fallback.

Fix. Usually none: the guardrail did its job and the rules plan is in place. If a tool should be allowed, add it to the skill's tools (Add a skill). If the ticket text tried to steer the agent, leave it to the escalation.

error

Symptom. "KI nicht erreichbar".

Diagnosis. Any other provider failure (AiUnavailable in packages/api/src/platform/ai/ai.structured.ts): network error, an HTTP 4xx or 5xx (wrong key, unknown model, quota, rate limit), a refusal or a stream error. The message is in the step's output.message (up to 300 characters); on a triage failure there is no step, only the reason. Not tested: a read tool that throws inside the model's tool loop most likely also ends as error.

Fix. A rejected key or an unknown AI_MODEL: correct the variable. Quota or rate limit: check the provider account. A provider outage: switch to the rules engine for the duration (see Turning the AI off).

budget_exceeded

Symptom. "KI-Budget erreicht". The runs list's "KI-Kosten heute" tile turns orange from 80 % of the daily budget.

Diagnosis. Before every model call the meter (harness.usage.ts) estimates the call's cost and refuses it if the run total plus the estimate would exceed AI_BUDGET_PER_RUN, or today's spend of the Organization plus the run plus the estimate would exceed AI_BUDGET_PER_DAY (USD, Europe/Berlin day). A refused call is not made and not charged. The trace has a guardrail step with status blocked, titled "KI-Budget des Laufs erreicht: Regel-Engine übernimmt" or "KI-Tagesbudget erreicht: Regel-Engine übernimmt", with output { reason: "budget_exceeded", scope: "run" | "day", spent, estimate, budget, currency }. Today's spend is the sum of agent_run.ai_cost, including the spend rows of AI evals (kind = 'eval', not shown in the runs list). AI evals go through the same meter: a refused call makes that case fall back to the rules with budget_exceeded in its eval result. A call that ended without reported usage (timeout, abort) is charged at its pre-call estimate (unreported: true in usage.calls), so it counts too.

To see what spent today's budget, by kind (eval rows are the AI eval runs):

select kind, count(*), sum(ai_cost)
from agent_run
where started_at >= date_trunc('day', now() at time zone 'Europe/Berlin') at time zone 'Europe/Berlin'
group by kind;

Runs that hit a budget:

select r.id, r.started_at, s.phase, s.output
from agent_step s join agent_run r on r.id = s.run_id
where s.kind = 'guardrail' and s.output ->> 'reason' = 'budget_exceeded'
order by r.started_at desc;

Fix. scope: "day": wait for the next Berlin day or raise AI_BUDGET_PER_DAY. scope: "run": the model is pricier than the run budget allows; raise AI_BUDGET_PER_RUN. If the cost is marked as estimated, the model is missing from MODEL_PRICES and is priced at the high fallback rate: add its price (Swap the model).

kill_switch

Symptom. "Grund für den Fallback: KI deaktiviert". Every harness page and the runs list show the notice "KI deaktiviert · Alle Läufe nutzen die Regel-Engine." with the admin's reason; the run detail header adds KI deaktiviert: <Grund> (von <Admin>, <Zeit>).

Diagnosis. The platform AI kill switch is on (harness_setting.ai_disabled = true, see Turning the AI off). The run has requested_engine = 'auto', engine = 'rules', fallback_reason = 'kill_switch', no model step and no "KI-Verbrauch" (no model call was made, no budget was asked). Its first step is a guardrail step (phase triage, status blocked) titled KI deaktiviert: <reason> (von <admin>, <time>), output { reason: "kill_switch", switch: { reason, by, at } }. Runs requested as "Nur Regeln" carry no reason. The switch also shows up as kill_switch on AI eval results and as AiDisabled (FORBIDDEN) on MCP write proposals.

select id, ai_disabled, ai_reason, ai_changed_at, ai_changed_by_label from harness_setting;

Fix. None while the incident lasts: the rules engine is doing its job. When the cause is gone, the platform admin turns the switch off with a reason (Turning the AI off); the next "Automatisch" run uses the AI again. Runs already finished stay on rules; replay one with "Automatisch" to compare.

A run failed or hangs

Not a fallback, but the same page. Failed: an unhandled error ended the run; the last step is "Lauf abgebrochen" (phase plan, status error, output { error }) and run.finished carries { status: "failed" }. One known cause: a disabled read tool makes the rules engine throw (Werkzeug <tool> ist deaktiviert), so the run fails instead of falling back. Re-enable the tool or replay after fixing the cause. Running for minutes: nothing marks such a run as failed. harness.runs.start ignores a running run older than 2 minutes, so a new "Agent starten" on the ticket works.

Replay with a chosen engine

  1. Open the run

    Go to /harness/laeufe/<runId> (support runs; a replay cannot be replayed again).

  2. Choose the engine

    In the replay panel choose "Nur Regeln" (default) or "Automatisch". Without a configured key the panel adds "Grund für den Fallback: Kein KI-Schlüssel konfiguriert", and an "Automatisch" replay runs on rules with no_key.

  3. Compare

    The replay (harness.runs.replay with { runId, engine: "auto" | "rules" }, then harness.runs.execute) runs the original input on current data. Its actions have the status replay ("Replay (nicht ausführbar)") and cannot be approved; the ticket is not changed. The panel compares root cause, actions, step count, engine and status, and says whether the original cause is gone.

Turning the AI off

The AI kill switch forces every harness run onto the rules engine: support tickets, the Elefant assistant (practice commands), replays, AI evals, and MCP proposals. It is one database row (harness_setting, id platform), not an env var, so it takes effect on the next run without a redeploy and is the same for every Workspace.

QuestionAnswer
Where/harness?tab=richtlinien, section "KI-Notschalter": "KI deaktivieren" / "KI wieder aktivieren" opens a dialog
WhoThe platform admin only (admin@ in the demo). The procedure is admin.harness.aiSwitch.set (adminProcedure): a support or practice session gets FORBIDDEN. Support members see the state read-only ("Nur die Plattform-Administration kann den Notschalter umlegen."); harness.engine.aiSwitch returns it to every member
ReasonRequired both ways, 3 to 300 characters (aiSwitchSetInput); an empty reason is BAD_REQUEST. Flipping to the state it already has is CONFLICT (AiSwitchUnchanged) and writes nothing
AuditEach flip appends ai.disabled or ai.enabled to the support Workspace's hash chain (agent_audit_event, actor = the admin, before/after = { disabled, reason }, target harness_setting), in the same transaction as the change. The template audit log gets harness.ai_switched
What changesThe engine router (routeEngine in harness.engine.ts) reads the switch once per run, before any model call and before the run and daily budget checks. A run requested as "Automatisch" ends on rules with kill_switch and the trace step above. MCP write proposals are refused with AiDisabled; MCP read tools keep working. Runs already executing finish as they started
  1. Turn it on

    Log in as the platform admin, open /harness?tab=richtlinien, press "KI deaktivieren", give the reason (for example "Anbieter-Störung, Ticket INC-42") and confirm. Every harness page now shows "KI deaktiviert" with the reason.

  2. Verify

    Start a run on a ticket with "Automatisch": the run detail shows "Grund für den Fallback: KI deaktiviert", the header line KI deaktiviert: <Grund> (von <Admin>, <Zeit>) and the same text as the first trace step. On /support/audit filter by "KI deaktiviert" and press "Kette prüfen": the entry is there and the chain is intact.

  3. Turn it off

    Same place, "KI wieder aktivieren", with a reason (for example "Anbieter wieder erreichbar"). The notice disappears, the audit gets ai.enabled, and the next "Automatisch" run uses the AI again (no fallback reason when a key is configured).

db:seed:reset-demo (and db:seed) turn the switch off and say so in their output (AI kill switch: OFF); the demo reset also clears the audit chain with its flips.

Last resort without the app. If the app itself cannot be reached, the env-level levers still work, but they need a redeploy and change more than the harness: remove AI_PROVIDER or its key (aiSettings() is null: runs get no_key, AI evals fail with AiNotConfigured, the template assistant is off too), or set AI_BUDGET_PER_RUN=0 / AI_BUDGET_PER_DAY=0 (every model call is refused with budget_exceeded). With database access the row can be set directly (update harness_setting set ai_disabled = true, ai_reason = '…', ai_changed_at = now() where id = 'platform'), but that bypasses the audit chain: prefer the UI.

Disabling a tool (enabled = false on /harness?tab=richtlinien) is not an engine switch: it removes the tool for both engines and for MCP, in every tenant. A disabled write tool is dropped from plans (the agent escalates instead, no_fix_available); a disabled read tool makes runs fail (see above).

Escalations

The agent hands a ticket to a person by proposing support_ticket_escalate (policy confirm) instead of ticket_resolve:

  • no customer practice found, or no category for a ticket that is not p4: reason unclear;
  • prompt injection suspected: guardrail, and the run proposes no fix at all, not even one the diagnosis supports (DEMO-1013 asks to unlock all accounts; not even the one locked account is unlocked). The AI engine does not ask the model for a plan. When the rules engine planned a fix, it is held back and shown as a blocked guardrail step Keine Korrektur vorgeschlagen (Verdacht auf Prompt-Injection): <tools> with the withheld actions in its input. The reply draft only says that a person will follow up;
  • confidence below 0.5: unclear;
  • a guardrail dropped a planned fix: no_fix_available if the tool was disabled or not allowed, otherwise guardrail;
  • the AI plan chose to escalate itself.

The proposal's preview already checks a named assignee against the support Workspace's members: a non-member drops the escalation with the flag input_invalid ("Vorschau für support_ticket_escalate nicht möglich: die zugewiesene Person ist kein Mitglied des Support-Teams"), and an MCP proposal gets AssigneeNotMember back. The agent itself names no assignee. On approval the ticket gets status "Eskaliert", an assignee (by default the Workspace's owner, else an admin), the reason and the note, and the audit event ticket.escalated (ticket.reassigned if it was already escalated). Find them in the inbox with ?status=escalated or "Mir zugewiesen" (?zugewiesen=mir).

People can do the same by hand on the ticket page ("Eskalieren", "Neu zuweisen"; procedures support.tickets.escalate and support.tickets.reassign, any member of the support Workspace). Manual reasons add needs_expertise, customer_request and other (which needs a note). Errors: AssigneeNotMember ("Die gewählte Person gehört nicht zum Support-Team dieses Tickets.": the assignee is not a member, or the Workspace has no lead) and TicketNotEscalated (reassigning a ticket that is not escalated).

If the proposed assignee left the support team before approval, "Freigeben" is refused with AssigneeNotMember and the action stays proposed: reject it and escalate by hand. If an escalation proposal fails to execute otherwise, the action is "Fehlgeschlagen" with "Die Ausführung ist fehlgeschlagen; es wurde nichts geändert.", an action.failed audit event is written, and dependent proposals are "Ersetzt". Escalate by hand.

Approval errors

Message (German UI)Cause
"Vier-Augen-Prinzip: Diese Aktion muss eine andere Person mit Freigaberecht (Lead oder Admin) freigeben."four_eyes action, and the decider is the requester or not an owner/admin (FourEyesRequired)
"Zuerst muss die vorherige Aktion entschieden werden. …"The action depends on one that has not been executed (DependencyPending)
"Diese Aktion wurde bereits entschieden. …"Someone else decided first, or it is a replay action (ActionNotPending)
"Dieses Werkzeug ist in den Richtlinien deaktiviert."The tool was disabled after the proposal (ToolDisabled)
"Das Ziel gehört nicht zur Praxis dieses Tickets."The input targets another practice or ticket (TargetOutOfScope)
"Die gewählte Person gehört nicht zum Support-Team dieses Tickets."An escalation's assignee is no longer a member of the support Workspace (AssigneeNotMember)

These refusals roll back the whole decision. An MCP proposal follows the same rules; the MCP user is its requester.

Verify the audit chain

Every run, proposal, decision, execution, policy change and ticket transition appends one row to agent_audit_event. Rows are chained per Organization: hash = sha256(prev_hash + canonical JSON of the row) (packages/db/src/audit-chain.ts), the first row's prev_hash is 64 zeros, seq is gap-free under a per-Organization advisory lock. A database trigger refuses UPDATE, DELETE and TRUNCATE on the table (the only exception is the demo reset).

  1. Check

    On /support/audit press "Kette prüfen" (support.audit.verify). It recomputes the whole chain of the support Workspace in seq order.

  2. Read the result

    Kette intakt: <n> Einträge geprüft, or Kette unterbrochen bei Eintrag #<seq> (<n> geprüft). A break is a seq gap, a prev_hash that is not the previous row's hash, or a hash that does not match the row's content.

  3. On a break

    Treat it as an incident: the table cannot be changed through the app, so a break means a write outside it. Do not repair rows. Export the rows from the break onwards, compare them with the run, action and ticket rows they reference (run_id, action_id, ticket_id), and find who had direct database access.

Policy changes (policy.updated) are written to the support Workspace's chain, whichever tenant uses the tool.

Auf GitHub bearbeitenDie Dokumentation ist auf Englisch.