Skip to main content

RFC: Autonomous AI Agent Phase 2 — AI-Assisted Refinement (refine proxy + Refine rail)

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. Sections marked N/A — reason are deliberately inapplicable, not deleted.

It is also agent-execution-ready: the §1 Design References (FE half)

  • §1 PRD-to-Schema Derivation (BE half), §2 Repo Reading Guide (Detail 2.0) for both layers, mermaid diagrams, the §2.G Cross-Layer Contract Verification, and §4 Agent Execution Plan + Verification & Rollback Recipe must be complete before §7 Ready for agent execution: yes.

Delivery & project management live elsewhere. This RFC is the technical artifact only — it deliberately holds no staffing, effort estimates, timeline, or rollout schedule. Those live in the initiative's delivery/ folder. Once this RFC is handed to delivery, the frontmatter delivery: link and the Metadata Delivery row point there. Until then both read not yet handed to delivery.

The YAML frontmatter at the very top is the machine-readable index agents parse. The metadata table below is the human-readable governance record. Both must agree on every shared field (status, owner, type, dates).

Metadata

FieldValueNotes
StatusDRAFT — open for engineering reviewYAML status: carries the linter enum (draft); review target: Eko (BE), FE reviewer, Data/ML owners (§2.4 contract + §5 OQ-1)
DRIEko ApriantoEngineering Lead (per PRD header). Per-task staffing lives in delivery/ artifacts — not here.
TeamchatbotBOT — Hadiningbot Squad
Author(s)Dimas Fauzi Hidayat (PM) — drafted via rfc-starterEngineering to co-author on review
ReviewersEko Aprianto (BE), Wulan Febyazzahra (Design), Data/ML Platform owners (noncore-mrag / mekari-agent)Cross-squad: the upstream endpoint owner must review §2.4
Approver(s)Eko ApriantoInfosec approver to be added at review (required before AGREED)
Submitted Date2026-07-05
Last Updated2026-07-08
Target Release2026-Q3
Target Quarter2026-Q3Carried from source PRD
Deliverynot yet handed to delivery
RelatedPRD — Phase 2: AI-Assisted Refinement · PRD — Phase 1: New Engine Migration · Upstream RFC: QON 51153994292 §10.3b · detail: refine-skill-pack endpoint
DiscussionConfluence — refine-skill-pack endpoint page

Type: full-stack Frontend sub-type: new-feature Backend sub-type: new-feature

Sections at a Glance

  1. Overview (incl. §1 Design References — FE half, and §1 PRD-to-Schema Derivation — BE half)
  2. Technical Design (Infrastructure Topology → Technical Decisions [ADR] → Repo Reading Guide → diagrams → APIs → cross-layer contract verification)
  3. High-Availability & Security
  4. Backwards Compatibility and Rollout Plan (incl. cross-layer rollout matrix, §4 Agent Execution Plan, Verification & Rollback Recipe)
  5. Concern, Questions, or Known Limitations
  6. Comment logs
  7. Ready for agent execution

1. Overview

Today the only way to change a configured autonomous agent is to hand-edit the Profile / Capabilities / Routing form tabs in AiAgentEditor.vue and re-save the whole config — a full-merge PATCH /v2/ai_agents/:id (chatbot BE: app/api/frontend_service/v2/ai_agent/use_cases/update_ai_agent.rb:87–101 replaces the entire profile / capabilities / routing blocks for any provided key). The Phase-1 drafter (POST /v2/ai_agents/generate → upstream draft-skill-pack) only generates from scratch; it cannot fix an existing agent.

This RFC adds the refiner:

  • Backend (chatbot) — a new stateless proxy POST /v2/ai_agents/:id/refine that serialises the pack the FE sends in the request body (ai_agent — the tenant's live, possibly unsaved editor state; the :id is resolved server-side for authz + engine_version guard only) capability_packskill_pack (via a shared SkillPackBuilder extracted from Repositories::SyncToAiService, parameterised by a pluggable vector-store resolver), gathers available_tools, and proxies the upstream refine-skill-pack endpoint (Data/ML-owned; documented "as built" in the companion doc QON 51226214880 — this RFC's §2.4 carries that contract verbatim; the remaining dependency is deployment/gateway verification, §5 OQ-1). The response (conversational reply + patches + an already-applied updated_skill_pack under the surgical-patch guarantee — one proposal per turn, wrapped BE-side into the options[] shape, one option at launch with multi-option an upstream fast-follow, ADR-6) is mapped back to capability_pack via the existing Mappers::SkillPackMapper and returned. Nothing is persisted by refine.
  • Frontend (chatbot-fe) — a "Refine" tab in the agent editor's right rail (beside the existing Preview rail): a multi-turn chat where the AI proposes option cards (per-field diff, Recommended flagged). Accept stages the option into the form (field highlight + tab switch); persistence is the editor's existing SavePATCH /v2/ai_agents/:id (Update + SyncToAiService).

Design source of truth is Wulan's qontak-designer prototype (app/pages/bot-automation/ai-agents/[id].vue) — this RFC's FE contract is grounded in that prototype's actual data shapes (RefineMessage, RefineOption, ProposedChange, PendingData).

Success Criteria

Engineering-verifiable outcomes (product metrics live in PRD §14):

  1. POST /v2/ai_agents/:id/refine returns reply + options for an autonomous-mode agent with the flag ON, and writes nothing (agent parameters and updated_at unchanged after any number of refine calls).
  2. Refine BE proxy overhead < 500 ms (total latency dominated by the upstream LLM call; perceived target ≤ 10 s p95, hard timeout 60 s — same as the drafter).
  3. The SkillPackBuilder extraction is behavior-preserving for Phase 1: SyncToAiService produces a byte-identical skill_pack request body before and after the refactor (locked by regression specs on spec/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service_spec.rb).
  4. Accepting an option stages changes into the editor form only; the agent's live config changes only via the existing Save path, with the standard validation (Validators::CapabilityRefPresence) and sync-failure rollback.
  5. Refining a non-autonomous (legacy) agent returns 422; flag OFF returns 403; roles outside owner/supervisor/admin return 403.
  6. Config-audit turns work without a specific complaint: a prompt like "review my configuration and find potential issues" against a pack with a known flaw (e.g. a routing rule referencing a missing capability, an action gated on an unreachable milestone) returns a diagnosis in reply and at least one option addressing it — verified in the Chunk-9 staging checklist against a seeded broken pack. This works because the BE sends the full current_skill_pack every turn and the upstream model is instructed with the pack's architecture invariants (companion doc §4) — the refiner can diagnose the config itself, not just react to a pasted error.

Out of Scope

From PRD §6 (Non-Goals), unchanged:

  1. No silent / auto-apply — every change is preview-then-apply.
  2. Autonomous-mode agents only (parameters['engine_version'] == 2); legacy tree_node / /ai-agent modal agents are rejected.
  3. No server-side session/history persistence — the BE is stateless; the FE owns the thread (in-memory; a reload starts fresh).
  4. Not a runtime test harness — behavior validation stays in Preview / the AI Agent Testing initiative.
  5. No knowledge-base content editing via refine (re-referencing an existing store the agent already owns is allowed; uploading/vectorising is not).
  6. No creating new actions/tools — the upstream hydrates known action refs from the registry (available_tools / company_tools); an invented action/kb_id ref that survives is rejected at Save by CapabilityRefPresence (400, no write). The as-built upstream does not strip refs at propose.
  7. One agent at a time — no bulk refine.

External-context reconciliation notes (repo wins where they disagree):

Source claimRepo realityResolution
PRD §10/§11: "prior config snapshotted in ai_agent_histories" on SaveThe V2 update path writes no AiAgentHistory row (zero history references under app/api/frontend_service/v2/ai_agent/); AiAgentHistory is the V1 versioning mechanism. V2 versioning is PaperTrail: has_paper_trail at app/models/ai_agent.rb:5Per-agent revert path in this RFC = restore prior parameters from the PaperTrail version, then normal update + re-sync (§4 Rollback). Corrected in PRD v1.5 (2026-07-05).
PRD §16: right rail "Preview tab — Phase-1 pending"A Preview rail already exists in prod: AiAgentEditor.vue:1668–1724 (showPreview ref at line 3015)Refine converts the single-purpose rail into a two-tab rail (Preview / Refine), following the prototype's rightRailTab. PRD OQ-7 "does refine ship before/with Preview" is resolved: the rail exists.
PRD §9/OQ-7: prototype "renders the editor in a modal"The prototype is a full page (definePageMeta({ layout: false }) at [id].vue:3626), not a modalNo structural reconciliation needed beyond the route-name delta (prototype plural /ai-agents/:id, prod singular /bot-automation/ai-agent/[id]). Prod route wins.
PRD §10: upstream returns one updated_skill_pack; PRD §11 UI: "one or more option cards"Prototype buildRefineOptions produces 1–3 options per turn, each with its own pendingData; the as-built upstream returns exactly one proposal per turn (companion doc §2/§3)FE contract keeps options[]; BE wraps the upstream's single proposal into options: [one] at launch. Multi-option per turn is an enhancement request to Data/ML (§5 OQ-1).
PRD §16 + §18 OQ-1: upstream refine-skill-pack "does not exist yet"Data/ML's companion doc (QON 51226214880, Grasia Meliolla, 2026-06-22) documents the refiner "as built" in mekari-agent (reachable via MEKARI_AGENT_BASE_URL), proxied by noncore-mragDependency reframed: from "needs building" to "verify deployment + proxy exposure + contract deltas" (§5 OQ-1). §2.4 upstream contract below is now sourced from their docs, not proposed from scratch.
PRD §17 rationale + upstream RFC §10.3b: refined pack "re-validated through the same defensive pipeline as the drafter (gate validation, tone coercion, orphan cleanup, reference filtering)"The companion doc (newer, as-built) says the opposite: "No schema coercion / no destructive post-processing on the refine path" — safety is the surgical-patch guarantee (per-op isolation, tripwire on empty skills, deep-copy apply), and "existing actions are never dropped for being absent [from available_tools] (that destructive behavior was removed)"*Companion doc wins pending Data/ML confirmation (§5 OQ-1). Consequence: PRD REFINE-S01/AC-4 (unknown refs "stripped + warned") does not match as-built behavior — the BOT-side safety net is the existing CapabilityRefPresence 400 at Apply. Corrected in PRD v1.5 (2026-07-05).
Upstream RFC §10.3b FE note: refine lives in a "Setup tab side-panel chat"Superseded by Wulan's right-rail design (PRD §17 decision; prototype PR #36)Right rail wins — already the PRD's resolved decision.

Assumptions

  1. The upstream refine-skill-pack endpoint is served under the same gateway prefix as the drafter (/qontak-ai-noncore-mrag/api/ai-agent/…) with the same auth posture (the existing Http client with organization_id routing, lib/ai_service/ai_agent.rb:9). Upstream RFC §10.3b confirms noncore-mrag proxies to mekari-agent's POST /refine-skill-pack.
  2. The upstream applies patches itself under its documented surgical-patch guarantee (deep-copy apply, per-op isolation — malformed op skipped with a warning, tripwire — empty/missing skills rejects the change and returns the original pack; companion doc §3). It does not run the drafter's destructive post-processing. The BE never applies RFC 6902 patches itself; the BOT-side validation net is CapabilityRefPresence at Apply.
  3. chat_history cap is 10 turns (FE truncates before sending) — matches the upstream's documented expectation ("last ~10 turns for conversational continuity", companion doc §5).
  4. The ai_agent_refine rollout flag reaches the FE through the same system-preferences payload the FE already consumes via preferencesStore() (store/system-preferences/), keyed rollout_ai_agent_refine.
  5. Figma frames remain TBD; the qontak-designer prototype is the canonical design reference for build (PRD Header + §7).

Dependencies

DependencyLayerOwnerStatusBlocking?
Upstream refine-skill-pack endpoint (mekari-agent, proxied by noncore-mrag)BE (external)Data / ML PlatformDocumented "as built" (companion doc QON 51226214880, 2026-06-22) — verify deployment/proxy exposure + the §5 OQ-1 contract deltas (multi-option, invented-ref behavior); §2.4 carries the as-built contractYES (verification, not build)
Phase-1 capability_pack model + drafter live on /v2/ai_agentsBEBOT — HadiningbotExists (ai_agents_controller.rb, generate.rb, sync_to_ai_service.rb)YES (stability)
capability_packskill_pack adapter (SkillPackMapper reverse + SkillPackBuilder extraction)BEBOT — HadiningbotMapper exists (use_cases/mappers/skill_pack_mapper.rb); builder extraction is Chunk 1 of this RFCYES
PaperTrail versions on ai_agents (per-agent revert path)BEBOT — HadiningbotExists (app/models/ai_agent.rb:5 has_paper_trail)NO
trace source (recent workflow_state / turns)BE + Data/MLoverlaps AI Agent Live MonitoringOptional — refine works degraded without it (§5 OQ-2)NO
Agent editor right rail (Preview)FEBOT — HadiningbotExistsAiAgentEditor.vue:1668–1724NO
Refine designDesignWulan FebyazzahraPrototyped in qontak-designer (canonical); Figma frames follow upNO

Design References (frontend half — required)

PRD-named surfaceFigma / design linkFrame nameDesign system versionDesign QA contactNotes
Refine tab (right rail)n/a — design pending; canonical: qontak-designer app/pages/bot-automation/ai-agents/[id].vue:1759–1980Right rail — Preview/Refine tabsMekari Pixel (Mp* components, per prototype MpText/MpButton/MpBadge)Wulan FebyazzahraPrototype is design SoT per PRD Header; Figma frames tracked in §5 OQ-5
Refine empty state + suggestion chipsn/a — design pending; canonical: prototype [id].vue:1806–1845Refine empty stateMekari PixelWulan FebyazzahraChip strings fixed in prototype (§2.A)
Refine option card (diff + Accept)n/a — design pending; canonical: prototype [id].vue:1882–1933Option cardMekari PixelWulan FebyazzahraRecommended banner + animated border (.recommended-border-anim, prototype [id].vue:9318–9344)
Form field highlight + tab switch on Acceptn/a — design pending; canonical: prototype acceptRefineOption [id].vue:4714–4779Mekari PixelWulan FebyazzahraaiHighlightClass outline, prototype [id].vue:9091

Per template rule these surfaces carry n/a — design pending for Figma; the PRD explicitly designates the live prototype as the source of truth until frames exist, so FE chunks are not blocked on Figma (§5 OQ-5 tracks the follow-up frames).

PRD-to-Schema Derivation (backend half — required)

No new tables — refine is stateless by decision (ADR-4). Every rule lands on existing storage or pure request/response behavior:

PRD-described entity / attribute / rulePersisted as (table.column)Exposed via (endpoint / event)Enforced whereSource (PRD §)
Agent's editable config (capability_pack: profile · capabilities · routing)ai_agents.parameters (jsonb — migration db/migrate/20260218101229_add_parameters_to_ai_agents.rb)Refine serialises the pack from the request body ai_agent (live editor state), not from the DB; POST /v2/ai_agents/:id/refine reads the DB row only for authz + engine_version; config is written only by PATCH /v2/ai_agents/:idUseCases::UpdateAiAgent full-merge (update_ai_agent.rb:87–101) + Validators::CapabilityRefPresence§3, §10
Refine proposes, never writes— (no write)POST /v2/ai_agents/:id/refine returns preview onlyUseCases::RefineAiAgent performs no repository write; spec asserts parameters/updated_at unchanged§6 NG-1, §10 #1
Refine restricted to autonomous-mode agentsai_agents.parameters->>'engine_version' = 2422 from /refine for legacy agentsGuard in UseCases::RefineAiAgent§6 NG-2, S01-NEG
Refine chat thread (multi-turn)not persisted (FE in-memory state)chat_history[] request field, capped at last N=10 turnsFE truncation in useRefineAgent.ts; BE params schema caps array size§6 NG-3, §8, S03
Roles owner/supervisor/admin only— (JWT role claim)403 otherwiseset_role(%w[owner supervisor admin]) (authorization_helpers.rb:6–11) + Middlewares::Ownership org check§8
Flag ai_agent_refine, default OFFsystem_preferences row (group_code: 'rollout', code: 'ai_agent_refine', enabled)403 from /refine when OFF; FE hides the tabFlag check in controller route (pattern: system_preference.rb:41–51) + FE rolloutPrefEnabled gate§8, §12
Invalid patch ops / broken-pack protectionwarnings[] in refine responseUpstream surgical-patch safeguards (per-op isolation + tripwire, companion doc §3); invented refs caught BOT-side by CapabilityRefPresence 400 at Apply§6 NG-6, S01/AC-4 (as-built behavior — see §1 reconciliation)
Apply = standard update with audit/revertai_agents.parameters + PaperTrail versions (ai_agent.rb:5)PATCH /v2/ai_agents/:id (existing)Existing transaction: Repositories::Update + SyncToAiService(mode: :update), rollback on sync failure (update_ai_agent.rb:58–68)§10 #3, S02
Available tools for reference filteringai_agent_tools (org/company-scoped, tool_id NOT NULL)available_tools[] in the upstream requestReuse of Repositories::Generate#available_tools query (generate.rb:52–57)§10 #1

