docs: correct 005-ai-support design to reuse the existing ticket state machine
003-ticketing's ticket-state-machine.ts already defines AI_ANALYZING/ AI_TROUBLESHOOTING/AI_VERIFYING/AI_RESOLVED/HUMAN_ESCALATION ticket statuses, clearly authored anticipating this feature. Corrects the plan before implementation: AISupportSession.status now drives Ticket.status through the existing ticketsService.updateStatus (reusing its optimistic concurrency), instead of an isolated status field the rest of the system never sees. Also clarifies that knowledge retrieval is an in-process service call through knowledge's index.ts, not an HTTP loopback. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
72dddcdf74
commit
eceb00632d
@@ -47,3 +47,15 @@
|
||||
§A2, not yet built anywhere in this codebase), model routing/fallback (doc 11 §B2), full cost
|
||||
dashboards (doc 09), localization (doc 11 §B7), and idle-session timeout/expiry.
|
||||
- All items pass; no revision iterations were needed.
|
||||
|
||||
## Planning notes (added during /speckit-plan research)
|
||||
|
||||
- **Found a real integration point, not a gap**: 003-ticketing's `ticket-state-machine.ts`
|
||||
already defines `AI_ANALYZING`/`AI_TROUBLESHOOTING`/`AI_VERIFYING`/`AI_RESOLVED`/
|
||||
`HUMAN_ESCALATION` ticket statuses — a near-exact match for doc 06's
|
||||
`AISupportSession.status` enum, clearly authored anticipating this feature. research.md's
|
||||
design was corrected during planning (before any code was written) to drive `Ticket.status`
|
||||
through this existing state machine via the existing `ticketsService.updateStatus(...)`, rather
|
||||
than leaving `AISupportSession.status` as an isolated field the rest of the system can't see —
|
||||
see research.md "AISupportSession.status drives Ticket.status through the existing state
|
||||
machine".
|
||||
|
||||
@@ -11,7 +11,7 @@ by FR-005).
|
||||
|---|---|---|
|
||||
| id | String @id @default(cuid()) | |
|
||||
| ticketId | String | FK → `Ticket`. One **active** session per ticket at a time (FR-001) — enforced in the repository (partial-condition check, not a DB constraint, since a ticket accumulates multiple ended sessions over time if re-escalated and re-opened) |
|
||||
| status | String | `analyzing` \| `troubleshooting` \| `verifying` \| `resolved` \| `escalated` \| `ended_by_agent` (doc 06's enum, plus `ended_by_agent` for FR-023) |
|
||||
| status | String | `analyzing` \| `troubleshooting` \| `verifying` \| `resolved` \| `escalated` \| `ended_by_agent` (doc 06's enum, plus `ended_by_agent` for FR-023). Every transition except `ended_by_agent` is mirrored onto `Ticket.status` via the *existing* 003 state machine (`AI_ANALYZING`/`AI_TROUBLESHOOTING`/`AI_VERIFYING`/`AI_RESOLVED`/`HUMAN_ESCALATION`) through `ticketsService.updateStatus(..., 'ai')` — research.md "AISupportSession.status drives Ticket.status" |
|
||||
| activeRunbookKey | String? | Set when a diagnosis matches a runbook (research.md "Runbook engine") |
|
||||
| currentStepIndex | Int? | App-owned index into the active runbook's `steps`; null when no runbook is active |
|
||||
| clarifyingQuestionsAsked | Int @default(0) | Counted against `AIConfidencePolicy.maxClarifyingQuestions` (FR-009) |
|
||||
|
||||
@@ -42,8 +42,14 @@ human — both were previously unimplementable (no AI session existed) and are a
|
||||
`src/modules/ai-support/{sessions,tools,troubleshooting,escalation}/` (standard module shape,
|
||||
research.md "Module placement"). New infra: `src/infrastructure/ai/` (Anthropic client
|
||||
singleton). New job: `src/jobs/ai-session/` (registered in `src/bootstrap/queue.bootstrap.ts`).
|
||||
Modifies `src/modules/ticketing/tickets/service/tickets.service.ts` (enqueue on ticket creation —
|
||||
research.md "Session triggering") and `prisma/schema.prisma`.
|
||||
Modifies `src/modules/ticketing/tickets/service/tickets.service.ts` — two hooks: enqueue on
|
||||
ticket creation (research.md "Session triggering"), and end any active `AISupportSession` inside
|
||||
`updateStatus` when a human actor moves the ticket (research.md "AISupportSession.status drives
|
||||
Ticket.status", FR-023). Also modifies `prisma/schema.prisma`. **Discovered during this planning
|
||||
pass**: 003-ticketing's `ticket-state-machine.ts` already defines the exact `AI_ANALYZING →
|
||||
AI_TROUBLESHOOTING → AI_VERIFYING → AI_RESOLVED` / `HUMAN_ESCALATION` states this feature drives
|
||||
— this feature reuses that state machine and `ticketsService.updateStatus` directly rather than
|
||||
introducing a parallel one.
|
||||
|
||||
**Project Type**: Backend service — single project.
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
pure structured-output call means it can never emit a stray tool proposal, and keeps the
|
||||
confidence score honest (it's the model's stated belief about the classification, not
|
||||
entangled with whatever it also did with tools that turn).
|
||||
2. Deterministically (no model call): call `GET /knowledge/retrieve` (004) scoped to the
|
||||
2. Deterministically (no model call): call `knowledgeService.retrieve(...)` (004, imported
|
||||
through `ai-support/knowledge`'s public `index.ts` — Principle III; an in-process call, not
|
||||
an HTTP loopback to this same service's own `GET /knowledge/retrieve` route) scoped to the
|
||||
diagnosis's product/feature and the ticket's category, and apply the confidence-band policy
|
||||
(FR-004) to the diagnosis. This decides the branch: ask / proceed / escalate — this decision
|
||||
is application code, never delegated to the model.
|
||||
@@ -84,8 +86,9 @@
|
||||
`src/modules/ai-support/tools/constants/tool-registry.ts`. Four tools ship in this feature:
|
||||
- `getTicketSnapshot` (low risk) — reads the ticket, its problem, and recent messages; real,
|
||||
read-only, always available.
|
||||
- `searchProductKnowledge` (low risk) — re-queries `GET /knowledge/retrieve` with a
|
||||
model-supplied feature/category refinement mid-conversation; real, read-only.
|
||||
- `searchProductKnowledge` (low risk) — re-runs `knowledgeService.retrieve(...)` (same
|
||||
in-process call as above) with a model-supplied feature/category refinement mid-conversation;
|
||||
real, read-only.
|
||||
- `verifyProductResolution` (low risk) — **a documented, fail-closed placeholder**, exactly the
|
||||
same pattern already established by `ticketing/attachments`'s
|
||||
`UnimplementedPlaceholderScanner`: it always returns `{ confirmed: false, status: "unknown"
|
||||
@@ -167,6 +170,43 @@
|
||||
codebase already has BullMQ wired up for exactly this "background work after a DB write"
|
||||
purpose.
|
||||
|
||||
## Decision: `AISupportSession.status` drives `Ticket.status` through the existing state machine
|
||||
|
||||
- **Decision**: `src/modules/ticketing/tickets/mapper/ticket-state-machine.ts` (built in
|
||||
003-ticketing, before this feature existed) already defines `NEW → AI_ANALYZING →
|
||||
AI_TROUBLESHOOTING → AI_VERIFYING → AI_RESOLVED`, with `HUMAN_ESCALATION` reachable from every
|
||||
AI_* state and `HUMAN_ESCALATION → IN_PROGRESS` as the human hand-off edge — a near-exact match
|
||||
for doc 06's `AISupportSession.status` enum (`analyzing | troubleshooting | verifying |
|
||||
resolved | escalated`). This feature does not invent a parallel status concept: every time a
|
||||
session's own status changes, it calls the **existing**
|
||||
`ticketsService.updateStatus(ticketId, newTicketStatus, expectedVersion, 'ai')` (003) to drive
|
||||
the ticket through the matching status (`analyzing→AI_ANALYZING`,
|
||||
`troubleshooting→AI_TROUBLESHOOTING`, `verifying→AI_VERIFYING`, `resolved→AI_RESOLVED`,
|
||||
`escalated→HUMAN_ESCALATION`), reusing 003's own optimistic-concurrency handling
|
||||
(`expectedVersion`/`409`) rather than adding a second one. The session's own `status` field
|
||||
still exists separately (data-model.md) because it carries session-scoped values the ticket
|
||||
state machine doesn't need to know about (`ended_by_agent` — see FR-023 below — never appears
|
||||
on `Ticket`), but for every value the two share, the ticket is the caller-visible source of
|
||||
truth and the session record is the AI-internal detail behind it.
|
||||
- **Rationale**: `Ticket.status` is what every other part of this codebase (agents, SLA,
|
||||
orchestration once built, the ticket-status contract in 003) already reads to know where a
|
||||
ticket stands — a session-only status field that never touched `Ticket.status` would make the
|
||||
ticket lie about its own state while an AI session was quietly doing something else internally.
|
||||
Reusing 003's state machine and its `updateStatus` method also means this feature inherits
|
||||
003's already-tested transition validation and concurrency guarantee for free, rather than
|
||||
re-deriving both.
|
||||
- **Alternatives considered**: A session-only status with no `Ticket.status` linkage — rejected
|
||||
per the rationale above. Building a second, AI-specific transition table — rejected; 003's
|
||||
table already defines exactly these states and edges, and doc 06's `AISupportSession.status`
|
||||
values were clearly authored to match it in the first place.
|
||||
- **FR-023 implementation note**: `ticketsService.updateStatus` gains one additional check — when
|
||||
it's called with an actor other than `'ai'` (a human agent action) while an `AISupportSession`
|
||||
for that ticket is still active, the session is ended (`status: ended_by_agent`) as part of the
|
||||
same call, before the ticket's own status update commits. This is the concrete mechanism behind
|
||||
"a human agent takes ownership ends the AI session the same way an escalation does" (FR-023) —
|
||||
there's no separate "agent claims ticket" endpoint yet (orchestration/assignment is a later
|
||||
phase), so any human-actor status transition is the signal this feature has available today.
|
||||
|
||||
## Decision: Runbook engine — the app selects the step, the model only phrases and interprets it
|
||||
|
||||
- **Decision**: `AISupportSession` gains `activeRunbookKey String?` and `currentStepIndex Int?`
|
||||
@@ -191,12 +231,16 @@
|
||||
|
||||
## Decision: Verification and resolution — a session can only close on tool evidence
|
||||
|
||||
- **Decision**: `AISupportSession.status` transitions to `resolved` only when a
|
||||
- **Decision**: `AISupportSession.status` (and, via the decision above, `Ticket.status`)
|
||||
transitions `verifying → resolved` (`AI_VERIFYING → AI_RESOLVED`) only when a
|
||||
`verifyProductResolution` tool result exists on the session with `confirmed: true` — which,
|
||||
given that tool's current fail-closed placeholder implementation (above), means resolution
|
||||
through this exact tool never actually auto-fires yet in this deployment. Customer confirmation
|
||||
(a message recorded during the session) is stored and surfaced in the escalation/resolution
|
||||
summary, but the status transition's guard checks tool evidence only, never message content.
|
||||
Once `Ticket.status` reaches `AI_RESOLVED`, this feature's own responsibility ends — whether the
|
||||
ticket then moves to `RESOLUTION_PENDING_CUSTOMER` or straight to `RESOLVED` is 003-ticketing's
|
||||
existing generic status-update surface, not something this feature further automates.
|
||||
- **Rationale**: FR-018/FR-019 verbatim — resolution requires verification evidence, customer
|
||||
confirmation is secondary-only. Guarding the state transition on a structured tool-result field
|
||||
(not on parsing what the AI "said" about the outcome) keeps this enforceable in code, not just
|
||||
|
||||
Reference in New Issue
Block a user