RFC: AI Agent Simplified Configuration — SOP Steps, Derivation & Lint
Document Conventions (do not remove)
This RFC follows the Qontak RFC Template format for governance — the metadata table, Confluence sections 1–6, and Comment logs are mandatory. It is also agent-execution-ready: §1 Design References + PRD-to-Schema Derivation, §2 Repo Reading Guide for both layers, mermaid diagrams, §2.G Cross-Layer Contract Verification, and the §4 Agent Execution Plan + Verification & Rollback Recipe are complete.
Delivery & project management live elsewhere. This RFC holds no staffing, effort, timeline, or rollout schedule — those live in the initiative's
delivery/artifacts once handed over.
Metadata
| Field | Value | Notes |
|---|---|---|
| Status | IDEA | YAML status: draft (linter enum) |
| DRI | Dimas Fauzi Hidayat | Accountable owner; staffing lives in delivery/ once handed over |
| Team | chatbot (BOT squad) | Advisory, carried from PRD |
| Author(s) | Dimas Fauzi Hidayat (via rfc-starter) | |
| Reviewers | BOT BE + FE engineering leads | Assigned at grooming |
| Approver(s) | BOT tech lead + infosec approver | Assigned at grooming |
| Submitted Date | 2026-07-22 | |
| Last Updated | 2026-07-23 | |
| Target Release | 2026-Q3 | |
| Target Quarter | 2026-Q3 | |
| Delivery | not yet handed to delivery | |
| Related | PRD v1.4 (READY, 9.0/9.0) · D1–D9 decision register · D3 probe brief · Epic BOT-4733 | |
| Discussion | #squad-bot (thread at grooming) |
Type: full-stack · Frontend sub-type: enhancement · Backend sub-type: enhancement
Sections at a Glance
- Overview (Design References — qontak-designer branch; PRD-to-Schema Derivation; traceability + per-story change map)
- Technical Design (topology → ADRs → Repo Reading Guide both layers → diagrams → jsonb model → APIs → matrices)
- High-Availability & Security
- Backwards Compatibility and Rollout Plan (cross-layer matrix, Agent Execution Plan, Verification & Rollback)
- Concern, Questions, or Known Limitations
- Comment logs
- Ready for agent execution
1. Overview
The shipped V2 Autonomous Agent asks tenants to hand-author DSAI wire-model artifacts
(required_slots, milestones + action maps, requires_milestone gates, completion) that
usability research proved incomprehensible (ut-2026-06-23: Milestones = #1 blocker across
all 16 participants). This RFC implements the Simplified Configuration ADJUSTMENT PRD
v1.4: tenants author an SOP — ordered Steps with
per-step WHEN/THEN Exceptions — and Chatbot BE derives and lints the wire-model fields
at sync time. The DSAI skill_pack wire contract is byte-compatible and unchanged; this
is a producer-side + FE + linter delta behind org flag ai_agent_simplified_config, plus a
new enum-exit handler at runtime. This is a delta spec on the shipped V2 engine
(anchor: Autonomous AI Agent — ANCHOR).
Success Criteria
- ⭐ Self-serve config rate ≥ 40% of new agents within 60 days of GA (baseline near-zero — PRD §12).
- Validation task test: ≥ 3 of 4 non-technical participants complete a 2-step agent unassisted (pre-launch gate).
- Runtime incidents from the 5 known misconfig classes → 0 for flag-ON orgs (lint catch).
- Derivation + lint overhead ≤ 500ms p95 on a 10-capability agent (no perceptible save regression).
Out of Scope
Mirrors PRD §5 Non-Goals: no skill_pack wire-format or DSAI validator change; no Refine
extension to the Steps model (Refine is disabled for flag-ON orgs — CHG-007; reconciliation
is a follow-up phase); no Tier-2 overrides; no V1 (tree) agent migration; no new action
executors; no visual flow map.
Related Documents
PRD v1.6 · D1–D9 grilling register · D3 probe brief · RFC gap analysis 2026-07-04 · PIH research chatbot/ai-agent-configuration/ut-2026-06-23/ + ut-2026-07/ (Iteration II — Steps direction validated Strong; prompt-hierarchy = new #1 risk) · design: qontak-designer branch feat/ai-agents-ut-updates
Assumptions
- The D3 staging probe result gates only the Resolve THEN option; both outcomes have a defined path (mint vs cut) — see probe brief.
- The design branch's drawer UI passes its pending visual verification without capability-model changes (its
docs.mdlists browser verification as not-yet-done). - DSAI validators keep operating unchanged on machine-generated field values (wire byte-compatible).
- The
/v1/actionsbackend for the real actions pool is a separate deliverable; AISC-S07 degrades gracefully until it ships.
Dependencies
| Dependency | Layer | Status | Owner |
|---|---|---|---|
qontak-designer feat/ai-agents-ut-updates (Steps model reference + exception-model restructure per change-request brief) | FE | exists — visual verification pending (quality gate, non-blocking) | Design (Wulan) |
| D3 staging probe (exit-reason relay + omitted-completion tolerance) | BE | to run — blocks Resolve THEN option only | BOT BE |
/v1/actions real actions pool | FE/BE | needs building (separate deliverable); ACTION_FALLBACK degradation until then | BOT |
| DSAI runtime | — | observational only — no DSAI build work in this RFC | Data & AI |
Design References (frontend half)
Design lives in the qontak-designer prototype repo, not Figma — the branch is the frame set.
| PRD-named surface | Design link | Frame name | Design system version | Design QA contact | Notes |
|---|---|---|---|---|---|
| Capability drawer — merged Instructions & Steps card | feat/ai-agents-ut-updates app/pages/bot-automation/ai-agents/[id].vue | Instructions & Steps card | Mekari Pixel 3 (@mekari/pixel3-autocomplete@0.0.28 verified on branch) | Wulan | drag-reorder step cards + MpAutocomplete action picker, as built. Plus prompt-hierarchy helper copy (PRD CHG-001, from ut-2026-07: hierarchy overlap = new #1 risk; Instructions-vs-Steps redundancy High) — copy: Instructions = always-on rules; Steps = the ordered procedure Qontak compiles |
| Per-step Exception WHEN/THEN cards | same branch, step exception sub-list | Step exceptions | Pixel 3 | Wulan | restructure required — structured WHEN (action + matchers), closed THEN incl. assign-to-human; drop switch_action/other (D8). Change-request brief owns this + THEN raised to equal visual weight with WHEN (ut-2026-07 finding 4: users read only "When" and leave rules half-configured) |
| Capability card (read-only, click-to-edit) | same branch, capability card | Capability card | Pixel 3 | Wulan | goal text + ≤4 keyword chips + +N, as built |
| Refine rail "coming soon" state (flag-ON) | n/a — design pending | — | Pixel 3 | Wulan | net-new state in the change-request brief; §5 Open Questions |
PRD-to-Schema Derivation (backend half)
No new tables. Everything lands in the existing ai_agents.parameters jsonb and the emitted skill_pack.
| PRD-described entity / attribute / rule | Persisted as | Exposed via | Enforced where | Source |
|---|---|---|---|---|
Capability = SOP: {title, keywords, goal, instructions, steps[]} | ai_agents.parameters -> 'capabilities' (new keys steps[], renamed keywords/goal) | POST/PATCH /v2/ai_agents (extended payload) | Grape params in ai_agents_controller.rb + SyncToAiService | PRD §7 CHG-001 |
Step exceptions {when: {action, body_contains?, error_code?}, then} | parameters -> 'capabilities' -> [] -> 'steps' -> [] -> 'exceptions' | same endpoints | compiled to pack-level routing_rules by build_routing_rules | PRD §7 CHG-004 |
Derived required_slots / milestones / milestone_action_map / requires_milestone / completion | not persisted — derived at sync time from the SOP | emitted in skill_pack push | new derivation module inside SyncToAiService | PRD §7 CHG-002, AISC-S02 |
| 6-check save-time linter | — (behavior) | sync abort with capability-named error | new lint module inside SyncToAiService, before push | PRD §7 CHG-003, AISC-S03 |
Assign-to-human target {type: agent|division, id} | parameters -> 'routing' -> [] -> 'target' | POST/PATCH /v2/ai_agents routing params (:243, :288) | new enum-exit handler in ProcessAsyncReply | PRD AISC-S05 |
| Derive-on-save non-destructiveness (authored fields preserved) | authored keys in parameters are never deleted | flag-OFF passthrough re-reads them | SyncToAiService branch on flag | PRD AISC-S08/AC-5 |
Flag ai_agent_simplified_config (default OFF) | system_preferences row (group_code: 'rollout') | existing preference lookup | FE editor branch + BE sync branch | PRD §6 |
| Refine disabled for flag-ON orgs | — (behavior) | POST /v2/ai_agents/:id/refine returns 403 for flag-ON orgs | guard in refine endpoint (ai_agents_controller.rb:404) | PRD §7 CHG-007 |
Detail 1.A — PRD Traceability (cross-layer)
Forward (PRD AC → RFC):
| PRD composite AC id | FE section / component | BE section / endpoint |
|---|---|---|
| AISC-S01/AC-1..3, ERR-1 | §2.A drawer contract; useAgentStore/useSaveAgent rework | §2.4 extended payload params |
| AISC-S02/AC-1..5, ERR-1, NEG-1 | n/a — BE-only | §2 ADR-1/2, Derivation module spec (§2.C.1) |
| AISC-S03/AC-1, ERR-1..6 | lint error rendering in drawer | Lint module spec (§2.C.2) |
| AISC-S04/AC-1..3, ERR-1 | exception WHEN/THEN cards + matcher inputs | build_routing_rules compilation (reused) |
| AISC-S05/AC-1..4, ERR-1..2 | THEN dropdown (closed) | Enum-exit handler (§2.C.3) |
| AISC-S06/AC-1..3 | drawer rebuild; task-test = validation gate | n/a — FE-only |
| AISC-S07/AC-1, ERR-1 | pickers wired to actions pool + degradation | /v1/actions (separate deliverable) |
| AISC-S08/AC-1..5, ERR-1, NEG-1 | "checkpoints recalculated" notice | derive-on-save branch + dry-run task (§2.C.4) |
| AISC-NEG-01..03 | dropdown contents; refine rail gating; V1 read-only | refine 403 guard; flag branch |
Reverse (RFC → PRD AC): every module in §2.C serves the AC ids listed beside it; no RFC-introduced surface lacks a PRD anchor.
UI / Consumer Surface Coverage
| PRD-named surface | Consumer | Required reads (BE) | Required writes (BE) | FE component | Status surface |
|---|---|---|---|---|---|
| Capability drawer (Steps card) | web | GET /v2/ai_agents/:id | POST/PATCH /v2/ai_agents | AiAgentEditor.vue drawer (rebuilt) | lint error envelope; save success |
| Step exception cards | web | same | same | new exception sub-components | compiled rule visible in saved config |
| Routing tab (intent + catch-all only) | web | same | same | existing Routing tab (action_error WHEN removed) | — |
| Refine rail (flag-ON: hidden) | web | flag via /v1/system_preferences | — | AiAgentEditorRail.vue gate | "coming soon" state |
| Runs at runtime (assign/resolve outcomes) | chatbot runtime | — | room assignment/resolve | n/a — BE | ai_agent_exit_reason_applied event |
Role Coverage
| PRD role | Authorization mechanism | Endpoints permitted (BE) | UI surface visibility (FE) | Cross-tenant? | Audit trail |
|---|---|---|---|---|---|
| Tenant owner / supervisor / admin | JWT session + set_role(%w[owner supervisor admin]) (ai_agents_controller.rb, POST / block) | POST/PATCH /v2/ai_agents, /refine (flag-OFF only) | full editor | no — org-scoped | existing request logging + PRD §11 events |
| Roles without bot-automation access | same guard | none | module not visible (unchanged) | no | n/a |
| DSAI runtime (system) | internal service auth (existing) | inbound async reply (internal_service v1) | n/a | no | ai_agent_exit_reason_* events |
PRD Section Coverage
| PRD section # | Title | Where covered |
|---|---|---|
| 2 | CB: Adjustment Context | §1 Overview + Out of Scope |
| 3–4 | One-liner/Problem · Personas | §1 Overview (delta spec) |
| 5 | Non-Goals | §1 Out of Scope |
| 6 | Constraints | §2 ADR-6 (perf budget), §4.B config contract, §3 |
| 7 | Feature Changes CHG-001..007 | §2.C modules + §2.A UI contract |
| 8 | API & Webhook Behavior | §2.4 |
| 9 | Stories + ACs | Detail 1.A / 1.C |
| 10 | Rollout | §4 |
| 11 | Observability | §3 Monitoring |
| 12 | Success Metrics | §1 Success Criteria |
| 13 | Launch Plan | n/a — delivery layer owns scheduling |
| 14 | Dependencies | §1 Dependencies |
| 15 | Key Decisions | §2 Technical Decisions (ADR) |
| 16 | Open Questions | §5 |
Detail 1.B — Decisions Closed (cross-layer)
Summary index — full ADR blocks in §2 Technical Decisions.
| Decision | Chosen option | Alternatives rejected | Why rejected | Layer |
|---|---|---|---|---|
| Gate derivation substrate | Chain gates from SOP step order (D1) | data-flow inference; name-matching heuristic | unimplementable (args carry no cross-action links); silently wrong | BE |
| Completion | Derived = last action of last actioned step; omitted for knowledge-only (D4) | authored dropdown | fights shipped design; derivation is the PRD's own philosophy | BE |
| Pattern A home | Per-step Exceptions only; Routing tab = intent + catch-all (D5) | global Routing action_error rules | recreates researched "same work three times" duplication | both |
| Migration | Derive-on-save, non-destructive (authored keys preserved) + batch dry-run (D2) | one-time forced re-derivation; destructive overwrite | changes live bots without tenant action; kills flag-OFF rollback | BE |
| Resolve THEN | Probe-then-BE-mint resolve_conversation; cut on enum enforcement (D3) | formal DSAI contract change | cross-team dependency against standing rule | BE |
| Refine | Disabled for flag-ON orgs via 403 guard (CHG-007) | teach SkillPackBuilder the Steps model now | doubles MVP scope; reconciliation is its own phase | both |
| Linter severity | All 6 checks hard-fail the sync (incl. check ⑥ collision) | warn-only for legacy data | dry-run pre-briefs support; deterministic beats lenient (closes PRD §16 row 2) | BE |
| Steps/exceptions caps | Max 20 steps/capability, 5 exceptions/step, validated at save | uncapped | LLM context budget + UI usability; closes PRD known gap | both |
| Flag topology | Single BE system_preferences rollout flag read by both layers | separate FE flag | two flags can disagree mid-rollout | both |
| Endpoints | POST/PATCH /v2/ai_agents extended (new optional keys); zero new public endpoints | new versioned endpoint | payload-compatible extension suffices; wire unchanged | BE |
Detail 1.C — Per-Story Change Map
| Story id | Title | Layer scope | FE changes | BE changes | Composite AC ids | Acceptance criteria (verifiable) | RFC anchors |
|---|---|---|---|---|---|---|---|
| AISC-S01 | Configure capability as SOP | FE + BE | drawer rebuild: Steps card, drag-reorder, multi-action picker, maxlength=10000 (net-new); useAgentStore/useSaveAgent new shape | Grape params accept steps[]/keywords/goal; persist to parameters | S01/AC-1..3, ERR-1 | vitest specs for store shape + payload builder pass; drawer renders no wire-model field | §2.A · §2.4 · §4.D chunks 5–6 |
| AISC-S02 | BE derives wire fields | BE-only | n/a | new SkillPackDerivation module in SyncToAiService | S02/AC-1..5, ERR-1, NEG-1 | rspec: derivation unit specs incl. knowledge-only + skipped-step chain + p95 budget spec | §2 ADR-1/2 · §2.C.1 · §4.D chunk 2 |
| AISC-S03 | Save-time linter | FE + BE | render lint error envelope in drawer | new SkillPackLint module (6 checks), abort before push | S03/AC-1, ERR-1..6 | rspec: 6 failing fixtures each abort sync, no partial push; FE shows capability-named error | §2.C.2 · §4.D chunks 3, 6 |
| AISC-S04 | Per-step exception (Pattern A) | FE + BE | exception cards: structured WHEN (action + body_contains/error_code inputs), closed THEN | compile exceptions → params['routing'] shape consumed by build_routing_rules (reused) | S04/AC-1..3, ERR-1 | rspec: exception compiles to exact rule JSON; Routing tab offers no action-failure WHEN | §2.4 · §2.C.1 · §4.D chunk 5 |
| AISC-S05 | Enum-exit handling | BE-only | THEN dropdown options only | new EnumExitHandler in ProcessAsyncReply; target from parameters.routing; probe-gated resolve_conversation | S05/AC-1..4, ERR-1..2 | rspec: handoff reason assigns stored target; unmapped reason → fallback + log; stale target → default path | §2.C.3 · §2.2 seq 2 · §4.D chunk 4 |
| AISC-S06 | Steps editor comprehension | FE-only | drawer rebuild is the deliverable; 3-tab structure untouched (regression guard) | n/a | S06/AC-1..3 | vitest: 3 tabs unchanged; no milestone artifact rendered; AC-3 = validation-session gate (not CI) | §2.A · §4.D chunk 6 |
| AISC-S07 | Actions pool in pickers | FE + BE | pickers read real pool; degrade to shipped 4-item set with visible flag | /v1/actions — separate deliverable | S07/AC-1, ERR-1 | vitest: degradation renders flagged fallback, never silent empty | §1 Dependencies · §4.D chunk 7 |
| AISC-S08 | Derive-on-save migration | BE-only (+notice in FE) | "checkpoints recalculated" notice | flag branch in sync (passthrough vs derive); authored keys preserved; DryRunLintTask batch job | S08/AC-1..5, ERR-1, NEG-1 | rspec: flag-OFF passthrough byte-identical pack fixture; derive-save preserves authored keys; dry-run exports would-fail CSV | §2.C.4 · §2.2 seq 3 · §4.D chunks 2, 8 |
| AISC-NEG-01..03 | Guard rails | FE + BE | dropdown contents; refine rail hidden (flag-ON); V1 agents untouched | refine 403 guard; flag branch | NEG-01/NEG-1 · NEG-02/NEG-1..2 · NEG-03/NEG-1 | vitest + rspec: dropdown enum snapshot; 403 spec; V1 path untouched spec | §2.4 · §3 Role×Endpoint · §4.D chunk 9 |
2. Technical Design
Infrastructure Topology (start here)
No new infrastructure. Every component below already runs in production; this RFC
changes code inside the chatbot Rails pods and the chatbot-fe bundle only. No new
queue, cache, database, or external API is introduced (the batch dry-run rides the
existing Sidekiq worker pool).
Deployment topology
flowchart TB
internet([Tenant admin browser]) -->|HTTPS| lb[Load Balancer / API Gateway]
lb -->|HTTP| fe["chatbot-fe (Nuxt SSR/static)"]
lb -->|HTTP| pods["chatbot Rails pods xN (frontend_service v2)"]
pods -->|read / write jsonb| db[("Postgres — ai_agents.parameters, system_preferences")]
pods -->|enqueue dry-run| queue[["Sidekiq"]]
queue -->|consume| workers["chatbot worker pods xM"]
workers -->|read-only lint| db
pods -->|"HTTPS push skill_pack (AI_SERVICE_ALICLOUD_BASE_URL)"| dsai(["DSAI qontak-ai-noncore-mrag"])
dsai -->|"async reply (internal_service v1)"| pods
Per-service responsibility
flowchart LR
subgraph chatbot_be["chatbot (Rails, BOT squad)"]
ep1["POST/PATCH /v2/ai_agents — save config (extended payload)"]
ep2["POST /v2/ai_agents/:id/refine — 403 for flag-ON orgs"]
uc1["SyncToAiService: derive + lint + push"]
uc2["ProcessAsyncReply: enum-exit handler"]
uc3["DryRunLintTask: batch report-only lint"]
end
subgraph chatbot_fe["chatbot-fe (Nuxt, BOT squad)"]
ui1["Capability drawer: Steps card + exceptions"]
ui2["Refine rail gate"]
end
ui1 --> ep1
ui2 --> ep2
ep1 --> uc1
uc1 -->|"HTTPS — skill_pack, wire unchanged"| dsai(["DSAI (Data & AI squad) — observational only"])
dsai -->|"async reply exit.reason"| uc2
uc3 -->|"report CSV"| support(["Support workspace"])
Technical Decisions (ADR-format)
Decision 1: Derivation is producer-side, at sync time, unpersisted
Context — The tenant no longer authors required_slots/milestones/gates/completion;
something must produce them for the unchanged wire. Where that happens determines
migration risk and rollback cost.
Options considered
- Option A — derive inside
SyncToAiServiceat sync time, never persist derived values: Pros: single producer choke-point (build_skillalready assembles the pack); flag-OFF rollback trivial (authored keys untouched); no schema change. Cons: derivation cost on every save (bounded by ADR-6 budget); derived values not directly inspectable in DB. - Option B — persist derived fields into
parameterson save: Pros: inspectable. Cons: two sources of truth; destructive to authored keys or doubles the jsonb; flag-OFF rollback needs a restore path. - Option C — derive in FE before send: Pros: zero BE change. Cons: puts wire-model knowledge back in the client — the exact coupling this project removes; unverifiable server-side.
Decision: Option A.
Rationale — build_skill (sync_to_ai_service.rb:123-137) is already the single
assembly point; deriving there keeps exactly one producer of the wire and makes AISC-S08's
non-destructive guarantee structural rather than procedural.
Consequences — Debugging a derived pack requires the (new) ai_agent_config_derived
event payload or a console re-derivation, not a DB row.
Reversibility — Remove the derivation branch; flag-OFF orgs were never touched. Cost: hours.
Decision 2: Chain gates from SOP step order (D1)
Context — v1.0's "derive gates from data flow" was unimplementable: action args are
{name, type, description} triples (build_action_args :445-484) with no cross-action links.
Options considered
- Option A — chain gates from step order: each actioned step gated on the previous actioned step's
<action>_succeeded; steps without actions are skipped in the chain. Pros: deterministic; tenant-authored (drag order); linter checks ①② pass by construction. Cons: over-sequences genuinely independent actions. - Option B — name-matching heuristic (arg name == upstream output field): Pros: no UI semantics change. Cons: silently wrong on naming coincidences.
- Option C — no gates: Pros: simplest. Cons: premature completion-tool fire returns — the exact production failure mode gates exist to prevent.
Decision: Option A.
Rationale — The Steps model makes execution order an explicit tenant decision; the derivation reads it instead of guessing. Multi-action steps: actions within one step are mutually ungated; the step boundary carries the gate.
Consequences — A tenant wanting parallel independent actions must put them in one step. Documented in the editor copy; Tier-2 manual gates remain the future escape hatch.
Reversibility — Derivation-internal; change the gate function, re-sync on next save. Cost: hours.
Decision 3: Non-destructive derive-on-save (D2 + AISC-S08/AC-5)
Context — Existing V2 agents carry hand-authored fields; a flag flip must never change a running bot, and flag-OFF after re-derivation must restore prior behavior.
Options considered
- Option A — preserve authored keys in
parametersforever; derived values exist only in the emitted pack: Pros: flag-OFF passthrough re-reads originals; zero data migration; dry-run is read-only. Cons: stale authored keys linger (accepted; logged divergence). - Option B — one-time batch re-derivation: Cons: changes live bots without tenant action — rejected in grilling (D2).
- Option C — snapshot table for authored config: Cons: new DDL + sync complexity for the same guarantee Option A gets free.
Decision: Option A. Rationale — cheapest guarantee that satisfies AC-5 exactly. Consequences — divergence risk after flag-OFF-then-ON cycles is accepted and logged. Reversibility — n/a (this is the reversibility mechanism).
Decision 4: Enum-exit handler in ProcessAsyncReply + target in parameters.routing (D3)
Context — ProcessAsyncReply routes only on exit.id tree-edges (:159-161); the RFC
§6.1 exit carries no target field; "assign to human" has no runtime effect today.
Options considered
- Option A — BE-side handler keyed on
exit.reason, target stored Rails-side inparameters.routing[].target: Pros: zero wire change; routing params already accepted (ai_agents_controller.rb:243, :288) andparametersjsonb already holds config (ai_agent.rb:38); one handler serves handoff + probe-gated resolve + unknown-reason fallback. Cons: reason semantics depend on DSAI relaying rule exits verbatim — hence the D3 probe. - Option B — extend the wire so the exit carries the target: Cons: DSAI contract change; against the standing dependency rule; unnecessary given Option A.
Decision: Option A; resolve_conversation minted only if the probe confirms verbatim
relay, else the Resolve THEN option is cut (both paths pre-decided — D3/D8).
Consequences — Unknown reasons must never crash: fallback intent + ai_agent_exit_reason_unmapped log/event is part of the handler contract.
Reversibility — Handler is additive after the exit.id branch; delete to revert. Cost: hours.
Decision 5: Refine disabled for flag-ON orgs via server-side 403 (CHG-007)
Context — Refine shipped end-to-end during PRD drafting (BE post '/refine'
ai_agents_controller.rb:404, 2026-07-10; FE useRefineAgent.ts, 2026-07-14). Its pack
builder (use_cases/mappers/skill_pack_builder.rb) reads authored fields and cannot serve
derived agents unchanged.
Options considered
- Option A — server-side flag guard returning 403 + FE rail hidden: Pros: smallest change; flag-OFF orgs untouched; defense in depth (FE hide alone is bypassable). Cons: flag-ON orgs temporarily lose a live feature (explicit product decision, PRD Non-Goal 2).
- Option B — teach
SkillPackBuilder+refine_skill_pack_mergerthe Steps model now: Cons: doubles MVP scope; reconciliation deserves its own phase (PRD §16 row 7).
Decision: Option A. Consequences — the second producer is neutralized rather than migrated; the follow-up phase owns reconciliation. Reversibility — remove the guard.
Decision 6: Linter is synchronous, hard-fail, budgeted (closes PRD §16 row 2)
Context — Six deterministic checks must run before any push; severity for check ⑥ (cross-capability collision) was left open in the PRD.
Options considered
- Option A — all 6 checks hard-fail the sync, synchronously, within the 500ms p95 combined budget: Pros: deterministic; no partial pack ever pushed (S03/ERR-6); the batch dry-run (report-only mode of the same module) pre-briefs support so legacy hard-fails are never a surprise. Cons: a legacy agent with a collision cannot re-save until fixed.
- Option B — warn-only for legacy-origin data: Cons: two severity regimes; a warned collision still produces ambiguous runtime routing — the failure mode we exist to kill.
Decision: Option A. Consequences — support playbook (from the dry-run would-fail list) is a rollout prerequisite. Reversibility — per-check severity is data, not architecture; downgrade a check in config if field evidence demands.
Minimum-coverage checklist: Storage — existing
ai_agents.parametersjsonb, no new tables (ADR-1/3). Sync vs async — derivation+lint synchronous in save (budgeted); batch dry-run async on Sidekiq (ADR-6, §2.C.4). Caching — n/a, no new cache; registry lookups keep the existing@registry_properties_cachememoization. Third-party — DSAI push unchanged (reused client,AI_SERVICE_ALICLOUD_BASE_URL). Consistency — the wholeskill_packis regenerated per sync (already the model); no partial updates. Multi-tenancy — org-scoped flag + org-scoped agents; no cross-tenant surface added. Reuse vs new — zero new public endpoints; payload-compatible extension only (Detail 1.B).
Detail 2.0 — Repo Reading Guide (read this first)
Repo Map (mermaid, both layers)
flowchart LR
subgraph fe["chatbot-fe/"]
editor["modules/bot-automation/components/AiAgentEditor.vue"]
save["modules/bot-automation/composables/useSaveAgent.ts"]
refinefe["modules/bot-automation/composables/useRefineAgent.ts"]
consts["modules/bot-automation/constants/"]
svc["common/services/main/v2/bot-automation-agents.ts"]
end
subgraph be["chatbot/app/api/frontend_service/v2/ai_agent/"]
ctrl["ai_agents_controller.rb"]
sync["repositories/sync_to_ai_service.rb"]
refinebe["repositories/refine.rb + use_cases/mappers/skill_pack_builder.rb"]
end
subgraph runtime["chatbot/app/api/internal_service/v1/ai_agent/"]
reply["use_cases/process_async_reply.rb"]
end
editor --> save --> svc --> ctrl --> sync
refinefe --> ctrl --> refinebe
sync -->|skill_pack| dsai(["DSAI"])
dsai --> reply
Existing Code Anchors
| Layer | Path | Why the agent reads it | What pattern it teaches |
|---|---|---|---|
| BE | chatbot/app/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service.rb | the single pack producer this RFC extends | build_skill_pack :95 / build_skill :123-137 / build_milestone_action_map :408 / gate tap :432 / args_with_unresolved_type? :439-443 / build_completion :506-518 (nil + .compact) / build_routing_rules :520-526 + when/then :528-546 |
| BE | chatbot/app/api/internal_service/v1/ai_agent/use_cases/process_async_reply.rb | where the enum-exit handler lands | contract validation :46 (optional(:exit).maybe(:hash)); exit.id routing :159-161; resolve-by-reference :182 |
| BE | chatbot/app/api/frontend_service/v2/ai_agent/ai_agents_controller.rb | payload params + refine guard site | routing params :243, :288, :305; set_role(%w[owner supervisor admin]); post '/refine' :404 |
| BE | chatbot/app/models/ai_agent.rb · app/models/system_preference.rb | storage + flag pattern | parameters->'capabilities' jsonb :38; rollout flag find_by(code:, group_code: 'rollout', enabled: true) :42-48 |
| BE | chatbot/app/api/frontend_service/v2/ai_agent/use_cases/{create,update}_ai_agent.rb | sync invocation + failure surfacing (Dry::Matcher) | where lint failure becomes an editor-visible error |
| BE | chatbot/.../use_cases/mappers/skill_pack_builder.rb + repositories/refine.rb | the second producer neutralized by CHG-007 | routing_rules :35; consumed at refine.rb:39 |
| FE | chatbot-fe/modules/bot-automation/composables/useSaveAgent.ts | the payload builder to rework | buildSkill() emits authored fields :238-284 — the exact keys flag-ON saves must stop sending |
| FE | chatbot-fe/modules/bot-automation/components/AiAgentEditor.vue | the drawer to rebuild | 3 tabs :3056-3060 / TabKey :2948 (unchanged); instruction textarea :2157 (no maxlength today) |
| FE | chatbot-fe/modules/bot-automation/constants/bot-automation-actions-constants.ts + modules/ai-agent/constants/ai-agent-actions-constants.ts | the duplicated 4-item ACTION_LIST to retire together | node_types at :8/:139/:249/:359 and :77/:203/:308/:418; ACTION_FALLBACK at common/services/main/v1/bot-automation-actions.ts:99 |
| FE | chatbot-fe/common/services/main/v1/system-preferences.ts + common/contants/request-payload.ts | FE flag consumption pattern | how the editor branches on ai_agent_simplified_config |
| Design | qontak-designer branch feat/ai-agents-ut-updates: app/composables/bot-automation/useAgentStore.ts + app/pages/bot-automation/ai-agents/[id].vue + docs.md | the Steps model reference | CapabilityStepData/CapabilityStepException shapes; exception restructure needs (D8) |
Existing Contracts to Reuse, Extend, or Replace (BE)
| Contract | Status | Justification | Owner |
|---|---|---|---|
POST /v2/ai_agents · PATCH /v2/ai_agents/:id | extended — payload gains optional capabilities[].steps[] (+ keywords/goal aliases); authored wire-model keys become optional | payload-compatible; zero consumers break | BOT |
POST /v2/ai_agents/:id/refine | extended — flag guard returns 403 for flag-ON orgs | CHG-007 | BOT |
skill_pack push to DSAI (AiService::AiAgent, AI_SERVICE_ALICLOUD_BASE_URL) | reused — byte-compatible | the whole point (PRD Non-Goal 1) | BOT → DSAI |
| DSAI async reply (internal_service v1) | reused — same schema; new handling of exit.reason | additive handler | BOT |
pack routing_rules via build_routing_rules | reused — exceptions compile into the existing params['routing'] shape | wire already supports {action_error, body_contains, error_code} | BOT |
/v1/actions real pool | new-with-justification — the 4-item hardcode cannot express the tenant pool; separate deliverable, this RFC only consumes with degradation | AISC-S07 | BOT |
Patterns to Follow
| Layer | Concern | Pattern in repo | Reference file | Deviation? |
|---|---|---|---|---|
| BE | Repository shape | ::Repositories::AbstractRepository subclass with call | sync_to_ai_service.rb:7,35 | none — new modules live inside/beside it |
| BE | Result surfacing | Dry::Matcher::ResultMatcher in controller | ai_agents_controller.rb POST block | none — lint failure rides the existing failure branch |
| BE | Flag lookup | SystemPreference.find_by(code:, group_code: 'rollout', enabled: true) | system_preference.rb:42-48 | none |
| BE | Logging | Rails.logger.error("V2 SyncToAiService …") prefix style | sync_to_ai_service.rb:50,178 | none |
| FE | Payload building | composable buildSkill() → typed service payload | useSaveAgent.ts:238 · bot-automation-agents.ts:52-67 | type + builder reshaped (Steps) |
| FE | Component library | Mekari Pixel 3 (MpAutocomplete, MpDrawer) | design branch [id].vue | none |
| Cross | snake_case API ↔ FE mapping | service-layer types in common/services/main/v2/ | bot-automation-agents.ts | none |
Reading Order for the Agent
../prds/simplified-configuration.md— the contract this RFC implements (stories + ACs).chatbot/.../repositories/sync_to_ai_service.rb— the producer you extend; read:95-137,:400-546closely.chatbot/.../use_cases/update_ai_agent.rb— how sync results surface to the editor.chatbot/app/api/internal_service/v1/ai_agent/use_cases/process_async_reply.rb— where the enum-exit handler slots in (:150-200).chatbot/.../ai_agents_controller.rb— params blocks:243+, refine:404.chatbot-fe/modules/bot-automation/composables/useSaveAgent.ts— the payload you reshape.chatbot-fe/modules/bot-automation/components/AiAgentEditor.vue— the drawer you rebuild (large file; read the drawer + tabs regions).qontak-designerbranchdocs.md+useAgentStore.ts— the Steps model reference + its known gaps.chatbot/.../use_cases/mappers/skill_pack_builder.rb— the producer you must NOT extend (CHG-007 neutralizes it).../documentations/d3-staging-probe-brief.md— the probe that gates the Resolve option.
Source Verification (anti-hallucination)
| Layer | Anchor / contract | Verified by | Evidence |
|---|---|---|---|
| BE | sync_to_ai_service.rb builders | read 2026-07-22 | required_slots :134, slot_action_args_map :135, build_milestone_action_map :408, gate .tap :432, build_completion :506 returns nil on blank, build_routing_rules :520 maps params['routing'], when/then :528-546 |
| BE | process_async_reply.rb | read 2026-07-22 | optional(:exit).maybe(:hash) :46; exit_data[:id].present? :160; qontak_chat_room_resolve reference check :182 |
| BE | controller routing + refine | grep/read 2026-07-22 | optional :routing, type: Array at :243, :288; params[:routing] passthrough :253, :305; post '/refine' at :404 (landed 2026-07-10, BOT-4594) |
| BE | ai_agent.rb jsonb · flag model | grep 2026-07-22 | jsonb_array_elements(COALESCE(parameters->'capabilities' …)) :38; find_by(code: 'unified_billing', group_code: 'rollout', enabled: true) :42 |
| BE | second producer | grep 2026-07-22 | skill_pack_builder.rb:35 routing_rules: build_routing_rules; refine.rb:39 UseCases::Mappers::SkillPackBuilder.call |
| FE | useSaveAgent.ts payload | subagent read 2026-07-22 | description :238, triggers :239, milestones :240, required_slots :242, per-action milestone :265, milestone_action_map :267, completion :275; type BotAutomationAgentSkill at bot-automation-agents.ts:52-67 |
| FE | editor tabs + textarea | subagent read 2026-07-22 | tabs array AiAgentEditor.vue:3056-3060 (3 tabs); TabKey :2948; instruction textarea :2157 no maxlength; CreateAiAgent.vue:139 has the only :maxlength="10000" |
| FE | ACTION_LIST ×2 + fallback | subagent read 2026-07-22 | bot-automation-actions-constants.ts:6 (consumed by EditCapabilityActionDrawer.vue et al.); ai-agent-actions-constants.ts:75; ACTION_FALLBACK 3 items bot-automation-actions.ts:99 |
| FE | refine consumer | grep 2026-07-22 | useRefineAgent.ts:171 "live PATCH /v2/ai_agents/:id/refine endpoint" (landed 2026-07-14, 9ed38487) |
| Design | Steps model | git show 2026-07-22 | CapabilityStepException {when: string, thenMode: "switch_action"|"switch_capability"|"other"}; CapabilityStepData {steps, exceptions}; branch docs.md phases 1–8 done, drawer visual verification pending |
| Test cmds | BE rspec / FE scripts | read 2026-07-22 | chatbot Gemfile:60 rspec + .rspec present; chatbot-fe package.json scripts: test: vitest run, test:e2e: playwright test, lint |
Design ↔ Code Mapping (frontend half)
| Design frame (branch) | Implementing file | Reuse vs new | Design tokens | Backing API endpoint(s) | Deviation |
|---|---|---|---|---|---|
| Instructions & Steps card | chatbot-fe/modules/bot-automation/components/AiAgentEditor.vue drawer region | extended | Pixel 3 defaults (as branch) | POST/PATCH /v2/ai_agents | none — port the branch card |
| Step exception WHEN/THEN cards | same + new sub-components | new (restructured vs branch) | Pixel 3, Routing-tab WHEN/THEN treatment | same | deliberate: structured WHEN + closed THEN per D8 — approved via change-request brief (Wulan) |
| Capability card click-to-edit | same file, card region | extended | Pixel 3 | GET /v2/ai_agents/:id | none |
| Refine "coming soon" state | AiAgentEditorRail.vue | new | Pixel 3 | flag via /v1/system_preferences | design pending — §5 |
Detail 2.1 — Architecture (mermaid)
End-to-end component diagram
flowchart TB
admin([Tenant admin]) --> drawer["Capability drawer (Steps card) FE"]
drawer --> saver["useSaveAgent.buildSkill (SOP shape)"]
saver --> ctrl["/v2/ai_agents controller/"]
ctrl --> flag{"flag ai_agent_simplified_config?"}
flag -- OFF --> passthrough["legacy passthrough (authored fields)"]
flag -- ON --> derive["SkillPackDerivation (new)"]
derive --> lint["SkillPackLint — 6 checks (new)"]
lint -- fail --> err["capability-named error to editor"]
lint -- pass --> pack["build_skill_pack (wire unchanged)"]
passthrough --> pack
pack --> dsai(["DSAI runtime"])
dsai --> reply["ProcessAsyncReply"]
reply --> exith["EnumExitHandler (new)"]
exith --> room[("Room assign / resolve")]
Data model — jsonb shape (no DDL change; mermaid erDiagram documents keys)
erDiagram
AI_AGENTS ||--o{ CAPABILITY_JSONB : "parameters.capabilities[]"
CAPABILITY_JSONB ||--o{ STEP_JSONB : "steps[] (ordered)"
STEP_JSONB ||--o{ EXCEPTION_JSONB : "exceptions[]"
AI_AGENTS ||--o{ ROUTING_JSONB : "parameters.routing[]"
AI_AGENTS {
bigint id PK
bigint organization_id
jsonb parameters
}
CAPABILITY_JSONB {
string id
string title
string goal
string instructions
string_array keywords
jsonb legacy_authored_fields "preserved, never deleted (ADR-3)"
}
STEP_JSONB {
string id
string name
string description
string_array actions "multi-select tool refs"
}
EXCEPTION_JSONB {
jsonb when "action, body_contains, error_code"
string then "switch_to_skill / assign_to_human / resolve"
}
ROUTING_JSONB {
string user_says
string then
jsonb target "type: agent or division, id (new key)"
}
State machine — capability save lifecycle (flag ON)
stateDiagram-v2
[*] --> editing
editing --> deriving: save
deriving --> linting: fields generated
linting --> lint_failed: any of 6 checks fails
lint_failed --> editing: admin fixes named capability
linting --> synced: pushed to DSAI (wire unchanged)
synced --> [*]
Branch & skip flow — flag + refine + knowledge-only
flowchart TD
save([agent save]) --> f{"flag ON for org?"}
f -- no --> legacy["passthrough — authored fields, no lint"]
f -- yes --> d["derive + lint"]
d --> k{"capability has zero actions?"}
k -- yes --> omit["omit completion + milestones (probe-verified)"]
k -- no --> full["derive all four field families"]
refine([refine request]) --> rf{"flag ON for org?"}
rf -- yes --> deny["403 — Refine disabled (CHG-007)"]
rf -- no --> allow["shipped refine flow unchanged"]
Detail 2.2 — Sequence (mermaid, per scenario incl. failure paths)
Sequence 1 — save → derive → lint → push
sequenceDiagram
actor A as Tenant admin
participant FE as chatbot-fe drawer
participant C as /v2/ai_agents
participant S as SyncToAiService
participant D as DSAI
A->>FE: Save capability (SOP shape)
FE->>C: PATCH payload (steps, exceptions — no authored wire fields)
C->>S: sync (flag ON)
S->>S: derive slots, milestones, gates, completion
S->>S: lint (6 checks, within 500ms p95 combined)
alt lint fails
S-->>C: failure(check_id, capability)
C-->>FE: error envelope — capability-named
FE-->>A: inline error, save blocked
else lint passes
S->>D: push skill_pack (wire unchanged)
alt push fails (timeout 10s / 5xx)
S-->>C: failure logged "V2 SyncToAiService failed"
C-->>FE: save persisted, sync retried on next save
else 2xx
S-->>C: success
C-->>FE: saved + synced
end
end
Sequence 2 — runtime exception → exit → assign (and unknown reason)
sequenceDiagram
participant D as DSAI runtime
participant R as ProcessAsyncReply
participant H as EnumExitHandler (new)
participant Room as Room service
D->>R: async reply with exit.reason
R->>H: handle(exit, agent)
alt reason = user_request_human_handoff
H->>H: read parameters.routing[].target
alt target exists
H->>Room: assign(division or agent)
else target stale (deleted)
H->>Room: org default assignment
H->>H: log stale target
end
else reason = resolve_conversation (probe-gated)
H->>Room: resolve via existing resolve path
else unknown reason
H->>R: fallback intent — no crash
H->>H: emit ai_agent_exit_reason_unmapped
end
Sequence 3 — batch lint dry-run (pre-rollout, report-only)
sequenceDiagram
actor Ops as BOT ops
participant W as DryRunLintTask (Sidekiq)
participant DB as Postgres
Ops->>W: enqueue(all orgs)
loop each live V2 agent
W->>DB: read parameters (read-only)
W->>W: SkillPackLint in report mode — no push, no write
end
W-->>Ops: would-fail CSV (org, agent, check_id) to support workspace
Detail 2.3 — Database Model (DDL)
No DDL. All state rides the existing ai_agents.parameters jsonb (shape documented in
Detail 2.1's erDiagram) and one new system_preferences row:
-- seed, not migration of schema:
INSERT INTO system_preferences (code, group_code, enabled)
VALUES ('ai_agent_simplified_config', 'rollout', false);
-- per-org enablement follows the existing rollout-group pattern (system_preference.rb:42-48)
Per-status lifecycle: n/a — no table carries a new status enum. The save lifecycle is a
runtime state machine (Detail 2.1), not persisted state. Authored legacy keys inside
parameters are preserved indefinitely (ADR-3); their cleanup is explicitly deferred to the
passthrough-retirement ticket (PRD §10.1 end state).
Detail 2.4 — APIs
Outbound endpoints (consumers call us)
| Endpoint | Method | AuthN/AuthZ | Request schema | Response schema | Status codes | Idempotency | Versioning | Reuse? |
|---|---|---|---|---|---|---|---|---|
/v2/ai_agents | POST | JWT + set_role(owner/supervisor/admin) | existing + optional capabilities[].steps[] {id,name,description,actions[],exceptions[]}, keywords, goal; authored wire keys optional | existing agent envelope | 201 / 422 (lint envelope: {check_id, capability, message}) / 403 | n/a (create) | payload-compatible extension | extended |
/v2/ai_agents/:id | PATCH | same | same extension | same | 200 / 422 lint / 403 | last-write-wins (unchanged) | same | extended |
/v2/ai_agents/:id/refine | PATCH/POST | same + flag guard | unchanged | unchanged; flag-ON orgs: 403 {error: "refine_unavailable_simplified_config"} | 200 / 403 | unchanged | unchanged | extended (guard only) |
/v1/actions (pool) | GET | JWT | — | tenant actions pool | 200 | read | v1 | new-with-justification — separate deliverable; this RFC consumes with ACTION_FALLBACK degradation |
Inbound webhooks (other services call us)
| Endpoint | Method | AuthN/AuthZ | Source | Request schema | Response schema | Status codes | Idempotency | Versioning |
|---|---|---|---|---|---|---|---|---|
| internal_service v1 async reply | POST | internal service auth (existing) | DSAI | unchanged: {status, output_text, references, exit{id?, reason?}, token_usage} (process_async_reply.rb:46) | unchanged | unchanged | unchanged (per-reply) | reused — new handling only |
Outbound to third party: skill_pack push via AiService::AiAgent (AI_SERVICE_ALICLOUD_BASE_URL) —
reused, byte-compatible; existing timeout/error logging pattern kept (sync_to_ai_service.rb:50).
Detail 2.A — UI Contract
- Capability drawer (flag ON): merged Instructions & Steps card — instructions textarea
(
maxlength=10000, net-new attribute) + drag-reorderable step cards (name, description, multi-select action picker viaMpAutocomplete); norequired_slots/ milestone / gate / completion field rendered. Empty state: zero steps, no auto-seeded blank. - Step exception card: WHEN = fixed to the step's action(s) + optional
message containstext input + optionalstatus codeinput; THEN = closed dropdownswitch capability · assign to human · resolve(resolve present only when probe verdict = verbatim). Adding an exception never requires the Routing tab. - Routing tab: unchanged layout;
action_errorWHEN option removed — intent + catch-all only. - Refine rail: flag-ON → hidden/disabled with "coming soon for Steps agents"; flag-OFF → untouched.
- Caps: 20 steps/capability, 5 exceptions/step — enforced at input (add-buttons disable at cap with tooltip) and re-validated BE-side.
Detail 2.B — Data-Fetching Strategy
Unchanged: existing agent load/save composables (useSaveAgent, agent store) and service
layer (bot-automation-agents.ts). No new polling, no cache; flag read once per editor
mount via the existing /v1/system_preferences service.
Detail 2.C — Module Specs (BE) & UI State Matrix
§2.C.1 SkillPackDerivation (new, invoked from build_skill): completion = last action
of last actioned step (nil for knowledge-only); required_slots = completion action's
AI-arg names (reuses build_action_args); milestones = <action>_succeeded 1:1 per attached
action; chain gates per ADR-2. Pure function of the capability hash — unit-testable without DB.
§2.C.2 SkillPackLint (new, after derivation, before push): checks ① circular gate
② unreachable-milestone gate ③ dangling exception/routing target ④ completion-not-in-action-list
⑤ slot/arg coherence (assert) ⑥ cross-capability collision — same action with conflicting
compiled action_error rules; plus cap validation (20/5). Hard-fail (ADR-6): returns
{check_id, capability, message}; sync aborts; surfaced through the existing Dry failure branch.
Report-only mode reused by §2.C.4.
§2.C.3 EnumExitHandler (new, in ProcessAsyncReply after the exit.id branch):
user_request_human_handoff → assign target from parameters.routing; resolve_conversation
(probe-gated) → existing resolve path; unknown → fallback intent + ai_agent_exit_reason_unmapped.
Never raises.
§2.C.4 DryRunLintTask (new Sidekiq task): iterate live V2 agents read-only, run
SkillPackLint report mode, export would-fail CSV (org_id, agent_id, capability, check_id).
No pack pushed, no row written. Emits ai_agent_dryrun_completed.
UI State Matrix (drawer, flag ON):
| State | Trigger | Render |
|---|---|---|
| Loading | agent config fetch | drawer skeleton |
| Empty | new capability | instructions empty, zero steps |
| Error | lint envelope | inline capability-named error, save blocked |
| Success | save + sync 2xx | card with goal + keyword chips + +N; "checkpoints recalculated" notice on first legacy save |
Detail 2.D — Data Integrity Matrix
| Integrity rule | Enforced by |
|---|---|
No partial/invalid skill_pack ever reaches DSAI | lint aborts before push (S03/ERR-6) |
| Authored legacy keys never deleted | derivation never writes to parameters (ADR-1/3); rspec fixture asserts byte-identical passthrough |
| Step order = gate order | single derivation source (ADR-2); drag-reorder persists order (S01/AC-2) |
| Caps 20/5 | FE input guard + BE lint validation (double enforcement) |
Detail 2.E — Concurrency Collision Map
| Scenario | Behavior |
|---|---|
| Two admins save the same agent | last-write-wins on parameters (existing behavior, unchanged); each save re-derives + re-lints its own full snapshot, so no torn pack is possible |
| Save during dry-run | dry-run is read-only; no interaction |
| Exit fires while agent being re-saved | runtime reads the last successfully pushed pack (DSAI side); handler reads current parameters.routing — worst case a just-deleted target hits the stale-target fallback (S05/ERR-2) |
Detail 2.F — Async Job / Event Consumer Spec
| Job | Queue | Trigger | Retry | Idempotency |
|---|---|---|---|---|
DryRunLintTask | existing Sidekiq default | manual (pre-rollout, per stage) | safe to re-run (read-only) | full re-run replaces prior CSV |
No new event consumers. The DSAI async reply consumer is existing; only its exit handling grows.
Detail 2.F.1 — Responsibility Boundary Matrix
| Step | Owning squad / service | Inbound trigger | Outbound effect | Failure handler | PRD anchor |
|---|---|---|---|---|---|
| Author SOP + save | BOT FE (chatbot-fe) | admin action | PATCH payload | inline validation | AISC-S01 |
| Derive + lint + push | BOT BE (chatbot) | save | skill_pack to DSAI | lint envelope / sync error log | AISC-S02/S03 |
| Rule evaluation at runtime | Data & AI (DSAI) — unchanged, observational | customer message | async reply with exit | DSAI-internal | PRD Non-Goal 1 |
| Exit application | BOT BE | async reply | room assign/resolve | fallback + unmapped event | AISC-S05 |
| Dry-run + support brief | BOT BE + Support | ops trigger | would-fail CSV | re-run | AISC-S08 |
| Exception-model design restructure | Design (Wulan) | change-request brief | updated prototype | n/a | AISC-S04 |
Detail 2.F.2 — State Surface Contract
| Entity | State field / event | Default | Updated by | Read via | Stale window |
|---|---|---|---|---|---|
| Agent config | parameters jsonb | — | save (controller → persistence) | GET /v2/ai_agents/:id | none (read-after-write) |
| Pushed pack | DSAI-side copy | — | sync push | n/a (DSAI internal) | until next successful sync |
| Exit outcome | ai_agent_exit_reason_applied event | — | EnumExitHandler | analytics/dashboards | event-stream latency |
| Lint failures | ai_agent_config_lint_failed event + error envelope | — | SkillPackLint | editor + dashboards | none |
Detail 2.G — Cross-Layer Contract Verification
| Endpoint | BE schema | FE expected | Match? | Gaps |
|---|---|---|---|---|
PATCH /v2/ai_agents/:id (flag ON) | optional steps[], keywords, goal; authored keys optional | useSaveAgent reshaped to emit exactly these keys | yes — by construction (this RFC changes both sides in one plan) | FE must stop sending required_slots/milestones/milestone_action_map/per-action milestone/completion when flag ON; chunk 5 acceptance asserts payload snapshot |
| lint error envelope | 422 {check_id, capability, message} | drawer renders capability-named error | yes | envelope shape fixed in chunk 3, consumed in chunk 6 |
/refine flag-ON | 403 {error: "refine_unavailable_simplified_config"} | rail hidden; if raced, FE handles 403 gracefully | yes | FE must not retry on this 403 |
Detail 2.H — End-to-End Data Flow
Admin drags step → drawer state (useAgentStore shape) → useSaveAgent.buildSkill → PATCH /v2/ai_agents/:id → controller params → persist parameters jsonb → SkillPackDerivation → SkillPackLint → build_skill_pack → DSAI push → (runtime) customer message → rule match → async reply exit.reason → EnumExitHandler → room assign/resolve → ai_agent_exit_reason_applied event → dashboards.
Side effects: PRD §11 events at derivation, lint-fail, exit-apply, unmapped-reason,
dry-run-complete. Ownership per step: Detail 2.F.1.
Detail 2.I — Scope Boundaries
- FE create: exception card sub-components; refine "coming soon" state.
FE modify:
AiAgentEditor.vue(drawer region only — tabs untouched),useSaveAgent.ts,useAgentStore-equivalent store shape,bot-automation-agents.tstypes, action-picker wiring,AiAgentEditorRail.vue(flag gate). FE NOT touched: Routing tab layout, V1 tree editor, inbox,useRefineAgent.tsinternals (only its gating). - BE create:
SkillPackDerivation,SkillPackLint,EnumExitHandler,DryRunLintTask, flag seed. BE modify:sync_to_ai_service.rb(flag branch + module invocation),process_async_reply.rb(additive handler),ai_agents_controller.rb(params + refine guard). BE NOT touched:skill_pack_builder.rb/refine_skill_pack_merger.rb(neutralized by CHG-007 — explicitly out of scope), action executors, RAG/KB pipeline, V1 paths. - Shared:
system_preferencesseed row; no shared library changes.
Detail 2.J — Asset Inventory (frontend half)
n/a — no new assets. All UI composes existing Mekari Pixel 3 components (verified on the
design branch: @mekari/pixel3-autocomplete@0.0.28); no new icons, illustrations, or fonts.
3. High-Availability & Security
No availability-model change: no new service, queue, or datastore. The two hot paths touched are the save/sync path (bounded by the perf budget) and the async-reply path (additive handler that never raises).
Performance Requirement
- Frontend: no bundle-budget change expected (Pixel components already shipped); drawer interaction remains 60fps with 20 steps (drag-reorder virtualization not required at this cap).
- Backend: derivation + lint combined ≤ 500ms p95 on a 10-capability agent (PRD §6,
AISC-S02/AC-5 — enforced by a perf spec); DSAI push keeps its existing timeout/error handling
(
sync_to_ai_service.rb:50pattern);EnumExitHandleradds O(1) lookup per reply.
Monitoring & Alerting
- Events (PRD §11, all new):
ai_agent_config_lint_failed {org_id, agent_id, capability, check_id}·ai_agent_config_derived {…, legacy_first_save}·ai_agent_exit_reason_applied {reason, outcome, target_type}·ai_agent_exit_reason_unmapped {reason}·ai_agent_dryrun_completed {org_count, agent_count, fail_count}. - Alerts:
ai_agent_exit_reason_unmapped > 0/day per org→ same-day investigation; lint-block rate > 20% of saves org-wide → investigate. - Dashboard owner: BOT squad. Cadence: weekly for first 4 weeks post-GA.
- Cross-layer tracing: existing request-id propagation unchanged; no new trace boundary.
Logging
- BE: existing
Rails.logger.error("V2 SyncToAiService …")prefix convention for derivation/ lint/push failures; handler logs unmapped reason + stale target with agent + rule context. No PII in any new log line (config metadata only). - FE: existing error-reporting path; lint envelope rendered, not logged client-side.
Security Implications
- No new authN/authZ surface: save endpoints keep
set_role(%w[owner supervisor admin]); the refine guard tightens access (403 for flag-ON orgs, server-side — FE hiding alone would be bypassable). - Input: step/exception strings are config data pushed to DSAI — same trust class as today's
instructions field;
maxlength+ caps bound payload size; no HTML rendering of tenant strings beyond existing escaping. - Multi-tenancy: flag, agents, and routing targets are all org-scoped; the handler resolves targets only within the agent's organization.
Role × Endpoint Authorization Matrix
| Role | Endpoint(s) | Methods | Tenant scope | UI visibility | Constraint | Audit |
|---|---|---|---|---|---|---|
| owner / supervisor / admin | /v2/ai_agents, /v2/ai_agents/:id | POST / PATCH | own org | full editor | lint may block save | request logs + §11 events |
| owner / supervisor / admin (flag-OFF org) | /v2/ai_agents/:id/refine | PATCH/POST | own org | rail visible | unchanged | existing |
| owner / supervisor / admin (flag-ON org) | /v2/ai_agents/:id/refine | PATCH/POST | own org | rail hidden | 403 refine_unavailable_simplified_config | 403s logged |
| non-bot-automation roles | all above | none | — | module not visible | unchanged | n/a |
| DSAI (system) | internal_service v1 reply | POST | n/a | n/a | unchanged auth | ai_agent_exit_reason_* |
Detail 3.A — Failure Mode Catalog (merged)
| Surface | FE behavior on failure | BE response on failure | Consistent? |
|---|---|---|---|
| Save with lint violation | inline capability-named error, save blocked | 422 lint envelope, no push | yes |
| DSAI push timeout/5xx | save persisted, "sync pending" messaging; retried next save | error logged, no partial pack | yes |
| Exit with unknown reason | n/a (runtime) | fallback intent, no crash, unmapped event | yes |
| Exit with stale target | n/a | org default assignment + log | yes |
| Actions pool unavailable | degrade to shipped 4-item set, flagged | n/a (separate service) | yes |
| Refine raced on flag-ON | graceful 403 handling, no retry | 403 envelope | yes |
Detail 3.A.1 — Branch & Skip Catalog
| Branch trigger | Checked where | Downstream effect | Audit | User-visible? |
|---|---|---|---|---|
| Flag OFF for org | SyncToAiService entry + FE editor mount | passthrough, no derivation/lint; legacy editor | none (default) | no |
| Knowledge-only capability | derivation | completion + milestones omitted | derived event payload | no |
| Agent not yet edited (flag ON) | derive-on-save semantics | pack stays byte-identical | none | no |
| Probe verdict = enum enforced | build-time config | Resolve absent from THEN dropdowns | PRD §16 row 1 record | yes (option absent) |
| Refine on flag-ON org | endpoint guard | 403 + hidden rail | 403 logs | yes ("coming soon") |
Detail 3.B — Error Response Catalog (BE)
| Code | Shape | When |
|---|---|---|
| 422 | {error: "lint_failed", check_id: 1..6, capability: <name>, message} | any lint check fails (incl. caps) |
| 403 | {error: "refine_unavailable_simplified_config"} | refine on flag-ON org |
| existing codes | unchanged | all other paths |
Detail 3.C — Error Message Catalog (FE)
| Trigger | Message pattern |
|---|---|
| lint 422 | "Can't save — {capability}: {check-specific sentence}" (per-check copy finalized with Wulan) |
| sync retry | "Saved. Syncing to AI is pending — retries on next save." |
| pool degraded | "Showing default actions — full list unavailable." |
Detail 3.D — Compliance & Data Governance
n/a — no new PII, no new retention surface. The dry-run CSV contains org/agent ids +
check ids only; retained until GA + 1 quarter (PRD §6 lifecycle row).
Detail 3.E — Accessibility
Steps drag-reorder must keep a keyboard path (Pixel drag handles + up/down affordance per design branch); exception THEN is a native select (keyboard-safe); lint errors are inline text, not color-only.
4. Backwards Compatibility and Rollout Plan
Compatibility
- BE: payload-compatible extension — old FE payloads (authored fields) remain valid forever on the flag-OFF path; new keys optional. Wire to DSAI byte-compatible.
- FE: no saved-state/cache migration; the drawer reads whatever
parametersholds (legacy capability → instructions preserved, steps empty — AISC-S08/AC-2). - Cross-layer: the flag is the coupling point — BE honors both payload shapes at all times, so FE/BE deploy order cannot produce an invalid state.
Rollout Strategy
- Deploy order: BE first, FE second — BE must accept the SOP payload and serve the flag before any FE build renders the new drawer. Both deploys precede any org's flag flip.
- Flag coordination: one BE flag (
ai_agent_simplified_config, rollout group) read by both layers — no dual-flag drift (Detail 1.B). - Stage sequence (scheduling lives in delivery): dry-run → internal QA orgs → design
partners (support pre-briefed with would-fail CSV) → GA staged. Stop condition: any
ai_agent_exit_reason_unmappedspike or lint-block rate > 20%. - Rollback per layer: flag OFF per org (instant, non-destructive — ADR-3) → FE rollback (old drawer, flag-OFF payload) → BE rollback last (payload extension is inert when unused).
Detail 4.A — Cross-Layer Rollout Compatibility Matrix
| Scenario | FE | BE | Works? | Mitigation |
|---|---|---|---|---|
| Pre-deploy | Old | Old | yes | baseline |
| Backend first | Old | New | yes | new keys optional; flag OFF default — passthrough identical |
| Frontend first | New | Old | avoided | deploy order forbids; if raced, FE flag lookup fails closed → legacy editor |
| Both deployed, flag OFF | New | New | yes | passthrough + legacy editor (NEG-1) |
| Both deployed, flag ON | New | New | yes | target state |
| Backend rollback | New | Old | degraded | flip all flags OFF first; FE falls back to legacy editor |
| Frontend rollback | Old | New | yes | old payload rides the flag-OFF path |
Detail 4.B — Configuration Contract
| Layer | Env var / flag | Type | Default | Required | Provisioner | Secret? |
|---|---|---|---|---|---|---|
| BE | system_preferences row ai_agent_simplified_config (group rollout) | per-org boolean | OFF | yes | seed + ops flip per org | no |
| BE | AI_SERVICE_ALICLOUD_BASE_URL | env (existing) | — | existing | unchanged | no |
| FE | none new | — | — | — | flag read via /v1/system_preferences | no |
Detail 4.C — Test Plan (commands the agent will run)
| Layer | Command (source) | What it must prove |
|---|---|---|
| BE unit | bundle exec rspec spec/.../ai_agent/ (rspec per Gemfile:60 + .rspec) | derivation (incl. knowledge-only, skipped-step chain, multi-action anchor), all 6 lint fixtures abort, handler outcomes, byte-identical passthrough fixture, authored-keys-preserved fixture |
| BE perf | tagged rspec perf spec | 10-capability derive+lint ≤ 500ms p95 |
| FE unit | pnpm test (package.json → vitest run) | store shape, payload snapshot (flag ON sends no authored keys), dropdown enum snapshot, degradation rendering |
| FE E2E | pnpm test:e2e (package.json → playwright test) | drawer round-trip: author 2-step capability → save → reload → order + exceptions persist |
| FE lint | pnpm lint | clean |
| Cross-layer | staging scenario: save flag-ON agent → verify pushed pack fields == derivation spec; fire exception → room assigned | end-to-end contract |
Detail 4.D — Agent Execution Plan
| Order | Layer | Chunk | Files | Commands | Acceptance criteria |
|---|---|---|---|---|---|
| 1 | BE | Flag seed + sync flag branch (passthrough untouched) | sync_to_ai_service.rb, seed | bundle exec rspec | flag-OFF fixture produces byte-identical pack |
| 2 | BE | SkillPackDerivation + non-destructive guarantee | new module + build_skill wiring | rspec | S02/AC-1..5 specs green incl. perf tag; authored keys untouched (S08/AC-5 spec) |
| 3 | BE | SkillPackLint (6 checks + caps) + 422 envelope | new module, use-case failure branch | rspec | 6 failing fixtures each abort with correct check_id; no push on failure |
| 4 | BE | EnumExitHandler + parameters.routing target params + refine 403 guard | process_async_reply.rb, ai_agents_controller.rb | rspec | S05/AC-1..2, ERR-1..2 specs; NEG-02 403 spec; run D3 probe now — record verdict, enable/cut resolve |
| 5 | FE | Store + payload reshape (SOP shape; flag-ON payload drops authored keys) | useSaveAgent.ts, store, bot-automation-agents.ts types | pnpm test | payload snapshot spec: steps/exceptions present, authored keys absent |
| 6 | FE | Drawer rebuild: Steps card + exception cards (structured WHEN, closed THEN) + maxlength + lint-error rendering + recalculated notice | AiAgentEditor.vue + new sub-components | pnpm test && pnpm lint | S01/AC-1..3, S06/AC-1..2 specs; tabs snapshot unchanged |
| 7 | FE | Pickers → actions pool with flagged degradation; retire ACTION_LIST ×2 | both constants files, picker wiring | pnpm test | S07/AC-1, ERR-1 specs; no silent empty state |
| 8 | BE | DryRunLintTask + CSV export | new Sidekiq task | rspec + staging run | dry-run over staging fixtures emits CSV with per-check counts; zero writes |
| 9 | both | Guard-rail sweep + Routing tab action_error removal + refine rail gate | Routing tab component, AiAgentEditorRail.vue | pnpm test && bundle exec rspec | NEG-01..03 specs green |
| 10 | both | E2E + staging cross-layer scenario | — | pnpm test:e2e + staging script | drawer round-trip + pushed-pack field assertion + exception→assign flow |
Detail 4.E — Verification & Rollback Recipe
- Pre-merge (per layer, in order):
- BE: 1.
bundle exec rspec(full ai_agent suite) 2. perf-tagged spec ≤ 500ms p95 - FE: 1.
pnpm lint2.pnpm test3.pnpm test:e2e
- BE: 1.
- Post-deploy signals:
ai_agent_config_derivedcount > 0 on QA org saves;ai_agent_config_lint_failedpresent with correctcheck_iddistribution;ai_agent_exit_reason_unmapped== 0; DSAI sync error log rate unchanged from baseline. - Rollback recipe: 1. Flip
ai_agent_simplified_configOFF for affected org(s) — passthrough resumes over preserved authored fields (ADR-3), effective next save/sync. 2. If FE regression independent of flag: roll back FE bundle (old drawer works against new BE). 3. If BE regression: flip all org flags OFF, then roll back BE deploy (extension inert when unused).
Detail 4.F — Resource & Cost Notes
Negligible: derivation/lint are in-request CPU within budget; dry-run is a bounded one-off batch over existing workers; no new storage beyond jsonb keys and one CSV.
5. Concern, Questions, or Known Limitations
| # | Item | Type | Owner | Impact |
|---|---|---|---|---|
| 1 | D3 probe result (verbatim relay vs enum enforcement) — brief | Open Question | BOT BE (chunk 4) | Resolve THEN option only; both outcomes pre-decided |
| 2 | Design-branch drawer UI pending visual verification (docs.md) + Refine "coming soon" state design | Open Question | Design (Wulan) | quality gate before chunk 6 merge |
| 3 | /v1/actions pool is a separate deliverable | Dependency | BOT | S07 degrades gracefully until it ships |
| 4 | Divergence after flag OFF→ON cycles (stale authored config re-served) | Known Limitation (accepted, D2/ADR-3) | — | logged; support playbook covers |
| 5 | Refine unavailable for flag-ON orgs until the reconciliation phase (PRD §16 row 7) | Known Limitation (product decision) | PM | explicit "coming soon" messaging |
| 6 | Open Question (narrowed) | PM + Research | gates Stage-2/GA, not the build | |
| 6b | ut-2026-07 new findings folded into scope: prompt-hierarchy helper copy (PRD CHG-001 + AISC-S06/AC-4), THEN visual weight + clickable capability cards (Wulan brief). Not folded (deferred/monitor): dummy-data templates + real-vs-placeholder signal (persona signal, design backlog), Credit/Token/Dialog wording (advanced-settings scope), Refiner-chips appetite (evidence FOR the Refine-reconciliation phase, CHG-007 unchanged) | Known Limitation / scope record | PM | — |
| 7 | REV-1 (review R1): exception→rule compilation ownership — proposed: SkillPackDerivation flattens step exceptions + agent routing into the merged rule list; exceptions win over catch-all. Confirm at grooming | Open Question | BOT BE | gates chunks 2/5 |
| 8 | REV-2 (review R1): handoff target disambiguation — proposed MVP scoping: one assign-to-human target per agent, enforced as lint check ⑦; per-rule targets deferred to the Refine-reconciliation phase | Open Question | PM + BOT BE | gates chunk 4 |
6. Comment logs
| Date | Comment(s) From | Action Item(s) |
|---|---|---|
| 2026-07-22 | rfc-starter authoring pass | All 9 mermaid blocks validated with mmdc (see commit); grooming thread to be opened in #squad-bot |
7. Ready for agent execution
- yes — with two explicitly scoped externals: the Resolve THEN option is probe-gated (chunk 4 runs the probe; both verdicts have a pre-decided path), and chunk 6's merge gate includes the design branch's pending visual verification. Neither blocks starting chunks 1–5.
- All gates pass: Design References + PRD-to-Schema complete; per-story change map covers all 8 stories + 3 guard rails; Repo Reading Guide + Source Verification evidenced for both layers; diagrams (component, ER, state, 3 sequences, branch/skip) present; no DDL with lifecycle documented as n/a; APIs tagged reused/extended/new-with-justification; cross-layer contract rows all "yes"; rollout matrix + deploy order chosen; config contract complete; every execution chunk has files + commands + assertable acceptance criteria.
Optional next: hand to
rfc-reviewerfor a second-pass PROCEED/HOLD score.
5. Concern, Questions, or Known Limitations
[to fill]
6. Comment logs
[to fill]
7. Ready for agent execution
[to fill]