Detail 1.A — PRD Traceability (cross-layer)

Forward (PRD AC → RFC):

PRD composite AC idFE section / componentBE section / endpoint
REFINE-S01/AC-1§2.A RefinePaneluseRefineAgent.refine()§2.4 POST /v2/ai_agents/:id/refine (no write)
REFINE-S01/AC-2§2.A RefineOptionCard (Recommended + ProposedChange diff + Accept)§2.4 response options[]
REFINE-S01/AC-3§2.C UI state NoChange§2.4 response with options: []
REFINE-S01/AC-4§2.A warnings render in thread; Save 400 surfaced clearly§2.4 warnings[] passthrough + CapabilityRefPresence at Apply — as-built upstream no longer strips refs; AC-4 needs PRD correction (§1 reconciliation)
REFINE-S01/ERR-1§2.C Error state + retry§3.A upstream timeout/5xx → 422 graceful error
REFINE-S01/ERR-2§2.C thread renders fallback reply, no cards§2.4 upstream deterministic fallback passthrough
REFINE-S02/AC-1§2.A acceptRefineOption port (stage + highlight + tab switch, no BE call)n/a — client-side
REFINE-S02/AC-2§2.H flow 2 (Save)§2.4 PATCH /v2/ai_agents/:id (reused)
REFINE-S02/AC-3§2.A discard behavior (no call)n/a — no write exists to suppress
REFINE-S02/ERR-1§2.C Save error state§2.D transaction rollback on sync failure (update_ai_agent.rb:58–68)
REFINE-S02/ERR-2§2.C Save 400 inline error§2.4 PATCH 400 via CapabilityRefPresence
REFINE-S03/AC-1§2.A chat_history threading§2.4 chat_history[] request field
REFINE-S03/AC-2§2.B history cap (last 10 turns)§2.4 param chat_history max size
REFINE-S03/AC-3§2.B no persistence — fresh thread on reloadADR-4 stateless BE
REFINE-S03/ERR-1§2.C per-turn retry, prior turns intactn/a — FE-only behavior
REFINE-S04/AC-1same thread/components as S01 (audit is a message)§2.4 "Config-audit turns" semantics + success criterion 6 + §4.C audit staging scenario
REFINE-S04/AC-2§2.C NoChange state (no fabricated cards)§2.4 empty-patches path
REFINE-S04/AC-3Save 400 surfaced clearly (existing)CapabilityRefPresence at Apply (unchanged PATCH path)
REFINE-S04/ERR-1defers to REFINE-S01 error handlingsame request path
REFINE-S01-NEG/NEG-1Refine tab hidden for legacy agents (§2.A gate)422 guard on engine_version != 2
REFINE-S01-NEG/NEG-2No auto-apply anywhere in FE flowRefine endpoint performs no write (spec-asserted)

Reverse (RFC → PRD AC):

New FE component / BE endpoint / dependencyPRD composite AC id it serves
POST /v2/ai_agents/:id/refineREFINE-S01/AC-1..4, ERR-1..2; S01-NEG/NEG-1
UseCases::RefineAiAgent + Repositories::RefineREFINE-S01/AC-1, ERR-1..2
AiService::AiAgent#refine_skill_packREFINE-S01/AC-1, ERR-1
Mappers::SkillPackBuilder extraction (+ read-only vector resolver)REFINE-S01/AC-1 (serialise without side effects)
RefinePanel.vue / RefineOptionCard.vue / useRefineAgent.tsREFINE-S01/AC-2..3, S02/AC-1, S03/AC-1..3
rightRailTab two-tab rail in AiAgentEditor.vueREFINE-S01/AC-1 (surface), S01-NEG/NEG-1
Flag ai_agent_refine (BE row + FE gate)REFINE-S01 permission model, S01-NEG/NEG-1

UI / Consumer Surface Coverage

PRD-named surfaceConsumerRequired reads (BE)Required writes (BE)FE componentStatus surface
Refine tab (right rail, editor)webPOST /v2/ai_agents/:id/refine (read-modeled: proposes only) · agent already loaded via GET /v2/ai_agents/:id (existing)none (refine writes nothing)RefinePanel.vuerefineIsGenerating + per-message streaming
Option card diff + Acceptwebn/a — data arrives in refine responsenone (Accept is client-side)RefineOptionCard.vueoption status: pending / accepted / dismissed
Editor form (highlight + tab switch on Accept)webn/a — existing formPATCH /v2/ai_agents/:id on Save (existing)AiAgentEditor.vue (activeTab, aiChangedFields)field-highlight flags

Role Coverage

PRD roleAuthorization mechanismEndpoints permitted (BE)UI surface visibility (FE)Cross-tenant?Audit trail
ownerJWT role claim → set_role (authorization_helpers.rb:6) + Middlewares::Ownership org scoping/refine, PATCH /:id (and all existing v2 ai_agent routes)Refine tab rendered (flag ON + autonomous agent)no — org-scoped via chatbot_organization_idPaperTrail version on every save; V2 RefineAiAgent structured logs
supervisorsamesamesamenosame
adminsamesamesamenosame
all other rolesset_role rejects → 403none of the refine/update routesRefine tab not renderedno403s logged by Grape error path

PRD Section Coverage

PRD §TitleWhere covered
2Phase Context§1 Overview + Related Documents
3One-liner + Problem§1 Overview
4If We Don't Shipn/a — business rationale; no engineering contract (PRD-owned)
5Target Users + Persona§1 Role Coverage (roles only; personas PRD-owned)
6Non-Goals§1 Out of Scope
7Scope Changes§2.I Scope Boundaries + §4 Execution Plan
8Constraints§2.4 (timeouts), §3 (authz, performance), §4.B (flag)
9New Features (Refine tab)§2.A UI Contract + §2.C UI State Matrix
10API & Webhook Behavior§2.4 APIs + §2.H End-to-End Data Flow
11System Flow + Stories + ACs§2.2 sequences + Detail 1.A / 1.C
12Rollout (+12.1 semantic rollback)§4 Rollout Strategy + Verification & Rollback
13Observability§3 Monitoring & Alerting + Logging
14Success Metrics§1 Success Criteria (engineering subset); product metrics PRD-owned
15Launch Plan & Stage Gatesn/a — delivery-layer concern (PRD/TPM-owned; RFC holds no schedule)
16Dependencies§1 Dependencies (with repo-reconciled statuses)
17Key Decisions§2 Technical Decisions (ADR blocks) + Detail 1.B
18Open Questions§5 (carried, updated with grounding results)

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

#DecisionChosen optionAlternatives rejectedWhy rejectedLayer§2 block
1Who applies the RFC 6902 patchesUpstream applies + re-validates; BE passes throughBE applies patches in RailsDuplicates the drafter's validation pipeline; drift riskBEDecision 1
2Apply/persist pathReuse PATCH /v2/ai_agents/:idDedicated /refine/apply endpointApply is an update — reuse authz, validation, sync, PaperTrail auditBEDecision 2
3capability_packskill_pack serialisation for refineExtract shared SkillPackBuilder w/ pluggable vector-store resolverDuplicate builder in Repositories::Refine; or call SyncToAiService privatesDuplication drifts; calling sync's privates couples refine to side-effecting resolutionBEDecision 3
4Refine session storageStateless BE; FE in-memory threadNew ai_agent_refine_sessions tableDDL + dual source of truth for a deferred needbothDecision 4
5Sync vs async refine callSynchronous proxy (60 s hard timeout, matches drafter)Sidekiq job + polling/websocketDrafter precedent is sync; adds infra for no PRD requirement (≤10 s perceived target)BEDecision 5
6Multi-option response shapeoptions[] array, each option { patches, updated_ai_agent } (1 element at launch)Single flat proposal on the response; or option with id/label/recommendedArray is additive for future multi-option; id/label/recommended dropped — no upstream source (fabrication rejected)bothDecision 6
7Diff rendering sourceFE computes ProposedChange[] by diffing current form model vs option's mapped updated_ai_agentFE renders raw RFC 6902 patchesPatch paths reference upstream skill_pack shape, unreadable against the public capability_pack (PRD OQ-6)FEDecision 7
8Auto-applyNever — review-then-apply onlyAuto-apply high-confidence patchesTrust/safety on live customer agents (PRD NG-1)bothno alternative considered — PRD Non-Goal 1 forbids it
9CachingNone — agent config read fresh from Postgres per requestCache serialised skill_packConfig must reflect unsaved-but-persisted state exactly; call volume is human-pacedBEDecision 5 (addressed inside)
10Legacy-agent guard422 with error code not_autonomous_agent404The agent exists; 404 would mislead FE debuggingBEDecision 6 (addressed inside)

Detail 1.C — Per-Story Change Map

Story idStory titleLayer scopeFE changesBE changesComposite AC ids coveredAcceptance criteria (verifiable)RFC anchors
REFINE-S01Refine an agent in natural languageFE + BERefinePanel.vue, RefineOptionCard.vue, useRefineAgent.ts, refine() in bot-automation-agents.ts, endpoint.ts entry, rail tabs in AiAgentEditor.vuepost '/:id/refine' route, UseCases::RefineAiAgent, Repositories::Refine, AiService::AiAgent#refine_skill_pack, Mappers::SkillPackBuilder extraction, models/refine_response.rbREFINE-S01/AC-1, AC-2, AC-3, AC-4, ERR-1, ERR-2bundle exec rspec spec/api/frontend_service/v2/ai_agent/refine_ai_agent_spec.rb passes: 200 + options for autonomous agent, DB row unchanged; warnings passthrough; stubbed upstream 5xx → 422 + refine failed log; pnpm test RefinePanel specs pass§2.4 rows 1–2 · §4.D chunks 1–6 · §1 PRD-to-Schema rows 1–2,7
REFINE-S02Accept an option and save the changeFE + BE existingacceptRefineOption/applyPendingData port into AiAgentEditor.vue (highlight via aiChangedFields, tab switch via activeTab), option dismissal, Save unchangednone — Save reuses PATCH /v2/ai_agents/:id (update_ai_agent.rb)REFINE-S02/AC-1, AC-2, AC-3, ERR-1, ERR-2FE unit spec: Accept mutates form state + sets highlight flags + switches tab, issues no HTTP call; existing update specs stay green; manual E2E: Save → 200, PaperTrail version count +1§2.A · §2.H flow 2 · §4.D chunk 7
REFINE-S03Iterative (multi-turn) refinementFE-only (+ BE contract field)thread state in useRefineAgent.ts; history cap (last 10) before send; fresh thread on reload (no storage)chat_history param accepted + forwarded (max 10 items enforced in Grape params)REFINE-S03/AC-1, AC-2, AC-3, ERR-1FE unit spec: 12-turn thread sends exactly last 10; reload yields empty thread; failed turn retryable with prior turns intact; BE spec: 11+ history items → 400§2.4 request schema · §2.B · §4.D chunk 6
REFINE-S04Audit the configuration for potential errorsRuntime / behaviornone — audit is a user_message variant through the S01 components; (design fast-follow: 5th suggestion chip, OQ-10)none — same endpoint/flow; full current_skill_pack already sent every turnREFINE-S04/AC-1, AC-2, AC-3, ERR-1staging audit scenario passes (§4.B row "Config-audit scenario"): seeded 2-flaw agent → reply names ≥1 flaw, ≥1 fixing option, applied fix passes ref validation§2.4 "Config-audit turns" · §1 success criterion 6 · §5 OQ-1g/OQ-10
REFINE-S01-NEGNo refine on legacy agents; never auto-applyFE + BERefine tab rendered only when engine_version === 2 and flag ON422 guard not_autonomous_agent; refine performs no writeREFINE-S01-NEG/NEG-1, NEG-2BE spec: legacy agent (no engine_version: 2) → 422; refine response asserted side-effect-free (parameters, updated_at unchanged); FE spec: tab absent for legacy agent§2.4 status codes · §3.A.1 branch rows 1–2 · §4.D chunk 5

2. Technical Design

Infrastructure Topology (start here)

Refine adds no new runtime components — it is a new synchronous route on the existing chatbot API pods, calling one new upstream path on the existing AI-service gateway. No queue, no cache, no new datastore.

Deployment topology

flowchart TB
internet([Tenant browser]) -->|HTTPS| lb["Load Balancer / API Gateway"]
lb -->|HTTP| pods["chatbot API pods xN<br/>(Rails + Grape, stateless)"]
pods -->|"read agent (refine) / write agent (save)"| db[("Postgres primary<br/>ai_agents.parameters jsonb")]
pods -->|HTTPS| gw["AI-service gateway<br/>(qontak-ai-noncore-mrag)"]
gw --> ml["mekari-agent<br/>(Data/ML — LLM refine pipeline)"]
pods -.->|"errors only"| rollbar(["Rollbar"])
fe["chatbot-fe (Nuxt 3)"] -->|HTTPS| lb
internet --> fe
  • No read-replica routing exists for this path today (Repositories::FindBy reads through the default ActiveRecord connection); refine follows that.
  • No Redis involvement: refine caches nothing (Decision 5).

Per-service responsibility

flowchart LR
subgraph chatbot["chatbot BE (BOT squad)"]
r1["POST /v2/ai_agents/:id/refine<br/>(new — propose changes)"]
r2["PATCH /v2/ai_agents/:id<br/>(existing — apply via Save)"]
r3["POST /v2/ai_agents/generate<br/>(existing — drafter, untouched)"]
end
subgraph mlsvc["noncore-mrag / mekari-agent (Data/ML squad)"]
m1["refine-skill-pack<br/>(new — LLM diagnose + patch + re-validate)"]
m2["draft-skill-pack (existing)"]
m3["PUT /ai-agent (existing — sync push)"]
end
r1 -->|"HTTPS, 60s timeout"| m1
r3 -->|"HTTPS, 60s timeout"| m2
r2 -->|"HTTPS via SyncToAiService"| m3
r1 -->|"SELECT ai_agents, ai_agent_tools"| db[("Postgres")]
r2 -->|"UPDATE ai_agents (txn)"| db
ServiceUse case in this RFCInternal callsExternal / third-party
chatbot BERefine proxy (serialise pack, gather tools, proxy, map back); Apply via existing updatePostgres (ai_agents, ai_agent_tools, system_preferences)AI-service gateway (refine-skill-pack new, PUT /ai-agent existing)
noncore-mrag / mekari-agentLLM refinement: diagnose user_message against skill_pack, emit options with the applied pack (surgical-patch guarantee — per-op isolation + tripwire)LLM provider (Data/ML-internal)— (owned by Data/ML; out of this repo)
chatbot-feRefine rail UI, thread state, option staging into formchatbot BE via $apiMainMixpanel (trackEvent)

Technical Decisions (ADR format)

Decision 1: Upstream applies patches (surgical-patch guarantee); BE is a thin passthrough

Context — A refine result must be safe to stage into a live agent's config. The as-built upstream (companion doc QON 51226214880 §3) provides this via a surgical-patch guarantee: the LLM emits only patches; the runtime applies them to a deep copy of current_skill_pack as opaque JSON (untouched fields stay byte-identical), with per-op isolation (malformed op → skipped + warning), a tripwire (empty/missing skills → change rejected, original returned), and pack-returned-unchanged on LLM/transport failure. If the BE applied RFC 6902 patches itself, those safeguards would need a second Rails implementation.

