Skip to main content

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

FieldValueNotes
StatusIDEAYAML status: draft (linter enum)
DRIDimas Fauzi HidayatAccountable owner; staffing lives in delivery/ once handed over
Teamchatbot (BOT squad)Advisory, carried from PRD
Author(s)Dimas Fauzi Hidayat (via rfc-starter)
ReviewersBOT BE + FE engineering leadsAssigned at grooming
Approver(s)BOT tech lead + infosec approverAssigned at grooming
Submitted Date2026-07-22
Last Updated2026-07-23
Target Release2026-Q3
Target Quarter2026-Q3
Deliverynot yet handed to delivery
RelatedPRD 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

  1. Overview (Design References — qontak-designer branch; PRD-to-Schema Derivation; traceability + per-story change map)
  2. Technical Design (topology → ADRs → Repo Reading Guide both layers → diagrams → jsonb model → APIs → matrices)
  3. High-Availability & Security
  4. Backwards Compatibility and Rollout Plan (cross-layer matrix, Agent Execution Plan, Verification & Rollback)
  5. Concern, Questions, or Known Limitations
  6. Comment logs
  7. 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

  1. ⭐ Self-serve config rate ≥ 40% of new agents within 60 days of GA (baseline near-zero — PRD §12).
  2. Validation task test: ≥ 3 of 4 non-technical participants complete a 2-step agent unassisted (pre-launch gate).
  3. Runtime incidents from the 5 known misconfig classes → 0 for flag-ON orgs (lint catch).
  4. 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.

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

  1. The D3 staging probe result gates only the Resolve THEN option; both outcomes have a defined path (mint vs cut) — see probe brief.
  2. The design branch's drawer UI passes its pending visual verification without capability-model changes (its docs.md lists browser verification as not-yet-done).
  3. DSAI validators keep operating unchanged on machine-generated field values (wire byte-compatible).
  4. The /v1/actions backend for the real actions pool is a separate deliverable; AISC-S07 degrades gracefully until it ships.

Dependencies

DependencyLayerStatusOwner
qontak-designer feat/ai-agents-ut-updates (Steps model reference + exception-model restructure per change-request brief)FEexists — visual verification pending (quality gate, non-blocking)Design (Wulan)
D3 staging probe (exit-reason relay + omitted-completion tolerance)BEto run — blocks Resolve THEN option onlyBOT BE
/v1/actions real actions poolFE/BEneeds building (separate deliverable); ACTION_FALLBACK degradation until thenBOT
DSAI runtimeobservational only — no DSAI build work in this RFCData & AI

Design References (frontend half)

Design lives in the qontak-designer prototype repo, not Figma — the branch is the frame set.

