@inflexa-ai/harness 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/report-builder.d.ts +2 -0
- package/dist/agents/report-builder.js +2 -0
- package/dist/agents/report-session-agent.d.ts +56 -0
- package/dist/agents/report-session-agent.js +72 -0
- package/dist/app/report-session-runtime.d.ts +72 -0
- package/dist/app/report-session-runtime.js +157 -0
- package/dist/app/spawn-report-session.d.ts +85 -0
- package/dist/app/spawn-report-session.js +83 -0
- package/dist/contracts/chat-events.d.ts +9 -0
- package/dist/contracts/index.d.ts +6 -2
- package/dist/contracts/index.js +3 -1
- package/dist/contracts/organ-system.d.ts +49 -0
- package/dist/contracts/organ-system.js +67 -0
- package/dist/contracts/report-blocks.d.ts +564 -0
- package/dist/contracts/report-blocks.js +117 -0
- package/dist/contracts/report-reference.d.ts +459 -0
- package/dist/contracts/report-reference.js +256 -0
- package/dist/contracts/schemas/chat-events.d.ts +2 -0
- package/dist/contracts/schemas/chat-events.js +2 -0
- package/dist/contracts/severity.d.ts +15 -0
- package/dist/contracts/severity.js +10 -0
- package/dist/contracts/target-dossier.d.ts +8675 -12972
- package/dist/contracts/target-dossier.js +534 -591
- package/dist/data/safety-panel-schema.d.ts +18 -5
- package/dist/data/safety-panel-schema.js +4 -4
- package/dist/execution/artifact-metadata.js +1 -0
- package/dist/execution/report-runner.js +1 -3
- package/dist/execution/run-synthesis.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +19 -0
- package/dist/lib/async-utils.d.ts +16 -4
- package/dist/lib/async-utils.js +20 -15
- package/dist/lib/host-concurrency.js +1 -0
- package/dist/loop/__fixtures__/scripted-provider.js +23 -2
- package/dist/loop/run-agent.js +58 -10
- package/dist/loop/tool-detail.d.ts +40 -0
- package/dist/loop/tool-detail.js +35 -3
- package/dist/loop/types.d.ts +13 -0
- package/dist/memory/thread-history.d.ts +10 -0
- package/dist/memory/thread-history.js +12 -1
- package/dist/memory/thread-store.d.ts +4 -4
- package/dist/memory/thread-store.js +18 -9
- package/dist/prompts/report-session.d.ts +1 -0
- package/dist/prompts/report-session.js +89 -0
- package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
- package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
- package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
- package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
- package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
- package/dist/prompts/target-assessment/investigation/index.js +3 -0
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
- package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
- package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
- package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
- package/dist/providers/__fixtures__/session.d.ts +5 -4
- package/dist/providers/__fixtures__/session.js +4 -4
- package/dist/providers/ai-sdk.d.ts +77 -0
- package/dist/providers/ai-sdk.js +149 -19
- package/dist/providers/errors.d.ts +14 -0
- package/dist/providers/errors.js +103 -18
- package/dist/providers/types.d.ts +16 -0
- package/dist/providers/types.js +10 -1
- package/dist/report-model/block-walk.d.ts +56 -0
- package/dist/report-model/block-walk.js +89 -0
- package/dist/report-model/draft-finish.d.ts +60 -0
- package/dist/report-model/draft-finish.js +56 -0
- package/dist/report-model/draft-operations.d.ts +125 -0
- package/dist/report-model/draft-operations.js +341 -0
- package/dist/report-model/draft-read.d.ts +59 -0
- package/dist/report-model/draft-read.js +109 -0
- package/dist/report-model/draft.d.ts +51 -0
- package/dist/report-model/draft.js +32 -0
- package/dist/report-model/fixture-resolver.d.ts +8 -0
- package/dist/report-model/fixture-resolver.js +252 -0
- package/dist/report-model/pin-snapshot.d.ts +30 -0
- package/dist/report-model/pin-snapshot.js +40 -0
- package/dist/report-model/reference-resolver.d.ts +113 -0
- package/dist/report-model/reference-resolver.js +60 -0
- package/dist/report-model/structural-validation.d.ts +22 -0
- package/dist/report-model/structural-validation.js +68 -0
- package/dist/report-model/validate.d.ts +72 -0
- package/dist/report-model/validate.js +95 -0
- package/dist/report-render/chart.d.ts +33 -0
- package/dist/report-render/chart.js +538 -0
- package/dist/report-render/page.d.ts +127 -0
- package/dist/report-render/page.js +307 -0
- package/dist/report-render/references.d.ts +32 -0
- package/dist/report-render/references.js +44 -0
- package/dist/report-render/render.d.ts +25 -0
- package/dist/report-render/render.js +125 -0
- package/dist/report-render/script-json.d.ts +13 -0
- package/dist/report-render/script-json.js +15 -0
- package/dist/report-render/types.d.ts +39 -0
- package/dist/report-render/types.js +10 -0
- package/dist/report-render/value-bridge.d.ts +62 -0
- package/dist/report-render/value-bridge.js +82 -0
- package/dist/report-render/views/chart-view.d.ts +11 -0
- package/dist/report-render/views/chart-view.js +23 -0
- package/dist/report-render/views/page-view.d.ts +20 -0
- package/dist/report-render/views/page-view.js +49 -0
- package/dist/report-render/views/prose.d.ts +28 -0
- package/dist/report-render/views/prose.js +75 -0
- package/dist/report-render/views/references-view.d.ts +20 -0
- package/dist/report-render/views/references-view.js +64 -0
- package/dist/report-render/views/values.d.ts +42 -0
- package/dist/report-render/views/values.js +43 -0
- package/dist/runtime/assemble.d.ts +15 -0
- package/dist/runtime/assemble.js +28 -6
- package/dist/runtime/boot.d.ts +3 -3
- package/dist/runtime/boot.js +15 -4
- package/dist/runtime/dbos.d.ts +4 -4
- package/dist/state/artifacts.d.ts +16 -0
- package/dist/state/artifacts.js +18 -0
- package/dist/state/init.js +54 -0
- package/dist/state/purge-analysis.js +2 -0
- package/dist/state/report-session-state.d.ts +112 -0
- package/dist/state/report-session-state.js +150 -0
- package/dist/state/report-versions.d.ts +129 -0
- package/dist/state/report-versions.js +167 -0
- package/dist/state/snapshot-parse.d.ts +34 -0
- package/dist/state/snapshot-parse.js +82 -0
- package/dist/tasks/data-profile.js +1 -0
- package/dist/tools/ad-hoc-router.d.ts +7 -2
- package/dist/tools/ad-hoc-router.js +2 -1
- package/dist/tools/bio/chembl.js +1 -0
- package/dist/tools/bio/comptox.js +1 -0
- package/dist/tools/bio/drug-gene-interactions.js +1 -0
- package/dist/tools/bio/gene-disease-evidence.js +1 -0
- package/dist/tools/bio/gene-preclinical-profile.js +1 -0
- package/dist/tools/bio/keys.d.ts +3 -2
- package/dist/tools/bio/keys.js +3 -2
- package/dist/tools/bio/lookup-annotation.js +1 -0
- package/dist/tools/bio/opentargets.d.ts +3 -3
- package/dist/tools/bio/opentargets.js +4 -3
- package/dist/tools/bio/pubchem.js +1 -0
- package/dist/tools/bio/search-clinical-trials.js +1 -0
- package/dist/tools/bio/search-faers.js +1 -0
- package/dist/tools/bio/search-geo-datasets.js +1 -0
- package/dist/tools/bio/search-interactions.js +1 -0
- package/dist/tools/bio/target-safety.d.ts +4 -6
- package/dist/tools/bio/target-safety.js +7 -6
- package/dist/tools/define-tool.d.ts +19 -12
- package/dist/tools/define-tool.js +4 -2
- package/dist/tools/display/show-user.js +1 -0
- package/dist/tools/iterate-report.js +2 -0
- package/dist/tools/lib/chembl-client.d.ts +2 -0
- package/dist/tools/lib/chembl-client.js +1 -0
- package/dist/tools/lib/monarch-client.d.ts +42 -0
- package/dist/tools/lib/monarch-client.js +100 -0
- package/dist/tools/report/build-report.js +1 -0
- package/dist/tools/report/mint-preview-url.js +1 -0
- package/dist/tools/report/preview-snapshot.js +1 -0
- package/dist/tools/report/submit-report.js +1 -0
- package/dist/tools/report/version-fs.js +4 -0
- package/dist/tools/report-authoring/authoring-tools.d.ts +241 -0
- package/dist/tools/report-authoring/authoring-tools.js +460 -0
- package/dist/tools/report-session/index.d.ts +8 -0
- package/dist/tools/report-session/index.js +8 -0
- package/dist/tools/report-session/preview-report.d.ts +90 -0
- package/dist/tools/report-session/preview-report.js +245 -0
- package/dist/tools/research/context7-docs.js +2 -0
- package/dist/tools/research/generate-analogy-report.js +1 -0
- package/dist/tools/research/generate-plan.js +7 -2
- package/dist/tools/research/inspect-data-profile.js +4 -0
- package/dist/tools/research/literature-reviewer.js +1 -0
- package/dist/tools/research/search-arxiv.js +1 -0
- package/dist/tools/research/search-github-repos.js +1 -0
- package/dist/tools/research/search-semantic-scholar.js +1 -0
- package/dist/tools/sandbox/list-available-packages.js +23 -0
- package/dist/tools/sandbox/list-available-refs.js +36 -0
- package/dist/tools/sandbox/report-blocker.js +1 -0
- package/dist/tools/sandbox/skills.js +2 -0
- package/dist/tools/workspace/file-stat.js +1 -0
- package/dist/tools/workspace/show-file.js +2 -0
- package/dist/tools/workspace/show-plan.js +1 -0
- package/dist/workflows/execute-analysis.d.ts +5 -1
- package/dist/workflows/execute-analysis.js +5 -5
- package/dist/workflows/execute-target-assessment.d.ts +13 -2
- package/dist/workflows/execute-target-assessment.js +78 -16
- package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
- package/dist/workflows/target-assessment/assemblers/index.js +1 -1
- package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
- package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
- package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
- package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
- package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
- package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
- package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
- package/dist/workflows/target-assessment/collectors/index.js +48 -5
- package/dist/workflows/target-assessment/coverage.d.ts +43 -27
- package/dist/workflows/target-assessment/coverage.js +33 -29
- package/dist/workflows/target-assessment/fanout/index.js +1 -0
- package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
- package/dist/workflows/target-assessment/investigation/index.js +419 -0
- package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
- package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
- package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
- package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
- package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
- package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
- package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
- package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
- package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
- package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
- package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
- package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
- package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
- package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
- package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
- package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
- package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
- package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
- package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
- package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
- package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
- package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
- package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
- package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
- package/dist/workflows/target-assessment/metrics.d.ts +1 -6
- package/dist/workflows/target-assessment/metrics.js +1 -13
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +2147 -807
- package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
- package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
- package/dist/workflows/target-assessment/phase5-persist.js +56 -173
- package/dist/workflows/target-assessment/schemas.d.ts +46 -1
- package/dist/workflows/target-assessment/schemas.js +19 -0
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +26 -62
- package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +27 -154
- package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
- package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
- package/dist/workflows/target-assessment/synthesis/index.js +4 -1
- package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
- package/package.json +4 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The value markup: the metric card, the data table, the figure, and the inline citation.
|
|
3
|
+
*
|
|
4
|
+
* The caller resolves each value, and it narrows the value to the shape that the block needs. The runtime
|
|
5
|
+
* escapes every interpolated string, thus a hostile label, a hostile cell, a hostile caption, or a
|
|
6
|
+
* hostile source reaches the page as text.
|
|
7
|
+
*/
|
|
8
|
+
import type { CitationBlock, FigureBlock, MetricBlock, TableBlock } from "../../contracts/report-blocks.js";
|
|
9
|
+
import type { ReferenceLedger } from "../references.js";
|
|
10
|
+
import type { RenderValue } from "../types.js";
|
|
11
|
+
/** The scalar value that a metric renders from. */
|
|
12
|
+
type ScalarValue = Extract<RenderValue, {
|
|
13
|
+
type: "scalar";
|
|
14
|
+
}>;
|
|
15
|
+
/** The table value that a table renders from. */
|
|
16
|
+
type TableValue = Extract<RenderValue, {
|
|
17
|
+
type: "table";
|
|
18
|
+
}>;
|
|
19
|
+
/** The figure value that a figure renders from. */
|
|
20
|
+
type FigureValue = Extract<RenderValue, {
|
|
21
|
+
type: "figure";
|
|
22
|
+
}>;
|
|
23
|
+
/** Render a metric block as a stat card with the label and the scalar value. */
|
|
24
|
+
export declare function renderMetric(block: MetricBlock, value: ScalarValue): string;
|
|
25
|
+
/**
|
|
26
|
+
* Render a table block. The header holds one cell for each column, and the body holds one row for each
|
|
27
|
+
* resolved row. A zero-row table renders the header alone. The title and the caption render only when the
|
|
28
|
+
* block carries one. An absent cell renders as an empty cell, thus a ragged row keeps its shape.
|
|
29
|
+
*/
|
|
30
|
+
export declare function renderTable(block: TableBlock, value: TableValue): string;
|
|
31
|
+
/**
|
|
32
|
+
* Render a figure block. The source rides the `src` attribute, and the caption renders below the image.
|
|
33
|
+
* The caption also fills the `alt` attribute, thus the escape keeps a hostile source and a hostile caption
|
|
34
|
+
* inside their slots.
|
|
35
|
+
*/
|
|
36
|
+
export declare function renderFigure(block: FigureBlock, value: FigureValue): string;
|
|
37
|
+
/**
|
|
38
|
+
* Render a citation block as a short line. The binding joins the ledger like a claim binding, thus one
|
|
39
|
+
* shared source keeps one number. The optional note renders after the marker.
|
|
40
|
+
*/
|
|
41
|
+
export declare function renderCitation(block: CitationBlock, ledger: ReferenceLedger): string;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
|
|
2
|
+
import { Marker } from "./references-view.js";
|
|
3
|
+
/** Render a metric block as a stat card with the label and the scalar value. */
|
|
4
|
+
export function renderMetric(block, value) {
|
|
5
|
+
return String(_jsxs("div", { class: "corner-accents border border-slate-200 bg-white p-6", children: [_jsx("div", { class: "font-mono text-3xl font-bold", style: "color: #576dea;", children: value.value }), _jsx("div", { class: "font-mono text-xs font-medium text-slate-500 mt-2 uppercase tracking-wider", children: block.label })] }));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The column order of a table. The value columns win when they are present. The keys of the first row
|
|
9
|
+
* give the order otherwise, in first-appearance order.
|
|
10
|
+
*/
|
|
11
|
+
function tableColumns(value) {
|
|
12
|
+
if (value.columns !== undefined) {
|
|
13
|
+
return value.columns;
|
|
14
|
+
}
|
|
15
|
+
const first = value.rows[0];
|
|
16
|
+
return first !== undefined ? Object.keys(first) : [];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Render a table block. The header holds one cell for each column, and the body holds one row for each
|
|
20
|
+
* resolved row. A zero-row table renders the header alone. The title and the caption render only when the
|
|
21
|
+
* block carries one. An absent cell renders as an empty cell, thus a ragged row keeps its shape.
|
|
22
|
+
*/
|
|
23
|
+
export function renderTable(block, value) {
|
|
24
|
+
const columns = tableColumns(value);
|
|
25
|
+
return String(_jsxs("div", { class: "report-table mb-8", children: [block.title !== undefined ? (_jsx("div", { class: "font-mono text-xs font-semibold uppercase tracking-widest text-primary-500 mb-3", children: block.title })) : null, _jsx("div", { class: "corner-accents border border-slate-200 bg-white overflow-hidden", children: _jsx("div", { class: "overflow-x-auto", children: _jsxs("table", { class: "w-full text-sm", children: [_jsx("thead", { children: _jsx("tr", { class: "border-b border-slate-200 bg-slate-50", children: columns.map((column) => (_jsx("th", { class: "px-4 py-3 text-left font-mono text-[11px] font-semibold text-slate-500 uppercase tracking-wider", children: column }))) }) }), _jsx("tbody", { children: value.rows.map((row) => (_jsx("tr", { class: "report-row border-b border-slate-100 last:border-b-0", children: columns.map((column) => (_jsx("td", { class: "px-4 py-2.5 text-slate-700", children: row[column] }))) }))) })] }) }) }), block.caption !== undefined ? _jsx("p", { class: "text-sm text-slate-400 mt-2", children: block.caption }) : null] }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Render a figure block. The source rides the `src` attribute, and the caption renders below the image.
|
|
29
|
+
* The caption also fills the `alt` attribute, thus the escape keeps a hostile source and a hostile caption
|
|
30
|
+
* inside their slots.
|
|
31
|
+
*/
|
|
32
|
+
export function renderFigure(block, value) {
|
|
33
|
+
const caption = block.caption;
|
|
34
|
+
return String(_jsxs("figure", { class: "report-figure mb-8", children: [_jsx("img", { src: value.src, alt: caption !== undefined ? caption : "", class: "max-w-full h-auto border border-slate-200" }), caption !== undefined ? _jsx("figcaption", { class: "text-sm text-slate-400 mt-2", children: caption }) : null] }));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Render a citation block as a short line. The binding joins the ledger like a claim binding, thus one
|
|
38
|
+
* shared source keeps one number. The optional note renders after the marker.
|
|
39
|
+
*/
|
|
40
|
+
export function renderCitation(block, ledger) {
|
|
41
|
+
const n = ledger.mark(block.binding);
|
|
42
|
+
return String(_jsxs("p", { class: "report-citation text-sm text-slate-500", children: [_jsx(Marker, { n: n }), block.note !== undefined ? (_jsxs(_Fragment, { children: [" ", _jsx("span", { class: "text-slate-600", children: block.note })] })) : null] }));
|
|
43
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type Result } from "neverthrow";
|
|
18
18
|
import { type ConversationAgentDeps } from "../agents/conversation-agent.js";
|
|
19
|
+
import { type ReportSessionRuntime } from "../app/report-session-runtime.js";
|
|
19
20
|
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
20
21
|
import type { ResourcePolicy } from "../config/resource-limits.js";
|
|
21
22
|
import type { AgentDefinition } from "../loop/types.js";
|
|
@@ -26,6 +27,7 @@ import { type ExecuteTargetAssessmentDeps, type ExecuteTargetAssessmentInput, ty
|
|
|
26
27
|
import { type DataProfileDeps, type DataProfileWorkflowInput } from "../tasks/data-profile.js";
|
|
27
28
|
import { type CitationResolverConfig } from "../citations/resolve.js";
|
|
28
29
|
import type { CitationResolver } from "../citations/types.js";
|
|
30
|
+
import type { ReferenceResolver } from "../report-model/reference-resolver.js";
|
|
29
31
|
/** Registered child sandbox-step callable the parent's child dispatch closes over. */
|
|
30
32
|
export type SandboxStepCallable = (input: SandboxStepInput) => Promise<SandboxStepResult>;
|
|
31
33
|
/**
|
|
@@ -78,6 +80,12 @@ export interface CoreRuntimeDeps {
|
|
|
78
80
|
readonly usageRecorder?: UsageRecorder;
|
|
79
81
|
/** Harness-owned citation capability configuration; no ambient lookup occurs. */
|
|
80
82
|
readonly citationResolverConfig?: CitationResolverConfig;
|
|
83
|
+
/**
|
|
84
|
+
* Reference-resolution seam for the report preview. Absent by default, thus the
|
|
85
|
+
* preview tool degrades as data until an embedder wires a realization. The
|
|
86
|
+
* realization itself stays out of the harness.
|
|
87
|
+
*/
|
|
88
|
+
readonly reportReferenceResolver?: ReferenceResolver;
|
|
81
89
|
}
|
|
82
90
|
/**
|
|
83
91
|
* The refusal a thread type carries no agent yet. `report` is a valid
|
|
@@ -120,6 +128,13 @@ export interface CoreRuntime {
|
|
|
120
128
|
* on the `Result` channel.
|
|
121
129
|
*/
|
|
122
130
|
readonly agents: ThreadAgentResolver;
|
|
131
|
+
/**
|
|
132
|
+
* The turn-start anchor of a report session. A serving path runs it at the
|
|
133
|
+
* turn start, thus a report tool call never arrives before the session state
|
|
134
|
+
* exists. The gateway stays behind the tool boundary, thus the runtime exposes
|
|
135
|
+
* the anchor operation alone here, and a later change wires it at the turn start.
|
|
136
|
+
*/
|
|
137
|
+
readonly reportSession: Pick<ReportSessionRuntime, "ensureSessionState">;
|
|
123
138
|
readonly workflows: RegisteredWorkflows;
|
|
124
139
|
readonly citationResolver: CitationResolver;
|
|
125
140
|
}
|
package/dist/runtime/assemble.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { err, ok } from "neverthrow";
|
|
18
18
|
import { createConversationAgent } from "../agents/conversation-agent.js";
|
|
19
|
+
import { createReportSessionAgent } from "../agents/report-session-agent.js";
|
|
20
|
+
import { createReportSessionRuntime } from "../app/report-session-runtime.js";
|
|
19
21
|
import { createNoopUsageRecorder } from "../billing/noop-usage-recorder.js";
|
|
20
22
|
import { registerExecuteAnalysis } from "../workflows/execute-analysis.js";
|
|
21
23
|
import { registerSandboxStep } from "../workflows/sandbox-step.js";
|
|
@@ -67,17 +69,37 @@ export function assembleCoreRuntime(deps) {
|
|
|
67
69
|
usageRecorder,
|
|
68
70
|
citationResolver,
|
|
69
71
|
});
|
|
70
|
-
// The
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
72
|
+
// The report agent is a singleton over the conversation deps. The session
|
|
73
|
+
// runtime binds the per-session state to the thread behind the tool boundary,
|
|
74
|
+
// thus one definition serves every report thread. Its preview tool writes the
|
|
75
|
+
// page into the analysis tree and returns the path, thus the page write reaches
|
|
76
|
+
// no host seam. The optional reference resolver is the one seam it can use, and
|
|
77
|
+
// it is absent by default.
|
|
78
|
+
const reportSession = createReportSessionRuntime({ pool: conversation.pool, ...(conversation.logger ? { logger: conversation.logger } : {}) });
|
|
79
|
+
const reportAgent = createReportSessionAgent({
|
|
80
|
+
model: conversation.model,
|
|
81
|
+
pool: conversation.pool,
|
|
82
|
+
embedding: conversation.embedding,
|
|
83
|
+
workspaceFs: conversation.workspaceFs,
|
|
84
|
+
gateway: reportSession.gateway,
|
|
85
|
+
resolveWorkspaceRoot: conversation.resolveWorkspaceRoot,
|
|
86
|
+
...(deps.reportReferenceResolver ? { resolver: deps.reportReferenceResolver } : {}),
|
|
87
|
+
...(conversation.logger ? { logger: conversation.logger } : {}),
|
|
88
|
+
});
|
|
89
|
+
// The single registration point for every typed agent. A future thread type's
|
|
90
|
+
// agent plugs in here at assembly with no embedder change. `conversation` is
|
|
91
|
+
// required at the type level because boot resolves it unconditionally
|
|
92
|
+
// (`boot.ts` backfill). `report` is required now that its agent registers here,
|
|
93
|
+
// so a dropped registration fails tsc, and it does not surface only as a
|
|
94
|
+
// resolution refusal. The `Partial` over the rest keeps the compiler honest
|
|
95
|
+
// about a type that has no agent yet.
|
|
76
96
|
const agents = {
|
|
77
97
|
conversation: conversationAgent,
|
|
98
|
+
report: reportAgent,
|
|
78
99
|
};
|
|
79
100
|
return {
|
|
80
101
|
agents: createThreadAgentResolver(agents),
|
|
102
|
+
reportSession: { ensureSessionState: reportSession.ensureSessionState },
|
|
81
103
|
workflows: {
|
|
82
104
|
executeAnalysis,
|
|
83
105
|
sandboxStep,
|
package/dist/runtime/boot.d.ts
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* to a no-op so a library host never acquires process-wide telemetry (or a
|
|
12
12
|
* console banner) it did not ask for. An embedder that wants harness OTel
|
|
13
13
|
* passes `initOtel` / `shutdownOtel` here.
|
|
14
|
-
* 2. `validateAgentSkills(skillsDir,
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* 2. `validateAgentSkills(skillsDir, ...)` — pure fs stat, zero external deps.
|
|
15
|
+
* A `meta.skills` typo or a `skillsDir` / image drift dies in milliseconds,
|
|
16
|
+
* before any Postgres or DBOS cost is paid (agent-skill-assignment).
|
|
17
17
|
* 3. `initCortexState(pool)` — app tables must exist before launch; recovery
|
|
18
18
|
* queries them on the first step.
|
|
19
19
|
* 4. `assertConnectionBudget(...)` — needs the live pool; gates launch so a
|
package/dist/runtime/boot.js
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* to a no-op so a library host never acquires process-wide telemetry (or a
|
|
12
12
|
* console banner) it did not ask for. An embedder that wants harness OTel
|
|
13
13
|
* passes `initOtel` / `shutdownOtel` here.
|
|
14
|
-
* 2. `validateAgentSkills(skillsDir,
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* 2. `validateAgentSkills(skillsDir, ...)` — pure fs stat, zero external deps.
|
|
15
|
+
* A `meta.skills` typo or a `skillsDir` / image drift dies in milliseconds,
|
|
16
|
+
* before any Postgres or DBOS cost is paid (agent-skill-assignment).
|
|
17
17
|
* 3. `initCortexState(pool)` — app tables must exist before launch; recovery
|
|
18
18
|
* queries them on the first step.
|
|
19
19
|
* 4. `assertConnectionBudget(...)` — needs the live pool; gates launch so a
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
40
40
|
import { SANDBOX_AGENT_META } from "../agents/sandbox/index.js";
|
|
41
|
+
import { REPORT_BUILDER_AGENT_ID, REPORT_BUILDER_SKILLS } from "../agents/report-builder.js";
|
|
41
42
|
import { validateAgentSkills } from "../agents/sandbox/validate-skills.js";
|
|
42
43
|
import { initCortexState } from "../state/init.js";
|
|
43
44
|
import { backfillConversationDisplayEnvelopes } from "../memory/conversation-display-backfill.js";
|
|
@@ -48,10 +49,20 @@ import { markDraining } from "./lifecycle.js";
|
|
|
48
49
|
import { runShutdownSequence } from "./shutdown.js";
|
|
49
50
|
const noop = () => { };
|
|
50
51
|
const noopAsync = () => Promise.resolve();
|
|
52
|
+
/**
|
|
53
|
+
* Every agent whose declared packs must resolve before the first run. The
|
|
54
|
+
* report-builder is not in the sandbox catalog — it is not plannable — but it
|
|
55
|
+
* declares a pack all the same, and a missing one there surfaces as a failed
|
|
56
|
+
* render partway through a report rather than at boot.
|
|
57
|
+
*/
|
|
58
|
+
const SKILL_DECLARING_AGENTS = {
|
|
59
|
+
...SANDBOX_AGENT_META,
|
|
60
|
+
[REPORT_BUILDER_AGENT_ID]: { id: REPORT_BUILDER_AGENT_ID, skills: REPORT_BUILDER_SKILLS },
|
|
61
|
+
};
|
|
51
62
|
export async function bootHarness(deps) {
|
|
52
63
|
const { core, pool, skillsDir, logger } = deps;
|
|
53
64
|
(deps.initTelemetry ?? noop)();
|
|
54
|
-
validateAgentSkills(skillsDir,
|
|
65
|
+
validateAgentSkills(skillsDir, SKILL_DECLARING_AGENTS);
|
|
55
66
|
await initCortexState(pool);
|
|
56
67
|
await assertConnectionBudget({ pool, logger, config: deps.connectionBudget });
|
|
57
68
|
const runtime = assembleCoreRuntime(core);
|
package/dist/runtime/dbos.d.ts
CHANGED
|
@@ -38,7 +38,8 @@ export interface DbosConfig {
|
|
|
38
38
|
*/
|
|
39
39
|
readonly logLevel?: string;
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
/** DBOS lifecycle facts a host's readiness probe reports on. */
|
|
42
|
+
export interface DbosState {
|
|
42
43
|
launched: boolean;
|
|
43
44
|
recoveryStarted: boolean;
|
|
44
45
|
}
|
|
@@ -85,9 +86,8 @@ export declare function sweepEphemeralWorkflows({ pool, logger: injectedLogger,
|
|
|
85
86
|
executorId: string;
|
|
86
87
|
}): Promise<void>;
|
|
87
88
|
/** Snapshot of DBOS lifecycle state — read by the readiness probe. */
|
|
88
|
-
export declare function dbosState():
|
|
89
|
+
export declare function dbosState(): DbosState;
|
|
89
90
|
/** Test hook: force-reset state without calling DBOS. Test-only. */
|
|
90
91
|
export declare function __resetDbosStateForTest(): void;
|
|
91
92
|
/** Test hook: mark launched without calling DBOS. Test-only. */
|
|
92
|
-
export declare function __setDbosStateForTest(next: Partial<
|
|
93
|
-
export {};
|
|
93
|
+
export declare function __setDbosStateForTest(next: Partial<DbosState>): void;
|
|
@@ -57,6 +57,22 @@ export interface StepArtifactRef {
|
|
|
57
57
|
* slice of it — and so the same call replays to the same rows.
|
|
58
58
|
*/
|
|
59
59
|
export declare function queryStepArtifactPaths(pool: Querier, resourceId: string, runId: string, stepId: string, limit: number): Promise<StepArtifactRef[]>;
|
|
60
|
+
/** One registered artifact of an analysis, as a reader of the full artifact set names it. */
|
|
61
|
+
export interface AnalysisArtifactRef {
|
|
62
|
+
/** Workspace-root-relative path (`runs/{runId}/{stepId}/output/de.csv`). */
|
|
63
|
+
path: string;
|
|
64
|
+
hash: string;
|
|
65
|
+
fileType: string | null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Every artifact of one analysis, ordered by path so the same call gives the same rows.
|
|
69
|
+
*
|
|
70
|
+
* The query applies no filter. An input row is a member, the same as a step output. A row that carries
|
|
71
|
+
* `unrecoverable_at` is a member too, because the artifact existed at the moment of this read. If the
|
|
72
|
+
* query drops such a row, a later refusal says that the artifact never existed, and that reason is
|
|
73
|
+
* false.
|
|
74
|
+
*/
|
|
75
|
+
export declare function queryAnalysisArtifacts(pool: Querier, analysisId: string): Promise<AnalysisArtifactRef[]>;
|
|
60
76
|
/** Count step-output artifacts produced by a run — for the run-completed card. */
|
|
61
77
|
export declare function countArtifactsForRun(pool: Querier, analysisId: string, runId: string): Promise<number>;
|
|
62
78
|
export declare function updateArtifactId(pool: Querier, resourceId: string, path: string, artifactId: string, fileType?: string | null): Promise<void>;
|
package/dist/state/artifacts.js
CHANGED
|
@@ -148,6 +148,24 @@ export async function queryStepArtifactPaths(pool, resourceId, runId, stepId, li
|
|
|
148
148
|
});
|
|
149
149
|
return result.rows.map((r) => ({ path: r.path, fileType: r.file_type ?? null }));
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Every artifact of one analysis, ordered by path so the same call gives the same rows.
|
|
153
|
+
*
|
|
154
|
+
* The query applies no filter. An input row is a member, the same as a step output. A row that carries
|
|
155
|
+
* `unrecoverable_at` is a member too, because the artifact existed at the moment of this read. If the
|
|
156
|
+
* query drops such a row, a later refusal says that the artifact never existed, and that reason is
|
|
157
|
+
* false.
|
|
158
|
+
*/
|
|
159
|
+
export async function queryAnalysisArtifacts(pool, analysisId) {
|
|
160
|
+
const result = await pool.query({
|
|
161
|
+
text: `SELECT path, hash, file_type
|
|
162
|
+
FROM cortex_artifacts
|
|
163
|
+
WHERE analysis_id = $1
|
|
164
|
+
ORDER BY path`,
|
|
165
|
+
values: [analysisId],
|
|
166
|
+
});
|
|
167
|
+
return result.rows.map((r) => ({ path: r.path, hash: r.hash, fileType: r.file_type ?? null }));
|
|
168
|
+
}
|
|
151
169
|
/** Count step-output artifacts produced by a run — for the run-completed card. */
|
|
152
170
|
export async function countArtifactsForRun(pool, analysisId, runId) {
|
|
153
171
|
const result = await pool.query({
|
package/dist/state/init.js
CHANGED
|
@@ -130,6 +130,60 @@ CREATE TABLE IF NOT EXISTS cortex_plans (
|
|
|
130
130
|
CREATE INDEX IF NOT EXISTS idx_cortex_plans_analysis
|
|
131
131
|
ON cortex_plans(analysis_id, created_at DESC);
|
|
132
132
|
|
|
133
|
+
-- Report versions — one immutable row for each recorded report version. A row
|
|
134
|
+
-- holds the block document, the pinned snapshot, and the anchor. A thread holds
|
|
135
|
+
-- at most one version. The named unique constraint on thread_id enforces the
|
|
136
|
+
-- rule, because the report policy binds one version to each report session. The
|
|
137
|
+
-- row outlives its thread. Thus the table has no foreign key to
|
|
138
|
+
-- cortex_analysis_threads, and the row carries the anchor columns (thread_id,
|
|
139
|
+
-- parent_thread_id, parent_seq) directly. A purge of the analysis removes the row
|
|
140
|
+
-- through the analysis_id cascade. The table is append-only, and a correction is
|
|
141
|
+
-- a new version, never an update to a recorded row.
|
|
142
|
+
CREATE TABLE IF NOT EXISTS cortex_report_versions (
|
|
143
|
+
version_id TEXT PRIMARY KEY,
|
|
144
|
+
analysis_id TEXT NOT NULL
|
|
145
|
+
REFERENCES cortex_analysis_state(analysis_id)
|
|
146
|
+
ON DELETE CASCADE,
|
|
147
|
+
thread_id TEXT NOT NULL,
|
|
148
|
+
-- Nullable pair: a root conversation anchor can be absent. A version with no
|
|
149
|
+
-- parent thread carries no parent_seq either.
|
|
150
|
+
parent_thread_id TEXT,
|
|
151
|
+
parent_seq BIGINT,
|
|
152
|
+
parent_version_id TEXT
|
|
153
|
+
REFERENCES cortex_report_versions(version_id)
|
|
154
|
+
ON DELETE SET NULL,
|
|
155
|
+
document JSONB NOT NULL,
|
|
156
|
+
snapshot JSONB NOT NULL,
|
|
157
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
158
|
+
CONSTRAINT cortex_report_versions_one_per_thread UNIQUE (thread_id)
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_report_versions_analysis
|
|
162
|
+
ON cortex_report_versions(analysis_id);
|
|
163
|
+
|
|
164
|
+
-- Report session state — one mutable row for each report session thread. The row
|
|
165
|
+
-- holds the in-progress draft document and the pinned snapshot of the session. The
|
|
166
|
+
-- thread id is the primary key, thus one session binds to one thread. The document
|
|
167
|
+
-- column and the snapshot column are each nullable, because the pin writes the
|
|
168
|
+
-- snapshot first and the document lands later. A purge of the analysis removes the
|
|
169
|
+
-- row through the analysis_id cascade. The explicit purge delete stays a backstop,
|
|
170
|
+
-- the same as the plans delete, because a database whose table predates the key
|
|
171
|
+
-- never acquires one. The thread id carries no foreign key, the same as the
|
|
172
|
+
-- messages rows, thus a purge of a thread cannot reach this row through a cascade
|
|
173
|
+
-- and purgeThread removes it by name.
|
|
174
|
+
CREATE TABLE IF NOT EXISTS cortex_report_session_state (
|
|
175
|
+
thread_id TEXT PRIMARY KEY,
|
|
176
|
+
analysis_id TEXT NOT NULL
|
|
177
|
+
REFERENCES cortex_analysis_state(analysis_id)
|
|
178
|
+
ON DELETE CASCADE,
|
|
179
|
+
document JSONB,
|
|
180
|
+
snapshot JSONB,
|
|
181
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_report_session_state_analysis
|
|
185
|
+
ON cortex_report_session_state(analysis_id);
|
|
186
|
+
|
|
133
187
|
-- Target assessments — organization-scoped, snapshot-style target dossiers.
|
|
134
188
|
-- Independent of analyses, projects, runs, and chat threads. The full
|
|
135
189
|
-- dossier is persisted as JSONB on the row, no separate artifacts table.
|
|
@@ -80,6 +80,8 @@ const ANALYSIS_KEYED_DELETES = [
|
|
|
80
80
|
{ op: "purgeAnalysis.asks", sql: "DELETE FROM cortex_asks WHERE analysis_id = $1" },
|
|
81
81
|
{ op: "purgeAnalysis.askGrants", sql: "DELETE FROM cortex_ask_grants WHERE analysis_id = $1" },
|
|
82
82
|
{ op: "purgeAnalysis.plans", sql: "DELETE FROM cortex_plans WHERE analysis_id = $1" },
|
|
83
|
+
{ op: "purgeAnalysis.reportVersions", sql: "DELETE FROM cortex_report_versions WHERE analysis_id = $1" },
|
|
84
|
+
{ op: "purgeAnalysis.reportSessionState", sql: "DELETE FROM cortex_report_session_state WHERE analysis_id = $1" },
|
|
83
85
|
{ op: "purgeAnalysis.analysisState", sql: "DELETE FROM cortex_analysis_state WHERE analysis_id = $1" },
|
|
84
86
|
];
|
|
85
87
|
/**
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The report session-state store -- the mutable working state of one report session.
|
|
3
|
+
*
|
|
4
|
+
* One row holds the state of one report session, keyed by the thread id. The row
|
|
5
|
+
* holds the in-progress draft document and the pinned snapshot. The row is mutable,
|
|
6
|
+
* and it differs from a recorded version this way. A session reads the state, writes
|
|
7
|
+
* the snapshot one time at the start, and persists the document as the composition
|
|
8
|
+
* proceeds.
|
|
9
|
+
*
|
|
10
|
+
* The snapshot writes at row creation. The write is insert-if-absent through
|
|
11
|
+
* ON CONFLICT DO NOTHING. Thus two concurrent first calls make one row, and the
|
|
12
|
+
* winner keeps its snapshot. The store reads the row back after the write, thus the
|
|
13
|
+
* caller sees the durable state whichever call won.
|
|
14
|
+
*
|
|
15
|
+
* The document column and the snapshot column are each nullable. A row exists with a
|
|
16
|
+
* snapshot and no document yet, because the pin writes the snapshot first and the
|
|
17
|
+
* document lands later. A null column reads as a null value.
|
|
18
|
+
*
|
|
19
|
+
* A read parses the stored document with the draft schema, and the stored snapshot
|
|
20
|
+
* with `parseSnapshot`. The document is the draft document, because an in-progress
|
|
21
|
+
* document can be incomplete. A column that fails a parse reads as a typed error, and
|
|
22
|
+
* it does not crash. An absent row reads as a normal absence.
|
|
23
|
+
*
|
|
24
|
+
* A persist is a compare-and-swap. The load reads the prior document, and the persist
|
|
25
|
+
* lands only when the row still holds it. Thus two concurrent turns cannot both land,
|
|
26
|
+
* and the loser reads the state again.
|
|
27
|
+
*
|
|
28
|
+
* A purge of the analysis removes the row through the analysis-id cascade.
|
|
29
|
+
*/
|
|
30
|
+
import { type ResultAsync } from "neverthrow";
|
|
31
|
+
import type { Pool } from "pg";
|
|
32
|
+
import { type DbError } from "../lib/db-result.js";
|
|
33
|
+
import type { Logger } from "../lib/logger.js";
|
|
34
|
+
import { type DraftDocument } from "../report-model/draft.js";
|
|
35
|
+
import type { ReportSnapshot } from "../report-model/reference-resolver.js";
|
|
36
|
+
import { type SchemaIssue } from "./snapshot-parse.js";
|
|
37
|
+
/** The state of one report session, as the store gives it back. */
|
|
38
|
+
export interface ReportSessionState {
|
|
39
|
+
readonly threadId: string;
|
|
40
|
+
readonly analysisId: string;
|
|
41
|
+
/** The in-progress draft document, or `null` before the first document lands. */
|
|
42
|
+
readonly document: DraftDocument | null;
|
|
43
|
+
/** The pinned snapshot, or `null` before the pin writes it. */
|
|
44
|
+
readonly snapshot: ReportSnapshot | null;
|
|
45
|
+
readonly createdAt: Date;
|
|
46
|
+
}
|
|
47
|
+
/** The snapshot and the anchor of a row to create. */
|
|
48
|
+
export interface WriteSnapshotInput {
|
|
49
|
+
readonly threadId: string;
|
|
50
|
+
readonly analysisId: string;
|
|
51
|
+
/** The pinned snapshot, stored as given. */
|
|
52
|
+
readonly snapshot: ReportSnapshot;
|
|
53
|
+
}
|
|
54
|
+
/** The document to persist against an existing row, and the prior document that the load read. */
|
|
55
|
+
export interface PersistDocumentInput {
|
|
56
|
+
readonly threadId: string;
|
|
57
|
+
/** The draft document, stored as given. */
|
|
58
|
+
readonly document: DraftDocument;
|
|
59
|
+
/**
|
|
60
|
+
* The prior document that the load read, or `null` before the first document lands.
|
|
61
|
+
* The persist lands only when the row still holds it, thus a concurrent turn that
|
|
62
|
+
* landed first turns this persist into a conflict.
|
|
63
|
+
*/
|
|
64
|
+
readonly expected: DraftDocument | null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The outcome of a persist. `persisted` landed the document. `conflict` means the row
|
|
68
|
+
* no longer holds the expected prior document, thus a concurrent turn landed first and
|
|
69
|
+
* this turn must read the state again. `absent` means no row holds the thread.
|
|
70
|
+
*/
|
|
71
|
+
export type PersistOutcome = "persisted" | "conflict" | "absent";
|
|
72
|
+
/**
|
|
73
|
+
* A stored row that a read cannot parse with the current schemas. It names the
|
|
74
|
+
* thread id and the part that failed, and it carries the reduced issues.
|
|
75
|
+
*/
|
|
76
|
+
export type SessionStateReadError = {
|
|
77
|
+
readonly type: "corrupt_session_state";
|
|
78
|
+
readonly op: string;
|
|
79
|
+
readonly threadId: string;
|
|
80
|
+
readonly part: "document" | "snapshot";
|
|
81
|
+
readonly issues: readonly SchemaIssue[];
|
|
82
|
+
};
|
|
83
|
+
export interface ReportSessionStateStore {
|
|
84
|
+
/**
|
|
85
|
+
* The state of a thread, or `null` when no row holds it. A parse failure on a
|
|
86
|
+
* stored column reads as a typed error, and an absent row reads as a normal
|
|
87
|
+
* absence.
|
|
88
|
+
*/
|
|
89
|
+
readState(threadId: string): ResultAsync<ReportSessionState | null, DbError | SessionStateReadError>;
|
|
90
|
+
/**
|
|
91
|
+
* Write the snapshot at row creation, insert-if-absent. Two concurrent first
|
|
92
|
+
* calls make one row through ON CONFLICT DO NOTHING, and the winner keeps its
|
|
93
|
+
* snapshot. The store reads the row back, thus the caller sees the durable state.
|
|
94
|
+
*/
|
|
95
|
+
writeSnapshot(input: WriteSnapshotInput): ResultAsync<ReportSessionState, DbError | SessionStateReadError>;
|
|
96
|
+
/**
|
|
97
|
+
* Persist the document of a thread against the prior document that the load read.
|
|
98
|
+
* The row must exist, pinned by `writeSnapshot` first. The update lands only when
|
|
99
|
+
* the row still holds the prior document. `persisted` landed it, `conflict` means a
|
|
100
|
+
* concurrent turn landed first, and `absent` means no row holds the thread.
|
|
101
|
+
*/
|
|
102
|
+
persistDocument(input: PersistDocumentInput): ResultAsync<PersistOutcome, DbError>;
|
|
103
|
+
}
|
|
104
|
+
export interface ReportSessionStateStoreDeps {
|
|
105
|
+
readonly pool: Pool;
|
|
106
|
+
readonly logger?: Logger;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Create a `ReportSessionStateStore` bound to a Postgres pool. The
|
|
110
|
+
* `cortex_report_session_state` table is provisioned by the state-init DDL.
|
|
111
|
+
*/
|
|
112
|
+
export declare function createReportSessionStateStore({ pool }: ReportSessionStateStoreDeps): ReportSessionStateStore;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The report session-state store -- the mutable working state of one report session.
|
|
3
|
+
*
|
|
4
|
+
* One row holds the state of one report session, keyed by the thread id. The row
|
|
5
|
+
* holds the in-progress draft document and the pinned snapshot. The row is mutable,
|
|
6
|
+
* and it differs from a recorded version this way. A session reads the state, writes
|
|
7
|
+
* the snapshot one time at the start, and persists the document as the composition
|
|
8
|
+
* proceeds.
|
|
9
|
+
*
|
|
10
|
+
* The snapshot writes at row creation. The write is insert-if-absent through
|
|
11
|
+
* ON CONFLICT DO NOTHING. Thus two concurrent first calls make one row, and the
|
|
12
|
+
* winner keeps its snapshot. The store reads the row back after the write, thus the
|
|
13
|
+
* caller sees the durable state whichever call won.
|
|
14
|
+
*
|
|
15
|
+
* The document column and the snapshot column are each nullable. A row exists with a
|
|
16
|
+
* snapshot and no document yet, because the pin writes the snapshot first and the
|
|
17
|
+
* document lands later. A null column reads as a null value.
|
|
18
|
+
*
|
|
19
|
+
* A read parses the stored document with the draft schema, and the stored snapshot
|
|
20
|
+
* with `parseSnapshot`. The document is the draft document, because an in-progress
|
|
21
|
+
* document can be incomplete. A column that fails a parse reads as a typed error, and
|
|
22
|
+
* it does not crash. An absent row reads as a normal absence.
|
|
23
|
+
*
|
|
24
|
+
* A persist is a compare-and-swap. The load reads the prior document, and the persist
|
|
25
|
+
* lands only when the row still holds it. Thus two concurrent turns cannot both land,
|
|
26
|
+
* and the loser reads the state again.
|
|
27
|
+
*
|
|
28
|
+
* A purge of the analysis removes the row through the analysis-id cascade.
|
|
29
|
+
*/
|
|
30
|
+
import { err, ok } from "neverthrow";
|
|
31
|
+
import { tryMutation, tryQuery } from "../lib/db-result.js";
|
|
32
|
+
import { DraftDocumentSchema } from "../report-model/draft.js";
|
|
33
|
+
import { parseSnapshot, reduceIssues } from "./snapshot-parse.js";
|
|
34
|
+
const _assertReadError = true;
|
|
35
|
+
/** The full row projection every read shares. */
|
|
36
|
+
const STATE_COLUMNS = "thread_id, analysis_id, document, snapshot, created_at";
|
|
37
|
+
/**
|
|
38
|
+
* The insert. The row carries the snapshot at creation, and the document lands later
|
|
39
|
+
* through `persistDocument`. ON CONFLICT DO NOTHING keeps the first row, thus a
|
|
40
|
+
* second first call cannot replace the snapshot.
|
|
41
|
+
*/
|
|
42
|
+
const INSERT_SQL = `INSERT INTO cortex_report_session_state
|
|
43
|
+
(thread_id, analysis_id, snapshot)
|
|
44
|
+
VALUES ($1, $2, $3::jsonb)
|
|
45
|
+
ON CONFLICT (thread_id) DO NOTHING`;
|
|
46
|
+
function rowToState(row) {
|
|
47
|
+
let document = null;
|
|
48
|
+
if (row.document !== null) {
|
|
49
|
+
const parsed = DraftDocumentSchema.safeParse(row.document);
|
|
50
|
+
if (!parsed.success) {
|
|
51
|
+
return err({
|
|
52
|
+
type: "corrupt_session_state",
|
|
53
|
+
op: "report-session-state.read",
|
|
54
|
+
threadId: row.thread_id,
|
|
55
|
+
part: "document",
|
|
56
|
+
issues: reduceIssues(parsed.error),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
document = parsed.data;
|
|
60
|
+
}
|
|
61
|
+
let snapshot = null;
|
|
62
|
+
if (row.snapshot !== null) {
|
|
63
|
+
const parsed = parseSnapshot(row.snapshot);
|
|
64
|
+
if (parsed.isErr()) {
|
|
65
|
+
return err({
|
|
66
|
+
type: "corrupt_session_state",
|
|
67
|
+
op: "report-session-state.read",
|
|
68
|
+
threadId: row.thread_id,
|
|
69
|
+
part: "snapshot",
|
|
70
|
+
issues: parsed.error,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
snapshot = parsed.value;
|
|
74
|
+
}
|
|
75
|
+
return ok({
|
|
76
|
+
threadId: row.thread_id,
|
|
77
|
+
analysisId: row.analysis_id,
|
|
78
|
+
document,
|
|
79
|
+
snapshot,
|
|
80
|
+
createdAt: row.created_at,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create a `ReportSessionStateStore` bound to a Postgres pool. The
|
|
85
|
+
* `cortex_report_session_state` table is provisioned by the state-init DDL.
|
|
86
|
+
*/
|
|
87
|
+
export function createReportSessionStateStore({ pool }) {
|
|
88
|
+
function selectRow(op, threadId) {
|
|
89
|
+
return tryQuery(op, () => pool.query({
|
|
90
|
+
text: `SELECT ${STATE_COLUMNS} FROM cortex_report_session_state WHERE thread_id = $1`,
|
|
91
|
+
values: [threadId],
|
|
92
|
+
})).map(({ rows }) => rows[0] ?? null);
|
|
93
|
+
}
|
|
94
|
+
function readState(threadId) {
|
|
95
|
+
return selectRow("report-session-state.readState", threadId).andThen((row) => {
|
|
96
|
+
if (!row)
|
|
97
|
+
return ok(null);
|
|
98
|
+
return rowToState(row);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function writeSnapshot(input) {
|
|
102
|
+
return tryMutation("report-session-state.writeSnapshot", async () => {
|
|
103
|
+
await pool.query({
|
|
104
|
+
text: INSERT_SQL,
|
|
105
|
+
values: [input.threadId, input.analysisId, JSON.stringify(input.snapshot)],
|
|
106
|
+
});
|
|
107
|
+
})
|
|
108
|
+
.andThen(() => selectRow("report-session-state.writeSnapshot.readBack", input.threadId))
|
|
109
|
+
.andThen((row) => {
|
|
110
|
+
if (!row) {
|
|
111
|
+
// The insert-if-absent guarantees a row. Absence here is not a normal
|
|
112
|
+
// condition, thus it rides the error channel.
|
|
113
|
+
return err({
|
|
114
|
+
type: "mutation_failed",
|
|
115
|
+
op: "report-session-state.writeSnapshot.readBack",
|
|
116
|
+
cause: new Error("row absent after insert-if-absent"),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return rowToState(row);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function persistDocument(input) {
|
|
123
|
+
return tryMutation("report-session-state.persistDocument", async () => {
|
|
124
|
+
// The prior document rides as SQL NULL when absent, thus the compare-and-swap
|
|
125
|
+
// matches a fresh row whose document column is null. A JSON `null` would be a
|
|
126
|
+
// distinct jsonb value, and it would never match the null column.
|
|
127
|
+
const expected = input.expected === null ? null : JSON.stringify(input.expected);
|
|
128
|
+
// One statement tells a conflict from an absence. `existed` counts the row, and
|
|
129
|
+
// `updated` counts the compare-and-swap. A row that exists but did not update
|
|
130
|
+
// holds a different prior document, thus a concurrent turn landed first.
|
|
131
|
+
const { rows } = await pool.query({
|
|
132
|
+
text: `WITH target AS (
|
|
133
|
+
SELECT 1 FROM cortex_report_session_state WHERE thread_id = $1
|
|
134
|
+
), updated AS (
|
|
135
|
+
UPDATE cortex_report_session_state
|
|
136
|
+
SET document = $2::jsonb
|
|
137
|
+
WHERE thread_id = $1 AND document IS NOT DISTINCT FROM $3::jsonb
|
|
138
|
+
RETURNING 1
|
|
139
|
+
)
|
|
140
|
+
SELECT (SELECT COUNT(*) FROM target)::int AS existed, (SELECT COUNT(*) FROM updated)::int AS updated`,
|
|
141
|
+
values: [input.threadId, JSON.stringify(input.document), expected],
|
|
142
|
+
});
|
|
143
|
+
const row = rows[0];
|
|
144
|
+
if (row === undefined || row.existed === 0)
|
|
145
|
+
return "absent";
|
|
146
|
+
return row.updated > 0 ? "persisted" : "conflict";
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return { readState, writeSnapshot, persistDocument };
|
|
150
|
+
}
|