Reference

Triage and risk engine

Two-tier risk gating, agent routing, and the final decision that auto-sends or parks a reply for human approval.

Every incoming message goes through the same triage pipeline. The goal is to classify risk, route to the right agent, ground the reply in knowledge bases, and only send automatically when the verdict is green.

Components

  • TriageOrchestrator.processIncoming(ticket) — drives the whole flow.
  • Router.pick(category, requiredSkills) — chooses the best enabled agent.
  • AgentService.executeAgent(agent, turns, { sources }) — builds the prompt and streams the draft.
  • RiskAssessor — Layer 1 deterministic rules and Layer 2 LLM classifier.

Message flow

IncomingMessage
  → persist message + upsert ticket
  → TriageOrchestrator.processIncoming
      ├── RiskAssessor.layer1(text)    → rules + level
      ├── RiskAssessor.layer2(text)    → { category, risk, confidence,
      │                                  reasoning, requiredSkills }
      │     skipped if layer1 = red
      ├── Router.pick(category, requiredSkills) → Agent
      ├── if agent.knowledgeBaseIds.length > 0:
      │     RAG.search(...) → SourceSnippet[]
      ├── AgentService.executeAgent(agent, turns, { sources })
      │     → build prompt (system + language + sources + turn)
      │     → runner.generate(stream) → draft
      ├── save agent draft + risk_assessment
      └── decide by finalLevel:
          ├── green  → send to channel, status = auto_replied
          └── yellow | red → status = waiting_operator
                               → operator Inbox → approve / edit / reject

Layer 1 — deterministic rules

Layer 1 is a fast rule engine that runs in less than 10 ms. Each rule is a pure function that returns { triggered, level, reason }. Rules live in the risk_rules table; built-in rules are loaded at startup and cannot be deleted, only disabled.

Built-in categories:

CategoryLevelExamples
piiyellowemail, international phone
credentialsredsk-…, xoxb-…, JWT, PEM, credit card (Luhn)
destructivereddrop table, rm -rf, "удалить всё"
destructiveyellowdisable, "отключить", "забанить"
financialredrefund, lawsuit, "возврат денег"
financialyellowprice, billing, "цена"
accessredgrant access, make admin, "выдать права"
volumeyellowmessage over 5000 chars, over 10 URLs
profanityyellowlightweight tone screen

Rule types:

  • regex — a RegExp string.
  • keywords — comma-separated list, case-insensitive, word-boundary match.
  • builtin — identifier of a built-in checker (luhn_credit_card, oversize_message, excess_urls, profanity_lite).

Layer 1 result:

{ level: 'green' | 'yellow' | 'red', triggeredRules: [...] }

The level is the maximum of triggered rules. Empty rules → green, and Layer 2 is invoked.

Layer 2 — LLM classifier

If Layer 1 is not red, a small classifier model (default qwen3-0.6b-q4) produces a structured JSON verdict via grammar:

type LLMRiskVerdict = {
  category: 'coding' | 'business' | 'marketing' | 'seo' | 'general' | 'other'
  risk: 'green' | 'yellow' | 'red'
  confidence: number        // 0..1
  reasoning: string         // one-line audit
}

The classifier prompt is stored in risk_rules_config.llm_prompt and can be edited in Settings → Risk & Triage.

Routing

Router.pick scores enabled agents by:

  1. agent.enabled === true.
  2. Exact role match (agent.role === category) or agent.role === 'general' as fallback.
  3. Highest skill intersection with requiredSkills.
  4. Tie-break by last_used_at ASC (round-robin).

Final verdict and decision

finalLevel = max(L1.level, L2.risk)
finalCategory = L2.category ?? firstFiredRule.category ?? 'general'
  • green — reply is sent automatically.
  • yellow or red — reply becomes a ticket in the operator Inbox. A human can approve, edit, or reject before it ever reaches the channel.

Audit

Every verdict is saved to risk_assessments:

  • layer1_result — full Layer 1 JSON.
  • layer2_result — full Layer 2 JSON, or null if skipped.
  • final_level, final_category.
  • created_at.

In the desktop app, open a ticket in the Inbox and expand the Assessment section to inspect the raw verdict.

Configuration

Operators manage risk in Settings → Risk & Triage:

  • Add, edit, enable, or disable rules with a live preview.
  • Edit the Layer 2 classifier prompt.
  • View built-in rules (disabled allowed, deletion blocked).