PRD-named surfaceDesign linkFrame nameDesign system versionDesign QA contactNotes
Capability drawer — merged Instructions & Steps cardfeat/ai-agents-ut-updates app/pages/bot-automation/ai-agents/[id].vueInstructions & Steps cardMekari Pixel 3 (@mekari/pixel3-autocomplete@0.0.28 verified on branch)Wulandrag-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 cardssame branch, step exception sub-listStep exceptionsPixel 3Wulanrestructure 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 cardCapability cardPixel 3Wulangoal text + ≤4 keyword chips + +N, as built
Refine rail "coming soon" state (flag-ON)n/a — design pendingPixel 3Wulannet-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 / rulePersisted asExposed viaEnforced whereSource
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 + SyncToAiServicePRD §7 CHG-001
Step exceptions {when: {action, body_contains?, error_code?}, then}parameters -> 'capabilities' -> [] -> 'steps' -> [] -> 'exceptions'same endpointscompiled to pack-level routing_rules by build_routing_rulesPRD §7 CHG-004
Derived required_slots / milestones / milestone_action_map / requires_milestone / completionnot persisted — derived at sync time from the SOPemitted in skill_pack pushnew derivation module inside SyncToAiServicePRD §7 CHG-002, AISC-S02
6-check save-time linter— (behavior)sync abort with capability-named errornew lint module inside SyncToAiService, before pushPRD §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 ProcessAsyncReplyPRD AISC-S05
Derive-on-save non-destructiveness (authored fields preserved)authored keys in parameters are never deletedflag-OFF passthrough re-reads themSyncToAiService branch on flagPRD AISC-S08/AC-5
Flag ai_agent_simplified_config (default OFF)system_preferences row (group_code: 'rollout')existing preference lookupFE editor branch + BE sync branchPRD §6
Refine disabled for flag-ON orgs— (behavior)POST /v2/ai_agents/:id/refine returns 403 for flag-ON orgsguard 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 idFE section / componentBE 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-1n/a — BE-only§2 ADR-1/2, Derivation module spec (§2.C.1)
AISC-S03/AC-1, ERR-1..6lint error rendering in drawerLint module spec (§2.C.2)
AISC-S04/AC-1..3, ERR-1exception WHEN/THEN cards + matcher inputsbuild_routing_rules compilation (reused)
AISC-S05/AC-1..4, ERR-1..2THEN dropdown (closed)Enum-exit handler (§2.C.3)
AISC-S06/AC-1..3drawer rebuild; task-test = validation gaten/a — FE-only
AISC-S07/AC-1, ERR-1pickers wired to actions pool + degradation/v1/actions (separate deliverable)
AISC-S08/AC-1..5, ERR-1, NEG-1"checkpoints recalculated" noticederive-on-save branch + dry-run task (§2.C.4)
AISC-NEG-01..03dropdown contents; refine rail gating; V1 read-onlyrefine 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 surfaceConsumerRequired reads (BE)Required writes (BE)FE componentStatus surface
Capability drawer (Steps card)webGET /v2/ai_agents/:idPOST/PATCH /v2/ai_agentsAiAgentEditor.vue drawer (rebuilt)lint error envelope; save success
Step exception cardswebsamesamenew exception sub-componentscompiled rule visible in saved config
Routing tab (intent + catch-all only)websamesameexisting Routing tab (action_error WHEN removed)
Refine rail (flag-ON: hidden)webflag via /v1/system_preferencesAiAgentEditorRail.vue gate"coming soon" state
Runs at runtime (assign/resolve outcomes)chatbot runtimeroom assignment/resolven/a — BEai_agent_exit_reason_applied event

Role Coverage

PRD roleAuthorization mechanismEndpoints permitted (BE)UI surface visibility (FE)Cross-tenant?Audit trail
Tenant owner / supervisor / adminJWT session + set_role(%w[owner supervisor admin]) (ai_agents_controller.rb, POST / block)POST/PATCH /v2/ai_agents, /refine (flag-OFF only)full editorno — org-scopedexisting request logging + PRD §11 events
Roles without bot-automation accesssame guardnonemodule not visible (unchanged)non/a
DSAI runtime (system)internal service auth (existing)inbound async reply (internal_service v1)n/anoai_agent_exit_reason_* events

PRD Section Coverage

PRD section #TitleWhere covered
2CB: Adjustment Context§1 Overview + Out of Scope
3–4One-liner/Problem · Personas§1 Overview (delta spec)
5Non-Goals§1 Out of Scope
6Constraints§2 ADR-6 (perf budget), §4.B config contract, §3
7Feature Changes CHG-001..007§2.C modules + §2.A UI contract
8API & Webhook Behavior§2.4
9Stories + ACsDetail 1.A / 1.C
10Rollout§4
11Observability§3 Monitoring
12Success Metrics§1 Success Criteria
13Launch Plann/a — delivery layer owns scheduling
14Dependencies§1 Dependencies
15Key Decisions§2 Technical Decisions (ADR)
16Open Questions§5

Detail 1.B — Decisions Closed (cross-layer)

Summary index — full ADR blocks in §2 Technical Decisions.