Options considered

  • Option A — upstream applies under its safeguards; BE maps and forwards
    • Pros: the safeguard suite lives where it's already built and tested; chatbot stays a thin proxy (same posture as Repositories::Generate); no JSON-Patch gem dependency; upstream failure semantics (error reply + pack unchanged, never a broken pack) come for free.
    • Cons: BE cannot independently verify the patch application; contract weight sits on the Data/ML dependency.
  • Option B — BE applies patches in Rails (JSON-Patch gem) and re-validates
    • Pros: BE-controlled; upstream only needs to emit patches.
    • Cons: re-implements per-op isolation/tripwire in a second language; guaranteed drift; new gem surface.

Decision: Option A.

Rationale — The upstream's apply step is the documented, already-built safety boundary. The BE already trusts upstream output for generate (mapped by SkillPackMapper, use_cases/generate.rb:46–50); refine extends the identical trust boundary. Note the refine path deliberately does not run the drafter's destructive post-processing (companion doc §3) — the BOT-side net for invalid refs is CapabilityRefPresence returning 400 at Apply, which this design keeps by routing Apply through the existing PATCH (Decision 2).

Consequences — The §2.4 upstream contract carries the already-applied updated_skill_pack, not just patches. An invented action/kb_id ref that survives upstream is caught at Save, not at propose time — the FE should surface CapabilityRefPresence 400s clearly (§5 OQ-1 asks Data/ML what the model does with invented refs at propose time).

Reversibility — Low cost: if upstream can only emit patches, add a Rails apply step behind the same use case without changing the FE contract (updated_ai_agent is computed server-side either way).

Decision 2: Apply reuses PATCH /v2/ai_agents/:id — no new write path

Context — After a tenant accepts an option, the change must go live with authz, validation, upstream re-sync, and audit.

Options considered

  • Option A — editor Save → existing PATCH /v2/ai_agents/:id
    • Pros: full reuse of set_role, CapabilityRefPresence validation, the update transaction with sync rollback (update_ai_agent.rb:58–68), and PaperTrail versioning; matches the prototype (Accept stages, Save persists).
    • Cons: refine-specific analytics must be emitted separately (FE events), since the BE cannot distinguish a refined save from a manual one.
  • Option B — dedicated POST /v2/ai_agents/:id/refine/apply
    • Pros: BE-side refine_applied telemetry for free.
    • Cons: duplicates authz/validation/sync/audit; second write path to keep consistent with full-merge semantics.

Decision: Option A.

Rationale — Apply is a config update. One write path means the concurrency, validation, and rollback semantics stay single-sourced.

Consequencesrefine_applied is an FE Mixpanel event (§3 Monitoring), correlated with save success. Acceptable per PRD §13 (events are product analytics, not billing-grade).

Reversibility — Trivial — an apply endpoint can be added later without breaking the FE (it would wrap the same use case).

Decision 3: Extract SkillPackBuilder from SyncToAiService with a pluggable vector-store resolver

Context — Refine must serialise the agent's current capability_packskill_pack to send upstream. That shaping logic exists only inside Repositories::SyncToAiService (build_skill_pack sync_to_ai_service.rb:93–103, build_skill 121–137, build_skill_actions 417–433, build_completion 502–514, build_routing_rules 516–522) — entangled with stateful vector resolution (resolve_capability_vector_store 147–193 creates vector DBs, mutates @vector_store_updates, persists via persist_vector_stores 385–404). Refine must not create vector stores.

Options considered

  • Option A — extract pure shaping into Mappers::SkillPackBuilder, inject a resolver strategy
    • Pros: one shaping implementation; sync keeps its stateful resolver, refine injects a read-only resolver (capability['vector_store'] as persisted); testable in isolation; the PRD's cross-phase constraint ("must not change Phase 1's sync behavior") is lockable with a byte-identical regression spec.
    • Cons: touches Phase-1 code (the one shared file); needs a careful seam.
  • Option B — duplicate the builders inside Repositories::Refine
    • Pros: zero Phase-1 risk at merge time.
    • Cons: two copies of ~150 lines of shaping logic; every capability-schema change now has a silent drift point — the exact failure mode the capability↔skill adapters exist to prevent.
  • Option C — call SyncToAiService's private builders from refine
    • Pros: no extraction.
    • Cons: .send(:build_skill_pack) against privates couples refine to sync internals and still runs inside a class whose initializer expects side-effect context; brittle.

Decision: Option A.

Rationale — The builder seam is the documented Phase-2 cross-phase dependency (PRD §2, §7, §17). A strategy-injected resolver is the minimal seam: SkillPackBuilder.new(ai_agent:, vector_store_resolver:).call, where sync passes a resolver wrapping its existing resolve_capability_vector_store behavior and refine passes ->(capability) { capability['vector_store'] } (read-only, returns the already-persisted store or nil).

Consequences — One Phase-1 file is refactored. Mitigation: Chunk 1 lands the extraction alone, gated by a regression spec asserting SyncToAiService emits an identical request body for a fixture agent before/after (existing spec file: spec/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service_spec.rb).

Reversibility — Medium: reverting means re-inlining the builders; the regression spec makes either direction verifiable.

Decision 4: Stateless BE — no refine session persistence

Context — Multi-turn refinement needs conversation context. Someone must own the thread.

Options considered

  • Option A — FE owns the thread in memory; BE stateless; chat_history sent per request
    • Pros: no DDL, no retention policy, no dual source of truth; matches the upstream RFC §10.3b posture and the prototype (refineMessages ref).
    • Cons: reload loses the thread (accepted — REFINE-S03/AC-3); no server-side audit of refine conversations this phase.
  • Option B — persist sessions in a new table
    • Pros: resumable threads; audit trail.
    • Cons: Rails DDL + retention/PII policy for pasted customer content, for a feature the PRD explicitly defers.

Decision: Option A.

Rationale — PRD §6 NG-3 defers persistence; the 26Q2 window favors zero schema risk. Token cost is bounded by the FE history cap (last 10 turns).

Consequenceschat_history is client-supplied input and must be treated as untrusted (size caps in Grape params, no logging of content — §3 Logging).

Reversibility — Easy: a session table can be added later; the request contract already carries the thread, so the BE could start persisting without an FE change.

Decision 5: Synchronous proxy call, 60 s hard timeout, no caching, no retry

Context — The upstream LLM call dominates latency (perceived target ≤ 10 s p95; PRD §8). The drafter already made this call pattern: synchronous @http.call(... open_timeout: 60, read_timeout: 60) (lib/ai_service/ai_agent.rb:49–53), no automatic retry, caller checks the status code.

Options considered

  • Option A — synchronous, 60 s open/read timeouts, user-initiated retry only
    • Pros: identical to the proven drafter path; no queue/polling infra; the FE already has a "still working / try again" pattern to apply beyond 10 s.
    • Cons: a worst-case request holds a Rails worker for up to 60 s.
  • Option B — enqueue a Sidekiq job, FE polls or receives a push
    • Pros: frees web workers; natural fit for >30 s generations.
    • Cons: new job/result plumbing + polling endpoint for a human-paced, low-volume interaction; the drafter has run synchronously in production without this.

Decision: Option A. Caching: none — the pack is serialised fresh from ai_agents.parameters on every call so the preview always reflects current persisted state. Retry: no automatic retries (an LLM call is not idempotent in cost); the FE exposes explicit retry (REFINE-S01/ERR-1).

Rationale — Parity with the drafter keeps operational behavior uniform; refine volume is bounded by human typing cadence, not machine fan-out.

Consequences — Worker-pool pressure if refine adoption spikes; watch p95 latency and worker saturation during rollout (§3 Monitoring). Rate limiting is a §5 concern (none exists on generate today either).

Reversibility — Medium: switching to async later changes the FE contract (job id + poll), so revisit before GA if Alpha latency data demands it.

Decision 6: Response carries options[] (1 at launch); each option is { patches, updated_ai_agent }; legacy agents rejected with 422

Built shape (v1): each option is exactly { patches, updated_ai_agent }no id/label/description/recommended, because the as-built upstream provides no source for those fields and a BE-fabricated label was rejected as invented data. updated_ai_agent is the upstream updated_skill_pack mapped back through SkillPackMapper (the FE stages this; patches are the raw skill_pack ops kept for debug / an optional raw view). Upstream warnings are not forwarded this phase (§5). options[] stays an array purely so a future multi-option upstream is additive.

Context — The as-built upstream returns exactly one proposal per turn (reply + patches + one updated_skill_pack — RFC §10.3b + companion doc), but the canonical design (prototype buildRefineOptions, [id].vue:4781–5297) presents 1–3 independently applicable options per AI turn, each with its own change set. The BE↔FE contract must absorb this mismatch — the array shape does, even though at launch it always holds one element (or zero).

Options considered

  • Option A — BE→FE contract is options[] (1..3); BE wraps the upstream's single proposal into options: [one] at launch; multi-option is an upstream enhancement request
    • Pros: FE is built once against the design's real shape; upstream can add multi-option later with zero FE change; launch is not blocked on upstream work beyond what is already built.
    • Cons: at launch every turn renders one option card (design degrades gracefully — the card layout is per-option already).
  • Option B — single-proposal BE→FE contract now, break it later for multi-option
    • Pros: minimal wrapping.
    • Cons: a later multi-option upstream forces an FE contract migration; the design's multi-card layout would be built against a shape that can't feed it.
  • Option C — FE fires N parallel refine calls to fake N options
    • Pros: multi-option UX at launch.
    • Cons: N× LLM cost + latency per turn; options wouldn't be coherent alternatives (independent samples).

Decision: Option A. Additionally: an agent whose parameters['engine_version'] != 2 is rejected with 422 not_autonomous_agent (the agent exists — 404 would mislead; 403 is reserved for role/flag denial).

Rationale — The prototype is the design source of truth (PRD §17), but the upstream is already built single-proposal; wrapping preserves both without blocking launch.

Consequences — At launch, one option card per turn. The multi-option enhancement (each option = coherent alternative with its own patch set) goes to Data/ML as a fast-follow ask (§5 OQ-1).

Reversibility — High — options[] absorbs 1..N without contract change.

Decision 7: FE renders diffs from mapped capability_packs, not raw RFC 6902 patches

Context — Upstream patches use RFC 6902 paths against the skill_pack shape (/skills/0/instructions), but the FE form model is the public capability_pack shape (profile/capabilities/routing). Rendering raw patches would require the FE to re-implement the skill↔capability mapping (PRD OQ-6).

Options considered

  • Option A — FE computes ProposedChange[] by diffing the current form model against the option's updated_ai_agent (BE-mapped), with a field-label map
    • Pros: diff is in the vocabulary the tenant sees (the form fields); the prototype's ProposedChange {type, field, label, currentValue, newValue} is exactly this shape; no skill-path knowledge leaks into the FE.
    • Cons: FE needs a deterministic differ over the pack structure (bounded: profile scalars, capability array by id, routing array by id).
  • Option B — FE renders upstream patches directly
    • Pros: zero diff code.
    • Cons: paths are unreadable (/skills/2/slot_action_args_map/...) and reference a shape the FE never otherwise handles.

Decision: Option A. The BE-mapped pack is returned per option as updated_ai_agent (built-shape key; was updated_capability_pack), and the FE computes ProposedChange[] by diffing the current form model against it. patches are still returned per option — raw skill_pack ops for debug / an optional raw view, not the diff source.

Rationale — The user-facing diff must speak form-field language; SkillPackMapper already produces the pack in that language on the BE, so the FE never needs skill_pack↔capability_pack knowledge.

Consequences — A small pure differ utility in useRefineAgent.ts with unit tests over the three pack sections.

Reversibility — High — the response carries both representations (patches + updated_ai_agent).

Minimum-coverage checklist: storage → Decision 4 (no new storage; existing jsonb); sync/async → Decision 5; caching → Decision 5 (none, reasoned); third-party integration → Decision 1 + 5 (direct HTTPS via existing Http client, no SDK); consistency → Decision 2 (apply inherits the update transaction; preview is read-committed snapshot); multi-tenancy → §3 (Ownership middleware + org-scoped queries, unchanged); reuse-vs-new → Detail 1.B rows 2, 6 and §2.4 Reuse? tags.


Detail 2.0 — Repo Reading Guide

Repo Map (mermaid, both layers)

flowchart LR
subgraph fe["chatbot-fe"]
editor["modules/bot-automation/components/<br/>AiAgentEditor.vue"]
refinecmp["modules/bot-automation/components/refine/ (new)"]
comps["modules/bot-automation/composables/<br/>useRefineAgent.ts (new), useSaveAgent.ts, useGenerateAgent.ts"]
svc["common/services/main/v2/<br/>bot-automation-agents.ts + endpoint.ts"]
end
subgraph be["chatbot BE — app/api/frontend_service/v2/ai_agent"]
ctrl["ai_agents_controller.rb"]
uc["use_cases/ (generate.rb, update_ai_agent.rb, refine_ai_agent.rb new)"]
repo["repositories/ (generate.rb, sync_to_ai_service.rb, refine.rb new)"]
map["use_cases/mappers/ (skill_pack_mapper.rb, skill_pack_builder.rb new)"]
end
lib["lib/ai_service/ai_agent.rb"]
db[("Postgres — ai_agents, ai_agent_tools, system_preferences")]
ml(["noncore-mrag / mekari-agent"])
editor --> refinecmp --> comps --> svc --> ctrl
ctrl --> uc --> repo --> lib --> ml
uc --> map
repo --> db

Existing Code Anchors

LayerPathWhy the agent reads itWhat pattern it teaches
BEapp/api/frontend_service/v2/ai_agent/ai_agents_controller.rbThe Grape controller refine's route joins; post '/generate' at :361 and patch '/:id' at :292 are the shape to mirrorparams do declaration, set_role, Dry::Matcher::ResultMatcher success/failure rendering
BEapp/api/frontend_service/v2/ai_agent/use_cases/generate.rbThe use case RefineAiAgent mirrorsdry-schema contract, Dry::Monads::Do, repository call → status check → SkillPackMapper.call (:46–50)
BEapp/api/frontend_service/v2/ai_agent/repositories/generate.rbThe repository Refine mirrorsrequest-body assembly (:26–33), available_tools from AiAgentTool (:52–57), @http.call result passthrough
BEapp/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service.rbSource of the SkillPackBuilder extractionbuild_skill_pack (:93–103), build_skill (:121–137), build_skill_actions (:417–433), build_completion (:502–514), build_routing_rules (:516–522), stateful resolve_capability_vector_store (:147–193)
BEapp/api/frontend_service/v2/ai_agent/use_cases/mappers/skill_pack_mapper.rbReverse mapping reused verbatim for refine outputself.call(skill_pack, organization_id:, agent_name:) (:32–34) → Entities::AiAgent; COMPLETION_TYPE_MAP (:17–23)
BEapp/api/frontend_service/v2/ai_agent/use_cases/update_ai_agent.rbThe apply path (unchanged) refine relies onfull-merge params (:87–101), transaction + sync + rollback (:58–68), CapabilityRefPresence macros (:26–34)
BElib/ai_service/ai_agent.rbWhere refine_skill_pack is addeddraft_skill_pack (:49–53): path constant, @http.call(method:, url:, body:, open_timeout: 60, read_timeout: 60)
BEapp/models/system_preference.rbFlag pattern for ai_agent_refinefind_by(code:, group_code: 'rollout', enabled: true) predicate (:41–51)
BEapp/models/ai_agent.rbVersioning + associations realityhas_paper_trail (:5); has_many :ai_agent_histories (:15) is V1-only
FEmodules/bot-automation/components/AiAgentEditor.vueHost component: rail + tabs + savetabs array (:2965–2969), activeTab mutation pattern (:3652, :3695–3702), Preview rail (:1668–1724), showPreview (:3015), handleSave (:3692–3742)
FEmodules/bot-automation/composables/useGenerateAgent.tsClosest composable pattern to useRefineAgentisGenerating ref, service call via { fetch }, error extraction (:59–97)
FEcommon/services/main/v2/bot-automation-agents.tsService layer refine() joins{ fetch, controller } return with AbortController (:232–248), $apiMain + endpoint.v2.ai_agents.*
FEmodules/bot-automation/composables/useKnowledgeSourceTypeAvailability.tsFE flag-gate pattern for ai_agent_refinepreferencesStore().lists + rolloutPrefEnabled(groupCode, code) (:65–91)
FEcommon/utils/tracking.tsAnalytics convention for refine_* eventstrackEvent(name, properties, jimoTrack) (:51–144) with auto user context
Designqontak-designer/app/pages/bot-automation/ai-agents/[id].vueCanonical Refine interaction spec (read-only repo)rightRailTab (:4066), RefineMessage/RefineOption/ProposedChange/PendingData interfaces (:4021–4064), acceptRefineOption (:4714–4779), applyPendingData (:4687–4712), chips (:1806–1845)

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

