practice slice 4 — pipeline, registry, API, feed
src/agent/subagents/practice-pipeline.ts (sibling of practice.ts —
orchestration vs calls): createPracticePipeline(graphStore+practiceStore
+chatStore+PracticeModels). Fire-and-forget front doors returning
{set, done}: createProblemSet (refuse → resolve selection (chatId =
ANY-provenance membership per brief; nodeIds raw) → chains → snapshot →
createSet partitioning → registry row → runPartitioner → stubs land
(generating) → ONE generator per problem behind the process-wide
3-permit abort-aware semaphore (__lhPracticeSlots, extraction idiom) →
per-problem failure isolation → ready (errored problems included);
abortPractice (idempotent: controllers + 'Aborted' marks; dropped
grades keep answers, re-submittable); submitAnswers (ready-only,
already-graded rejects, one grading job per answer under the same
semaphore); deleteProblemSet (abort-first). Per-set write mutex on
globalThis (__lhPracticeLocks, audit-lock idiom); registry
__lhPracticeRuns one row per set per kind; boot sweep marks orphaned
partitioning/generating sets 'Interrupted (server restarted)'. Typed
errors: EmptySelection/UnknownChat/SetNotFound/SetNotReady/BadAnswers.
Usage folds stage logs into the registry row (v1: completion-folded).
src/server/practice.ts: thin service — strict params parsing, typed
error → response mapping (503 envelope family, 400/404/409), list/get.
Routes: POST+GET /api/practice/sets (202 {id}), GET+DELETE
/api/practice/sets/[id], POST /api/practice/sets/[id]/answers (202).
Composition root: chat-handler wires createPracticePipeline (engineFor
fresh + providerKeyMissing gate) exported as ; barrel re-
exports. Feed: observability activityFeed() += practice rows
(ActivityPractice); dock blobs (process 'practice', GraduationCap
icon, set-keyed abort); /api/activity/abort kind 'practice'.
tests/practice-pipeline.test.ts — 13 tests: end-to-end (stubs mid-
flight via gated generators, coverage complete), failure isolation,
3-permit semaphore pin, abort mid-generation, registry shape pins,
refusal-before-create, chatId any-provenance selection, typed errors,
answers→grades land + graded rejects, boot sweep (orphan + live-row),
delete. 1114/1114, tsc 0, lint 0, build 0.
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import { extractionStatuses, type ExtractionPhase } from "../agent/subagents/extraction.ts";
|
||||
import { practiceStatuses, type PracticePhase } from "../agent/subagents/practice-pipeline.ts";
|
||||
import { chatStore } from "../store/chats.ts";
|
||||
import { graphRevision } from "./graph.ts";
|
||||
import { catalogContextLimits } from "./model-catalog.ts";
|
||||
@@ -27,6 +28,7 @@ import { listJobs } from "./jobs.ts";
|
||||
import type { JobRecord } from "../core/contracts.ts";
|
||||
|
||||
export { abortExtraction, extractionStatuses, type ExtractionPhase, type RunningExtraction } from "../agent/subagents/extraction.ts";
|
||||
export { practiceStatuses, type PracticePhase, type RunningPractice } from "../agent/subagents/practice-pipeline.ts";
|
||||
export { chatStore } from "../store/chats.ts";
|
||||
|
||||
/** A running sub-agent job, joined with its chat's title for the dock.
|
||||
@@ -54,6 +56,16 @@ export interface ActivityExtraction {
|
||||
startedAt: number;
|
||||
}
|
||||
|
||||
/** A live practice run in the dock's feed — mirrors ActivityExtraction
|
||||
* (kind names which pipeline half: generation or grading). */
|
||||
export interface ActivityPractice {
|
||||
id: string;
|
||||
kind: "generation" | "grading";
|
||||
title: string;
|
||||
phase: PracticePhase;
|
||||
startedAt: number;
|
||||
}
|
||||
|
||||
/** The dock's whole world in one payload: every running process, every view.
|
||||
* Jobs are RUNNING-ONLY — finished jobs belong to their chat's transcript;
|
||||
* the blob disappears when the process ends. Cost math is NOT done here:
|
||||
@@ -65,7 +77,12 @@ export interface ActivityExtraction {
|
||||
* graph changed underneath me" (a merge landed, a deletion purged) and
|
||||
* refetch clusters/layout. Reads never move it; the DELETE response
|
||||
* returns the same stamp. */
|
||||
export function activityFeed(): { jobs: ActivityJob[]; extractions: ActivityExtraction[]; graphRevision: number } {
|
||||
export function activityFeed(): {
|
||||
jobs: ActivityJob[];
|
||||
extractions: ActivityExtraction[];
|
||||
practice: ActivityPractice[];
|
||||
graphRevision: number;
|
||||
} {
|
||||
const chats = chatStore.listChats();
|
||||
const limits = catalogContextLimits();
|
||||
const jobs: ActivityJob[] = [];
|
||||
@@ -93,11 +110,19 @@ export function activityFeed(): { jobs: ActivityJob[]; extractions: ActivityExtr
|
||||
phase: run.phase,
|
||||
startedAt: run.startedAt,
|
||||
}));
|
||||
const practice: ActivityPractice[] = practiceStatuses().map((row) => ({
|
||||
id: row.setId,
|
||||
kind: row.kind,
|
||||
title: row.label,
|
||||
phase: row.phase,
|
||||
startedAt: row.startedAt,
|
||||
}));
|
||||
// oldest-first on last activity: the stable blob order (ISO strings sort chronologically)
|
||||
jobs.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
|
||||
return {
|
||||
jobs,
|
||||
extractions: extractions.sort((a, b) => a.startedAt - b.startedAt),
|
||||
practice: practice.sort((a, b) => a.startedAt - b.startedAt),
|
||||
graphRevision: graphRevision(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user