DecisionChosen optionAlternatives rejectedWhy rejectedLayer
Gate derivation substrateChain gates from SOP step order (D1)data-flow inference; name-matching heuristicunimplementable (args carry no cross-action links); silently wrongBE
CompletionDerived = last action of last actioned step; omitted for knowledge-only (D4)authored dropdownfights shipped design; derivation is the PRD's own philosophyBE
Pattern A homePer-step Exceptions only; Routing tab = intent + catch-all (D5)global Routing action_error rulesrecreates researched "same work three times" duplicationboth
MigrationDerive-on-save, non-destructive (authored keys preserved) + batch dry-run (D2)one-time forced re-derivation; destructive overwritechanges live bots without tenant action; kills flag-OFF rollbackBE
Resolve THENProbe-then-BE-mint resolve_conversation; cut on enum enforcement (D3)formal DSAI contract changecross-team dependency against standing ruleBE
RefineDisabled for flag-ON orgs via 403 guard (CHG-007)teach SkillPackBuilder the Steps model nowdoubles MVP scope; reconciliation is its own phaseboth
Linter severityAll 6 checks hard-fail the sync (incl. check ⑥ collision)warn-only for legacy datadry-run pre-briefs support; deterministic beats lenient (closes PRD §16 row 2)BE
Steps/exceptions capsMax 20 steps/capability, 5 exceptions/step, validated at saveuncappedLLM context budget + UI usability; closes PRD known gapboth
Flag topologySingle BE system_preferences rollout flag read by both layersseparate FE flagtwo flags can disagree mid-rolloutboth
EndpointsPOST/PATCH /v2/ai_agents extended (new optional keys); zero new public endpointsnew versioned endpointpayload-compatible extension suffices; wire unchangedBE

Detail 1.C — Per-Story Change Map

Story idTitleLayer scopeFE changesBE changesComposite AC idsAcceptance criteria (verifiable)RFC anchors
AISC-S01Configure capability as SOPFE + BEdrawer rebuild: Steps card, drag-reorder, multi-action picker, maxlength=10000 (net-new); useAgentStore/useSaveAgent new shapeGrape params accept steps[]/keywords/goal; persist to parametersS01/AC-1..3, ERR-1vitest specs for store shape + payload builder pass; drawer renders no wire-model field§2.A · §2.4 · §4.D chunks 5–6
AISC-S02BE derives wire fieldsBE-onlyn/anew SkillPackDerivation module in SyncToAiServiceS02/AC-1..5, ERR-1, NEG-1rspec: derivation unit specs incl. knowledge-only + skipped-step chain + p95 budget spec§2 ADR-1/2 · §2.C.1 · §4.D chunk 2
AISC-S03Save-time linterFE + BErender lint error envelope in drawernew SkillPackLint module (6 checks), abort before pushS03/AC-1, ERR-1..6rspec: 6 failing fixtures each abort sync, no partial push; FE shows capability-named error§2.C.2 · §4.D chunks 3, 6
AISC-S04Per-step exception (Pattern A)FE + BEexception cards: structured WHEN (action + body_contains/error_code inputs), closed THENcompile exceptions → params['routing'] shape consumed by build_routing_rules (reused)S04/AC-1..3, ERR-1rspec: exception compiles to exact rule JSON; Routing tab offers no action-failure WHEN§2.4 · §2.C.1 · §4.D chunk 5
AISC-S05Enum-exit handlingBE-onlyTHEN dropdown options onlynew EnumExitHandler in ProcessAsyncReply; target from parameters.routing; probe-gated resolve_conversationS05/AC-1..4, ERR-1..2rspec: 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-S06Steps editor comprehensionFE-onlydrawer rebuild is the deliverable; 3-tab structure untouched (regression guard)n/aS06/AC-1..3vitest: 3 tabs unchanged; no milestone artifact rendered; AC-3 = validation-session gate (not CI)§2.A · §4.D chunk 6
AISC-S07Actions pool in pickersFE + BEpickers read real pool; degrade to shipped 4-item set with visible flag/v1/actions — separate deliverableS07/AC-1, ERR-1vitest: degradation renders flagged fallback, never silent empty§1 Dependencies · §4.D chunk 7
AISC-S08Derive-on-save migrationBE-only (+notice in FE)"checkpoints recalculated" noticeflag branch in sync (passthrough vs derive); authored keys preserved; DryRunLintTask batch jobS08/AC-1..5, ERR-1, NEG-1rspec: 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..03Guard railsFE + BEdropdown contents; refine rail hidden (flag-ON); V1 agents untouchedrefine 403 guard; flag branchNEG-01/NEG-1 · NEG-02/NEG-1..2 · NEG-03/NEG-1vitest + 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 SyncToAiService at sync time, never persist derived values: Pros: single producer choke-point (build_skill already 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 parameters on 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.

Rationalebuild_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 parameters forever; 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)

