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 — reasonare 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 frontmatterdelivery:link and the Metadata Delivery row point there. Until then both readnot 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
| Field | Value | Notes |
|---|---|---|
| Status | DRAFT — open for engineering review | YAML status: carries the linter enum (draft); review target: Eko (BE), FE reviewer, Data/ML owners (§2.4 contract + §5 OQ-1) |
| DRI | Eko Aprianto | Engineering Lead (per PRD header). Per-task staffing lives in delivery/ artifacts — not here. |
| Team | chatbot | BOT — Hadiningbot Squad |
| Author(s) | Dimas Fauzi Hidayat (PM) — drafted via rfc-starter | Engineering to co-author on review |
| Reviewers | Eko 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 Aprianto | Infosec approver to be added at review (required before AGREED) |
| Submitted Date | 2026-07-05 | |
| Last Updated | 2026-07-08 | |
| Target Release | 2026-Q3 | |
| Target Quarter | 2026-Q3 | Carried from source PRD |
| Delivery | not yet handed to delivery | |
| Related | PRD — Phase 2: AI-Assisted Refinement · PRD — Phase 1: New Engine Migration · Upstream RFC: QON 51153994292 §10.3b · detail: refine-skill-pack endpoint | |
| Discussion | Confluence — refine-skill-pack endpoint page |
Type: full-stack Frontend sub-type: new-feature Backend sub-type: new-feature
Sections at a Glance
- Overview (incl. §1 Design References — FE half, and §1 PRD-to-Schema Derivation — BE half)
- Technical Design (Infrastructure Topology → Technical Decisions [ADR] → Repo Reading Guide → diagrams → APIs → cross-layer contract verification)
- High-Availability & Security
- Backwards Compatibility and Rollout Plan (incl. cross-layer rollout matrix, §4 Agent Execution Plan, Verification & Rollback Recipe)
- Concern, Questions, or Known Limitations
- Comment logs
- 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 proxyPOST /v2/ai_agents/:id/refinethat serialises the pack the FE sends in the request body (ai_agent— the tenant's live, possibly unsaved editor state; the:idis resolved server-side for authz +engine_versionguard only)capability_pack→skill_pack(via a sharedSkillPackBuilderextracted fromRepositories::SyncToAiService, parameterised by a pluggable vector-store resolver), gathersavailable_tools, and proxies the upstreamrefine-skill-packendpoint (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 (conversationalreply+patches+ an already-appliedupdated_skill_packunder the surgical-patch guarantee — one proposal per turn, wrapped BE-side into theoptions[]shape, one option at launch with multi-option an upstream fast-follow, ADR-6) is mapped back tocapability_packvia the existingMappers::SkillPackMapperand 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,Recommendedflagged). Accept stages the option into the form (field highlight + tab switch); persistence is the editor's existing Save →PATCH /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):
POST /v2/ai_agents/:id/refinereturnsreply+ options for an autonomous-mode agent with the flag ON, and writes nothing (agentparametersandupdated_atunchanged after any number of refine calls).- 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).
- The
SkillPackBuilderextraction is behavior-preserving for Phase 1:SyncToAiServiceproduces a byte-identicalskill_packrequest body before and after the refactor (locked by regression specs onspec/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service_spec.rb). - 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. - Refining a non-autonomous (legacy) agent returns 422; flag OFF returns 403;
roles outside
owner/supervisor/adminreturn 403. - 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
replyand 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 fullcurrent_skill_packevery 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:
- No silent / auto-apply — every change is preview-then-apply.
- Autonomous-mode agents only (
parameters['engine_version'] == 2); legacytree_node//ai-agentmodal agents are rejected. - No server-side session/history persistence — the BE is stateless; the FE owns the thread (in-memory; a reload starts fresh).
- Not a runtime test harness — behavior validation stays in Preview / the AI Agent Testing initiative.
- No knowledge-base content editing via refine (re-referencing an existing store the agent already owns is allowed; uploading/vectorising is not).
- No creating new actions/tools — the upstream hydrates known action
refs from the registry (
available_tools/company_tools); an invented action/kb_idref that survives is rejected at Save byCapabilityRefPresence(400, no write). The as-built upstream does not strip refs at propose. - One agent at a time — no bulk refine.
Related Documents
- PRD (1:1 pair): phase-2-ai-assisted-refinement.md
- Phase-1 PRD (engine + drafter this RFC reuses): phase-1-new-engine-migration.md
- Anchor: autonomous-ai-agent-anchor.md
- Upstream (Data/ML) RFC: QON 51153994292 §10.3b · refine-skill-pack endpoint detail
- Design prototype (canonical design SoT):
qontak-designer→app/pages/bot-automation/ai-agents/[id].vue(read-only; designer-owned)
External-context reconciliation notes (repo wins where they disagree):
| Source claim | Repo reality | Resolution |
|---|---|---|
PRD §10/§11: "prior config snapshotted in ai_agent_histories" on Save | The 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:5 | Per-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 modal | No 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-mrag | Dependency 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
- The upstream
refine-skill-packendpoint is served under the same gateway prefix as the drafter (/qontak-ai-noncore-mrag/api/ai-agent/…) with the same auth posture (the existingHttpclient withorganization_idrouting,lib/ai_service/ai_agent.rb:9). Upstream RFC §10.3b confirms noncore-mrag proxies to mekari-agent'sPOST /refine-skill-pack. - 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
skillsrejects 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 isCapabilityRefPresenceat Apply. chat_historycap is 10 turns (FE truncates before sending) — matches the upstream's documented expectation ("last ~10 turns for conversational continuity", companion doc §5).- The
ai_agent_refinerollout flag reaches the FE through the same system-preferences payload the FE already consumes viapreferencesStore()(store/system-preferences/), keyedrollout_ai_agent_refine. - Figma frames remain TBD; the
qontak-designerprototype is the canonical design reference for build (PRD Header + §7).
Dependencies
| Dependency | Layer | Owner | Status | Blocking? |
|---|---|---|---|---|
Upstream refine-skill-pack endpoint (mekari-agent, proxied by noncore-mrag) | BE (external) | Data / ML Platform | Documented "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 contract | YES (verification, not build) |
Phase-1 capability_pack model + drafter live on /v2/ai_agents | BE | BOT — Hadiningbot | Exists (ai_agents_controller.rb, generate.rb, sync_to_ai_service.rb) | YES (stability) |
capability_pack↔skill_pack adapter (SkillPackMapper reverse + SkillPackBuilder extraction) | BE | BOT — Hadiningbot | Mapper exists (use_cases/mappers/skill_pack_mapper.rb); builder extraction is Chunk 1 of this RFC | YES |
PaperTrail versions on ai_agents (per-agent revert path) | BE | BOT — Hadiningbot | Exists (app/models/ai_agent.rb:5 has_paper_trail) | NO |
trace source (recent workflow_state / turns) | BE + Data/ML | overlaps AI Agent Live Monitoring | Optional — refine works degraded without it (§5 OQ-2) | NO |
| Agent editor right rail (Preview) | FE | BOT — Hadiningbot | Exists — AiAgentEditor.vue:1668–1724 | NO |
| Refine design | Design | Wulan Febyazzahra | Prototyped in qontak-designer (canonical); Figma frames follow up | NO |
Design References (frontend half — required)
| PRD-named surface | Figma / design link | Frame name | Design system version | Design QA contact | Notes |
|---|---|---|---|---|---|
| Refine tab (right rail) | n/a — design pending; canonical: qontak-designer app/pages/bot-automation/ai-agents/[id].vue:1759–1980 | Right rail — Preview/Refine tabs | Mekari Pixel (Mp* components, per prototype MpText/MpButton/MpBadge) | Wulan Febyazzahra | Prototype is design SoT per PRD Header; Figma frames tracked in §5 OQ-5 |
| Refine empty state + suggestion chips | n/a — design pending; canonical: prototype [id].vue:1806–1845 | Refine empty state | Mekari Pixel | Wulan Febyazzahra | Chip strings fixed in prototype (§2.A) |
| Refine option card (diff + Accept) | n/a — design pending; canonical: prototype [id].vue:1882–1933 | Option card | Mekari Pixel | Wulan Febyazzahra | Recommended banner + animated border (.recommended-border-anim, prototype [id].vue:9318–9344) |
| Form field highlight + tab switch on Accept | n/a — design pending; canonical: prototype acceptRefineOption [id].vue:4714–4779 | — | Mekari Pixel | Wulan Febyazzahra | aiHighlightClass outline, prototype [id].vue:9091 |
Per template rule these surfaces carry
n/a — design pendingfor 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 / rule | Persisted as (table.column) | Exposed via (endpoint / event) | Enforced where | Source (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/:id | UseCases::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 only | UseCases::RefineAiAgent performs no repository write; spec asserts parameters/updated_at unchanged | §6 NG-1, §10 #1 |
| Refine restricted to autonomous-mode agents | ai_agents.parameters->>'engine_version' = 2 | 422 from /refine for legacy agents | Guard 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 turns | FE truncation in useRefineAgent.ts; BE params schema caps array size | §6 NG-3, §8, S03 |
| Roles owner/supervisor/admin only | — (JWT role claim) | 403 otherwise | set_role(%w[owner supervisor admin]) (authorization_helpers.rb:6–11) + Middlewares::Ownership org check | §8 |
Flag ai_agent_refine, default OFF | system_preferences row (group_code: 'rollout', code: 'ai_agent_refine', enabled) | 403 from /refine when OFF; FE hides the tab | Flag check in controller route (pattern: system_preference.rb:41–51) + FE rolloutPrefEnabled gate | §8, §12 |
| Invalid patch ops / broken-pack protection | — | warnings[] in refine response | Upstream 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/revert | ai_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 filtering | ai_agent_tools (org/company-scoped, tool_id NOT NULL) | available_tools[] in the upstream request | Reuse 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 id | FE section / component | BE section / endpoint |
|---|---|---|
| REFINE-S01/AC-1 | §2.A RefinePanel → useRefineAgent.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 reload | ADR-4 stateless BE |
| REFINE-S03/ERR-1 | §2.C per-turn retry, prior turns intact | n/a — FE-only behavior |
| REFINE-S04/AC-1 | same 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-3 | Save 400 surfaced clearly (existing) | CapabilityRefPresence at Apply (unchanged PATCH path) |
| REFINE-S04/ERR-1 | defers to REFINE-S01 error handling | same request path |
| REFINE-S01-NEG/NEG-1 | Refine tab hidden for legacy agents (§2.A gate) | 422 guard on engine_version != 2 |
| REFINE-S01-NEG/NEG-2 | No auto-apply anywhere in FE flow | Refine endpoint performs no write (spec-asserted) |
Reverse (RFC → PRD AC):
| New FE component / BE endpoint / dependency | PRD composite AC id it serves |
|---|---|
POST /v2/ai_agents/:id/refine | REFINE-S01/AC-1..4, ERR-1..2; S01-NEG/NEG-1 |
UseCases::RefineAiAgent + Repositories::Refine | REFINE-S01/AC-1, ERR-1..2 |
AiService::AiAgent#refine_skill_pack | REFINE-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.ts | REFINE-S01/AC-2..3, S02/AC-1, S03/AC-1..3 |
rightRailTab two-tab rail in AiAgentEditor.vue | REFINE-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 surface | Consumer | Required reads (BE) | Required writes (BE) | FE component | Status surface |
|---|---|---|---|---|---|
| Refine tab (right rail, editor) | web | POST /v2/ai_agents/:id/refine (read-modeled: proposes only) · agent already loaded via GET /v2/ai_agents/:id (existing) | none (refine writes nothing) | RefinePanel.vue | refineIsGenerating + per-message streaming |
| Option card diff + Accept | web | n/a — data arrives in refine response | none (Accept is client-side) | RefineOptionCard.vue | option status: pending / accepted / dismissed |
| Editor form (highlight + tab switch on Accept) | web | n/a — existing form | PATCH /v2/ai_agents/:id on Save (existing) | AiAgentEditor.vue (activeTab, aiChangedFields) | field-highlight flags |
Role Coverage
| PRD role | Authorization mechanism | Endpoints permitted (BE) | UI surface visibility (FE) | Cross-tenant? | Audit trail |
|---|---|---|---|---|---|
| owner | JWT 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_id | PaperTrail version on every save; V2 RefineAiAgent structured logs |
| supervisor | same | same | same | no | same |
| admin | same | same | same | no | same |
| all other roles | set_role rejects → 403 | none of the refine/update routes | Refine tab not rendered | no | 403s logged by Grape error path |
PRD Section Coverage
| PRD § | Title | Where covered |
|---|---|---|
| 2 | Phase Context | §1 Overview + Related Documents |
| 3 | One-liner + Problem | §1 Overview |
| 4 | If We Don't Ship | n/a — business rationale; no engineering contract (PRD-owned) |
| 5 | Target Users + Persona | §1 Role Coverage (roles only; personas PRD-owned) |
| 6 | Non-Goals | §1 Out of Scope |
| 7 | Scope Changes | §2.I Scope Boundaries + §4 Execution Plan |
| 8 | Constraints | §2.4 (timeouts), §3 (authz, performance), §4.B (flag) |
| 9 | New Features (Refine tab) | §2.A UI Contract + §2.C UI State Matrix |
| 10 | API & Webhook Behavior | §2.4 APIs + §2.H End-to-End Data Flow |
| 11 | System Flow + Stories + ACs | §2.2 sequences + Detail 1.A / 1.C |
| 12 | Rollout (+12.1 semantic rollback) | §4 Rollout Strategy + Verification & Rollback |
| 13 | Observability | §3 Monitoring & Alerting + Logging |
| 14 | Success Metrics | §1 Success Criteria (engineering subset); product metrics PRD-owned |
| 15 | Launch Plan & Stage Gates | n/a — delivery-layer concern (PRD/TPM-owned; RFC holds no schedule) |
| 16 | Dependencies | §1 Dependencies (with repo-reconciled statuses) |
| 17 | Key Decisions | §2 Technical Decisions (ADR blocks) + Detail 1.B |
| 18 | Open Questions | §5 (carried, updated with grounding results) |
Detail 1.B — Decisions Closed (cross-layer)
| # | Decision | Chosen option | Alternatives rejected | Why rejected | Layer | §2 block |
|---|---|---|---|---|---|---|
| 1 | Who applies the RFC 6902 patches | Upstream applies + re-validates; BE passes through | BE applies patches in Rails | Duplicates the drafter's validation pipeline; drift risk | BE | Decision 1 |
| 2 | Apply/persist path | Reuse PATCH /v2/ai_agents/:id | Dedicated /refine/apply endpoint | Apply is an update — reuse authz, validation, sync, PaperTrail audit | BE | Decision 2 |
| 3 | capability_pack→skill_pack serialisation for refine | Extract shared SkillPackBuilder w/ pluggable vector-store resolver | Duplicate builder in Repositories::Refine; or call SyncToAiService privates | Duplication drifts; calling sync's privates couples refine to side-effecting resolution | BE | Decision 3 |
| 4 | Refine session storage | Stateless BE; FE in-memory thread | New ai_agent_refine_sessions table | DDL + dual source of truth for a deferred need | both | Decision 4 |
| 5 | Sync vs async refine call | Synchronous proxy (60 s hard timeout, matches drafter) | Sidekiq job + polling/websocket | Drafter precedent is sync; adds infra for no PRD requirement (≤10 s perceived target) | BE | Decision 5 |
| 6 | Multi-option response shape | options[] array, each option { patches, updated_ai_agent } (1 element at launch) | Single flat proposal on the response; or option with id/label/recommended | Array is additive for future multi-option; id/label/recommended dropped — no upstream source (fabrication rejected) | both | Decision 6 |
| 7 | Diff rendering source | FE computes ProposedChange[] by diffing current form model vs option's mapped updated_ai_agent | FE renders raw RFC 6902 patches | Patch paths reference upstream skill_pack shape, unreadable against the public capability_pack (PRD OQ-6) | FE | Decision 7 |
| 8 | Auto-apply | Never — review-then-apply only | Auto-apply high-confidence patches | Trust/safety on live customer agents (PRD NG-1) | both | no alternative considered — PRD Non-Goal 1 forbids it |
| 9 | Caching | None — agent config read fresh from Postgres per request | Cache serialised skill_pack | Config must reflect unsaved-but-persisted state exactly; call volume is human-paced | BE | Decision 5 (addressed inside) |
| 10 | Legacy-agent guard | 422 with error code not_autonomous_agent | 404 | The agent exists; 404 would mislead FE debugging | BE | Decision 6 (addressed inside) |
Detail 1.C — Per-Story Change Map
| Story id | Story title | Layer scope | FE changes | BE changes | Composite AC ids covered | Acceptance criteria (verifiable) | RFC anchors |
|---|---|---|---|---|---|---|---|
| REFINE-S01 | Refine an agent in natural language | FE + BE | RefinePanel.vue, RefineOptionCard.vue, useRefineAgent.ts, refine() in bot-automation-agents.ts, endpoint.ts entry, rail tabs in AiAgentEditor.vue | post '/:id/refine' route, UseCases::RefineAiAgent, Repositories::Refine, AiService::AiAgent#refine_skill_pack, Mappers::SkillPackBuilder extraction, models/refine_response.rb | REFINE-S01/AC-1, AC-2, AC-3, AC-4, ERR-1, ERR-2 | bundle 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-S02 | Accept an option and save the change | FE + BE existing | acceptRefineOption/applyPendingData port into AiAgentEditor.vue (highlight via aiChangedFields, tab switch via activeTab), option dismissal, Save unchanged | none — Save reuses PATCH /v2/ai_agents/:id (update_ai_agent.rb) | REFINE-S02/AC-1, AC-2, AC-3, ERR-1, ERR-2 | FE 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-S03 | Iterative (multi-turn) refinement | FE-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-1 | FE 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-S04 | Audit the configuration for potential errors | Runtime / behavior | none — 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 turn | REFINE-S04/AC-1, AC-2, AC-3, ERR-1 | staging 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-NEG | No refine on legacy agents; never auto-apply | FE + BE | Refine tab rendered only when engine_version === 2 and flag ON | 422 guard not_autonomous_agent; refine performs no write | REFINE-S01-NEG/NEG-1, NEG-2 | BE 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::FindByreads 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
| Service | Use case in this RFC | Internal calls | External / third-party |
|---|---|---|---|
chatbot BE | Refine proxy (serialise pack, gather tools, proxy, map back); Apply via existing update | Postgres (ai_agents, ai_agent_tools, system_preferences) | AI-service gateway (refine-skill-pack new, PUT /ai-agent existing) |
noncore-mrag / mekari-agent | LLM 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-fe | Refine rail UI, thread state, option staging into form | chatbot BE via $apiMain | Mixpanel (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;
chatbotstays a thin proxy (same posture asRepositories::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.
- Pros: the safeguard suite lives where it's already built and tested;
- 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,CapabilityRefPresencevalidation, 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.
- Pros: full reuse of
- Option B — dedicated
POST /v2/ai_agents/:id/refine/apply- Pros: BE-side
refine_appliedtelemetry for free. - Cons: duplicates authz/validation/sync/audit; second write path to keep consistent with full-merge semantics.
- Pros: BE-side
Decision: Option A.
Rationale — Apply is a config update. One write path means the concurrency, validation, and rollback semantics stay single-sourced.
Consequences — refine_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_pack →
skill_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.
- Pros: one shaping implementation; sync keeps its stateful resolver, refine
injects a read-only resolver (
- 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_historysent per request- Pros: no DDL, no retention policy, no dual source of truth; matches the
upstream RFC §10.3b posture and the prototype (
refineMessagesref). - Cons: reload loses the thread (accepted — REFINE-S03/AC-3); no server-side audit of refine conversations this phase.
- Pros: no DDL, no retention policy, no dual source of truth; matches the
upstream RFC §10.3b posture and the prototype (
- 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).
Consequences — chat_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 intooptions: [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'supdated_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 byid).
- Pros: diff is in the vocabulary the tenant sees (the form fields); the
prototype's
- Option B — FE renders upstream
patchesdirectly- 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
Httpclient, 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.4Reuse?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
| Layer | Path | Why the agent reads it | What pattern it teaches |
|---|---|---|---|
| BE | app/api/frontend_service/v2/ai_agent/ai_agents_controller.rb | The Grape controller refine's route joins; post '/generate' at :361 and patch '/:id' at :292 are the shape to mirror | params do declaration, set_role, Dry::Matcher::ResultMatcher success/failure rendering |
| BE | app/api/frontend_service/v2/ai_agent/use_cases/generate.rb | The use case RefineAiAgent mirrors | dry-schema contract, Dry::Monads::Do, repository call → status check → SkillPackMapper.call (:46–50) |
| BE | app/api/frontend_service/v2/ai_agent/repositories/generate.rb | The repository Refine mirrors | request-body assembly (:26–33), available_tools from AiAgentTool (:52–57), @http.call result passthrough |
| BE | app/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service.rb | Source of the SkillPackBuilder extraction | build_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) |
| BE | app/api/frontend_service/v2/ai_agent/use_cases/mappers/skill_pack_mapper.rb | Reverse mapping reused verbatim for refine output | self.call(skill_pack, organization_id:, agent_name:) (:32–34) → Entities::AiAgent; COMPLETION_TYPE_MAP (:17–23) |
| BE | app/api/frontend_service/v2/ai_agent/use_cases/update_ai_agent.rb | The apply path (unchanged) refine relies on | full-merge params (:87–101), transaction + sync + rollback (:58–68), CapabilityRefPresence macros (:26–34) |
| BE | lib/ai_service/ai_agent.rb | Where refine_skill_pack is added | draft_skill_pack (:49–53): path constant, @http.call(method:, url:, body:, open_timeout: 60, read_timeout: 60) |
| BE | app/models/system_preference.rb | Flag pattern for ai_agent_refine | find_by(code:, group_code: 'rollout', enabled: true) predicate (:41–51) |
| BE | app/models/ai_agent.rb | Versioning + associations reality | has_paper_trail (:5); has_many :ai_agent_histories (:15) is V1-only |
| FE | modules/bot-automation/components/AiAgentEditor.vue | Host component: rail + tabs + save | tabs array (:2965–2969), activeTab mutation pattern (:3652, :3695–3702), Preview rail (:1668–1724), showPreview (:3015), handleSave (:3692–3742) |
| FE | modules/bot-automation/composables/useGenerateAgent.ts | Closest composable pattern to useRefineAgent | isGenerating ref, service call via { fetch }, error extraction (:59–97) |
| FE | common/services/main/v2/bot-automation-agents.ts | Service layer refine() joins | { fetch, controller } return with AbortController (:232–248), $apiMain + endpoint.v2.ai_agents.* |
| FE | modules/bot-automation/composables/useKnowledgeSourceTypeAvailability.ts | FE flag-gate pattern for ai_agent_refine | preferencesStore().lists + rolloutPrefEnabled(groupCode, code) (:65–91) |
| FE | common/utils/tracking.ts | Analytics convention for refine_* events | trackEvent(name, properties, jimoTrack) (:51–144) with auto user context |
| Design | qontak-designer/app/pages/bot-automation/ai-agents/[id].vue | Canonical 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)
| Contract | Status | Justification | Owner |
|---|---|---|---|
PATCH /v2/ai_agents/:id | reused (apply path, unchanged) | — | BOT |
POST /v2/ai_agents/generate | reused as pattern only (not modified) | — | BOT |
POST /v2/ai_agents/:id/refine | new-with-justification | Searched app/ + lib/ for refine — zero hits; no existing endpoint proposes config changes without persisting; generate cannot take an existing pack as input | BOT |
Upstream POST /qontak-ai-noncore-mrag/api/ai-agent/refine-skill-pack | new-with-justification | Upstream serves only draft-skill-pack (from-scratch) and CRUD push (PUT /ai-agent); no refinement contract exists | Data/ML |
Upstream PUT /qontak-ai-noncore-mrag/api/ai-agent | reused (save re-sync, via SyncToAiService) | — | Data/ML |
ai_agents.parameters jsonb | reused (read by refine; written only by update) | — | BOT |
ai_agent_tools | reused (available_tools query) | — | BOT |
system_preferences rollout row | extended (new row ai_agent_refine; no schema change) | — | BOT |
Patterns to Follow
| Layer | Concern | Pattern in repo | Reference file | Deviation? |
|---|---|---|---|---|
| BE | HTTP handler shape | Grape route + set_role + ResultMatcher | ai_agents_controller.rb:361–379 | none |
| BE | Use case | APIAbstractUseCase + dry-schema contract + monads | use_cases/generate.rb | none |
| BE | Repository / upstream call | AbstractRepository + @http.call passthrough | repositories/generate.rb:16–20 | none |
| BE | Error response shape | Failure(build_fail_params(status_code:, message:)) → error_response | use_cases/generate.rb:34–40, update_ai_agent.rb:66 | none |
| BE | Logging | Rails.logger.error("V2 <Class> …") + Rollbar.error(e, 'V2 …', context) | sync_to_ai_service.rb:47–51 | none |
| FE | Composable state | refs + async fn returning typed result | useGenerateAgent.ts:59–97 | none |
| FE | Error / retry | extract response._data.error.messages[0], expose error ref | useGenerateAgent.ts:79–90 | none |
| FE | Service method | { fetch, controller } + $apiMain | bot-automation-agents.ts:232–248 | none |
| Cross | Naming (snake_case API ↔ FE) | FE consumes snake_case response fields directly (as generate does) | useGenerateAgent.ts (parseDetailResponse) | none |
Reading Order for the Agent
chatbot/app/api/frontend_service/v2/ai_agent/ai_agents_controller.rb— route + authz + rendering shape (/generateat :361).chatbot/app/api/frontend_service/v2/ai_agent/use_cases/generate.rb— the use-case skeleton to mirror.chatbot/app/api/frontend_service/v2/ai_agent/repositories/generate.rb— request assembly +available_tools.chatbot/lib/ai_service/ai_agent.rb— client methods + timeout convention.chatbot/app/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service.rb— the builders to extract (and what must NOT change).chatbot/app/api/frontend_service/v2/ai_agent/use_cases/mappers/skill_pack_mapper.rb— reverse mapping reused for output.chatbot/app/api/frontend_service/v2/ai_agent/use_cases/update_ai_agent.rb— the untouched apply path.qontak-designer/app/pages/bot-automation/ai-agents/[id].vue(:4021–4064, :4687–4779, :5364–5438) — the FE interaction contract (read-only).chatbot-fe/modules/bot-automation/components/AiAgentEditor.vue(:1668–1724, :2965–3015, :3692–3742) — rail, tabs, save.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)
| Layer | Anchor / pattern / contract | Verified by | Evidence |
|---|---|---|---|
| BE | app/api/frontend_service/api.rb mount | read | mount V2::AiAgent::AiAgentsController => '/v2/ai_agents' at :59 |
| BE | ai_agents_controller.rb generate + patch routes | read | post '/generate' at :361; patch '/:id' at :292; set_role(%w[owner supervisor admin]) at :293/:362 |
| BE | use_cases/generate.rb | read | class Generate < ::UseCases::API::APIAbstractUseCase at :7; Mappers::SkillPackMapper.call(...) at :46–50 |
| BE | repositories/generate.rb | read | request_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 |
| BE | lib/ai_service/ai_agent.rb#draft_skill_pack | read | path '/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 |
| BE | repositories/sync_to_ai_service.rb builders | read | build_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 |
| BE | use_cases/mappers/skill_pack_mapper.rb | read | self.call :32–34 → Entities::AiAgent.new(engine_version: 2, profile:, capabilities:, routing:) :42–48; COMPLETION_TYPE_MAP :17–23 |
| BE | use_cases/update_ai_agent.rb full-merge + txn | read | merge 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 |
| BE | helpers/authorization_helpers.rb | read | def set_role(roles) raising 403 ErrorException at :6–11 |
| BE | middlewares/ownership.rb | read | 403 unless env['user']['chatbot_organization_id'] at :5+ |
| BE | app/models/system_preference.rb flag pattern | read | find_by(code: 'unified_billing', group_code: 'rollout', enabled: true) :41–51 |
| BE | app/models/ai_agent.rb versioning | read + grep | has_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) |
| BE | ai_agents.parameters column | read | migration db/migrate/20260218101229_add_parameters_to_ai_agents.rb: add_column :ai_agents, :parameters, :jsonb |
| BE | No existing refine code | grep | grep -ri refine app/ lib/ → no matches |
| BE | Test commands | ls + read | .rspec (--require spec_helper); spec dir spec/api/frontend_service/v2/ai_agent/ incl. repositories/sync_to_ai_service_spec.rb |
| FE | AiAgentEditor.vue rail + tabs + save | read | Preview <aside v-if="showPreview"> :1668–1724; showPreview = ref(false) :3015; tabs array :2965–2969; activeTab.value = 2 :3652; handleSave :3692–3742 |
| FE | useSaveAgent.ts | read | save(isNew, agentId, args) :329–369 → botAutomationAgentsService.update(...) |
| FE | bot-automation-agents.ts + endpoint.ts | read | update() PATCH with endpoint.v2.ai_agents.update "/v2/ai_agents/:id:" :232–248; generate: "/v2/ai_agents/generate" |
| FE | useGenerateAgent.ts | read | isGenerating ref, botAutomationAgentsService.generate(nuxtApp, payload) :59–97 |
| FE | Flag gate pattern | read | rolloutPrefEnabled(groupCode, code) reading preferencesStore().lists[${groupCode}_${code}].enabled — useKnowledgeSourceTypeAvailability.ts:65–91 |
| FE | tracking.ts | read | trackEvent(name, properties?, jimoTrack?) :51–144, merges Role/Email/Company context, $mixpanelTrack |
| FE | Test/build commands | read | package.json: test: vitest run (:17), lint (:15), build: nuxt build (:11), test:e2e: playwright test (:22) |
| FE | No existing refine code | grep | repo grep for refine → only unrelated marketing/comment strings |
| Design | Prototype refine contract | read | rightRailTab = 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 new | Design tokens | Backing 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) | extended | Mekari 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) | new | MpText sizes h2/body/body-small, text.brand | POST /v2/ai_agents/:id/refine | none — chip strings copied verbatim |
Message thread + streaming (:1850–1943, streamRefineText :4625–4667) | RefinePanel.vue + useRefineAgent.ts | new | MpText, streaming word opacity | same | Streaming 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) | new | MpBadge types completed/announcement, MpButton sm secondary, .recommended-border-anim | same | none |
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) | extended | highlight outline per aiHighlightClass (:9091) | Save: PATCH /v2/ai_agents/:id | Prototype'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 indb/migrate/20260218101229_add_parameters_to_ai_agents.rb) andai_agent_tools; it writes nothing. - Apply writes through the existing update path (PaperTrail
versionsrow per save — the per-agent revert source). - One data seed (not DDL): a
system_preferencesrow{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.tsrefs); 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/dismissedenum is ephemeral FE state — diagrammed in §2.1.)
Detail 2.4 — APIs
Outbound endpoints (consumers call us — chatbot BE)
| Endpoint | Method | AuthN/AuthZ | Request schema | Response schema | Status codes | Idempotency | Versioning | Reuse? |
|---|---|---|---|---|---|---|---|---|
/v2/ai_agents/:id/refine | POST | Session JWT → Middlewares::Ownership (org present) + set_role(%w[owner supervisor admin]) + rollout flag ai_agent_refine | see Refine request below | see Refine response below | 200, 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/:id | PATCH | same middleware + set_role (existing, ai_agents_controller.rb:292–293) | existing full-merge params (profile, capabilities, routing, …) | existing update_ai_agent_response | 200, 400 (ref validation), 403, 404, 422 (sync failure → rolled back) | last-writer-wins full merge (unchanged — see §2.E) | v2 | reused — 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:idis still resolved server-side viaRepositories::FindBy(id, org_id)for authorization +engine_versionguard 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 byCapabilityRefPresenceat Save.traceis 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-levelwarnings(not surfaced to the FE this phase — see §5; upstream still returns them, the BE simply does not forward).updated_capability_packis renamedupdated_ai_agentto 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)
| Endpoint | Method | Auth | Timeout / retry | Status | Reuse? |
|---|---|---|---|---|---|
/qontak-ai-noncore-mrag/api/ai-agent/refine-skill-pack | POST | Existing 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 422 | documented "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) | PUT | same | existing SyncToAiService semantics | exists (ai_agent.rb:43) | reused |
Upstream refine-skill-pack contract — as 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 ofcurrent_skill_packas 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 leavesskillsmissing/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_packunchanged with an errorreply(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 inreply→ 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, validexit.reasonenum, one-reply-per-turn) plus debugging heuristics — so an audit-styleuser_messagewith 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 asreplydiagnosis +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 hydrateid/argswhen the model adds an action. ThechatbotBE still sends itsAiAgentToollist explicitly (drafter parity) rather than relying on noncore'scompany_toolsauto-fill — whether the two registries are in sync is §5 OQ-1c.- Model config (§6, Data/ML-owned):
REFINE_SKILL_PACK_MODEL, falling back toDRAFT_SKILL_PACK_MODEL(defaultgpt-5.1). Setup-time only — not on the/predictionscustomer 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 patches → options: []. 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 asuseGenerateAgent.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 whilerefineIsGenerating. - Analytics events (via
trackEvent,tracking.ts:51): see §3 Monitoring. - Conditional rendering: options list only when
msg.options?.length; skeleton cards whileloadingOptions; warnings rendered as a muted list under the reply. - A11y: chips and Accept are
<button>elements; thread containerrole="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 fromchanges, footer = Accept button or status badge (✓ Applied/Skipped) — per prototype:1882–1933.
AiAgentEditor.vue (modified)
showPreview: ref<boolean>(:3015) generalises torightRailTab: ref<"preview" | "refine">with the existing Preview markup as thepreviewpane (prototype:4066pattern); rail tab buttons per prototype:1762–1776.- New
aiChangedFields: reactive<Record<string, boolean>>+ apply handler: onaccept-option, writependingDatainto the real form model (AgentDetailConfig—useAgentStore.ts:79–86), set highlight flags, and switchactiveTab(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
$apiMainservice call viabot-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
AbortControllerfrom the service is aborted on editor unmount / rail close to avoid orphaned 60 s requests. - History cap:
useRefineAgent.tssliceschat_historyto the last 10 turns before sending (REFINE-S03/AC-2; N pending ML confirmation §5 OQ-3).
Detail 2.C — UI State Matrix
| Surface | Loading | Empty | Error | Partial | Success |
|---|---|---|---|---|---|
| Refine thread | streaming indicator on AI turn; input + chips disabled (refineIsGenerating) | "Refine your agent" + 4 suggestion chips | error turn "couldn't generate a suggestion — agent unchanged" + Retry; prior turns intact | reply with options: [] → "no actionable change" turn, no cards (S01/AC-3); warnings shown under reply | reply + 1..3 option cards, Recommended flagged |
| Option card | skeleton cards while loadingOptions | n/a — only rendered when options exist | n/a — errors never render cards | some sibling cards dismissed after one accepted | pending → Accept enabled; accepted → "✓ Applied"; dismissed → "Skipped" |
| Editor form (post-Accept) | n/a | n/a | Save failure: inline "couldn't save — agent unchanged" + retry (S02/ERR-1..2) | staged-but-unsaved: highlighted fields + unsaved-changes state | Save 200 → highlights persist until edit/reload; config live |
Detail 2.D — Data Integrity Matrix
| Write path | Transaction scope | Partial failure behavior | Idempotency key + TTL | Consistency model | Duplicate-event handling | Stale-read handling |
|---|---|---|---|---|---|---|
POST /:id/refine | none — no write (spec-asserted: parameters + updated_at unchanged) | n/a | n/a — safe to repeat | read-committed snapshot of parameters at request time | repeat calls just produce new proposals | preview 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/a | full-merge overwrites — see §2.E collision rows |
Detail 2.E — Concurrency Collision Map
| Resource | Writers | Collision scenario | Resolution mechanism | Behavior when it fails |
|---|---|---|---|---|
ai_agents.parameters | any owner/supervisor/admin via Save | Tenant A applies a refined pack while Tenant B saved a manual edit after A's refine preview was generated | last-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 thread | single browser session | two in-flight refine sends | input disabled while refineIsGenerating (single-flight per panel) | n/a |
| Vector stores | SyncToAiService only | refine must never race sync's vector creation | refine's resolver is read-only (capability['vector_store'] passthrough) — it cannot create/purge stores by construction | n/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 / service | Inbound trigger | Outbound effect | Failure handler | PRD anchor |
|---|---|---|---|---|---|
| 1. Render Refine tab (flag + engine gates) | BOT / chatbot-fe | editor mount | — | tab hidden when gated | §9 |
| 2. Accept + validate refine request (authz, flag, engine_version) | BOT / chatbot BE | POST /:id/refine | 403/404/422 on gate failure | Grape error response | §8, §10 #1 |
3. Serialise request-body ai_agent pack + gather tools (read-only) | BOT / chatbot BE | step 2 pass | upstream request body | 422 on unexpected serialisation error (logged) | §10 #1 |
| 4. LLM refinement: diagnose, patch, apply, re-validate, reference-filter | Data/ML / mekari-agent via noncore-mrag | upstream POST | reply + 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, return | BOT / chatbot BE | upstream 2xx | 200 response, nothing persisted | 422 on transport failure (S01/ERR-1) | §10 #1 |
| 6. Render options, Accept stages into form | BOT / chatbot-fe | user click | form state + highlights + tab switch | n/a (client-side, reversible) | §10 #2 |
| 7. Save persists + re-syncs | BOT / chatbot BE | PATCH /:id | DB write + PUT /ai-agent push | txn 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
| Entity | State field / event | Default | Updated by | Read via | Stale window |
|---|---|---|---|---|---|
| AI agent config | ai_agents.parameters | — | PATCH /:id (Save) only | GET /v2/ai_agents/:id (existing detail endpoint) | preview may age while the tenant reads options (§2.E row 1) |
| Refine thread | refineMessages[] (FE memory) | [] | useRefineAgent per turn | component state | lost on reload — by design (S03/AC-3) |
| Option status | option.status | pending | Accept handler (accepted + siblings dismissed) | component state | ephemeral |
| Staged-but-unsaved form | editor form model + aiChangedFields | pristine | applyPendingData port | editor state; Save serialises it | until Save / discard / reload |
| Flag state | system_preferences row → FE preferencesStore().lists['rollout_ai_agent_refine'] | enabled: false | ops toggle | existing preferences bootstrap | session (prefs loaded at app start) |
Detail 2.G — Cross-Layer Contract Verification
| Endpoint | BE response schema | FE expected schema | Match? | Gaps |
|---|---|---|---|---|
POST /:id/refine | data.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_agent | yes | none — 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 /:id | existing update_ai_agent_response | existing useSaveAgent.buildPayload | yes | none — unchanged contract |
upstream refine-skill-pack | status, 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) | yes | remaining 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 → RefinePanel → useRefineAgent.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_agent → Repositories::Refine gathers
available_tools + proxies upstream) → SkillPackMapper maps
updated_skill_pack → updated_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.rbapp/api/frontend_service/v2/ai_agent/repositories/refine.rbapp/api/frontend_service/v2/ai_agent/use_cases/mappers/skill_pack_builder.rbapp/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— addpost '/:id/refine'routeapp/api/frontend_service/v2/ai_agent/repositories/sync_to_ai_service.rb— delegate shaping toSkillPackBuilder(behavior-preserving; regression-locked)lib/ai_service/ai_agent.rb— addrefine_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.vuemodules/bot-automation/components/refine/RefineOptionCard.vuemodules/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.vue—showPreview→ two-tabrightRailTabrail;aiChangedFields+ apply handler; flag/engine gatecommon/services/main/v2/bot-automation-agents.ts— addrefine()common/services/main/endpoint.ts— addv2.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 event | Implementation | Properties |
|---|---|---|
refine_requested | FE 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_succeeded | FE trackEvent + BE log V2 RefineAiAgent succeeded agent=<id> options=<n> warnings=<n> latency_ms=<n> | patch/option counts, latency |
refine_failed | FE trackEvent + BE `Rails.logger.error("V2 RefineAiAgent failed agent= | 5xx |
refine_accepted / refine_applied / refine_discarded | FE 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_reverted | operational 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_requested10% 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 SyncToAiServiceerrors). - Dashboard: extend the squad's existing service dashboard with the
V2 RefineAiAgentlog-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 SyncToAiServicespikes; 2) grep pod logs forV2 RefineAiAgent failed … reason=; 3) if upstream-wide, flipai_agent_refineOFF (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|succeededwithagent_id, organization_id, history_turns, message_len, options, warnings, latency_ms; (error):V2 RefineAiAgent failed+ Rollbar with the same context hash — matching theV2 <Class>prefix convention. - PII scrubbing:
user_messageandchat_historycontent 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_historysteering 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 theai_agentpack 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_idref 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 — nov-htmlanywhere in the refine components (explicit rule for review); (f) the FE-suppliedai_agentpack is accepted because refine is propose-only — it writes nothing, so a crafted pack produces only a throwaway proposal; the:idis still resolved server-side (Repositories::FindBy(id, org_id)) to enforce ownership (cross-org → 404) and theengine_versionguard (legacy → 422), and going live still requires the standard Save path whereCapabilityRefPresencere-validates every reference (400, no write). The pack shape is never persisted from the refine path. - Input validation per field:
user_message1..4000 chars required;chat_history≤ 10 items, eachroleenum +content≤ 4000;ai_agentrequired object (profile/capabilities/routing) — parsed bySkillPackBuilder, rejected if not a well-formed pack; size-capped (Grape-enforced).traceomitted 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
Httpclient 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
generateendpoint; 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) andpnpm 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
| Role | Endpoint(s) | Permitted methods | Tenant scope | UI surface visibility (FE) | Additional constraint | Audit trail |
|---|---|---|---|---|---|---|
| owner | /v2/ai_agents/:id/refine · /v2/ai_agents/:id | POST · PATCH | own org only (Ownership middleware + org-scoped FindBy) | Refine tab visible (flag ON + engine v2) | flag ai_agent_refine ON; agent must be autonomous | PaperTrail on apply; V2 RefineAiAgent logs |
| supervisor | same | same | same | same | same | same |
| admin | same | same | same | same | same | same |
| any other role | none (403 from set_role) | — | — | Refine tab not rendered | — | 403 in request logs |
Detail 3.A — Failure Mode Catalog (merged)
| Surface | FE behavior on failure | BE response on failure | Code-shape consistency |
|---|---|---|---|
| Refine send — upstream timeout (60 s) / 5xx | error turn + Retry; thread intact; agent unchanged | 422 {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 fallback | yes |
| Refine send — 403 (role/flag) | tab shouldn't render; if raced, toast + hide tab | 403 | yes |
| Refine send — 404 (wrong org / deleted) | error turn "agent not found" | 404 | yes |
| Refine send — 422 legacy agent | tab shouldn't render (gate); defensive error turn | 422 not_autonomous_agent | yes |
| Save — validation | inline 400 error, no write | 400 (CapabilityRefPresence) | yes — existing |
| Save — sync failure | inline "couldn't save — agent unchanged" + retry | 422 after txn rollback | yes — existing |
| Network offline (FE) | send fails immediately → retryable turn | — | n/a |
| Editor unmount mid-request | AbortController.abort() — no dangling handler | request 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 trigger | Where checked | Downstream effect | Audit trail | User-visible? |
|---|---|---|---|---|
Flag ai_agent_refine OFF | FE gate (tab hidden) + BE route guard | endpoint 403; feature invisible | request logs | no (hidden) |
Legacy agent (engine_version != 2) | FE gate + BE use-case guard | 422; refine impossible on legacy agents (NEG-1) | request logs | no (tab hidden) |
| No actionable change in message | upstream | options: [], reply only (S01/AC-3) | options=0 in success log | yes — reply without cards |
| Malformed patch op from the LLM | upstream apply step (per-op isolation, companion doc §3) | op skipped; warning appended; rest of the patch set applies | warnings=<n> in success log | yes — warning list |
Patch set would break the pack (skills empty/missing) | upstream tripwire (companion doc §3) | change rejected; original pack returned unchanged | upstream warning passthrough | yes — reply explains |
Invented action / kb_id ref surviving upstream | BOT-side at Apply: CapabilityRefPresence (as-built upstream no longer strips refs — S01/AC-4 drift, see §1 reconciliation) | Save returns 400; nothing written | 400 in request logs | yes — Save error (S02/ERR-2 path) |
| Tenant accepts none / closes | FE | nothing written; refine_discarded (S02/AC-3) | Mixpanel event | yes (implicit) |
trace absent | BE (optional param) | refine proceeds degraded (no runtime context) | trace_present=false in log | no |
Detail 3.B — Error Response Catalog (BE)
Shape: { "error": { "messages": ["<human-readable>"] } } (existing Grape convention).
| Endpoint | Error case | HTTP | Message key | When | User-facing? |
|---|---|---|---|---|---|
POST /:id/refine | missing/oversized user_message, >10 history items | 400 | Grape param validation messages | request shape invalid | yes (inline) |
POST /:id/refine | role not permitted / flag OFF | 403 | Permission denied (existing set_role message) | gate failure | yes |
POST /:id/refine | agent not found in org | 404 | AI Agent not found | FindBy miss | yes |
POST /:id/refine | legacy agent | 422 | not_autonomous_agent | engine_version guard | yes |
POST /:id/refine | upstream timeout/5xx | 422 | Failed to refine AI Agent | transport failure | yes |
PATCH /:id | ref validation | 400 | existing CapabilityRefPresence messages | invalid pack | yes — existing |
PATCH /:id | sync failure | 422 | Failed to sync AI Agent to AI Service (existing, update_ai_agent.rb:66) | upstream push fails | yes — existing |
Detail 3.C — Error Message Catalog (FE)
| Error code | User-facing message (i18n key) | Surface | User-facing? |
|---|---|---|---|
| refine 422/timeout | bot_automation.refine.error_generic — "Couldn't generate a suggestion — your agent is unchanged. Try again." | error turn in thread + Retry | yes |
| refine 400 | bot_automation.refine.error_invalid — "That message couldn't be sent. Shorten it and try again." | inline under input | yes |
| refine 403 | bot_automation.refine.error_forbidden — "You don't have access to Refine." | toast; tab hidden | yes |
| save 400/422 | existing save error keys (unchanged) | inline in editor | yes |
(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.
| Field | Classification | Legal basis | Retention | Encryption | Access audit | Right-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 chatbot | not logged (content-scrub rule, §3 Logging) | n/a — nothing stored to delete |
ai_agents.parameters (applied config) | config, non-PII | existing | existing | existing (DB at rest) | PaperTrail | existing |
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-animspin; 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 theSkillPackBuilderextraction, 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', defaultenabled: 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_failedrate > 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
| Scenario | FE | BE | Works? | Mitigation |
|---|---|---|---|---|
| Pre-deploy | Old | Old | yes | baseline |
| Backend first | Old | New | yes | new route unused; flag OFF |
| Frontend first | New | Old | degraded-safe | tab hidden unless flag payload exists; a raced call 404s into the error turn — avoided by BE-first order |
| Both deployed, flag OFF | New | New | yes | feature invisible (target pre-rollout state) |
| Both deployed, flag ON | New | New | yes | target state |
| Backend rollback | New | Old | degraded-safe | refine sends fail into retryable error turn; editor/save unaffected; flip flag OFF to hide tab |
| Frontend rollback | Old | New | yes | endpoint idle |
Detail 4.B — Configuration Contract
| Layer | Config | Type | Default | Required | Provisioner | Secret? |
|---|---|---|---|---|---|---|
| BE + FE | system_preferences row rollout / ai_agent_refine | boolean (enabled) | false | yes | ops (console/seed, per existing rollout-row practice) | no |
| BE | upstream path constant /qontak-ai-noncore-mrag/api/ai-agent/refine-skill-pack | code constant | — | yes | code (Chunk 3) | no |
| BE | refine timeouts open_timeout: 60, read_timeout: 60 | code constants | 60 s | yes | code (drafter parity) | no |
| FE | history cap REFINE_HISTORY_MAX_TURNS | const | 10 | yes | code (revisit per §5 OQ-3) | no |
Detail 4.C — Test Plan (commands the agent will run)
| Layer | Command (source) | What it must prove |
|---|---|---|
| BE unit/request | bundle 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 / security | bundle exec rubocop · bundle exec brakeman (repo toolchain) | style + no new security findings |
| FE unit | pnpm test (source: package.json:17 → vitest run) | useRefineAgent history cap + differ correctness; RefinePanel/RefineOptionCard states; Accept mutates form + highlights + switches tab with zero HTTP |
| FE lint | pnpm lint (source: package.json:15) | ts + prettier clean |
| FE E2E | pnpm test:e2e (source: package.json:22 → playwright test) — one spec: refine happy path against a mocked BE | thread → 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 available | contract §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
| Order | Layer | Chunk | Files to modify/create | Commands to run | Acceptance criteria (verifiable) |
|---|---|---|---|---|---|
| 1 | BE | Extract 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 snapshot | bundle exec rspec spec/api/frontend_service/v2/ai_agent/ · bundle exec rubocop | all 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) |
| 2 | BE | Flag row + helpers | seed/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 |
| 3 | BE | Upstream client method | modify lib/ai_service/ai_agent.rb: add refine_skill_pack(body:) → POST /qontak-ai-noncore-mrag/api/ai-agent/refine-skill-pack, 60 s timeouts | bundle exec rspec (client spec stubbing Http) | method issues POST to exact path with body + timeouts (asserted via mock) |
| 4 | BE | Repositories::Refine | create 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 client | bundle exec rspec | request body matches §2.4 upstream schema verbatim for a fixture pack (current_skill_pack from Chunk-1 builder w/ read-only resolver) |
| 5 | BE | UseCases::RefineAiAgent + route + response model | create 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.rb | full 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 |
| 6 | FE | Service + composable | modify 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_agent → ProposedChange[], any card id/label/recommended fabricated FE-side per OQ-1b, error extraction per useGenerateAgent.ts:79–90) | pnpm test · pnpm lint | unit 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 |
| 7 | FE | Rail + panel components | modify AiAgentEditor.vue (showPreview → rightRailTab two-tab rail; aiChangedFields; apply handler writing pendingData into the form model + activeTab switch; gate = flag && engineVersion===2); create RefinePanel.vue, RefineOptionCard.vue | pnpm test · pnpm lint · pnpm build | component 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 |
| 8 | FE | Analytics + i18n | wire trackEvent calls (`refine_requested | succeeded | failed |
| 9 | both | E2E + staging verification | FE Playwright spec (mocked BE); staging run against ML stub | pnpm test:e2e · manual staging checklist | E2E 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. fullbundle exec rspec(repo gate) - FE: 1.
pnpm lint· 2.pnpm test· 3.pnpm build· 4.pnpm test:e2e(refine spec)
- BE: 1.
- 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 SyncToAiServiceerror classes after deploy (pre-flag-ON, the endpoint should log nothing at all). - Mixpanel funnel:
refine_requested→refine_succeededconversion ≥ 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-agentbody (spot-check against a pre-deploy capture).
- Log query:
- Rollback recipe (ordered):
- Feature level: set
system_preferencesrollout/ai_agent_refineenabled: false(console; no deploy). Tab disappears; endpoint 403s; manual editing unaffected. - Per-agent config level: restore the prior
parametersfrom the agent's PaperTrail version (ai_agent.versions—has_paper_trail,ai_agent.rb:5), then save through the standard update path soSyncToAiServicere-pushes the oldskill_packupstream. (Corrects PRD'sai_agent_historiesreference — that mechanism is V1-only.) - Code level (only if Chunk 1 itself regressed sync): revert the extraction PR; the byte-identical snapshot spec pinpoints any divergence.
- Confirm: Rollbar quiet for 15 min; a staging save round-trips 2xx.
- Feature level: set
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:
| # | Type | Question / concern | Owner | Needed by |
|---|---|---|---|---|
| OQ-1 | Blocker (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/ML | before Chunk 9 real-upstream pass (PRD: 2026-07-15) |
| OQ-2 | trace: 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/ML | closed 2026-07-07 | |
| OQ-3 | chat_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-4 | Open | KB 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/ML | before Closed Beta |
| OQ-5 | Follow-up | Figma 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. | Design | before Open Beta |
| OQ-6 | Known limitation | Stale-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. | Eng | acknowledged; revisit if Alpha shows collisions |
| OQ-7 | Pre-GA action | No rate limiting on refine (parity with generate) while each call spends LLM tokens. Add per-org throttling (e.g. N refines/minute) before GA. | Eng | before GA |
| OQ-8 | Ops | Seeding 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. | Eng | before Alpha |
| OQ-9 | Product/compliance | Refine 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. | PM | noted for Phase 3+ |
| OQ-10 | Design (partially resolved) | Config-audit mode surfacing. (a) [id].vue:1806–1845). Engineering cost zero — same endpoint, same flow. | Design | before Closed Beta |
| OQ-11 | Open | Refine 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/ML | before Chunk 9 real-upstream pass |
| OQ-12 | Open | OpenAPI 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/ML | before Chunk 9 real-upstream pass |
| OQ-13 | Risk / follow-up | Applied-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 + Eng | before GA |
| REV-1 | Review 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-2 | Review 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. | Eng | before agent execution |
| REV-6 | Review 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-7 | Review 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." | PM | housekeeping |
| 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
| Date | Comment(s) From | Action Item(s) |
|---|---|---|
| 2026-07-08 | Eng (Eko) — BE contract simplification | Built-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_pack → updated_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-07 | TPM (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-05 | rfc-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-06 | Pre-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-05 | PRD correction sweep | PRD 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-05 | PRD sync | PRD 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-05 | PM (Dimas) — reliability requirement | Made 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-05 | Data/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):
- 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.
- KB-affecting-patch policy (OQ-4) — needs a product decision before Closed Beta; does not block chunks 1–8.
- Upstream deployment/exposure unverified (OQ-1a). The contract is now
documented as-built (§2.4 quotes it verbatim), so this is a verification
- 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-reviewerfor a second-pass PROCEED/HOLD score once the Data/ML contract review lands.