Appearance
ai-service Agent Tasks
Agent workflow: follow README.md for Audit -> Investigate -> Code -> Test -> Fix; keep this pack's writable scope and verification commands authoritative.
Dispatch type: per-service
Dispatch ID: SVC-015
Current implementation: services/ai-classifier-service.
Owns: AI provider adapters, AI jobs, model usage, prompt/result logs, generation/explanation/grading AI result envelopes.
Must read:
docs/agents/handoff-phase-6-ai-classifier-*.md- legacy AI provider/settings and usage files:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/admin/ai-settings.service.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/admin/ai-usage.service.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/question-classification.service.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/question-classification.queue.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/question-classification.worker.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/question-generation.service.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/question-generation.service.spec.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/question-solution.service.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/question-solution.service.spec.ts,/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/grading/grading.spec.ts.
Legacy source evidence:
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/admin/ai-*.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/question-classification*.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/question-generation*.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/questions/question-solution*.ts/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/grading/**- legacy provider usage and queue specs under
/Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/**.
Writable files:
services/ai-classifier-service/**contracts/openapi/services/ai-classifier-service.yamldocs/api/ai-classifier-service.md
Orchestrator-owned files:
- AI route examples under
deploy/gateway/routes.ai-*.json,deploy/gateway/routes.question-classification-*.json, route tests underscripts/test/ai-*.shandscripts/test/question-classification-*.sh, provider secrets, default route table, shared deploy manifests, and billing/ wallet/usage integrations unless explicitly delegated.
Delegated gateway verification scope:
- Only when the orchestrator assigns an AI route/live proof subtask, this service agent may update non-default
deploy/gateway/routes.ai-*.json,deploy/gateway/routes.question-classification-*.json,scripts/test/ai-*.sh,scripts/test/question-classification-*.sh,docs/qa/question-classification-*.md, and rootMakefiletest-ai-*/test-question-classification-*targets. - Provider secrets, billing/wallet/usage integrations, and the default gateway route table remain orchestrator-owned; public routes stay legacy until browser/runtime proof and rollback notes exist.
Initial tasks:
- Extend classification foundation toward generate/explain/essay-grade task slices only after contracts are explicit.
- Done: add an AI contract slice before implementation: typed internal request/result envelopes for
classify,generate,explain, andgrade, provider feature flags, idempotency keys, cancellation semantics, usage event fields, and rollback notes. The contract is internal-only and does not promote public/api/questions/ai-*routes. - Done: add a generation slice from
QuestionGenerationService: internalPOST /v1/ai/generation/previewaccepts prompt metadata and optional document references, uses the configured non-mock provider for preview questions, records success or failure provider usage events, and carries an explicitquestion-servicehandoff for canonical question writes. The default mock is deterministic for local development. Public/api/questions/ai-generation/*routes stay legacy-proxied. - Done: add an explanation slice from
QuestionSolutionService: internalPOST /v1/ai/explanation/previewaccepts editor question content, optional answer key, and existing solution text; uses the configured non-mock provider for preview solution text/steps, records success or failurequestion-solutionusage metadata, and keeps canonical question solution writes inquestion-service. The default mock is deterministic for local development. Public/api/questions/ai-solutions/*routes stay legacy-proxied. - Done: add a non-default Gateway rehearsal for the two teacher preview flows: exact authenticated
POST /api/questions/ai-generation/generateandPOST /api/questions/ai-solutions/editormap to the internal preview owner endpoints throughroutes.teacher-ai-preview-native-*.json. The default route table remains legacy-proxied, the service returns preview payloads only, and neither flow creates canonical questions or solutions. - Audit status: generation and explanation previews are provider-backed when a non-mock adapter is configured, while mock remains deterministic in local development. Reviewed generation drafts and explanation solutions both use Question Bank versioned, idempotent owner APIs; generation settlement is also available behind its disabled-by-default usage/wallet adapter. Do not cite an AI preview alone as proof of a canonical Question Bank write.
- Done: the teacher UI can submit a reviewed generation candidate through the exact opt-in Gateway path to Question Bank
POST /v1/questions/ai-preview-drafts. AI service still returns only previews; Question Bank owns the forcedDRAFTwrite, provenance, and preview-id idempotency. Reviewed explanation previews now use the separate Question BankPOST /v1/questions/ai-preview-solutionsowner boundary to append a canonical solution version; AI remains preview-only and does not own Question Bank writes. - Done: add the bounded P6-047 generation monetization adapter. It is disabled by default and, when explicitly enabled, checks four usage-service quota windows before a provider call, reads wallet balance, then settles successful output through idempotent
usage/consumeand walletUSAGE_DEBITrequests. The adapter uses the legacy default of three AZ Credits per generated question and one thousand token units per credit. AI remains only the workload orchestrator; it does not own quota, balance, counters, or ledger storage. - Done: reduce P6-047 owner-call latency without changing owner boundaries. The four read-only quota checks fan out concurrently before the wallet read; after an accepted provider result and one wallet debit, the four resource-keyed
usage/consumecalls fan out concurrently. Both phases retain the documented resource-order error precedence, skip wallet reads on quota denial, and preserve the existing owner idempotency keys so P6-049 can reconcile any partial settlement. - Done: extend P6-048 charged-generation replay recovery. Immediately after provider output, the ai-classifier owner DB stores a 24-hour pending replay by organization, actor, and normalized generation hash. A same-key retry after owner settlement or telemetry-recording failure skips Question Bank reference loading, preflight, and provider execution, then retries settlement with the persisted provider usage. Only settlement plus telemetry success turns the record into a settled cache response. Pending replay save failures still return an error and can re-enter the provider later. Recovery relies on the existing owner-service idempotency keys; it does not compensate a partially applied wallet/usage sequence, and same-instance
singleflightis not evidence of multi-pod in-flight coordination. This remains opt-in behindAI_GENERATION_USAGE_WALLET_ENABLED; it does not promote a default route. - Done: add P6-049 durable settlement reconciliation. With both
AI_GENERATION_USAGE_WALLET_ENABLED=1andAI_GENERATION_SETTLEMENT_WORKER_ENABLED=1, a separate worker claims due unsettled generation replays through the AI-owned Postgres store usingSKIP LOCKED, lease, persisted attempt count, retry timestamp, and last error. Pending rows remain recoverable after preview-cache expiry until the original wallet/usage idempotency keys settle successfully. The worker never reruns provider, preflight, Question Bank context, or Question Bank writes. Partial owner sequences advance forward only; no compensation or public route promotion is implied. - Done: add a hermetic HTTP fault-injection proof for the P6-049 worker. It sends one real AI preview request through the HTTP monetization client, fails one usage-consume call after a wallet debit and three usage mutations, then runs the production settlement worker loop. Owner idempotency yields one effective debit and one effective consume per resource without rerunning the provider or quota preflight. This uses in-process owner fixtures only and does not prove real usage/wallet binaries, stores, or public-route promotion.
- Done: add a Docker-independent P6-049 owner-runtime proof. The local PostgreSQL runner builds the three current service binaries, applies their current migrations to separate fresh databases, and restarts the AI worker after a real Wallet debit plus a fail-once real Usage consume. It asserts the durable replay, owner counters, immutable debit, audit outcome pair, and cached replay without a second owner call. It is local mock-provider evidence only and does not promote a Gateway route.
- Done: the equivalent disposable Compose owner-runtime recovery passed at
output/qa/ai-generation-settlement-runtime-20260714-084542/. It used unique owner Postgres volumes, observed the durable1:true:0to1:false:1recovery across AI recreation, then removed all project containers, volumes, and its loopback fault proxy. It remains local mock-provider evidence only. - Done for the bounded P6-046 preview contract: generation accepts typed
fresh,advanced, andsimilarmodes. Similar mode requires at most fivereferenceQuestionIdsand resolves them through the existing scoped question-bank context API; it does not accept raw reference content from a caller. This remains a no-write preview and does not replace the approved question-service write or usage/wallet-preflight production slice. - Done: a fresh Compose Physics DOCX owner-runtime recheck starts from a real imported Question Bank reference, proves the similar and explanation calls both remain mock
PREVIEWpayloads with no usage/wallet consumption, then sends teacher-reviewed results to the two versioned Question Bank APIs. The similar draft and solution-version replays are idempotent, and the reviewed solution keeps the source DOCXimportJobId. This is local owner evidence, not provider-credential, default Gateway, browser, or promotion proof. Seedocs/qa/docx-fixture-persistence-runtime-proof.md. - Done: add an essay-grade slice from grading evidence: internal
POST /v1/ai/essay-grade/previewaccepts attempt/question/answer snapshots by value plus rubric criteria, uses a configured OpenAI-compatible, Gemini, local, or Ollama adapter for rubric-constrained advisory scores with a deterministic mock fallback, recordsessay-gradeprovider usage metadata without Usage/Wallet consumption, and carries an explicitattempt-servicehandoff for stored grades. Public/api/attempts/:attemptId/submitstays legacy-proxied. - Done: add owner provider-status slice:
GET /v1/ai/providers/statusreports environment or database-selected provider/model readiness for admin-serviceai.providerssource snapshots without returning provider secrets or calling the live provider network. - Done: wire the AI-owned
provider_settingsdefault selection into startup and runtime. InternalADMIN-onlyGET/PUT /v1/admin/ai/providers/settingsstores provider/model/base URL plus an allowlisted secret env reference only; it validates the candidate before persistence, swaps the active provider, and does not add a Gateway/public route. - Done: add owner queue/failure projection and native retry command:
GET /v1/admin/ai-classifier/queue-summarysummarizes service-ownedclassification_jobscounts, pending/running depth, and recent failures for adminqueue.depths;POST /v1/ai-classifier/jobs/{id}/retryretries onlyFAILEDorCANCELLEDnative jobs through the configured provider. Admin-service/BFF may call these owner APIs but must not read the AI database directly. - Done: generation settlement integrates usage-service and wallet-service only through their owner HTTP check, consume, balance, and ledger APIs; the AI service never reads or writes either owner database.
- Done: add workflow coverage that an in-flight
CancelJobis not overwritten by a later provider result fromProcessJob. - Done: add workflow coverage for provider retry recovery usage, circuit-breaker-open usage, and SSE
job.updateddelivery after a pending job is cancelled. - Done: enforce defense-in-depth actor scope on interactive AI previews and classification jobs. The owner service now rejects missing actor/organization headers, allows only
ADMIN/TEACHERpreview/job callers, scopes list/read commands to that actor and organization, and requiresADMINfor the global queue projection. Gateway remains the authenticated header source; this does not promote any default public route. - Expand classifier jobs to request bounded question content, current classification, and taxonomy candidates from question-service before provider calls. Done locally through the optional
QUESTION_BANK_SERVICE_BASE_URL/QUESTION_BANK_SERVICE_URLcontext client; keep it configured in runtime manifests and do not fall back silently when a configured client fails. - Done: add queue worker/batching semantics before public job route cutover. Deferred native classification jobs can be claimed from service-owned
classification_jobswith attempts/max-attempts,nextRunAt,lockedUntil, andworkerId; the opt-in runtime worker drains due jobs, processes provider calls in fixed batches of 5 question IDs, records one provider usage event per batch, and requeues worker-owned failures with exponential backoff. Public/api/questions/ai-classify/*routes remain legacy-proxied. - Done: add bounded per-job provider batch concurrency for latency. The runtime defaults
AI_CLASSIFIER_BATCH_CONCURRENCY=1and clamps it to five, independently ofAI_CLASSIFIER_WORKER_CONCURRENCY. Higher values fan out a wave of fixed-size provider batches while retaining ordered result items, batch-indexed usage telemetry, and sequential progress persistence. The focused race test proves three concurrent provider calls and stable output; this is not provider-rate-limit, cross-pod, browser, or default-route proof. - Done: add an opt-in disposable owner-runtime smoke for the native worker. It starts an in-memory or fresh Postgres-backed
ai-classifier-servicewith the mock provider, creates a deferred twelve-question job, proves worker drain, native job detail/SSE and queue summary, then removes the process, container, and temporary data. Itsquestion-bankmode separately starts a disposable memory-backed Question Bank, creates twelve canonical questions through the owner API, and proves the completed AI job retains all twelve bounded prompt rows. Its optionaljob-readmode uses the dedicated non-default Gateway route table to prove authenticated native list/detail/SSE reads, route headers, and a student-role deny without changingroutes.json. Seedocs/qa/ai-classification-worker-runtime-smoke.md. A separate Teacher read-only job surface now consumes its exact list/detail Gateway paths with a disposable browser session and renders worker context/result fields. The worker harness itself still does not claim concurrent Postgres lock, a deployed Gateway, public route, provider credential, Question Bank Postgres/taxonomy, or usage/wallet settlement coverage. - Add an apply endpoint or explicit apply handoff to question-service with idempotent results and audit events.
Acceptance:
- AI-service does not decide quota/payment locally.
- Provider secrets remain env/secret managed.
- Public job routes stay legacy until BullMQ/runtime/browser parity is proven.
- Generate/explain/grade results are previews or explicit handoff payloads; they do not mutate question-service, exam-service, or attempt-service state without a versioned owner-service API.
- Provider settings are AI-owned runtime selection data, while usage-service and wallet-service remain the only owners for quota checks and credit movement.
- Provider status is exposed through ai-classifier-service owner API only; admin-service may probe it but must not own provider settings or secrets.
- Queue depth/failure state and retry commands are exposed through ai-classifier-service owner APIs only; admin-service and BFF must not inspect
classification_jobsdirectly. - The first generation/explanation/grading slices must keep mock fixtures and provider failure paths testable without live provider credentials.
- Prompt payloads include enough real question context for parity; ids-only payloads are not considered production-ready.
- Provider usage events are test-covered on success and failure. Generation's opt-in usage/wallet settlement is tested through owner APIs; explanation and grade charging remain separate workload decisions.
- Queue worker claims, batch progress, retry backoff, and final max-attempt failure are test-covered before any public route promotion.
- Generation settlement worker claim/lease/backoff is test-covered before any charged public route promotion. It has no terminal retry cap because an already-debited wallet must stay recoverable through owner idempotency keys.
Verification:
GOTOOLCHAIN=go1.25.11 go test ./services/ai-classifier-service/...make test-ai-generation-settlement-reconciliationmake test-ai-generation-settlement-fault-injectionAI_GENERATION_SETTLEMENT_RUNTIME_CONFIRM=disposable-runtime make test-ai-generation-settlement-runtimestarts real AI, Usage, and Wallet binaries with fresh Compose volumes, injects onecredits_weeklyconsume failure after the real wallet debit, then checks durable worker recovery and replay-cache behavior. It passed atoutput/qa/ai-generation-settlement-runtime-20260714-084542/, requires Docker, and is local evidence only; it does not promote a Gateway route.AI_GENERATION_SETTLEMENT_LOCAL_POSTGRES_CONFIRM=local-postgres make test-ai-generation-settlement-local-postgresstarts the same proof with a fresh/tmpPostgreSQL cluster and native binaries. It does not depend on Docker or a configured local database.- The same target guards Compose, Helm, static K8s, and the service env example for the opt-in settlement worker; both charging and settlement-worker flags stay false by default.
AI_CLASSIFIER_POSTGRES_DSN=postgres://... GOTOOLCHAIN=go1.25.11 go test ./services/ai-classifier-service/internal/repository -run TestPostgresGenerationSettlementClaimPersistsAcrossStores -count=1- Focused owner queue/retry regression:
GOTOOLCHAIN=go1.25.11 go test ./services/ai-classifier-service/internal/http -run TestAIClassifierAdminQueueSummaryAndRetry -count=1 GOTOOLCHAIN=go1.25.11 go test ./services/ai-classifier-service/internal/usecase -run 'TestClassifierServiceGeneratePreview|TestClassifierServiceExplainPreview|TestClassifierServiceGradePreview|TestClassifierServiceProcessJob|TestHTTPQuestionContextClient' -count=1 -vGOTOOLCHAIN=go1.25.11 go test ./services/ai-classifier-service/internal/http -run 'TestAIProviderStatus|TestAIClassifierGenerationPreview|TestAIClassifierExplanationPreview|TestAIClassifierEssayGradePreview|TestAIClassifierJobUsesQuestionContextClient|TestNewQuestionContextClientFromEnv' -count=1 -vmake test-ai-provider-settingsmake test-ai-classification-job-route-guardmake test-ai-classification-job-routesmake test-teacher-ai-preview-routespnpm exec vitest run packages/ui/src/modules/teacher/teacher-workspace.test.tsxAI_CLASSIFICATION_JOB_SELF_TEST=1 make test-ai-classification-job-liveAI_CLASSIFICATION_WORKER_SMOKE_CONFIRM=disposable-runtime make test-ai-classification-worker-runtimeAI_CLASSIFICATION_WORKER_SMOKE_CONFIRM=disposable-runtime AI_CLASSIFICATION_WORKER_SMOKE_STORE=postgres make test-ai-classification-worker-runtimeAI_CLASSIFICATION_WORKER_SMOKE_CONFIRM=disposable-runtime AI_CLASSIFICATION_WORKER_SMOKE_CONTEXT=question-bank make test-ai-classification-worker-runtimeAI_CLASSIFICATION_WORKER_SMOKE_CONFIRM=disposable-runtime AI_CLASSIFICATION_WORKER_SMOKE_STORE=postgres AI_CLASSIFICATION_WORKER_SMOKE_CONTEXT=question-bank make test-ai-classification-worker-runtimeAI_CLASSIFICATION_WORKER_SMOKE_CONFIRM=disposable-runtime AI_CLASSIFICATION_WORKER_SMOKE_CONTEXT=question-bank AI_CLASSIFICATION_WORKER_SMOKE_GATEWAY=job-read make test-ai-classification-worker-runtimeAI_CLASSIFICATION_WORKER_SMOKE_CONFIRM=disposable-runtime AI_CLASSIFICATION_WORKER_SMOKE_STORE=postgres AI_CLASSIFICATION_WORKER_SMOKE_CONTEXT=question-bank AI_CLASSIFICATION_WORKER_SMOKE_GATEWAY=job-read make test-ai-classification-worker-runtime- For an owner-local browser diagnostic only:
AI_CLASSIFICATION_WORKER_SMOKE_CONFIRM=disposable-runtime AI_CLASSIFICATION_WORKER_SMOKE_STORE=postgres AI_CLASSIFICATION_WORKER_SMOKE_CONTEXT=question-bank AI_CLASSIFICATION_WORKER_SMOKE_GATEWAY=job-read AI_CLASSIFICATION_WORKER_SMOKE_HOLD_SECONDS=240 make test-ai-classification-worker-runtime, then use the in-app browser against the temporary Gateway and clear its disposable session before cleanup. make test-question-classification-apply-routes- Runnable AI classifier smoke before route promotion:
make test-question-classification-apply-live. make test-ai-classification-job-liveis a live legacy-route guard for job list/SSE headers. Native job SSE/cancel route promotion still needs a later disposable-fixture smoke before any public route changes.- Future generate/explain/grade slices must add focused service tests under
services/ai-classifier-service/internal/**, OpenAPI mirror checks when contracts change, and route guards proving default public routes stay legacy.