ContextProcessAsyncReply 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 in parameters.routing[].target: Pros: zero wire change; routing params already accepted (ai_agents_controller.rb:243, :288) and parameters jsonb 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_merger the 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.parameters jsonb, 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_cache memoization. Third-party — DSAI push unchanged (reused client, AI_SERVICE_ALICLOUD_BASE_URL). Consistency — the whole skill_pack is 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

LayerPathWhy the agent reads itWhat pattern it teaches
BEchatbot/app/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service.rbthe single pack producer this RFC extendsbuild_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
BEchatbot/app/api/internal_service/v1/ai_agent/use_cases/process_async_reply.rbwhere the enum-exit handler landscontract validation :46 (optional(:exit).maybe(:hash)); exit.id routing :159-161; resolve-by-reference :182
BEchatbot/app/api/frontend_service/v2/ai_agent/ai_agents_controller.rbpayload params + refine guard siterouting params :243, :288, :305; set_role(%w[owner supervisor admin]); post '/refine' :404
BEchatbot/app/models/ai_agent.rb · app/models/system_preference.rbstorage + flag patternparameters->'capabilities' jsonb :38; rollout flag find_by(code:, group_code: 'rollout', enabled: true) :42-48
BEchatbot/app/api/frontend_service/v2/ai_agent/use_cases/{create,update}_ai_agent.rbsync invocation + failure surfacing (Dry::Matcher)where lint failure becomes an editor-visible error
BEchatbot/.../use_cases/mappers/skill_pack_builder.rb + repositories/refine.rbthe second producer neutralized by CHG-007routing_rules :35; consumed at refine.rb:39
FEchatbot-fe/modules/bot-automation/composables/useSaveAgent.tsthe payload builder to reworkbuildSkill() emits authored fields :238-284 — the exact keys flag-ON saves must stop sending
FEchatbot-fe/modules/bot-automation/components/AiAgentEditor.vuethe drawer to rebuild3 tabs :3056-3060 / TabKey :2948 (unchanged); instruction textarea :2157 (no maxlength today)
FEchatbot-fe/modules/bot-automation/constants/bot-automation-actions-constants.ts + modules/ai-agent/constants/ai-agent-actions-constants.tsthe duplicated 4-item ACTION_LIST to retire togethernode_types at :8/:139/:249/:359 and :77/:203/:308/:418; ACTION_FALLBACK at common/services/main/v1/bot-automation-actions.ts:99
FEchatbot-fe/common/services/main/v1/system-preferences.ts + common/contants/request-payload.tsFE flag consumption patternhow the editor branches on ai_agent_simplified_config
Designqontak-designer branch feat/ai-agents-ut-updates: app/composables/bot-automation/useAgentStore.ts + app/pages/bot-automation/ai-agents/[id].vue + docs.mdthe Steps model referenceCapabilityStepData/CapabilityStepException shapes; exception restructure needs (D8)

Existing Contracts to Reuse, Extend, or Replace (BE)

ContractStatusJustificationOwner
POST /v2/ai_agents · PATCH /v2/ai_agents/:idextended — payload gains optional capabilities[].steps[] (+ keywords/goal aliases); authored wire-model keys become optionalpayload-compatible; zero consumers breakBOT
POST /v2/ai_agents/:id/refineextended — flag guard returns 403 for flag-ON orgsCHG-007BOT
skill_pack push to DSAI (AiService::AiAgent, AI_SERVICE_ALICLOUD_BASE_URL)reused — byte-compatiblethe whole point (PRD Non-Goal 1)BOT → DSAI
DSAI async reply (internal_service v1)reused — same schema; new handling of exit.reasonadditive handlerBOT
pack routing_rules via build_routing_rulesreused — exceptions compile into the existing params['routing'] shapewire already supports {action_error, body_contains, error_code}BOT
/v1/actions real poolnew-with-justification — the 4-item hardcode cannot express the tenant pool; separate deliverable, this RFC only consumes with degradationAISC-S07BOT

Patterns to Follow