ContractStatusJustificationOwner
PATCH /v2/ai_agents/:idreused (apply path, unchanged)BOT
POST /v2/ai_agents/generatereused as pattern only (not modified)BOT
POST /v2/ai_agents/:id/refinenew-with-justificationSearched app/ + lib/ for refine — zero hits; no existing endpoint proposes config changes without persisting; generate cannot take an existing pack as inputBOT
Upstream POST /qontak-ai-noncore-mrag/api/ai-agent/refine-skill-packnew-with-justificationUpstream serves only draft-skill-pack (from-scratch) and CRUD push (PUT /ai-agent); no refinement contract existsData/ML
Upstream PUT /qontak-ai-noncore-mrag/api/ai-agentreused (save re-sync, via SyncToAiService)Data/ML
ai_agents.parameters jsonbreused (read by refine; written only by update)BOT
ai_agent_toolsreused (available_tools query)BOT
system_preferences rollout rowextended (new row ai_agent_refine; no schema change)BOT

Patterns to Follow

LayerConcernPattern in repoReference fileDeviation?
BEHTTP handler shapeGrape route + set_role + ResultMatcherai_agents_controller.rb:361–379none
BEUse caseAPIAbstractUseCase + dry-schema contract + monadsuse_cases/generate.rbnone
BERepository / upstream callAbstractRepository + @http.call passthroughrepositories/generate.rb:16–20none
BEError response shapeFailure(build_fail_params(status_code:, message:))error_responseuse_cases/generate.rb:34–40, update_ai_agent.rb:66none
BELoggingRails.logger.error("V2 <Class> …") + Rollbar.error(e, 'V2 …', context)sync_to_ai_service.rb:47–51none
FEComposable staterefs + async fn returning typed resultuseGenerateAgent.ts:59–97none
FEError / retryextract response._data.error.messages[0], expose error refuseGenerateAgent.ts:79–90none
FEService method{ fetch, controller } + $apiMainbot-automation-agents.ts:232–248none
CrossNaming (snake_case API ↔ FE)FE consumes snake_case response fields directly (as generate does)useGenerateAgent.ts (parseDetailResponse)none

Reading Order for the Agent

  1. chatbot/app/api/frontend_service/v2/ai_agent/ai_agents_controller.rb — route + authz + rendering shape (/generate at :361).
  2. chatbot/app/api/frontend_service/v2/ai_agent/use_cases/generate.rb — the use-case skeleton to mirror.
  3. chatbot/app/api/frontend_service/v2/ai_agent/repositories/generate.rb — request assembly + available_tools.
  4. chatbot/lib/ai_service/ai_agent.rb — client methods + timeout convention.
  5. chatbot/app/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service.rb — the builders to extract (and what must NOT change).
  6. chatbot/app/api/frontend_service/v2/ai_agent/use_cases/mappers/skill_pack_mapper.rb — reverse mapping reused for output.
  7. chatbot/app/api/frontend_service/v2/ai_agent/use_cases/update_ai_agent.rb — the untouched apply path.
  8. qontak-designer/app/pages/bot-automation/ai-agents/[id].vue (:4021–4064, :4687–4779, :5364–5438) — the FE interaction contract (read-only).
  9. chatbot-fe/modules/bot-automation/components/AiAgentEditor.vue (:1668–1724, :2965–3015, :3692–3742) — rail, tabs, save.
  10. chatbot-fe/common/services/main/v2/bot-automation-agents.ts + chatbot-fe/modules/bot-automation/composables/useGenerateAgent.ts — service + composable pattern.

Source Verification (anti-hallucination — required)

LayerAnchor / pattern / contractVerified byEvidence
BEapp/api/frontend_service/api.rb mountreadmount V2::AiAgent::AiAgentsController => '/v2/ai_agents' at :59
BEai_agents_controller.rb generate + patch routesreadpost '/generate' at :361; patch '/:id' at :292; set_role(%w[owner supervisor admin]) at :293/:362
BEuse_cases/generate.rbreadclass Generate < ::UseCases::API::APIAbstractUseCase at :7; Mappers::SkillPackMapper.call(...) at :46–50
BErepositories/generate.rbreadrequest_body keys has_knowledge_base/available_tools/company_id/description at :26–33; ::AiAgentTool.where(organization_id: …).where.not(tool_id: nil) at :52–57
BElib/ai_service/ai_agent.rb#draft_skill_packreadpath '/qontak-ai-noncore-mrag/api/ai-agent/draft-skill-pack', open_timeout: 60, read_timeout: 60 at :49–53; update_ai_agent PUT at :43
BErepositories/sync_to_ai_service.rb buildersreadbuild_skill_pack :93–103 (keys version, description, tone_of_voice, guidance, skills, routing_rules, knowledge_base); resolve_capability_vector_store :147–193 raises VectorStoreSyncError at :178; mode dispatch @mode == :update ? ai_service.update_ai_agent : ai_service.train_ai_agent at :39
BEuse_cases/mappers/skill_pack_mapper.rbreadself.call :32–34 → Entities::AiAgent.new(engine_version: 2, profile:, capabilities:, routing:) :42–48; COMPLETION_TYPE_MAP :17–23
BEuse_cases/update_ai_agent.rb full-merge + txnreadmerge of profile/capabilities/routing when keys present :87–101; ActiveRecord::Base.transaction { Update … SyncToAiService(mode: :update, previous_parameters:) } :58–68; capability_refs_present / routing_refs_present macros :26–34
BEhelpers/authorization_helpers.rbreaddef set_role(roles) raising 403 ErrorException at :6–11
BEmiddlewares/ownership.rbread403 unless env['user']['chatbot_organization_id'] at :5+
BEapp/models/system_preference.rb flag patternreadfind_by(code: 'unified_billing', group_code: 'rollout', enabled: true) :41–51
BEapp/models/ai_agent.rb versioningread + grephas_paper_trail :5; has_many :ai_agent_histories :15; grep of app/api/frontend_service/v2/ai_agent/ for history → zero hits (V2 writes no history rows)
BEai_agents.parameters columnreadmigration db/migrate/20260218101229_add_parameters_to_ai_agents.rb: add_column :ai_agents, :parameters, :jsonb
BENo existing refine codegrepgrep -ri refine app/ lib/ → no matches
BETest commandsls + read.rspec (--require spec_helper); spec dir spec/api/frontend_service/v2/ai_agent/ incl. repositories/sync_to_ai_service_spec.rb
FEAiAgentEditor.vue rail + tabs + savereadPreview <aside v-if="showPreview"> :1668–1724; showPreview = ref(false) :3015; tabs array :2965–2969; activeTab.value = 2 :3652; handleSave :3692–3742
FEuseSaveAgent.tsreadsave(isNew, agentId, args) :329–369 → botAutomationAgentsService.update(...)
FEbot-automation-agents.ts + endpoint.tsreadupdate() PATCH with endpoint.v2.ai_agents.update "/v2/ai_agents/:id:" :232–248; generate: "/v2/ai_agents/generate"
FEuseGenerateAgent.tsreadisGenerating ref, botAutomationAgentsService.generate(nuxtApp, payload) :59–97
FEFlag gate patternreadrolloutPrefEnabled(groupCode, code) reading preferencesStore().lists[${groupCode}_${code}].enableduseKnowledgeSourceTypeAvailability.ts:65–91
FEtracking.tsreadtrackEvent(name, properties?, jimoTrack?) :51–144, merges Role/Email/Company context, $mixpanelTrack
FETest/build commandsreadpackage.json: test: vitest run (:17), lint (:15), build: nuxt build (:11), test:e2e: playwright test (:22)
FENo existing refine codegreprepo grep for refine → only unrelated marketing/comment strings
DesignPrototype refine contractreadrightRailTab = ref<"preview" | "refine">("preview") :4066; interface ProposedChange :4021–4028; interface PendingData :4030–4043; acceptRefineOption :4714–4779; applyPendingData :4687–4712; chips :1806–1845; definePageMeta({ layout: false }) :3626

Design ↔ Code Mapping (frontend half)

Design reference (prototype)Implementing file (chatbot-fe)Reuse vs newDesign tokensBacking API endpoint(s)Deviation from design
Right rail Preview/Refine tabs (rightRailTab, [id].vue:1759–1980)modules/bot-automation/components/AiAgentEditor.vue (extend existing Preview aside :1668–1724 into a two-tab rail)extendedMekari Pixel Mp* components per prototype— (rail is chrome)Prod route stays singular /bot-automation/ai-agent/[id] (prototype uses plural)
Refine panel: empty state + chips (:1806–1845)modules/bot-automation/components/refine/RefinePanel.vue (new)newMpText sizes h2/body/body-small, text.brandPOST /v2/ai_agents/:id/refinenone — chip strings copied verbatim
Message thread + streaming (:1850–1943, streamRefineText :4625–4667)RefinePanel.vue + useRefineAgent.tsnewMpText, streaming word opacitysameStreaming is client-side animation over a non-streamed response (as in prototype); no SSE this phase
Option card (:1882–1933, RefineOption :4045–4053)modules/bot-automation/components/refine/RefineOptionCard.vue (new)newMpBadge types completed/announcement, MpButton sm secondary, .recommended-border-animsamenone
Accept → stage into form (acceptRefineOption :4714–4779, applyPendingData :4687–4712, markChanged :4669–4680)AiAgentEditor.vue (new aiChangedFields reactive map + apply fn wired to real form model)extendedhighlight outline per aiHighlightClass (:9091)Save: PATCH /v2/ai_agents/:idPrototype's PendingData maps to prototype form fields; prod maps to the real AgentDetailConfig form model (useAgentStore.ts:79–86) — field-map table maintained in useRefineAgent.ts

Detail 2.1 — Architecture (mermaid)

End-to-end component diagram

flowchart TB
user([Tenant]) --> rail["Refine tab (right rail)"]
rail --> panel["RefinePanel.vue"]
panel --> hook["useRefineAgent.ts<br/>(thread state, differ, history cap)"]
hook --> svcfe["bot-automation-agents.refine()"]
svcfe --> route["POST /v2/ai_agents/:id/refine<br/>(Grape, set_role, flag gate)"]
route --> ucase["UseCases::RefineAiAgent"]
ucase --> builder["Mappers::SkillPackBuilder<br/>(read-only vector resolver)"]
ucase --> repo["Repositories::Refine"]
repo --> client["AiService::AiAgent#refine_skill_pack"]
client --> ml(["refine-skill-pack<br/>(noncore-mrag / mekari-agent)"])
ucase --> mapper["Mappers::SkillPackMapper<br/>(skill_pack → capability_pack)"]
ucase --> db[("ai_agents / ai_agent_tools")]
panel --> editorform["Editor form (Accept stages pendingData)"]
editorform --> save["existing Save → PATCH /v2/ai_agents/:id"]

Data model (mermaid erDiagram)

No new tables. The slice refine touches (existing columns only; jsonb keys shown as comments):

