Background pipeline status: registry + /api/graph/status + graph-view chips
The background extraction pipeline is invisible on the home surface — essential observability per the user's directive: the graph view must show what is currently running (extracting / auditing / merging) and for which chat. - extraction.ts: module-level registry of in-flight runs (chatId, phase, startedAt), updated at each phase transition in runExtraction and cleared in finally (done or failed). extractionStatuses() is the read; one process, one registry. - GET /api/graph/status: thin transport over the registry, chat titles joined from the chat store. - graph-view: polls every 2s and shows one chip per running pipeline (spinner + phase + title + elapsed seconds, bottom-left); when a run finishes (some -> none) the graph refetches, so merges land live. Status is cosmetic — poll errors never surface. - Unit test drives a mocked pipeline run and observes the registry mid-flight: 'extracting' during the extractor's generate, 'auditing' during the auditor's, empty after.
This commit is contained in:
@@ -34,7 +34,7 @@ import type {
|
||||
SessionFindings,
|
||||
} from "../src/core/contracts.ts";
|
||||
import type { GraphStore } from "../src/core/ports.ts";
|
||||
import { createExtractionPipeline, transcriptView } from "../src/agent/subagents/extraction.ts";
|
||||
import { createExtractionPipeline, extractionStatuses, transcriptView } from "../src/agent/subagents/extraction.ts";
|
||||
import { chatStore } from "../src/store/chats.ts";
|
||||
import { PATHS } from "../config.ts";
|
||||
|
||||
@@ -163,13 +163,16 @@ const FINDINGS: SessionFindings = {
|
||||
attribution: { quiz: { "probe-1": "p1", "check-1": "p2" } },
|
||||
};
|
||||
|
||||
/** A model whose ONE tool call is `name` with the given input; then text. */
|
||||
function submitModel(name: string, input: unknown): MockLanguageModelV4 {
|
||||
/** A model whose ONE tool call is `name` with the given input; then text.
|
||||
* `onCall` (optional) fires at the moment of the first generate — the
|
||||
* observation point for mid-run assertions. */
|
||||
function submitModel(name: string, input: unknown, onCall?: () => void): MockLanguageModelV4 {
|
||||
let turn = 0;
|
||||
return new MockLanguageModelV4({
|
||||
doGenerate: async () => {
|
||||
turn++;
|
||||
if (turn === 1) {
|
||||
onCall?.();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@@ -342,6 +345,39 @@ describe("the extraction pipeline (extractor → auditor → merger)", () => {
|
||||
assert.equal(ledgers.readCheckpoints().length, 0);
|
||||
});
|
||||
|
||||
it("reports its phase through the status registry while running, and clears it when done", async () => {
|
||||
const { chatId } = fixtureChat();
|
||||
const ledgers = memoryLedgers();
|
||||
const existingId = "n_existing";
|
||||
ledgers.appendNode({
|
||||
id: existingId,
|
||||
name: "Derivative",
|
||||
statement: "Derivatives measure rates of change",
|
||||
aliases: [],
|
||||
createdAt: "2026-08-01T00:00:00.000Z",
|
||||
provenance: [],
|
||||
});
|
||||
const seen: (string | undefined)[] = [];
|
||||
const phaseOf = () => extractionStatuses().find((r) => r.chatId === chatId)?.phase;
|
||||
const verdict: AuditResult = {
|
||||
propositions: [{ ...FINDINGS.propositions[0]!, linkTo: existingId }],
|
||||
edges: [],
|
||||
attribution: { quiz: {} },
|
||||
dropped: [{ ref: "p2", reason: "evidence not found" }],
|
||||
};
|
||||
const pipeline = createExtractionPipeline({
|
||||
chatStore,
|
||||
graphStore: ledgers,
|
||||
// Each stage's first generate is the observation point: the
|
||||
// registry must already carry this chat at the right phase.
|
||||
extractor: submitModel("submit_findings", FINDINGS, () => seen.push(phaseOf())),
|
||||
auditor: submitModel("submit_review", verdict, () => seen.push(phaseOf())),
|
||||
});
|
||||
await pipeline.runExtraction(chatId);
|
||||
assert.deepEqual(seen, ["extracting", "auditing"]);
|
||||
assert.equal(extractionStatuses().length, 0, "the registry clears when the run ends");
|
||||
});
|
||||
|
||||
it("is single-flight per chat — a concurrent second run is a no-op", async () => {
|
||||
const { chatId } = fixtureChat();
|
||||
const ledgers = memoryLedgers();
|
||||
|
||||
Reference in New Issue
Block a user