LayerConcernPattern in repoReference fileDeviation?
BERepository shape::Repositories::AbstractRepository subclass with callsync_to_ai_service.rb:7,35none — new modules live inside/beside it
BEResult surfacingDry::Matcher::ResultMatcher in controllerai_agents_controller.rb POST blocknone — lint failure rides the existing failure branch
BEFlag lookupSystemPreference.find_by(code:, group_code: 'rollout', enabled: true)system_preference.rb:42-48none
BELoggingRails.logger.error("V2 SyncToAiService …") prefix stylesync_to_ai_service.rb:50,178none
FEPayload buildingcomposable buildSkill() → typed service payloaduseSaveAgent.ts:238 · bot-automation-agents.ts:52-67type + builder reshaped (Steps)
FEComponent libraryMekari Pixel 3 (MpAutocomplete, MpDrawer)design branch [id].vuenone
Crosssnake_case API ↔ FE mappingservice-layer types in common/services/main/v2/bot-automation-agents.tsnone

Reading Order for the Agent

  1. ../prds/simplified-configuration.md — the contract this RFC implements (stories + ACs).
  2. chatbot/.../repositories/sync_to_ai_service.rb — the producer you extend; read :95-137, :400-546 closely.
  3. chatbot/.../use_cases/update_ai_agent.rb — how sync results surface to the editor.
  4. chatbot/app/api/internal_service/v1/ai_agent/use_cases/process_async_reply.rb — where the enum-exit handler slots in (:150-200).
  5. chatbot/.../ai_agents_controller.rb — params blocks :243+, refine :404.
  6. chatbot-fe/modules/bot-automation/composables/useSaveAgent.ts — the payload you reshape.
  7. chatbot-fe/modules/bot-automation/components/AiAgentEditor.vue — the drawer you rebuild (large file; read the drawer + tabs regions).
  8. qontak-designer branch docs.md + useAgentStore.ts — the Steps model reference + its known gaps.
  9. chatbot/.../use_cases/mappers/skill_pack_builder.rb — the producer you must NOT extend (CHG-007 neutralizes it).
  10. ../documentations/d3-staging-probe-brief.md — the probe that gates the Resolve option.

Source Verification (anti-hallucination)

LayerAnchor / contractVerified byEvidence
BEsync_to_ai_service.rb buildersread 2026-07-22required_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
BEprocess_async_reply.rbread 2026-07-22optional(:exit).maybe(:hash) :46; exit_data[:id].present? :160; qontak_chat_room_resolve reference check :182
BEcontroller routing + refinegrep/read 2026-07-22optional :routing, type: Array at :243, :288; params[:routing] passthrough :253, :305; post '/refine' at :404 (landed 2026-07-10, BOT-4594)
BEai_agent.rb jsonb · flag modelgrep 2026-07-22jsonb_array_elements(COALESCE(parameters->'capabilities' …)) :38; find_by(code: 'unified_billing', group_code: 'rollout', enabled: true) :42
BEsecond producergrep 2026-07-22skill_pack_builder.rb:35 routing_rules: build_routing_rules; refine.rb:39 UseCases::Mappers::SkillPackBuilder.call
FEuseSaveAgent.ts payloadsubagent read 2026-07-22description :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
FEeditor tabs + textareasubagent read 2026-07-22tabs array AiAgentEditor.vue:3056-3060 (3 tabs); TabKey :2948; instruction textarea :2157 no maxlength; CreateAiAgent.vue:139 has the only :maxlength="10000"
FEACTION_LIST ×2 + fallbacksubagent read 2026-07-22bot-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
FErefine consumergrep 2026-07-22useRefineAgent.ts:171 "live PATCH /v2/ai_agents/:id/refine endpoint" (landed 2026-07-14, 9ed38487)
DesignSteps modelgit show 2026-07-22CapabilityStepException {when: string, thenMode: "switch_action"|"switch_capability"|"other"}; CapabilityStepData {steps, exceptions}; branch docs.md phases 1–8 done, drawer visual verification pending
Test cmdsBE rspec / FE scriptsread 2026-07-22chatbot 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 fileReuse vs newDesign tokensBacking API endpoint(s)Deviation
Instructions & Steps cardchatbot-fe/modules/bot-automation/components/AiAgentEditor.vue drawer regionextendedPixel 3 defaults (as branch)POST/PATCH /v2/ai_agentsnone — port the branch card
Step exception WHEN/THEN cardssame + new sub-componentsnew (restructured vs branch)Pixel 3, Routing-tab WHEN/THEN treatmentsamedeliberate: structured WHEN + closed THEN per D8 — approved via change-request brief (Wulan)
Capability card click-to-editsame file, card regionextendedPixel 3GET /v2/ai_agents/:idnone
Refine "coming soon" stateAiAgentEditorRail.vuenewPixel 3flag via /v1/system_preferencesdesign 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)