erDiagram
AI_AGENTS ||--o{ AI_AGENT_TOOLS : "organization-scoped tools"
AI_AGENTS ||--o{ VERSIONS : "PaperTrail audit (has_paper_trail)"
AI_AGENTS {
uuid id PK
int organization_id
string company_id
jsonb parameters "engine_version, profile, capabilities[], routing[], version_id"
string vendor_ai_agent_id "set by SyncToAiService"
}
AI_AGENT_TOOLS {
uuid id PK
int organization_id
string company_id
string tool_id "NOT NULL filter for available_tools"
string tool_name
text description
}
VERSIONS {
bigint id PK
string item_type "AiAgent"
text object "prior state (revert source)"
}

State machine — Refine panel (UI)

stateDiagram-v2
[*] --> Empty: Open Refine tab
Empty --> Loading: Submit message or chip
Loading --> SuccessOptions: reply with option card(s)
Loading --> NoChange: reply, options empty
Loading --> Error: upstream timeout or 5xx
Error --> Loading: Retry
NoChange --> Loading: Send another message
SuccessOptions --> FormApplied: Accept an option
SuccessOptions --> Loading: Send follow-up
FormApplied --> Saved: Editor Save (PATCH)
FormApplied --> Loading: Keep refining
Saved --> [*]
Error --> [*]: Close (agent unchanged)

State machine — option card status

stateDiagram-v2
[*] --> pending: option rendered
pending --> accepted: tenant clicks Accept
pending --> dismissed: sibling option accepted
accepted --> [*]
dismissed --> [*]

Branch & skip flow (non-error policy branches)

flowchart TD
req([refine request]) --> flag{"flag ai_agent_refine ON?"}
flag -- no --> deny["403 — feature disabled"]
flag -- yes --> mode{"engine_version == 2?"}
mode -- no --> legacy["422 not_autonomous_agent"]
mode -- yes --> upstream["proxy refine-skill-pack"]
upstream --> refs{"invented action or kb_id referenced?"}
refs -- yes --> caught["survives propose — rejected at Save by CapabilityRefPresence (400)"]
refs -- no --> pass["options as-is"]
caught --> resp([200 reply + options + warnings])
pass --> resp
upstream --> actionable{"actionable change found?"}
actionable -- no --> nochange["200 reply, options empty"]

Detail 2.2 — Sequence (mermaid, end-to-end incl. failure paths)

Happy path — refine turn

sequenceDiagram
actor T as Tenant
participant FE as chatbot-fe RefinePanel
participant LB as Load Balancer
participant API as chatbot API pod
participant DB as Postgres primary
participant ML as noncore-mrag / mekari-agent

T->>FE: describe issue / paste error
FE->>FE: truncate chat_history to last 10 turns
FE->>LB: POST /v2/ai_agents/:id/refine (+ ai_agent = live editor pack)
LB->>API: route (round-robin)
API->>API: Ownership + set_role + flag gate
API->>DB: SELECT ai_agents by id + org_id (authz + engine_version guard only)
API->>DB: SELECT ai_agent_tools (org/company, tool_id NOT NULL)
API->>API: SkillPackBuilder over request-body ai_agent (read-only resolver — no vector DB created)
API->>ML: POST refine-skill-pack (skill_pack, user_message, chat_history, available_tools)
Note right of ML: LLM call dominates — target p95 ≤ 10s, hard timeout 60s
ML-->>API: reply + patches + updated_skill_pack (+ warnings, not forwarded)
API->>API: SkillPackMapper (updated_skill_pack → updated_ai_agent) per option
API-->>FE: 200 reply + options with patches and updated_ai_agent (nothing persisted)
FE->>FE: diff current form vs each option updated_ai_agent into ProposedChange list
FE-->>T: streamed reply + option cards

Failure path — upstream timeout / 5xx (REFINE-S01/ERR-1)

sequenceDiagram
participant FE as chatbot-fe RefinePanel
participant API as chatbot API pod
participant DB as Postgres primary
participant ML as noncore-mrag / mekari-agent

FE->>API: POST /v2/ai_agents/:id/refine
API->>DB: SELECT agent + tools (reads only)
API->>ML: POST refine-skill-pack
Note right of ML: no response within 60s, or 5xx
ML--xAPI: timeout / 5xx
API->>API: Rails.logger.error V2 RefineAiAgent + Rollbar
API-->>FE: 422 graceful error — agent unchanged
FE-->>FE: error turn in thread + Retry affordance

Apply path — Save after Accept (REFINE-S02), incl. sync-failure rollback

sequenceDiagram
actor T as Tenant
participant FE as AiAgentEditor
participant API as chatbot API pod
participant DB as Postgres primary
participant ML as AI service (PUT /ai-agent)

T->>FE: Accept option (form staged, highlighted, tab switched — no HTTP)
T->>FE: Save
FE->>API: PATCH /v2/ai_agents/:id (full capability_pack)
API->>API: CapabilityRefPresence validation
alt validation fails
API-->>FE: 400 — no write (REFINE-S02/ERR-2)
else valid
API->>DB: BEGIN, UPDATE ai_agents.parameters (+ PaperTrail version)
API->>ML: PUT /ai-agent (SyncToAiService mode update, stateful vector resolver)
alt sync ok
ML-->>API: 2xx
API->>DB: COMMIT
API-->>FE: 200 updated agent (live)
else sync fails
ML--xAPI: error / non-2xx
API->>DB: ROLLBACK
API-->>FE: 422 — agent stays on prior config (REFINE-S02/ERR-1)
end
end

Detail 2.3 — Database Model (DDL)

N/A — no new or altered tables, by design (ADR-4). Rails migration count for this RFC: zero.

  • Refine reads ai_agents.parameters (jsonb, added in db/migrate/20260218101229_add_parameters_to_ai_agents.rb) and ai_agent_tools; it writes nothing.
  • Apply writes through the existing update path (PaperTrail versions row per save — the per-agent revert source).
  • One data seed (not DDL): a system_preferences row {group_code: 'rollout', code: 'ai_agent_refine', enabled: false} per environment, following the existing rollout-row pattern (system_preference.rb:41–51). Provisioned via console/ops runbook like sibling rollout flags (no migration needed — confirm seeding convention with BE at review).
  • Client-side persistence: none. The refine thread lives in component state (useRefineAgent.ts refs); no localStorage/IndexedDB — a reload starts a fresh thread (REFINE-S03/AC-3). No migration/eviction concerns.
  • Per-status lifecycle: n/a — no new entity with a status enum is persisted. (The option-card pending/accepted/dismissed enum is ephemeral FE state — diagrammed in §2.1.)

Detail 2.4 — APIs

Outbound endpoints (consumers call us — chatbot BE)

EndpointMethodAuthN/AuthZRequest schemaResponse schemaStatus codesIdempotencyVersioningReuse?
/v2/ai_agents/:id/refinePOSTSession JWT → Middlewares::Ownership (org present) + set_role(%w[owner supervisor admin]) + rollout flag ai_agent_refinesee Refine request belowsee Refine response below200, 400 (param/shape), 403 (role or flag OFF), 404 (agent not found in org), 422 (legacy agent · upstream failure)Safe to repeat — endpoint writes nothing (idempotent by construction; each call may return different LLM output)v2 (additive)new-with-justification (no existing propose-without-persist contract; see Detail 2.0 contracts table)
/v2/ai_agents/:idPATCHsame middleware + set_role (existing, ai_agents_controller.rb:292–293)existing full-merge params (profile, capabilities, routing, …)existing update_ai_agent_response200, 400 (ref validation), 403, 404, 422 (sync failure → rolled back)last-writer-wins full merge (unchanged — see §2.E)v2reused — the apply path, zero changes

Refine request (Grape params do — resolves PRD §10 placeholder):

POST /v2/ai_agents/:id/refine
{
"user_message": "string — required, 1..4000 chars",
"chat_history": [ // optional; FE sends at most the last 10 turns
{ "role": "user" | "assistant", "content": "string, 1..4000 chars" }
],
"ai_agent": { // required — the FE's live (possibly unsaved) editor pack
"profile": {}, "capabilities": [], "routing": []
}
}

Current-pack source (built shape). The pack serialised upstream is taken from the request body ai_agent (the tenant's live, possibly unsaved editor state) — not re-loaded from the DB. The :id is still resolved server-side via Repositories::FindBy(id, org_id) for authorization + engine_version guard only (cross-org → 404, legacy → 422); only the pack shape comes from the client. Safe because refine is propose-only (writes nothing) and any invalid pack is re-validated by CapabilityRefPresence at Save. trace is omitted from v1 (OQ-2 resolved).

Refine response (200):

{
"data": {
"reply": "string — conversational diagnosis (always present)",
"options": [ // 0..1 at launch (upstream returns one proposal);
// empty = no actionable change (S01/AC-3);
// options[] kept for future 1..3 multi-option (OQ-1e)
{
"patches": [ // RFC 6902 vs skill_pack — raw upstream ops (debug / optional raw view)
{ "op": "replace", "path": "/skills/0/instructions", "value": "…" }
],
"updated_ai_agent": { // upstream updated_skill_pack mapped via SkillPackMapper — the FE stages THIS
"profile": {}, "capabilities": [], "routing": []
}
}
]
}
}

Dropped from the built shape (vs the earlier draft): option id/label/ description/recommended (the as-built upstream provides no source for them — a fixed BE-derived label was rejected as fabricated data), and option-level + turn-level warnings (not surfaced to the FE this phase — see §5; upstream still returns them, the BE simply does not forward). updated_capability_pack is renamed updated_ai_agent to match the FE form model key.

Error shape (all non-2xx) — existing Grape convention: { "error": { "messages": ["…"] } } with codes per the table above; 422 legacy-agent responses carry message key not_autonomous_agent.

Upstream endpoints (we call — Data/ML)

EndpointMethodAuthTimeout / retryStatusReuse?
/qontak-ai-noncore-mrag/api/ai-agent/refine-skill-packPOSTExisting Http client auth (Bearer + organization_id routing, lib/ai_service/ai_agent.rb:9)open 60 s / read 60 s (drafter parity, ai_agent.rb:49–53); no automatic retry (ADR-5); on timeout/5xx BE returns 422documented "as built" in mekari-agent, proxied by noncore-mrag (RFC §10.3b + companion doc); deployment/proxy exposure to be verified (§5 OQ-1)new-with-justification (new to chatbot BE)
/qontak-ai-noncore-mrag/api/ai-agent (PUT)PUTsameexisting SyncToAiService semanticsexists (ai_agent.rb:43)reused

Upstream refine-skill-pack contractas documented by Data/ML (RFC §10.3b, quoted; companion doc adds behavior semantics). This replaces the earlier from-scratch proposal; the BE adapts to this shape:

// request (chatbot BE → noncore-mrag) — RFC §10.3b, verbatim shape
{
"company_id": "string",
"current_skill_pack": { /* full skill_pack JSON — SkillPackBuilder output */ },
"user_message": "string (min 1 char)",
"chat_history": [
{ "role": "user", "content": "..." },
{ "role": "assistant", "content": "..." }
], // last ~10 turns (companion doc §5)
"trace": { // optional
"workflow_state": { /* recent workflow_state snapshot (mekari-agent DB) */ },
"recent_turns": [ /* recent conversation turns */ ]
},
"available_tools": [
{ "name": "getfleets_1778471387", "description": "...", "id": "uuid",
"type": "qontak_function_call", "args": {} }
] // noncore auto-fills from company_tools when sent empty (companion doc §5)
}
// response — RFC §10.3b, verbatim shape (single proposal per turn)
{
"status": "success",
"reply": "I've updated the routing rule to switch to registration when createorder fails with 'belum terdaftar'. Here's what changed:",
"patches": [
{ "op": "replace", "path": "/routing_rules/0/when/body_contains", "value": "belum terdaftar" },
{ "op": "add", "path": "/skills/0/milestones/-", "value": "order_attempted" }
],
"updated_skill_pack": { /* full patched pack — deep-copy apply, see semantics below */ },
"warnings": []
}

As-built behavior semantics (companion doc QON 51226214880):

  • Surgical-patch guarantee (§3): the LLM emits only patches; the runtime applies them to a deep copy of current_skill_pack as opaque JSON — every field the tenant didn't raise stays byte-identical. Per-op isolation: a malformed op is skipped with a warning. Tripwire: if applying leaves skills missing/empty, the change is rejected and the original pack returned. No drafter-style destructive post-processing on this path.
  • Failure semantics: LLM/transport failure inside upstream → returns current_skill_pack unchanged with an error reply (never a broken pack) → maps to REFINE-S01/ERR-2. Gateway-level timeout/5xx is the only path to the BE's 422 (REFINE-S01/ERR-1).
  • Question-only turns (§4): a message asking for no change returns empty patches + the answer in reply → maps to REFINE-S01/AC-3.
  • Config-audit turns (derived from §4): the model holds the pack's architecture invariants (tool gates must reference a reachable milestone, milestone firing via milestone_action_map / [MILESTONE: x] tags, terminal-action gating, valid exit.reason enum, one-reply-per-turn) plus debugging heuristics — so an audit-style user_message with no specific complaint ("find potential errors in my configuration") is served by the same contract: the full pack is in every request, and violations the model finds come back as reply diagnosis + patches. Whether Data/ML has explicitly tuned/tested complaint-free audit prompts is §5 OQ-1g.
  • available_tools (§5): never used to drop existing actions (that destructive behavior was removed); used to hydrate id/args when the model adds an action. The chatbot BE still sends its AiAgentTool list explicitly (drafter parity) rather than relying on noncore's company_tools auto-fill — whether the two registries are in sync is §5 OQ-1c.
  • Model config (§6, Data/ML-owned): REFINE_SKILL_PACK_MODEL, falling back to DRAFT_SKILL_PACK_MODEL (default gpt-5.1). Setup-time only — not on the /predictions customer path; stateless across replicas.

BE adaptation notes (upstream shape → §2.4 FE response): Repositories::Refine sends current_skill_pack (built by SkillPackBuilder from the request-body ai_agent); UseCases::RefineAiAgent wraps the single {reply, patches, updated_skill_pack, warnings} into options: [ { patches, updated_ai_agent: SkillPackMapper(updated_skill_pack) } ]. Empty patchesoptions: []. The upstream warnings and the absent option id/label/recommended are not carried through (built-shape decision — no upstream source for the labels; warnings deferred). options[] stays an array so a future multi-option upstream (OQ-1e) is additive with no reshape.

Inbound webhooks (other services call us)

N/A — none. Refine is fully synchronous; no callbacks (the async-callback mismatch pattern from the V2-engine RFC is deliberately avoided here).

Per-endpoint extras: no pagination (single-turn RPC). Payload limits: user_message and each history content ≤ 4000 chars, chat_history ≤ 10 items, ai_agent a required well-formed pack object (size-capped) — all Grape-enforced. Rate limits: none exist on generate today; refine launches with the same posture + a §5 concern to add per-org throttling before GA. Backward compatibility: purely additive (new route + new upstream path).

Detail 2.A — UI Contract

RefinePanel.vue (new — modules/bot-automation/components/refine/RefinePanel.vue)

  • Design reference: prototype [id].vue:1806–1976 (n/a Figma — design pending; §5 OQ-5)
  • Props:
interface RefinePanelProps {
agentId: string; // required
engineVersion: number; // required — panel renders only when === 2
currentPack: AgentCapabilityPack; // required — the live form model snapshot for diffing
}
  • Emits: accept-option { optionId: string; pendingData: RefinePendingData; changes: ProposedChange[] }
  • State ownership: thread state lives in useRefineAgent.ts (composable-scoped refs, one instance per editor mount — same pattern as useGenerateAgent.ts).
// useRefineAgent.ts — ported from the prototype interfaces ([id].vue:4021–4064)
interface RefineMessage {
id: string;
role: "user" | "ai";
content: string;
streaming?: boolean;
hasOptions?: boolean;
loadingOptions?: boolean;
options?: RefineOption[];
}
interface RefineOption {
id: string; // fabricated FE-side — no upstream source (OQ-1b)
label: string; // fabricated FE-side (e.g. "Proposed change")
description: string; // fabricated FE-side
isRecommended?: boolean; // fabricated FE-side (single option → true)
changes: ProposedChange[]; // computed FE-side (ADR-7)
pendingData: RefinePendingData; // derived from the response option's updated_ai_agent
status: "pending" | "accepted" | "dismissed";
}
interface ProposedChange {
type: "update" | "add" | "remove";
field: string; // form field id (e.g. "tone", "goals", capability id)
label: string; // human label (e.g. "Tone of voice")
currentValue?: string;
newValue?: string;
snippet?: string;
}
  • Empty state: heading "Refine your agent" + 4 suggestion chips, strings verbatim from the prototype ([id].vue:1818–1841): "The refund answer is not correct, fix it" · "Add order tracking capability" · "Make it faster to escalate to a human agent" · "Make the tone more formal".
  • Input: text field, Enter submits (@keydown.enter.prevent), disabled while refineIsGenerating.
  • Analytics events (via trackEvent, tracking.ts:51): see §3 Monitoring.
  • Conditional rendering: options list only when msg.options?.length; skeleton cards while loadingOptions; warnings rendered as a muted list under the reply.
  • A11y: chips and Accept are <button> elements; thread container role="log" + aria-live="polite" for streamed replies; input labelled "Describe what you'd like to change".

RefineOptionCard.vue (new)

interface RefineOptionCardProps {
option: RefineOption; // required
index: number; // required — "Option {n}: {label}"
disabled?: boolean; // while a sibling accept is animating
}
  • Emits: accept (optionId: string)
  • Renders: Recommended banner (when isRecommended), label + description, per-field diff rows from changes, footer = Accept button or status badge (✓ Applied / Skipped) — per prototype :1882–1933.

AiAgentEditor.vue (modified)

  • showPreview: ref<boolean> (:3015) generalises to rightRailTab: ref<"preview" | "refine"> with the existing Preview markup as the preview pane (prototype :4066 pattern); rail tab buttons per prototype :1762–1776.
  • New aiChangedFields: reactive<Record<string, boolean>> + apply handler: on accept-option, write pendingData into the real form model (AgentDetailConfiguseAgentStore.ts:79–86), set highlight flags, and switch activeTab (0 Profile / 1 Capabilities / 2 Routing) using the existing mutation pattern (activeTab.value = n, :3652).
  • Refine tab visibility gate: engineVersion === 2 && rolloutPrefEnabled('rollout', 'ai_agent_refine') (pattern: useKnowledgeSourceTypeAvailability.ts:75–91).

Detail 2.B — Data-Fetching Strategy

  • Library: none/new — direct $apiMain service call via bot-automation-agents.refine(nuxtApp, agentId, payload) returning { fetch, controller } (repo convention, bot-automation-agents.ts:232–248).
  • Cache key structure: n/a — refine is a non-idempotent-output RPC; responses are never cached.
  • TTL & refetch triggers: n/a — user-initiated sends only.
  • Stale-while-revalidate: no.
  • Optimistic updates: no — the thread appends the user turn immediately (pure UI), but form changes only land on explicit Accept; failures mark the turn retryable without rollback complexity (REFINE-S03/ERR-1).
  • Cancellation: the AbortController from the service is aborted on editor unmount / rail close to avoid orphaned 60 s requests.
  • History cap: useRefineAgent.ts slices chat_history to the last 10 turns before sending (REFINE-S03/AC-2; N pending ML confirmation §5 OQ-3).

Detail 2.C — UI State Matrix

SurfaceLoadingEmptyErrorPartialSuccess
Refine threadstreaming indicator on AI turn; input + chips disabled (refineIsGenerating)"Refine your agent" + 4 suggestion chipserror turn "couldn't generate a suggestion — agent unchanged" + Retry; prior turns intactreply with options: [] → "no actionable change" turn, no cards (S01/AC-3); warnings shown under replyreply + 1..3 option cards, Recommended flagged
Option cardskeleton cards while loadingOptionsn/a — only rendered when options existn/a — errors never render cardssome sibling cards dismissed after one acceptedpending → Accept enabled; accepted → "✓ Applied"; dismissed → "Skipped"
Editor form (post-Accept)n/an/aSave failure: inline "couldn't save — agent unchanged" + retry (S02/ERR-1..2)staged-but-unsaved: highlighted fields + unsaved-changes stateSave 200 → highlights persist until edit/reload; config live

Detail 2.D — Data Integrity Matrix

Write pathTransaction scopePartial failure behaviorIdempotency key + TTLConsistency modelDuplicate-event handlingStale-read handling
POST /:id/refinenone — no write (spec-asserted: parameters + updated_at unchanged)n/an/a — safe to repeatread-committed snapshot of parameters at request timerepeat calls just produce new proposalspreview reflects persisted state at call time; see §2.E for concurrent-edit window
PATCH /:id (apply via Save — existing, unchanged)ActiveRecord::Base.transaction: Repositories::Update + SyncToAiService(mode: :update) (update_ai_agent.rb:58–68)sync failure → full rollback, agent on prior config (S02/ERR-1)none (full-merge last-writer-wins — existing behavior)strong within chatbot DB; eventual vs AI service (sync-in-txn makes divergence fail the write)n/afull-merge overwrites — see §2.E collision rows

Detail 2.E — Concurrency Collision Map

ResourceWritersCollision scenarioResolution mechanismBehavior when it fails
ai_agents.parametersany owner/supervisor/admin via SaveTenant A applies a refined pack while Tenant B saved a manual edit after A's refine preview was generatedlast-writer-wins full merge (existing update_ai_agent.rb:87–101; no optimistic locking today)B's intervening edit is overwritten by A's save. Unchanged from current editor behavior; refine widens the stale window (preview age). Mitigation this phase: FE warns if updated_at from a fresh GET /v2/ai_agents/:id differs from the loaded snapshot at Save time — flagged as a known limitation in §5 (adding lock_version is out of scope; would change the shared update path)
Refine threadsingle browser sessiontwo in-flight refine sendsinput disabled while refineIsGenerating (single-flight per panel)n/a
Vector storesSyncToAiService onlyrefine must never race sync's vector creationrefine's resolver is read-only (capability['vector_store'] passthrough) — it cannot create/purge stores by constructionn/a

Detail 2.F — Async Job / Event Consumer Spec

N/A — none. Refine introduces no worker, cron, queue consumer, or event handler (ADR-5). The only async-adjacent machinery is the existing SyncToAiService call inside the update transaction, which is synchronous and unchanged.

Detail 2.F.1 — Responsibility Boundary Matrix

Step (execution order)Owning squad / serviceInbound triggerOutbound effectFailure handlerPRD anchor
1. Render Refine tab (flag + engine gates)BOT / chatbot-feeditor mounttab hidden when gated§9
2. Accept + validate refine request (authz, flag, engine_version)BOT / chatbot BEPOST /:id/refine403/404/422 on gate failureGrape error response§8, §10 #1
3. Serialise request-body ai_agent pack + gather tools (read-only)BOT / chatbot BEstep 2 passupstream request body422 on unexpected serialisation error (logged)§10 #1
4. LLM refinement: diagnose, patch, apply, re-validate, reference-filterData/ML / mekari-agent via noncore-mragupstream POSTreply + options[] (+ deterministic fallback on LLM issues)fallback reply w/ empty options (never 5xx for LLM issues)§10 #1, §16 row 1
5. Map options back to capability_pack, returnBOT / chatbot BEupstream 2xx200 response, nothing persisted422 on transport failure (S01/ERR-1)§10 #1
6. Render options, Accept stages into formBOT / chatbot-feuser clickform state + highlights + tab switchn/a (client-side, reversible)§10 #2
7. Save persists + re-syncsBOT / chatbot BEPATCH /:idDB write + PUT /ai-agent pushtxn rollback on sync failure (S02/ERR-1)§10 #3

No ownership disagreements with the PRD found; PRD §16's "right rail is a Phase-1 pending item" was stale (rail exists) — recorded in §1 reconciliation notes, not a boundary dispute.

Detail 2.F.2 — State Surface Contract

EntityState field / eventDefaultUpdated byRead viaStale window
AI agent configai_agents.parametersPATCH /:id (Save) onlyGET /v2/ai_agents/:id (existing detail endpoint)preview may age while the tenant reads options (§2.E row 1)
Refine threadrefineMessages[] (FE memory)[]useRefineAgent per turncomponent statelost on reload — by design (S03/AC-3)
Option statusoption.statuspendingAccept handler (accepted + siblings dismissed)component stateephemeral
Staged-but-unsaved formeditor form model + aiChangedFieldspristineapplyPendingData porteditor state; Save serialises ituntil Save / discard / reload
Flag statesystem_preferences row → FE preferencesStore().lists['rollout_ai_agent_refine']enabled: falseops toggleexisting preferences bootstrapsession (prefs loaded at app start)

Detail 2.G — Cross-Layer Contract Verification

EndpointBE response schemaFE expected schemaMatch?Gaps
POST /:id/refinedata.reply, data.options[].{patches, updated_ai_agent} (snake_case)useRefineAgent consumes snake_case directly (repo convention — useGenerateAgent.ts does the same); derives changes/pendingData from updated_ai_agentyesnone — derivation (not mismatch) is by design (ADR-7); updated_ai_agent shape identical to the existing GET /:id detail + generate responses the FE already parses (parseDetailResponse). Option id/label/recommended are absent (no upstream source) — if the design's card title/badge need them, the FE fabricates locally (OQ-1b)
PATCH /:idexisting update_ai_agent_responseexisting useSaveAgent.buildPayloadyesnone — unchanged contract
upstream refine-skill-packstatus, reply, patches[], updated_skill_pack (skill_pack shape), warnings[] — single proposal (RFC §10.3b, as built)BE SkillPackMapper parses the same skill_pack shape it already handles from draft-skill-pack; use case wraps into options: [one] (ADR-6)yesremaining asks tracked in §5 OQ-1: deployment/proxy verification, option label/description fields, multi-option fast-follow, invented-ref behavior

Detail 2.H — End-to-End Data Flow

Flow 1 — refine turn: Tenant message → RefinePaneluseRefineAgent.refine() (history capped to 10; sends ai_agent = live editor pack) → bot-automation-agents.refine() POST /v2/ai_agents/:id/refine → Grape route (Ownership → set_role → flag gate) → UseCases::RefineAiAgent (FindBy for authz + engine_version guard only → SkillPackBuilder read-only serialise of the request-body ai_agentRepositories::Refine gathers available_tools + proxies upstream) → SkillPackMapper maps updated_skill_packupdated_ai_agent per option → 200 → FE differ builds ProposedChange[] → thread renders reply + cards. Side effects: BE structured log lines (refine requested/succeeded/failed), FE Mixpanel refine_requested/refine_succeeded. Ownership per step: §2.F.1.

Flow 2 — accept + save: Accept → applyPendingData into form model + aiChangedFields highlights + activeTab switch (no HTTP; siblings dismissed) → Save → useSaveAgent.save() PATCH /v2/ai_agents/:id → validation → txn (Update + PaperTrail version + SyncToAiService(mode: :update)PUT /ai-agent) → 200 → FE refine_applied event. Failure: 400 no write; sync failure → rollback (S02/ERR-1).

Flow 3 — discard: tenant ignores/dismisses options or closes editor → no HTTP, no write; FE refine_discarded event.

Detail 2.I — Scope Boundaries

BE (chatbot) — files to create:

  • app/api/frontend_service/v2/ai_agent/use_cases/refine_ai_agent.rb
  • app/api/frontend_service/v2/ai_agent/repositories/refine.rb
  • app/api/frontend_service/v2/ai_agent/use_cases/mappers/skill_pack_builder.rb
  • app/api/frontend_service/v2/ai_agent/models/refine_response.rb
  • specs: spec/api/frontend_service/v2/ai_agent/refine_ai_agent_spec.rb, spec/api/frontend_service/v2/ai_agent/use_cases/mappers/skill_pack_builder_spec.rb

BE — files to modify (+ reason):

  • app/api/frontend_service/v2/ai_agent/ai_agents_controller.rb — add post '/:id/refine' route
  • app/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service.rb — delegate shaping to SkillPackBuilder (behavior-preserving; regression-locked)
  • lib/ai_service/ai_agent.rb — add refine_skill_pack(body:)
  • spec/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service_spec.rb — add byte-identical request-body regression coverage

BE — explicitly NOT touched: use_cases/update_ai_agent.rb, repositories/update.rb, use_cases/generate.rb, repositories/generate.rb, skill_pack_mapper.rb (consumed, not modified), all V1 ai_agent code, all migrations.

FE (chatbot-fe) — files to create:

  • modules/bot-automation/components/refine/RefinePanel.vue
  • modules/bot-automation/components/refine/RefineOptionCard.vue
  • modules/bot-automation/composables/useRefineAgent.ts (+ differ util)
  • specs under the module's existing vitest layout

FE — files to modify (+ reason):

  • modules/bot-automation/components/AiAgentEditor.vueshowPreview → two-tab rightRailTab rail; aiChangedFields + apply handler; flag/engine gate
  • common/services/main/v2/bot-automation-agents.ts — add refine()
  • common/services/main/endpoint.ts — add v2.ai_agents.refine: "/v2/ai_agents/:id:/refine"

FE — explicitly NOT touched: useSaveAgent.ts, useGenerateAgent.ts, useAgentStore.ts interfaces, the Preview pane's internals, routing/pages.

Shared modules touched + impact: sync_to_ai_service.rb is the single shared Phase-1 file (impact = every autonomous agent save); the regression spec in Chunk 1 is the guard. AiAgentEditor.vue is large (5,416 lines) and shared with all agent editing — rail changes are additive markup + refs, gated behind the flag.

Detail 2.J — Asset Inventory (frontend half)

N/A — no new binary assets. All visuals are Mekari Pixel components (MpText, MpButton, MpBadge) + CSS (the .recommended-border-anim conic-gradient border is pure CSS, ported from prototype [id].vue:9318–9344). Icons, if any, come from the design system's existing icon set (same source as the editor's current chatbot/competencies/workflow tab icons).


3. High-Availability & Security

HA narrative. Refine is stateless on the BE (no session, no write), so pod restarts/redeploys lose nothing server-side; an in-flight request fails to the FE's retryable error turn and the agent is untouched. When the upstream AI service is degraded or down, refine degrades to an explicit error state while manual config editing remains fully functional (the editor form + Save do not depend on refine). The FE thread survives BE failures (turns stay client-side, failed turn retryable). Save keeps its existing all-or-nothing transaction: a failed sync can never leave the DB and the AI service divergent.

Performance Requirement

  • Frontend: no new route — the rail mounts inside the existing editor page. Bundle delta budget: ≤ 15 KB gzip for the two new components + composable (no new deps; differ is hand-rolled). LCP/CLS unaffected (rail is user-toggled, below-the-fold interaction); INP: Accept staging must complete < 200 ms (pure state mutation). Browser support + a11y level: repo status quo (WCAG AA per Detail 3.E).
  • Backend: volume is human-paced (chat cadence) — design load is tens of RPM org-wide, not RPS-scale. Budgets: BE proxy overhead (everything except the upstream call) < 500 ms p95; end-to-end perceived target ≤ 10 s p95 (upstream-dominated, PRD §8); hard timeout 60 s. Worker-pool watch: each in-flight refine holds a Rails worker up to 60 s (ADR-5 consequence) — monitor saturation during Alpha before widening rollout.
  • Load test plan: n/a — human-paced feature behind a flag with staged rollout; revisit if Alpha shows unexpected fan-out (documented trigger in §5).

Monitoring & Alerting

The PRD §13 event names map to the two telemetry systems that actually exist in these repos (no BE statsd/event bus exists in the ai_agent paths — verified convention is Rails.logger + Rollbar):

PRD §13 eventImplementationProperties
refine_requestedFE trackEvent("refine_requested", …) (Mixpanel, tracking.ts:51) + BE log V2 RefineAiAgent requested agent=<id> org=<org> history_turns=<n> message_len=<n>company/agent ids auto-added by trackEvent context
refine_succeededFE trackEvent + BE log V2 RefineAiAgent succeeded agent=<id> options=<n> warnings=<n> latency_ms=<n>patch/option counts, latency
refine_failedFE trackEvent + BE `Rails.logger.error("V2 RefineAiAgent failed agent= reason=<timeout5xx
refine_accepted / refine_applied / refine_discardedFE trackEvent only (Accept/discard are client-side; apply is indistinguishable from a manual save on the BE — ADR-2)option id, patch/field count
refine_revertedoperational review query over PaperTrail versions (config changed then reverted ≤ 48 h) — Mixpanel-correlated; no automated emitter this phase
  • Alerts (owner: BOT squad, per PRD §13): refine_failed/refine_requested

    10% over 1 h → page on-call + notify PM; refine latency p95 > 10 s over 1 h → notify squad (upstream latency check); reverted/applied > 20% weekly → PM quality review. Wired on whatever alerting consumes Rollbar + logs today for this service (same sinks as existing V2 SyncToAiService errors).

  • Dashboard: extend the squad's existing service dashboard with the V2 RefineAiAgent log-derived rate/error/duration panels; Mixpanel report for the funnel (requested → succeeded → accepted → applied).
  • SLO: refine availability tracks the AI-service dependency; no independent SLO this phase (flag-gated, non-critical path — manual editing is the fallback).
  • "Debug at 3 am" runbook: 1) check Rollbar for V2 RefineAiAgent / V2 SyncToAiService spikes; 2) grep pod logs for V2 RefineAiAgent failed … reason=; 3) if upstream-wide, flip ai_agent_refine OFF (no deploy) — manual editing unaffected; 4) confirm agent integrity via PaperTrail versions (refine itself cannot have written).

Logging

  • BE structured lines (info): V2 RefineAiAgent requested|succeeded with agent_id, organization_id, history_turns, message_len, options, warnings, latency_ms; (error): V2 RefineAiAgent failed + Rollbar with the same context hash — matching the V2 <Class> prefix convention.
  • PII scrubbing: user_message and chat_history content are never logged (tenants paste customer conversations/error traces) — only lengths and counts. Same rule on the FE: Mixpanel properties carry counts/ids, never message text.
  • FE: no console logging in production paths; errors surface via the existing error-extraction pattern.

Security Implications

  • Threat model: (a) cross-tenant refine — tenant A refining tenant B's agent; (b) role escalation — non-admin proposing/applying config; (c) prompt injection via user_message/chat_history steering the LLM into harmful config; (d) config injection — a crafted "refined" pack referencing tools/ KBs the tenant doesn't own; (e) stored XSS via LLM-generated strings rendered in the FE; (f) client-supplied pack — the refine request carries the ai_agent pack from the FE body (built shape), so a caller can send an arbitrary pack under any :id.
  • Mitigations: (a) Middlewares::Ownership + Repositories::FindBy(id, org_id) scoping — cross-org ids 404; (b) set_role(%w[owner supervisor admin]) on refine, unchanged on PATCH; (c)+(d) the refined pack is only a proposal — going live requires the standard update validation (Validators::CapabilityRefPresence), which rejects any invented action/kb_id ref at Save with a 400 and no write — the as-built upstream does not strip refs at propose time (S01/AC-4); no auto-apply (ADR-8); (e) all LLM strings render through Vue text interpolation — no v-html anywhere in the refine components (explicit rule for review); (f) the FE-supplied ai_agent pack is accepted because refine is propose-only — it writes nothing, so a crafted pack produces only a throwaway proposal; the :id is still resolved server-side (Repositories::FindBy(id, org_id)) to enforce ownership (cross-org → 404) and the engine_version guard (legacy → 422), and going live still requires the standard Save path where CapabilityRefPresence re-validates every reference (400, no write). The pack shape is never persisted from the refine path.
  • Input validation per field: user_message 1..4000 chars required; chat_history ≤ 10 items, each role enum + content ≤ 4000; ai_agent required object (profile/capabilities/routing) — parsed by SkillPackBuilder, rejected if not a well-formed pack; size-capped (Grape-enforced). trace omitted from v1 (OQ-2).
  • Injection: ActiveRecord parameterization (no raw SQL added); outbound URL is a fixed path constant (no SSRF surface — no user-supplied URLs).
  • Secrets: none added — reuses the existing Http client credentials (lib/ai_service/ai_agent.rb:9); nothing logged.
  • Audit: every applied change produces a PaperTrail version on ai_agents (ai_agent.rb:5) — action, before/after, timestamp; refine proposals are deliberately unaudited server-side this phase (stateless — §5 notes the follow-up if compliance requires).
  • Rate limiting: none exists on the sibling generate endpoint; refine launches at parity with a pre-GA action item to add per-org throttling (§5 concern) since each call spends LLM tokens.
  • Tenancy isolation enforcement point: Grape middleware + org-scoped repo queries (single enforcement layer, consistent with all v2 ai_agent routes).
  • Static analysis: bundle exec brakeman (BE, existing toolchain) and pnpm lint (FE) run in the pre-merge recipe (§4.E).
  • Public exposure: endpoint sits behind the existing authenticated admin gateway — no anonymous surface.
  • ISO 27001/27701: no new data category stored (nothing stored); pasted customer content transits to the same AI service that already processes agent conversations — no new processor relationship.

Role × Endpoint Authorization Matrix

RoleEndpoint(s)Permitted methodsTenant scopeUI surface visibility (FE)Additional constraintAudit trail
owner/v2/ai_agents/:id/refine · /v2/ai_agents/:idPOST · PATCHown org only (Ownership middleware + org-scoped FindBy)Refine tab visible (flag ON + engine v2)flag ai_agent_refine ON; agent must be autonomousPaperTrail on apply; V2 RefineAiAgent logs
supervisorsamesamesamesamesamesame
adminsamesamesamesamesamesame
any other rolenone (403 from set_role)Refine tab not rendered403 in request logs

Detail 3.A — Failure Mode Catalog (merged)

SurfaceFE behavior on failureBE response on failureCode-shape consistency
Refine send — upstream timeout (60 s) / 5xxerror turn + Retry; thread intact; agent unchanged422 {error: {messages: […]}} after logging (S01/ERR-1)yes — FE parses response._data.error.messages[0] (pattern useGenerateAgent.ts:79–90)
Refine send — upstream LLM/validation issue (non-transport)fallback reply rendered, no cards (S01/ERR-2)200 passthrough of upstream deterministic fallbackyes
Refine send — 403 (role/flag)tab shouldn't render; if raced, toast + hide tab403yes
Refine send — 404 (wrong org / deleted)error turn "agent not found"404yes
Refine send — 422 legacy agenttab shouldn't render (gate); defensive error turn422 not_autonomous_agentyes
Save — validationinline 400 error, no write400 (CapabilityRefPresence)yes — existing
Save — sync failureinline "couldn't save — agent unchanged" + retry422 after txn rollbackyes — existing
Network offline (FE)send fails immediately → retryable turnn/a
Editor unmount mid-requestAbortController.abort() — no dangling handlerrequest completes/aborts server-side harmlessly (no write)n/a

Race narratives: double-send prevented by refineIsGenerating single-flight; navigation-during-fetch handled by abort-on-unmount; concurrent editor saves — §2.E row 1 (known limitation).

Detail 3.A.1 — Branch & Skip Catalog

Branch triggerWhere checkedDownstream effectAudit trailUser-visible?
Flag ai_agent_refine OFFFE gate (tab hidden) + BE route guardendpoint 403; feature invisiblerequest logsno (hidden)
Legacy agent (engine_version != 2)FE gate + BE use-case guard422; refine impossible on legacy agents (NEG-1)request logsno (tab hidden)
No actionable change in messageupstreamoptions: [], reply only (S01/AC-3)options=0 in success logyes — reply without cards
Malformed patch op from the LLMupstream apply step (per-op isolation, companion doc §3)op skipped; warning appended; rest of the patch set applieswarnings=<n> in success logyes — warning list
Patch set would break the pack (skills empty/missing)upstream tripwire (companion doc §3)change rejected; original pack returned unchangedupstream warning passthroughyes — reply explains
Invented action / kb_id ref surviving upstreamBOT-side at Apply: CapabilityRefPresence (as-built upstream no longer strips refs — S01/AC-4 drift, see §1 reconciliation)Save returns 400; nothing written400 in request logsyes — Save error (S02/ERR-2 path)
Tenant accepts none / closesFEnothing written; refine_discarded (S02/AC-3)Mixpanel eventyes (implicit)
trace absentBE (optional param)refine proceeds degraded (no runtime context)trace_present=false in logno

Detail 3.B — Error Response Catalog (BE)

Shape: { "error": { "messages": ["<human-readable>"] } } (existing Grape convention).

EndpointError caseHTTPMessage keyWhenUser-facing?
POST /:id/refinemissing/oversized user_message, >10 history items400Grape param validation messagesrequest shape invalidyes (inline)
POST /:id/refinerole not permitted / flag OFF403Permission denied (existing set_role message)gate failureyes
POST /:id/refineagent not found in org404AI Agent not foundFindBy missyes
POST /:id/refinelegacy agent422not_autonomous_agentengine_version guardyes
POST /:id/refineupstream timeout/5xx422Failed to refine AI Agenttransport failureyes
PATCH /:idref validation400existing CapabilityRefPresence messagesinvalid packyes — existing
PATCH /:idsync failure422Failed to sync AI Agent to AI Service (existing, update_ai_agent.rb:66)upstream push failsyes — existing

Detail 3.C — Error Message Catalog (FE)

Error codeUser-facing message (i18n key)SurfaceUser-facing?
refine 422/timeoutbot_automation.refine.error_generic — "Couldn't generate a suggestion — your agent is unchanged. Try again."error turn in thread + Retryyes
refine 400bot_automation.refine.error_invalid — "That message couldn't be sent. Shorten it and try again."inline under inputyes
refine 403bot_automation.refine.error_forbidden — "You don't have access to Refine."toast; tab hiddenyes
save 400/422existing save error keys (unchanged)inline in editoryes

(i18n keys follow the module's existing namespace; exact key names finalized against the FE locale files at build.)

Detail 3.D — Compliance & Data Governance

Trigger check: refine transits user-pasted content (may contain end-customer PII from conversation snippets/error traces) but stores nothing new.

FieldClassificationLegal basisRetentionEncryptionAccess auditRight-to-delete path
user_message / chat_history (in transit only)potentially PII (tenant-pasted customer content)same processing basis as existing agent-conversation AI processing (UU PDP — processor role unchanged)not persisted by chatbot BE (stateless); upstream retention = Data/ML's existing LLM-call policy (confirm in §5 OQ-1 contract review)TLS in transit; nothing at rest in chatbotnot logged (content-scrub rule, §3 Logging)n/a — nothing stored to delete
ai_agents.parameters (applied config)config, non-PIIexistingexistingexisting (DB at rest)PaperTrailexisting

Detail 3.E — Accessibility

  • WCAG AA. Keyboard: chips, input (Enter to send), Accept buttons, and rail tabs all reachable in DOM order; rail tabs are buttons with aria-selected/role="tab" semantics.
  • Focus management: on Accept, focus moves to the first highlighted form field (mirrors the prototype's scroll-to-field, implemented with the editor's existing nextTick + focus pattern, AiAgentEditor.vue:3695–3697).
  • ARIA: thread role="log" aria-live="polite"; streaming turns announce once complete (avoid per-word announcements); option cards labelled "Option n: {label}, recommended".
  • Contrast: Pixel tokens (verified at design QA with Wulan once frames land).
  • prefers-reduced-motion: disable the word-streaming animation and the .recommended-border-anim spin; render text instantly.

4. Backwards Compatibility and Rollout Plan

Compatibility

  • BE: purely additive — one new route, one new upstream client method, one new flag row. PATCH /v2/ai_agents/:id, POST /generate, and all V1 routes unchanged. The only shared-code change is the SkillPackBuilder extraction, which is behavior-preserving and regression-locked (Chunk 1).
  • FE: additive components behind the flag; the Preview rail's existing behavior is preserved as the default tab. No saved client state affected (refine stores none).
  • Cross-layer: old FE + new BE — refine endpoint simply unused; new FE + old BE — impossible in normal deploy order (see matrix), and the tab is flag-gated anyway.
  • API versioning: additive under v2; no deprecations.

Rollout Strategy

  • Deploy order: BE first (route live but flag OFF), FE second, then flag ON per workspace. Rationale: FE calls fail 404 against an old BE; flag gating makes the window moot, but BE-first removes it entirely.
  • Flag coordination: a single flag ai_agent_refine (system_preferences: group_code 'rollout', code 'ai_agent_refine', default enabled: false) gates both layers — BE route guard + FE tab visibility via the preferences payload. One toggle, no coupling rules.
  • Stages (audience per PRD §12; gates/dates are delivery-layer): Internal Alpha (specialist orgs, 26Q2 cohort) → Closed Beta (3–5 customer workspaces) → Open Beta (opt-in) → GA (default ON). Advancement evidence = PRD §15 gates read off the §3 funnel metrics.
  • Stop conditions: refine_failed rate > 10% over 1 h; p95 > 10 s sustained over 1 h; revert/applied > 20% weekly (PRD §12.1 thresholds).
  • Rollback mechanism: flip the flag OFF (no deploy — endpoint 403s, tab disappears; manual editing unaffected). Mid-session users: next send fails 403 gracefully into the error turn.
  • Blast radius (worst case): tenants on flagged workspaces lose the refine panel; no config integrity risk — refine cannot write, and apply rides the pre-existing update path.
  • PIC + timeline: delivery-layer (not yet handed to delivery).

Detail 4.A — Cross-Layer Rollout Compatibility Matrix

ScenarioFEBEWorks?Mitigation
Pre-deployOldOldyesbaseline
Backend firstOldNewyesnew route unused; flag OFF
Frontend firstNewOlddegraded-safetab hidden unless flag payload exists; a raced call 404s into the error turn — avoided by BE-first order
Both deployed, flag OFFNewNewyesfeature invisible (target pre-rollout state)
Both deployed, flag ONNewNewyestarget state
Backend rollbackNewOlddegraded-saferefine sends fail into retryable error turn; editor/save unaffected; flip flag OFF to hide tab
Frontend rollbackOldNewyesendpoint idle

Detail 4.B — Configuration Contract

LayerConfigTypeDefaultRequiredProvisionerSecret?
BE + FEsystem_preferences row rollout / ai_agent_refineboolean (enabled)falseyesops (console/seed, per existing rollout-row practice)no
BEupstream path constant /qontak-ai-noncore-mrag/api/ai-agent/refine-skill-packcode constantyescode (Chunk 3)no
BErefine timeouts open_timeout: 60, read_timeout: 60code constants60 syescode (drafter parity)no
FEhistory cap REFINE_HISTORY_MAX_TURNSconst10yescode (revisit per §5 OQ-3)no

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

LayerCommand (source)What it must prove
BE unit/requestbundle exec rspec spec/api/frontend_service/v2/ai_agent/ (source: .rspec + existing spec dir layout)refine 200/400/403/404/422 matrix; no-write assertion (parameters/updated_at unchanged); warnings/options passthrough; legacy-agent 422
BE regression (extraction)bundle exec rspec spec/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service_spec.rb (existing file)SyncToAiService request body byte-identical pre/post SkillPackBuilder extraction; vector-store create/reuse/purge flows unchanged
BE lint / securitybundle exec rubocop · bundle exec brakeman (repo toolchain)style + no new security findings
FE unitpnpm test (source: package.json:17vitest run)useRefineAgent history cap + differ correctness; RefinePanel/RefineOptionCard states; Accept mutates form + highlights + switches tab with zero HTTP
FE lintpnpm lint (source: package.json:15)ts + prettier clean
FE E2Epnpm test:e2e (source: package.json:22playwright test) — one spec: refine happy path against a mocked BEthread → options → accept → form staged → save payload contains staged pack
Cross-layer (manual, staging)refine a seeded autonomous agent end-to-end against the ML stub, then real upstream when availablecontract §2.4 holds on the wire; latency within budget
Config-audit scenario (staging, real upstream)seed an agent with two known flaws (routing rule → missing capability id; action gated on an unreachable milestone), send "Review my configuration and find potential issues"reply names at least one seeded flaw; ≥1 option's patches addresses it; applying + saving passes CapabilityRefPresence (success criterion 6)

Detail 4.D — Agent Execution Plan

OrderLayerChunkFiles to modify/createCommands to runAcceptance criteria (verifiable)
1BEExtract SkillPackBuilder (behavior-preserving; lands alone)create use_cases/mappers/skill_pack_builder.rb (+ spec); modify repositories/sync_to_ai_service.rb to delegate build_skill_pack/build_skill/build_skill_actions/build_completion/build_routing_rules through the builder with an injected stateful resolver; extend spec/.../sync_to_ai_service_spec.rb with a full-fixture request-body snapshotbundle exec rspec spec/api/frontend_service/v2/ai_agent/ · bundle exec rubocopall existing sync specs green; new snapshot spec proves byte-identical request body; builder spec covers read-only resolver returning persisted vector_store without any create_vector_db call (assert via mock)
2BEFlag row + helpersseed/console note for system_preferences rollout/ai_agent_refine; flag predicate where the controller can call it (pattern system_preference.rb:41–51)bundle exec rspec (flag predicate spec)predicate true only when row enabled; default absent → false
3BEUpstream client methodmodify lib/ai_service/ai_agent.rb: add refine_skill_pack(body:) → POST /qontak-ai-noncore-mrag/api/ai-agent/refine-skill-pack, 60 s timeoutsbundle exec rspec (client spec stubbing Http)method issues POST to exact path with body + timeouts (asserted via mock)
4BERepositories::Refinecreate repositories/refine.rb: assemble {company_id, current_skill_pack, user_message, chat_history, available_tools} per the as-built §2.4 upstream schema (current_skill_pack = SkillPackBuilder over the request-body ai_agent; trace omitted v1 — OQ-2; tools query per generate.rb:52–57, formatted with type: 'qontak_function_call' per RFC §10.3b), call clientbundle exec rspecrequest body matches §2.4 upstream schema verbatim for a fixture pack (current_skill_pack from Chunk-1 builder w/ read-only resolver)
5BEUseCases::RefineAiAgent + route + response modelcreate use_cases/refine_ai_agent.rb (contract: id, org/company ids, user_message 1..4000, chat_history ≤10, ai_agent required pack; guards: flag → 403, engine_version → 422 [read from the DB row via FindBy, not the body]; wraps the upstream proposal into options: [ { patches, updated_ai_agent: SkillPackMapper(updated_skill_pack) } ], empty patches → options: []), models/refine_response.rb; modify ai_agents_controller.rb add post '/:id/refine' with set_role(%w[owner supervisor admin])bundle exec rspec spec/api/frontend_service/v2/ai_agent/refine_ai_agent_spec.rbfull status matrix (200/400/403/404/422) green; 200 response matches §2.4 (data.reply + options[].{patches, updated_ai_agent}); DB row unchanged after call (explicit spec); upstream 5xx stub → 422 + error log line
6FEService + composablemodify endpoint.ts (+refine), bot-automation-agents.ts (+refine() sending {user_message, chat_history, ai_agent}, returning {fetch, controller}); create useRefineAgent.ts (thread refs, single-flight, history cap 10, differ of current form vs option.updated_ai_agentProposedChange[], any card id/label/recommended fabricated FE-side per OQ-1b, error extraction per useGenerateAgent.ts:79–90)pnpm test · pnpm lintunit specs: cap slices to last 10; differ emits correct add/update/remove rows for profile scalar, capability-by-id, routing-by-id fixtures; abort on scope dispose
7FERail + panel componentsmodify AiAgentEditor.vue (showPreviewrightRailTab two-tab rail; aiChangedFields; apply handler writing pendingData into the form model + activeTab switch; gate = flag && engineVersion===2); create RefinePanel.vue, RefineOptionCard.vuepnpm test · pnpm lint · pnpm buildcomponent specs: empty/loading/error/no-change/success states; Accept: form mutated, highlights set, tab switched, siblings dismissed, no HTTP; tab absent when flag OFF or legacy agent; build passes
8FEAnalytics + i18nwire trackEvent calls (`refine_requestedsucceededfailed
9bothE2E + staging verificationFE Playwright spec (mocked BE); staging run against ML stubpnpm test:e2e · manual staging checklistE2E green; staging: refine turn ≤ 10 s p95 against stub, save applies staged pack, PaperTrail version written

Chunks 1–5 (BE) can proceed against the §2.4 stub before the upstream endpoint exists; Chunk 9's real-upstream pass is blocked on Data/ML delivery (§5 OQ-1).

Detail 4.E — Verification & Rollback Recipe

  • Pre-merge verification (in order):
    • BE: 1. bundle exec rubocop · 2. bundle exec brakeman · 3. bundle exec rspec spec/api/frontend_service/v2/ai_agent/ · 4. full bundle exec rspec (repo gate)
    • FE: 1. pnpm lint · 2. pnpm test · 3. pnpm build · 4. pnpm test:e2e (refine spec)
  • Post-deploy verification signals:
    • Log query: "V2 RefineAiAgent succeeded" count > 0 and "V2 RefineAiAgent failed" / requested < 10% over the first hour of each rollout stage.
    • Rollbar: zero new V2 RefineAiAgent / V2 SyncToAiService error classes after deploy (pre-flag-ON, the endpoint should log nothing at all).
    • Mixpanel funnel: refine_requestedrefine_succeeded conversion ≥ 90% in Alpha.
    • Sync-regression canary: after the Chunk-1 deploy (before any refine traffic), an ordinary agent save on staging produces an unchanged PUT /ai-agent body (spot-check against a pre-deploy capture).
  • Rollback recipe (ordered):
    1. Feature level: set system_preferences rollout/ai_agent_refine enabled: false (console; no deploy). Tab disappears; endpoint 403s; manual editing unaffected.
    2. Per-agent config level: restore the prior parameters from the agent's PaperTrail version (ai_agent.versionshas_paper_trail, ai_agent.rb:5), then save through the standard update path so SyncToAiService re-pushes the old skill_pack upstream. (Corrects PRD's ai_agent_histories reference — that mechanism is V1-only.)
    3. Code level (only if Chunk 1 itself regressed sync): revert the extraction PR; the byte-identical snapshot spec pinpoints any divergence.
    4. Confirm: Rollbar quiet for 15 min; a staging save round-trips 2xx.

Detail 4.F — Resource & Cost Notes (advisory)

  • Compute: no new pods/workers; marginal request load on existing chatbot API pods (human-paced). Watch worker occupancy vs 60 s ceilings (§3).
  • DB: +2 lightweight SELECTs per refine turn (agent + tools); no new connections; storage growth zero (stateless).
  • Network egress: one upstream HTTPS call per turn; payload ≈ skill_pack size (tens of KB typical).
  • New infra: none. Cost center is upstream LLM tokens — owned/priced by Data/ML; the FE history cap (10 turns) is the token-cost bound on our side.

5. Concern, Questions, or Known Limitations

Carried from PRD §18, updated with grounding results; plus new engineering concerns:

#TypeQuestion / concernOwnerNeeded by
OQ-1Blocker (verification)Upstream refine-skill-pack is documented "as built" in mekari-agent (companion doc); §2.4 now carries that contract verbatim. Remaining with Data/ML: (a) confirm the endpoint is deployed + exposed through the noncore-mrag gateway path (/qontak-ai-noncore-mrag/api/ai-agent/refine-skill-pack) in staging/prod; (b) ask for option id/label/description/recommended fields in the response — the built shape drops them (no upstream source; fabrication rejected), so the FE currently makes up any card title/badge locally; the design's option-card title wants real upstream text; (c) confirm whether noncore's company_tools registry and chatbot's AiAgentTool stay in sync (BE sends tools explicitly; auto-fill is the upstream fallback); (d) what the model does with an invented action/kb_id at propose time (as-built: not stripped; BOT catches at Apply via 400); (e) multi-option per turn as a fast-follow (design shows 1–3 options; as-built returns 1); (f) upstream retention policy for transited PII (companion doc is silent); (g) confirm complaint-free config-audit prompts ("find potential errors in my configuration") are tuned/tested — the invariants in the model's instructions (companion doc §4) imply the capability, and it's core to the feature's reliability goal, but the documented heuristics are all complaint-triggered; ask whether the model also proactively flags invariant violations it notices while fixing something else.PM (Dimas) + Data/MLbefore Chunk 9 real-upstream pass (PRD: 2026-07-15)
OQ-2Open Resolvedtrace: the documented shape is {workflow_state, recent_turns}, but workflow_state lives in the mekari-agent DB (upstream RFC §12.1) — the chatbot BE cannot supply it. Decided before RFC: omit trace entirely from v1 (not merely reshaped/degraded) — chatbot BE sends only user_message + chat_history. Revisiting trace sourcing (upstream self-fetches workflow_state by company/agent id, or chatbot sends recent_turns only) is deferred to a later phase if the need resurfaces. Overlaps AI Agent Live Monitoring signals.PM + Eng (Eko) + Data/MLclosed 2026-07-07
OQ-3Assumption Resolvedchat_history cap = 10 turns (REFINE_HISTORY_MAX_TURNS), matching the upstream's documented "last ~10 turns" (companion doc §5). Truncation: FE keeps the most recent 10.closed 2026-07-05
OQ-4OpenKB scope on apply: if a refinement changes a capability's sources/vector-store reference, Save's stateful resolver will re-resolve (create/purge vector DBs) per existing sync semantics. Recommendation: allow it (it's the same behavior a manual edit triggers); if product wants KB-affecting patches rejected instead, add a guard in the FE differ + BE update validation. Decide before Beta. Needs Data/ML to confirm the details of exactly which patches/functions the refine endpoint can update — until that scope is confirmed we don't know whether a KB-affecting patch is even something the upstream can propose.PM + Eng + Data/MLbefore Closed Beta
OQ-5Follow-upFigma frames for the Refine rail are TBD; prototype is canonical (PRD Header). Design QA (Wulan) to bless the built UI against the prototype until frames exist.Designbefore Open Beta
OQ-6Known limitationStale-preview / concurrent-edit window (§2.E row 1): full-merge PATCH has no optimistic locking; a refined save can overwrite a concurrent manual edit. Phase mitigation: FE updated_at freshness warning at Save. Real fix (lock_version on the shared update path) is deliberately out of scope.Engacknowledged; revisit if Alpha shows collisions
OQ-7Pre-GA actionNo rate limiting on refine (parity with generate) while each call spends LLM tokens. Add per-org throttling (e.g. N refines/minute) before GA.Engbefore GA
OQ-8OpsSeeding convention for the system_preferences rollout row across environments (console vs seed file) — confirm with BE at review; no migration path exists for these rows today.Engbefore Alpha
OQ-9Product/complianceRefine conversations are unaudited server-side (stateless by ADR-4). If compliance later requires an audit trail of AI-proposed changes, that's the deferred session-persistence phase.PMnoted for Phase 3+
OQ-10Design (partially resolved)Config-audit mode surfacing. (a) PM: add an audit user story to the PRDdone 2026-07-05: PRD v1.4 added [REFINE-S04] with a seeded-flaw staging fixture (this RFC's Detail 1.A/1.C carry its rows). (b) Open — Design (Wulan): a fifth suggestion chip, e.g. "Check my configuration for issues" (change-request to the prototype, not an edit by us; prototype chips at [id].vue:1806–1845). Engineering cost zero — same endpoint, same flow.Designbefore Closed Beta
OQ-11OpenRefine request shape vs multi-turn conversational context. The Data/ML refine-skill-pack endpoint is designed around a single POST that carries the full current state (current_skill_pack, user_message, chat_history) in one request, but the Refine rail is a multi-turn chat where a follow-up ("now also handle the timeout case") depends on context from earlier chat bubbles. Need to confirm with Data/ML how chat_history should be structured/bounded per turn so prior-turn context actually carries through, rather than assuming a flat re-send of everything (capped at 10 turns per OQ-3) is sufficient.PM + Data/MLbefore Chunk 9 real-upstream pass
OQ-12OpenOpenAPI specs for Data/ML's endpoints. Need a formal API spec (OpenAPI/Swagger — see swagger.io/specification) for refine-skill-pack (and related upstream endpoints), or direct access to the Data/ML-owned repository, so §2.4's contract can be implemented and tested against a documented spec instead of the companion doc's prose.PM + Data/MLbefore Chunk 9 real-upstream pass
OQ-13Risk / follow-upApplied-but-worse config — revert mechanism reconsideration. Current mitigation (§2.4, §4) is preview-then-apply (no auto-apply) + per-agent revert from the PaperTrail versions on ai_agents. Grooming follow-up: we also intend to bring back the AI Agent Draft versioning capability we already own as an additional revert mechanism, although it hasn't actually shipped in production yet (needs verification against the real chatbot-be implementation) and was built for the old AI Engine, not the new Autonomous AI Engine — so it needs alignment with Data/ML to accommodate this engine's config model. For now, assume revert-from-UI may not be available this phase; explore supporting revert behind the scenes (server-side, not user-facing) as a fallback alongside the existing PaperTrail path.PM + Engbefore GA
REV-1Review finding (major)Stale line-number citations. The rfc-reviewer R1 grounding pass (2026-07-08) verified ~60 symbol/pattern/contract claims against the real chatbot/chatbot-fe/qontak-designer trees — all real — but the line numbers have drifted since the 2026-07-05 grounding: AiAgentEditor.vue off ~+66…+86 (showPreview :3015→:3081, activeTab.value=2 :3652→:3738, handleSave :3692→:3778; file 5,416→5,529 lines) and the prototype ~+18 (rightRailTab :4066→:4084, ProposedChange :4021→:4039, .recommended-border-anim :9318→:9422). Symbols are correct so a grep-first agent recovers, but Detail 2.0 presents these as authoritative. Fix: re-ground the numbers or switch to symbol-anchored citations and note these two files drift.Eng (author)before agent execution
REV-2Review finding (minor)Two BE paths one directory too deep. Detail 2.0 Source Verification places authorization_helpers.rb and ownership.rb under app/api/frontend_service/v2/ai_agent/{helpers,middlewares}/, but they actually live at app/api/frontend_service/{helpers,middlewares}/ (shared FE-service scope): set_role/403 at helpers/authorization_helpers.rb:6–10, Ownership 403 at middlewares/ownership.rb:7. Symbols/behavior correct. Fix: correct the two paths.Engbefore agent execution
REV-6Review finding (minor)updated_capability_pack → form-model field/label map deferred. Detail 2.A + Design↔Code name the sources (useAgentStore.ts:79–86 AgentDetailConfig + prototype applyPendingData) but tabulate no explicit field/label map — the single largest "agent figures it out" surface for the FE differ + applyPendingData port. Fix: add an explicit field-map table to Detail 2.A.Eng (FE)before Chunk 6/7
REV-7Review finding (minor)Internal staleness on AC-4. Detail 1.A (AC-4 row) and the §3.A.1 branch catalog still say REFINE-S01/AC-4 "needs PRD correction," but the PRD already corrected AC-4 to the as-built surgical-patch guarantee (v1.5/v1.5.1) — the §1 reconciliation row correctly says "Corrected in PRD v1.5." Fix: update both cells to "corrected in PRD v1.5.1."PMhousekeeping

| OQ-14 | Known limitation | Upstream warnings not surfaced to the FE (built shape). The as-built upstream returns warnings[] (e.g. skipped malformed patch op, tripwire "routing_rule removed"), but the built BE→FE response drops them — so the tenant does not see what the surgical-patch safeguards did on their behalf. Acceptable for v1 (the mapped updated_ai_agent already reflects the applied result), but reconsider forwarding warnings before Beta if the tripwire/skip actions need to be visible. | Eng | before Beta |

Known limits: 60 s hard ceiling per turn (worker-held); options [] or one at launch (multi-option is OQ-1e); history capped at 10 turns; thread lost on reload (by design); upstream warnings not forwarded (OQ-14).


6. Comment logs

DateComment(s) FromAction Item(s)
2026-07-08Eng (Eko) — BE contract simplificationBuilt-shape decisions locked with BE, RFC reconciled to match: (1) refine request now carries the pack in the body as ai_agent (the FE's live, possibly-unsaved editor state) — BE serialises that via SkillPackBuilder, not a DB reload; the :id DB row is read for authz + engine_version guard only (new §3 threat (f) + mitigation; updated §1, §2.2 sequence, §2.4 request, PRD-to-Schema row, Detail 2.F.1 step 3, Detail 2.H Flow 1). (2) Response option shape reduced to { patches, updated_ai_agent } — dropped id/label/description/recommended (no upstream source; fabrication rejected — FE makes up any card title/badge locally, OQ-1b) and dropped forwarded warnings (OQ-14); renamed updated_capability_packupdated_ai_agent (ADR-6, ADR-7, Detail 1.B rows 6–7, §2.4 response, Detail 2.G, §2.A RefineOption, Chunks 4–6). (3) trace fully removed from the request schema (OQ-2). options[] array retained for the future multi-option fast-follow (OQ-1e). No change to the apply path, the stateless-BE decision (ADR-4 — FE still owns the thread, no Redis), or the SkillPackBuilder extraction.
2026-07-07TPM (Hilmi) — grooming notes§5 open questions refreshed: OQ-2 (trace) resolved — decided to omit trace entirely from v1, before RFC. OQ-4 (KB scope on apply) annotated — needs Data/ML to confirm exactly which patches/functions refine can update. Added OQ-11 (refine request shape vs multi-turn conversational context — single-POST design vs chat-bubble history, for Data/ML), OQ-12 (need OpenAPI specs / repo access for Data/ML's endpoints), and OQ-13 (applied-but-worse config revert mechanism reconsideration — reusing our existing, unshipped, old-engine Draft-versioning logic pending Data/ML alignment for the new engine; revert-from-UI may be deferred in favor of a behind-the-scenes fallback). No design/schema changes.
2026-07-05rfc-starter (authoring pass)All 11 mermaid blocks validated with @mermaid-js/mermaid-cli (mmdc) — parse clean. Grounding verified against chatbot, chatbot-fe, and qontak-designer working trees (see Detail 2.0 Source Verification). PRD corrections surfaced: ai_agent_histories → PaperTrail (V2), Preview rail already shipped, prototype is a page not a modal.
2026-07-06Pre-grooming three-way alignment check (PRD ⇄ design ⇄ RFC)Verified: design prototype unchanged since grounding (shapes/chip strings match §2.A); all 5 PRD stories covered in Detail 1.A/1.C. Fixed residual as-built contradictions inside the RFC: Non-Goal 6, the §2.4 response-example warning, the §3 security ref-filtering claim, the branch/skip flowchart node, one per-service 're-validated' cell, and the §1 Overview 'does not exist yet / this RFC defines the contract' — all now reflect the documented as-built (upstream does not strip refs at propose; caught at Save via CapabilityRefPresence). Re-validated the touched mermaid block with mmdc.
2026-07-05PRD correction sweepPRD bumped to v1.5: all flagged drift corrected — PaperTrail revert path (8 sites), as-built AC-4/Non-Goal-6 semantics, upstream "as built" status, Preview-rail existence, OQ-3/6/7 resolved + OQ-1/OQ-2 reframed. Reconciliation rows in §1 updated to "corrected".
2026-07-05PRD syncPRD bumped to v1.4: [REFINE-S04] audit story added (+ S04 test coverage matrix, §18 OQ-8). This RFC's Detail 1.A forward matrix and Detail 1.C change map updated with the S04 rows (Runtime / behavior — no new artifacts). OQ-10(a) resolved; OQ-10(b) (5th chip) remains with Design.
2026-07-05PM (Dimas) — reliability requirementMade the config-audit capability explicit: the refiner must be able to read the current configuration and find potential errors in it, not only react to described misbehavior. Added: success criterion 6 (audit prompt against a seeded broken pack), §2.4 "Config-audit turns" behavior note, a staging test-plan scenario, OQ-1g (confirm complaint-free audit tuning + proactive invariant flagging with Data/ML), OQ-10 (surface it: PRD audit story + a fifth suggestion chip as a design ask to Wulan).
2026-07-05Data/ML contract pass (Confluence RFC §10.3b + companion doc QON 51226214880)§2.4 upstream contract replaced with the as-built shape (current_skill_pack in; single {status, reply, patches, updated_skill_pack, warnings} out). ADR-1/ADR-6 revised (surgical-patch guarantee; BE wraps single proposal into options:[one]). OQ-3 (history cap 10) resolved; OQ-1 reframed from "needs building" to deployment verification + asks a–f; OQ-2 reshaped (workflow_state is mekari-agent-side — v1 omits trace). New PRD drift flagged: REFINE-S01/AC-4 reference-stripping does not match as-built behavior; the two Data-team pages contradict each other on refine-path post-processing (companion doc, newer, wins pending confirmation).

7. Ready for agent execution

  • no
  • Missing gates (everything else in the §7 checklist is complete):
    1. Upstream deployment/exposure unverified (OQ-1a). The contract is now documented as-built (§2.4 quotes it verbatim), so this is a verification
      • small-asks review with Data/ML (OQ-1a–f), not a design negotiation. Chunks 1–8 are executable against the documented contract today.
    2. KB-affecting-patch policy (OQ-4) — needs a product decision before Closed Beta; does not block chunks 1–8.
  • Once OQ-1a (deployment confirmation) lands, flip this to yes and bump last_updated — OQ-1b/e are fast-follows, not gates.

Next step (optional): run rfc-reviewer for a second-pass PROCEED/HOLD score once the Data/ML contract review lands.