AI-triaged inboxes (ai-triage)
Classify incoming requests against your own action catalogue. A small
helpdesk feature stores requests; one write-handler bridges into the
enterprise ai-triage feature via ctx.query, hands it the catalogue this
feature owns, and stores the model’s category plus its best action suggestion
for review — executing an action stays a human decision.
What it shows
Section titled “What it shows”- The catalogue is the contract —
ai-triageis domain-agnostic and only proposes action types from the list the caller supplies. DefiningHELPDESK_ACTIONSin this feature is the whole integration step; the test asserts the catalogue reaches the model. - Persist the decision, return the evidence — the request keeps the
category and the single top suggestion (what you query later); the full
ranked list rides back on the write response via
withResponseData(what you render once). - Handling the result union explicitly —
triageis persisted for review,clarifyhands the model’s question back as a structured422(request untouched),errorrejects without storing anything. - Per-tenant prompt tuning, no deploy — setting the
ai-triage:config:triage-system-promptconfig-key changes what reaches the LLM provider on the next call; the integration test asserts the override arrives verbatim.
When to reach for it
Section titled “When to reach for it”Any shared inbox where requests need routing before anyone acts — helpdesks,
operations queues, tenant communication. Pair it with the ai-draft recipe:
triage picks the lane, draft writes the reply, a human stays in the loop for
both.
Wiring
Section titled “Wiring”ai-triage requires ai-foundation (provider host), config, and
prompt-store; mount an LLM provider plugin (ai-provider-anthropic,
ai-provider-openai-compat, or a mock in tests) alongside. The integration
test under src/__tests__/ boots that exact stack against real Postgres +
Redis and scripts the provider, so every assertion above is proven, not
described.
bun --env-file=../.env test --config=bunfig.integration.toml samples/recipes/ai-triageSource code
Section titled “Source code”The complete feature — embedded straight from the source file, so the code here is exactly what runs:
// AI-Triage Sample// Shows: a helpdesk inbox that consumes the enterprise `ai-triage` feature// server-side through ctx.query. The action catalogue is owned by THIS// feature — ai-triage stays domain-agnostic and only proposes actions from// the list it is handed. The triage decision is stored for review; executing// an action stays a human decision.
import { createEntity, createEntityExecutor, createLongTextField, createTextField, defineEntityCreateHandler, defineEntityDetailHandler, defineEntityListHandler, defineFeature, withResponseData,} from "@cosmicdrift/kumiko-framework/engine";import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";import { type AvailableAction, aiTriageFeature, TRIAGE_HANDLER_QN, type TriageMessageResult,} from "@cosmicdriftgamestudio/kumiko-ai-triage";import { z } from "zod";
// The catalogue is the integration contract: ai-triage never invents action// types, so adding a capability to this list is the whole wiring step.export const HELPDESK_ACTIONS: readonly AvailableAction[] = [ { type: "reset-password", description: "Send a self-service password-reset link." }, { type: "create-incident", description: "Open an incident for the on-call engineer." }, { type: "escalate-network-team", description: "Hand the request to the network team queue." }, { type: "close-as-duplicate", description: "Close it; an open request already covers this." },];
export const requestEntity = createEntity({ table: "read_sample_helpdesk_requests", fields: { subject: createTextField({ required: true }), from: createTextField({ required: true }), body: createTextField({ required: true }), // Filled by triage-request: the category plus the single best suggestion. // The full ranked list goes back in the write response only — persist // what you query later, return what you render now. triagedCategory: createTextField(), suggestedAction: createTextField(), suggestedActionRationale: createLongTextField({ maxLength: 10_000 }), },});
// ai-triage's query is gated to TenantAdmin/SystemAdmin, and ctx.query runs// as the CURRENT user — so every handler that bridges into it carries the// same gate instead of silently escalating via queryAs.const adminOnly = { access: { roles: ["TenantAdmin", "SystemAdmin"] } } as const;
export const helpdeskInboxFeature = defineFeature("helpdesk-inbox", (r) => { r.requires(aiTriageFeature.name); r.entity("helpdesk-request", requestEntity);
const { executor } = createEntityExecutor("helpdesk-request", requestEntity);
r.writeHandler(defineEntityCreateHandler("helpdesk-request", requestEntity, adminOnly)); r.queryHandler(defineEntityListHandler("helpdesk-request", requestEntity, adminOnly)); r.queryHandler(defineEntityDetailHandler("helpdesk-request", requestEntity, adminOnly));
// The point of this sample: classify a request against OUR catalogue and // store the model's pick for review. r.writeHandler( "triage-request", z.object({ id: z.uuid() }), async (event, ctx) => { const request = await executor.detail({ id: event.payload.id }, event.user, ctx.db); if (!request) { return failNotFound("helpdesk-request", event.payload.id); }
// Cross-feature bridge: runs ai-triage's queryHandler as the current // user, sharing the active transaction — same pattern as the ai-draft // recipe. const result = (await ctx.query(TRIAGE_HANDLER_QN, { message: { subject: request["subject"] as string, from: request["from"] as string, body: request["body"] as string, }, availableActions: HELPDESK_ACTIONS, })) as TriageMessageResult; // boundary: ctx.query erases the target handler's return type
switch (result.type) { case "triage": { // The model may return zero suggestions (e.g. spam → category only); // keep the highest-confidence one when there is any. const top = [...result.proposedActions].sort((a, b) => b.confidence - a.confidence)[0]; const updated = await executor.update( { id: event.payload.id, version: request["version"] as number, changes: { triagedCategory: result.category, ...(top && { suggestedAction: top.actionType, suggestedActionRationale: top.rationale, }), }, }, event.user, ctx.db, ); // Response-only payload: callers render the ranked list once, // nobody queries it later. return withResponseData(updated, { id: event.payload.id, proposedActions: result.proposedActions, }); } case "clarify": // The model needs input it cannot assume — hand the question back // to the caller; the request is left untouched. return failUnprocessable("triage needs clarification", { question: result.question, reason: result.reason, }); case "error": return failUnprocessable("triage failed", { reason: result.reason }); } }, adminOnly, );});Enterprise recipe — samples/recipes/ai-triage/src/feature.ts in the private kumiko-enterprise workspace.