EndpointMethodAuthN/AuthZRequest schemaResponse schemaStatus codesIdempotencyVersioningReuse?
/v2/ai_agentsPOSTJWT + set_role(owner/supervisor/admin)existing + optional capabilities[].steps[] {id,name,description,actions[],exceptions[]}, keywords, goal; authored wire keys optionalexisting agent envelope201 / 422 (lint envelope: {check_id, capability, message}) / 403n/a (create)payload-compatible extensionextended
/v2/ai_agents/:idPATCHsamesame extensionsame200 / 422 lint / 403last-write-wins (unchanged)sameextended
/v2/ai_agents/:id/refinePATCH/POSTsame + flag guardunchangedunchanged; flag-ON orgs: 403 {error: "refine_unavailable_simplified_config"}200 / 403unchangedunchangedextended (guard only)
/v1/actions (pool)GETJWTtenant actions pool200readv1new-with-justification — separate deliverable; this RFC consumes with ACTION_FALLBACK degradation

Inbound webhooks (other services call us)

EndpointMethodAuthN/AuthZSourceRequest schemaResponse schemaStatus codesIdempotencyVersioning
internal_service v1 async replyPOSTinternal service auth (existing)DSAIunchanged: {status, output_text, references, exit{id?, reason?}, token_usage} (process_async_reply.rb:46)unchangedunchangedunchanged (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 via MpAutocomplete); no required_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 contains text input + optional status code input; THEN = closed dropdown switch 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_error WHEN 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):

StateTriggerRender
Loadingagent config fetchdrawer skeleton
Emptynew capabilityinstructions empty, zero steps
Errorlint envelopeinline capability-named error, save blocked
Successsave + sync 2xxcard with goal + keyword chips + +N; "checkpoints recalculated" notice on first legacy save

Detail 2.D — Data Integrity Matrix

Integrity ruleEnforced by
No partial/invalid skill_pack ever reaches DSAIlint aborts before push (S03/ERR-6)
Authored legacy keys never deletedderivation never writes to parameters (ADR-1/3); rspec fixture asserts byte-identical passthrough
Step order = gate ordersingle derivation source (ADR-2); drag-reorder persists order (S01/AC-2)
Caps 20/5FE input guard + BE lint validation (double enforcement)

Detail 2.E — Concurrency Collision Map

ScenarioBehavior
Two admins save the same agentlast-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-rundry-run is read-only; no interaction
Exit fires while agent being re-savedruntime 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

