checkpoint: preserve semantic decomposition investigation state
This commit is contained in:
+176
-11
@@ -34,11 +34,123 @@ import {
|
||||
validateGraphReferences,
|
||||
validateGraphUpdate,
|
||||
} from "./utils.js";
|
||||
import { getProvider } from "@/lib/llm/provider";
|
||||
|
||||
function cloneJsonSafe(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function buildSemanticDecompositionPrompt({ parentNode, graph }) {
|
||||
return `You are proposing narrower child unknowns for one unresolved compound parent unknown.
|
||||
|
||||
Return exactly one JSON object. Return JSON only.
|
||||
|
||||
This is NOT a graph update task.
|
||||
Do NOT mutate graph state.
|
||||
Do NOT select a child.
|
||||
Do NOT rank children.
|
||||
Do NOT resolve the parent.
|
||||
Do NOT create conclusions, actions, recommendations, or unrelated graph content.
|
||||
|
||||
Required top-level fields:
|
||||
- parentNodeId
|
||||
- proposedChildren
|
||||
|
||||
Field rules:
|
||||
- parentNodeId must exactly equal the supplied parent node id.
|
||||
- proposedChildren must be an array of 0-5 objects.
|
||||
- each child object must contain only: label, description.
|
||||
- each child must preserve the parent's meaning while making it narrower and directly answerable.
|
||||
- each child must express only one uncertainty.
|
||||
|
||||
Parent unknown:
|
||||
- id: ${parentNode.id}
|
||||
- label: ${parentNode.label}
|
||||
- description: ${parentNode.description || ""}
|
||||
|
||||
Consider breaking into dimensions like: customer type, team capability, responsibility distribution, pricing structure, time factors — whatever creates the dependency.
|
||||
|
||||
Each candidate child should:
|
||||
- be a genuine uncertainty (not an action or conclusion)
|
||||
- be narrow enough to address in one analytical step
|
||||
- focus on one specific dimension
|
||||
- stay grounded in the parent's core meaning`;
|
||||
}
|
||||
|
||||
function validateSemanticDecompositionResult(result, parentNodeId) {
|
||||
const errors = [];
|
||||
if (!result || typeof result !== "object") {
|
||||
return ["Semantic decomposition result must be an object."];
|
||||
}
|
||||
if (result.parentNodeId !== parentNodeId) {
|
||||
errors.push(
|
||||
"Semantic decomposition result parentNodeId did not match the requested parent node id.",
|
||||
);
|
||||
}
|
||||
if (!Array.isArray(result.proposedChildren)) {
|
||||
errors.push(
|
||||
"Semantic decomposition result must include a proposedChildren array.",
|
||||
);
|
||||
return errors;
|
||||
}
|
||||
if (result.proposedChildren.length > 5) {
|
||||
errors.push("Semantic decomposition result proposed too many children.");
|
||||
}
|
||||
for (const child of result.proposedChildren) {
|
||||
if (!child || typeof child !== "object") {
|
||||
errors.push("Each proposed child must be an object.");
|
||||
continue;
|
||||
}
|
||||
const keys = Object.keys(child);
|
||||
if (!keys.includes("label") || !keys.includes("description")) {
|
||||
errors.push("Each proposed child must include label and description.");
|
||||
}
|
||||
const unexpectedKeys = keys.filter(
|
||||
(key) => !["label", "description"].includes(key),
|
||||
);
|
||||
if (unexpectedKeys.length > 0) {
|
||||
errors.push(
|
||||
`Proposed child contained unexpected fields: ${unexpectedKeys.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (typeof child.label !== "string" || child.label.trim().length === 0) {
|
||||
errors.push("Each proposed child label must be a non-empty string.");
|
||||
}
|
||||
if (
|
||||
typeof child.description !== "string" ||
|
||||
child.description.trim().length === 0
|
||||
) {
|
||||
errors.push("Each proposed child description must be a non-empty string.");
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
async function proposeSemanticDecompositionChildren({
|
||||
parentNode,
|
||||
graph,
|
||||
provider,
|
||||
modelName,
|
||||
}) {
|
||||
const llmProvider = provider ?? getProvider();
|
||||
const raw = await llmProvider.generateReconstruction(
|
||||
buildSemanticDecompositionPrompt({ parentNode, graph }),
|
||||
modelName ?? process.env.OLLAMA_MODEL,
|
||||
);
|
||||
const validationErrors = validateSemanticDecompositionResult(raw, parentNode.id);
|
||||
if (validationErrors.length > 0) {
|
||||
return { success: false, proposedChildren: [], errors: validationErrors };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
proposedChildren: raw.proposedChildren.map((child) => ({
|
||||
label: child.label.trim(),
|
||||
description: child.description.trim(),
|
||||
})),
|
||||
errors: [],
|
||||
};
|
||||
}
|
||||
|
||||
function zodIssuesToErrors(error) {
|
||||
return (
|
||||
error?.issues?.map((issue) => {
|
||||
@@ -1820,8 +1932,32 @@ function buildDecompositionTemplates(parentNode, graph, depth = 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
function buildCompositeUnknownChildren(parentNode, graph, depth = 0) {
|
||||
const templates = buildDecompositionTemplates(parentNode, graph, depth);
|
||||
async function buildCompositeUnknownChildren(
|
||||
parentNode,
|
||||
graph,
|
||||
depth = 0,
|
||||
activeReasoningPattern,
|
||||
options = {},
|
||||
) {
|
||||
let templates = buildDecompositionTemplates(parentNode, graph, depth);
|
||||
|
||||
let usedSemanticFallback = false;
|
||||
let semanticFallbackErrors = [];
|
||||
|
||||
if (templates.length === 0 && options.allowSemanticFallback) {
|
||||
const semanticFallback = await proposeSemanticDecompositionChildren({
|
||||
parentNode,
|
||||
graph,
|
||||
provider: options.provider,
|
||||
modelName: options.modelName,
|
||||
});
|
||||
if (semanticFallback.success) {
|
||||
templates = semanticFallback.proposedChildren;
|
||||
usedSemanticFallback = true;
|
||||
} else {
|
||||
semanticFallbackErrors = semanticFallback.errors;
|
||||
}
|
||||
}
|
||||
|
||||
if (templates.length === 0) {
|
||||
return {
|
||||
@@ -1833,6 +1969,8 @@ function buildCompositeUnknownChildren(parentNode, graph, depth = 0) {
|
||||
acceptedChildCount: 0,
|
||||
rejectedChildren: [],
|
||||
childQualitySummary: [],
|
||||
usedSemanticFallback,
|
||||
semanticFallbackErrors,
|
||||
reason:
|
||||
"Decomposition stopped because no meaning-preserving child family was justified for this parent.",
|
||||
};
|
||||
@@ -1936,6 +2074,8 @@ function buildCompositeUnknownChildren(parentNode, graph, depth = 0) {
|
||||
acceptedChildCount,
|
||||
rejectedChildren,
|
||||
childQualitySummary,
|
||||
usedSemanticFallback,
|
||||
semanticFallbackErrors,
|
||||
reason:
|
||||
acceptedChildCount === 0
|
||||
? "Decomposition stopped because all proposed children failed quality checks."
|
||||
@@ -1952,6 +2092,8 @@ function buildCompositeUnknownChildren(parentNode, graph, depth = 0) {
|
||||
acceptedChildCount,
|
||||
rejectedChildren,
|
||||
childQualitySummary,
|
||||
usedSemanticFallback,
|
||||
semanticFallbackErrors,
|
||||
reason:
|
||||
childNodes.length > 0
|
||||
? "Decomposed a composite unknown into smaller broad candidate dimensions before asking the next question."
|
||||
@@ -2768,10 +2910,12 @@ function buildSelectedQuestionResult({
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAmbiguousGraphBackedSelection({
|
||||
async function resolveAmbiguousGraphBackedSelection({
|
||||
graphSnapshot,
|
||||
updatedSituationGraph,
|
||||
deterministicSelection,
|
||||
provider = null,
|
||||
modelName = null,
|
||||
}) {
|
||||
const orderedCandidateIds =
|
||||
deterministicSelection?.displayOrder ||
|
||||
@@ -2785,7 +2929,7 @@ function resolveAmbiguousGraphBackedSelection({
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidateResult = runDeterministicDecomposition({
|
||||
const candidateResult = await runDeterministicDecomposition({
|
||||
graphSnapshot,
|
||||
proposalSnapshot: {
|
||||
addedNodes: [],
|
||||
@@ -2804,6 +2948,8 @@ function resolveAmbiguousGraphBackedSelection({
|
||||
reason:
|
||||
"Selected this tied candidate for deterministic decomposition-based reselection.",
|
||||
},
|
||||
provider,
|
||||
modelName,
|
||||
});
|
||||
|
||||
if (!candidateResult.success) {
|
||||
@@ -2874,7 +3020,11 @@ function reseatSelectionAfterQuestionRejection({
|
||||
const QUESTION_FORMULATION_REJECTION_NO_QUESTION_REASON =
|
||||
"The selected investigation target remains active, but its current graph-backed question formulation was rejected as too complex.";
|
||||
|
||||
export function determineGraphBackedQuestion({ situationGraph }) {
|
||||
export async function determineGraphBackedQuestion({
|
||||
situationGraph,
|
||||
provider = null,
|
||||
modelName = null,
|
||||
}) {
|
||||
const graphSnapshot = cloneJsonSafe(situationGraph);
|
||||
let updatedSituationGraph = cloneJsonSafe(situationGraph);
|
||||
let deterministicSelection = selectActiveUnknownCandidate(
|
||||
@@ -2882,7 +3032,7 @@ export function determineGraphBackedQuestion({ situationGraph }) {
|
||||
updatedSituationGraph.resolvedNodeIds || [],
|
||||
);
|
||||
|
||||
const decompositionResult = runDeterministicDecomposition({
|
||||
const decompositionResult = await runDeterministicDecomposition({
|
||||
graphSnapshot,
|
||||
proposalSnapshot: {
|
||||
addedNodes: [],
|
||||
@@ -2896,6 +3046,8 @@ export function determineGraphBackedQuestion({ situationGraph }) {
|
||||
updatedSituationGraph,
|
||||
reasoningResolution: { reasoningStateOverride: {} },
|
||||
deterministicSelection,
|
||||
provider,
|
||||
modelName,
|
||||
});
|
||||
|
||||
if (!decompositionResult.success) {
|
||||
@@ -2952,10 +3104,12 @@ export function determineGraphBackedQuestion({ situationGraph }) {
|
||||
deterministicSelection?.status === "ambiguous" &&
|
||||
!questionResult.selectedQuestion?.question
|
||||
) {
|
||||
const reselectionResult = resolveAmbiguousGraphBackedSelection({
|
||||
const reselectionResult = await resolveAmbiguousGraphBackedSelection({
|
||||
graphSnapshot,
|
||||
updatedSituationGraph,
|
||||
deterministicSelection,
|
||||
provider,
|
||||
modelName,
|
||||
});
|
||||
|
||||
if (reselectionResult) {
|
||||
@@ -3015,13 +3169,15 @@ export function determineGraphBackedQuestion({ situationGraph }) {
|
||||
};
|
||||
}
|
||||
|
||||
function runDeterministicDecomposition({
|
||||
async function runDeterministicDecomposition({
|
||||
graphSnapshot,
|
||||
proposalSnapshot,
|
||||
updatedSituationGraph,
|
||||
reasoningResolution,
|
||||
deterministicSelection,
|
||||
structurallyAdmittedNodeIds = new Set(),
|
||||
provider = null,
|
||||
modelName = null,
|
||||
}) {
|
||||
let workingGraph = updatedSituationGraph;
|
||||
let workingSelection = deterministicSelection;
|
||||
@@ -3189,11 +3345,16 @@ function runDeterministicDecomposition({
|
||||
}
|
||||
|
||||
decompositionAttempted = true;
|
||||
const decomposition = buildCompositeUnknownChildren(
|
||||
const decomposition = await buildCompositeUnknownChildren(
|
||||
selectedNode,
|
||||
workingGraph,
|
||||
decompositionDepth,
|
||||
activeReasoningPattern,
|
||||
{
|
||||
allowSemanticFallback: Boolean(provider),
|
||||
provider,
|
||||
modelName,
|
||||
},
|
||||
);
|
||||
|
||||
proposedChildCount = decomposition.proposedChildCount;
|
||||
@@ -3805,11 +3966,13 @@ function deriveReasoningStateOverride({
|
||||
};
|
||||
}
|
||||
|
||||
export function applyValidatedProposal({
|
||||
export async function applyValidatedProposal({
|
||||
situationGraph,
|
||||
proposal,
|
||||
previousQuestion = null,
|
||||
answer = null,
|
||||
provider = null,
|
||||
modelName = null,
|
||||
}) {
|
||||
const graphValidation = situationGraphSchema.safeParse(situationGraph);
|
||||
const proposalValidation = graphUpdateSchema.safeParse(proposal);
|
||||
@@ -4093,13 +4256,15 @@ export function applyValidatedProposal({
|
||||
updatedSituationGraph.resolvedNodeIds,
|
||||
);
|
||||
|
||||
const decompositionResult = runDeterministicDecomposition({
|
||||
const decompositionResult = await runDeterministicDecomposition({
|
||||
graphSnapshot,
|
||||
proposalSnapshot,
|
||||
updatedSituationGraph,
|
||||
reasoningResolution,
|
||||
deterministicSelection,
|
||||
structurallyAdmittedNodeIds,
|
||||
provider,
|
||||
modelName,
|
||||
});
|
||||
|
||||
if (!decompositionResult.success) {
|
||||
|
||||
@@ -343,7 +343,7 @@ function buildUpdateDiagnostics({
|
||||
};
|
||||
}
|
||||
|
||||
export async function startCase(body) {
|
||||
export async function startCase(body, dependencies = {}) {
|
||||
const parsedRequest = startCaseRequestSchema.safeParse(body);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
@@ -399,8 +399,12 @@ export async function startCase(body) {
|
||||
const graphReferenceValidation = validateGraphReferences(
|
||||
initialSituationGraph,
|
||||
);
|
||||
const initialQuestionResult = determineGraphBackedQuestion({
|
||||
const provider = dependencies.provider ?? getProvider();
|
||||
const modelName = dependencies.modelName ?? analysis?.modelName ?? null;
|
||||
const initialQuestionResult = await determineGraphBackedQuestion({
|
||||
situationGraph: initialSituationGraph,
|
||||
provider,
|
||||
modelName,
|
||||
});
|
||||
const situationGraph = initialQuestionResult.success
|
||||
? initialQuestionResult.updatedSituationGraph
|
||||
|
||||
Reference in New Issue
Block a user