JobQueueTriggerRetryIdempotency
DryRunLintTaskexisting Sidekiq defaultmanual (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

StepOwning squad / serviceInbound triggerOutbound effectFailure handlerPRD anchor
Author SOP + saveBOT FE (chatbot-fe)admin actionPATCH payloadinline validationAISC-S01
Derive + lint + pushBOT BE (chatbot)saveskill_pack to DSAIlint envelope / sync error logAISC-S02/S03
Rule evaluation at runtimeData & AI (DSAI) — unchanged, observationalcustomer messageasync reply with exitDSAI-internalPRD Non-Goal 1
Exit applicationBOT BEasync replyroom assign/resolvefallback + unmapped eventAISC-S05
Dry-run + support briefBOT BE + Supportops triggerwould-fail CSVre-runAISC-S08
Exception-model design restructureDesign (Wulan)change-request briefupdated prototypen/aAISC-S04

Detail 2.F.2 — State Surface Contract

EntityState field / eventDefaultUpdated byRead viaStale window
Agent configparameters jsonbsave (controller → persistence)GET /v2/ai_agents/:idnone (read-after-write)
Pushed packDSAI-side copysync pushn/a (DSAI internal)until next successful sync
Exit outcomeai_agent_exit_reason_applied eventEnumExitHandleranalytics/dashboardsevent-stream latency
Lint failuresai_agent_config_lint_failed event + error envelopeSkillPackLinteditor + dashboardsnone

Detail 2.G — Cross-Layer Contract Verification

EndpointBE schemaFE expectedMatch?Gaps
PATCH /v2/ai_agents/:id (flag ON)optional steps[], keywords, goal; authored keys optionaluseSaveAgent reshaped to emit exactly these keysyes — 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 envelope422 {check_id, capability, message}drawer renders capability-named erroryesenvelope shape fixed in chunk 3, consumed in chunk 6
/refine flag-ON403 {error: "refine_unavailable_simplified_config"}rail hidden; if raced, FE handles 403 gracefullyyesFE 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.ts types, action-picker wiring, AiAgentEditorRail.vue (flag gate). FE NOT touched: Routing tab layout, V1 tree editor, inbox, useRefineAgent.ts internals (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_preferences seed 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:50 pattern); EnumExitHandler adds 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

RoleEndpoint(s)MethodsTenant scopeUI visibilityConstraintAudit
owner / supervisor / admin/v2/ai_agents, /v2/ai_agents/:idPOST / PATCHown orgfull editorlint may block saverequest logs + §11 events
owner / supervisor / admin (flag-OFF org)/v2/ai_agents/:id/refinePATCH/POSTown orgrail visibleunchangedexisting
owner / supervisor / admin (flag-ON org)/v2/ai_agents/:id/refinePATCH/POSTown orgrail hidden403 refine_unavailable_simplified_config403s logged
non-bot-automation rolesall abovenonemodule not visibleunchangedn/a
DSAI (system)internal_service v1 replyPOSTn/an/aunchanged authai_agent_exit_reason_*

Detail 3.A — Failure Mode Catalog (merged)

SurfaceFE behavior on failureBE response on failureConsistent?
Save with lint violationinline capability-named error, save blocked422 lint envelope, no pushyes
DSAI push timeout/5xxsave persisted, "sync pending" messaging; retried next saveerror logged, no partial packyes
Exit with unknown reasonn/a (runtime)fallback intent, no crash, unmapped eventyes
Exit with stale targetn/aorg default assignment + logyes
Actions pool unavailabledegrade to shipped 4-item set, flaggedn/a (separate service)yes
Refine raced on flag-ONgraceful 403 handling, no retry403 envelopeyes

Detail 3.A.1 — Branch & Skip Catalog

Branch triggerChecked whereDownstream effectAuditUser-visible?
Flag OFF for orgSyncToAiService entry + FE editor mountpassthrough, no derivation/lint; legacy editornone (default)no
Knowledge-only capabilityderivationcompletion + milestones omittedderived event payloadno
Agent not yet edited (flag ON)derive-on-save semanticspack stays byte-identicalnoneno
Probe verdict = enum enforcedbuild-time configResolve absent from THEN dropdownsPRD §16 row 1 recordyes (option absent)
Refine on flag-ON orgendpoint guard403 + hidden rail403 logsyes ("coming soon")

Detail 3.B — Error Response Catalog (BE)

CodeShapeWhen
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 codesunchangedall other paths

Detail 3.C — Error Message Catalog (FE)

TriggerMessage 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 parameters holds (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_unmapped spike 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

ScenarioFEBEWorks?Mitigation
Pre-deployOldOldyesbaseline
Backend firstOldNewyesnew keys optional; flag OFF default — passthrough identical
Frontend firstNewOldavoideddeploy order forbids; if raced, FE flag lookup fails closed → legacy editor
Both deployed, flag OFFNewNewyespassthrough + legacy editor (NEG-1)
Both deployed, flag ONNewNewyestarget state
Backend rollbackNewOlddegradedflip all flags OFF first; FE falls back to legacy editor
Frontend rollbackOldNewyesold payload rides the flag-OFF path

Detail 4.B — Configuration Contract

LayerEnv var / flagTypeDefaultRequiredProvisionerSecret?
BEsystem_preferences row ai_agent_simplified_config (group rollout)per-org booleanOFFyesseed + ops flip per orgno
BEAI_SERVICE_ALICLOUD_BASE_URLenv (existing)existingunchangedno
FEnone newflag read via /v1/system_preferencesno

Detail 4.C — Test Plan (commands the agent will run)

LayerCommand (source)What it must prove
BE unitbundle 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 perftagged rspec perf spec10-capability derive+lint ≤ 500ms p95
FE unitpnpm test (package.jsonvitest run)store shape, payload snapshot (flag ON sends no authored keys), dropdown enum snapshot, degradation rendering
FE E2Epnpm test:e2e (package.jsonplaywright test)drawer round-trip: author 2-step capability → save → reload → order + exceptions persist
FE lintpnpm lintclean
Cross-layerstaging scenario: save flag-ON agent → verify pushed pack fields == derivation spec; fire exception → room assignedend-to-end contract

Detail 4.D — Agent Execution Plan

OrderLayerChunkFilesCommandsAcceptance criteria
1BEFlag seed + sync flag branch (passthrough untouched)sync_to_ai_service.rb, seedbundle exec rspecflag-OFF fixture produces byte-identical pack
2BESkillPackDerivation + non-destructive guaranteenew module + build_skill wiringrspecS02/AC-1..5 specs green incl. perf tag; authored keys untouched (S08/AC-5 spec)
3BESkillPackLint (6 checks + caps) + 422 envelopenew module, use-case failure branchrspec6 failing fixtures each abort with correct check_id; no push on failure
4BEEnumExitHandler + parameters.routing target params + refine 403 guardprocess_async_reply.rb, ai_agents_controller.rbrspecS05/AC-1..2, ERR-1..2 specs; NEG-02 403 spec; run D3 probe now — record verdict, enable/cut resolve
5FEStore + payload reshape (SOP shape; flag-ON payload drops authored keys)useSaveAgent.ts, store, bot-automation-agents.ts typespnpm testpayload snapshot spec: steps/exceptions present, authored keys absent
6FEDrawer rebuild: Steps card + exception cards (structured WHEN, closed THEN) + maxlength + lint-error rendering + recalculated noticeAiAgentEditor.vue + new sub-componentspnpm test && pnpm lintS01/AC-1..3, S06/AC-1..2 specs; tabs snapshot unchanged
7FEPickers → actions pool with flagged degradation; retire ACTION_LIST ×2both constants files, picker wiringpnpm testS07/AC-1, ERR-1 specs; no silent empty state
8BEDryRunLintTask + CSV exportnew Sidekiq taskrspec + staging rundry-run over staging fixtures emits CSV with per-check counts; zero writes
9bothGuard-rail sweep + Routing tab action_error removal + refine rail gateRouting tab component, AiAgentEditorRail.vuepnpm test && bundle exec rspecNEG-01..03 specs green
10bothE2E + staging cross-layer scenariopnpm test:e2e + staging scriptdrawer 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 lint 2. pnpm test 3. pnpm test:e2e
  • Post-deploy signals: ai_agent_config_derived count > 0 on QA org saves; ai_agent_config_lint_failed present with correct check_id distribution; ai_agent_exit_reason_unmapped == 0; DSAI sync error log rate unchanged from baseline.
  • Rollback recipe: 1. Flip ai_agent_simplified_config OFF 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

#ItemTypeOwnerImpact
1D3 probe result (verbatim relay vs enum enforcement) — briefOpen QuestionBOT BE (chunk 4)Resolve THEN option only; both outcomes pre-decided
2Design-branch drawer UI pending visual verification (docs.md) + Refine "coming soon" state designOpen QuestionDesign (Wulan)quality gate before chunk 6 merge
3/v1/actions pool is a separate deliverableDependencyBOTS07 degrades gracefully until it ships
4Divergence after flag OFF→ON cycles (stale authored config re-served)Known Limitation (accepted, D2/ADR-3)logged; support playbook covers
5Refine unavailable for flag-ON orgs until the reconciliation phase (PRD §16 row 7)Known Limitation (product decision)PMexplicit "coming soon" messaging
6Steps-design validation not in PIH Resolved 2026-07-23: ut-2026-07 (Iteration II, merged by Devina) landed — Steps direction validated Strong, verdict "move toward a limited pilot". Remaining gate: the external non-technical round (D6 unassisted task test = PRD AISC-S06/AC-3, plus the AC-4 hierarchy-predictability probe) before Stage-2/GAOpen Question (narrowed)PM + Researchgates Stage-2/GA, not the build
6but-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 recordPM
7REV-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 groomingOpen QuestionBOT BEgates chunks 2/5
8REV-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 phaseOpen QuestionPM + BOT BEgates chunk 4

6. Comment logs

DateComment(s) FromAction Item(s)
2026-07-22rfc-starter authoring passAll 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-reviewer for 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]