experiment: add passive behaviour selection

Implement Experiment 19: deterministic behaviour selector with five
behaviours (Acknowledge, Clarify, Summarise, Continue, Pause).

- lib/behaviour-selection/behaviour-selector.js — Pure function selector
  applying v0.1 rules in priority order (acknowledge > clarify > summarise >
  pause > continue). Defaults to Continue with low confidence when no rule
  matches or assessment is incomplete. Guards against partial objects.

- tests/behaviour-selector.test.js — 51 tests covering all five behaviours,
  priority ordering, contract conformance, determinism, edge cases, and
  scenario-based validation with mock investigations.

- docs/design-evolution-log.md — Close Experiment 18 (record what assessor
  enabled for Behaviour Selection), add Experiment 19 section with hypothesis,
  scope, evaluation criteria, and open questions.

Passive integration only: no changes to reasoning engine, prompts, graph
generation, decomposition, narrative generation, API contracts, UI behaviour,
or Ollama integration.
This commit is contained in:
2026-08-05 18:26:31 +01:00
parent 1273861f0c
commit 1df4669b32
4 changed files with 805 additions and 1 deletions
@@ -0,0 +1,174 @@
/**
* Behaviour Selection — Experiment 19 (Passive)
*
* A small deterministic selector that maps Investigation State Assessment
* output to one of five behaviours: Acknowledge, Clarify, Summarise, Continue,
* Pause.
*
* This experiment tests whether behaviour selection is useful. It does not
* change engine behaviour — it only observes and reports through Developer
* Details.
*
* Design reference: docs/behaviour-selection.md (v0.1 Implementation Brief)
*/
/* ── Behaviour constants ──────────────────────────────────── */
const BEHAVIOURS = [
"acknowledge",
"clarify",
"summarise",
"continue",
"pause",
];
const PRIORITIES = {
acknowledge: 1,
clarify: 2,
summarise: 3,
pause: 4,
continue: 5, // default
};
/* ── Selection rules (one rule per behaviour) ─────────────── */
function selectAcknowledge(assessment) {
if (assessment.conversationHealth.value === "healthy" && assessment.phase.confidence !== "low") {
return {
behaviour: "acknowledge",
confidence: "medium",
reason: "Healthy conversation with established context — user provided useful information that warrants acknowledgment before introducing new uncertainty."
};
}
return null;
}
function selectClarify(assessment) {
if (assessment.conversationHealth.value === "too_broad") {
return {
behaviour: "clarify",
confidence: "high",
reason: "Conversation health is too broad — investigation may be spreading too thin. Narrow focus through a specific clarification question."
};
}
if (assessment.phase.value === "orienting" && assessment.phase.evidence?.observationDensity < 3) {
return {
behaviour: "clarify",
confidence: "medium",
reason: "Investigation is in orienting phase with insufficient observations (< 3). A targeted clarification question will anchor the starting point."
};
}
return null;
}
function selectSummarise(assessment) {
if (assessment.phase.value === "synthesising") {
return {
behaviour: "summarise",
confidence: "high",
reason: "Investigation is in synthesising phase — connected observations have accumulated and a restatement of current understanding will compress without losing detail."
};
}
if (assessment.phase.value === "concluding") {
return {
behaviour: "summarise",
confidence: "high",
reason: "Investigation is concluding — a summary of resolved understanding provides closure anchor before the user decides next steps."
};
}
// Turn-count based summarisation (conservative threshold)
if (assessment.phase.evidence?.resolvedNodeCount >= 3 && assessment.progress.value === "steady") {
return {
behaviour: "summarise",
confidence: "medium",
reason: "Three or more items resolved with steady progress — enough accumulated understanding warrants a compression pass."
};
}
return null;
}
function selectPause(assessment) {
if (assessment.phase.value === "focusing" && assessment.progress.value === "stalled") {
return {
behaviour: "pause",
confidence: "high",
reason: "Focusing phase with stalled progress — the investigation has reached a single active unknown but momentum has stopped. Hold space rather than pushing for more."
};
}
if (assessment.conversationHealth.value === "user_overloaded") {
return {
behaviour: "pause",
confidence: "medium",
reason: "User appears overloaded — reduce pressure by acknowledging progress before inviting further contribution."
};
}
return null;
}
/* ── Main selector ─────────────────────────────────────────── */
/**
* Select a behaviour based on investigation state assessment.
*
* Applies five deterministic rules in priority order. If no rule fires,
* returns continue (the default).
*
* @param {Object} assessment — Investigation State Assessment from Exp 18
* @param {string} assessment.version — Assessment version
* @param {string} assessment.confidence — Overall confidence (high/medium/low)
* @param {Object} assessment.phase — Phase assessment { value, confidence, signals, evidence }
* @param {Object} assessment.progress — Progress assessment { value, confidence, signals, evidence }
* @param {Object} assessment.conversationHealth — Health assessment { value, confidence, signals, evidence }
* @returns {{ behaviour: string, confidence: string, reason: string }}
*/
export function selectBehaviour(assessment) {
if (!assessment) {
return {
behaviour: "continue",
confidence: "low",
reason: "No assessment available — defaulting to continue (ask next question).",
priority: PRIORITIES.continue
};
}
// Guard against partial assessment objects with missing sub-structures
if (!assessment.conversationHealth || !assessment.phase) {
return {
behaviour: "continue",
confidence: "low",
reason: "Assessment incomplete — missing required dimensions, defaulting to continue (ask next question).",
priority: PRIORITIES.continue
};
}
// Apply rules in priority order
let result = selectAcknowledge(assessment);
if (result) return { ...result, priority: PRIORITIES.acknowledge };
result = selectClarify(assessment);
if (result) return { ...result, priority: PRIORITIES.clarify };
result = selectSummarise(assessment);
if (result) return { ...result, priority: PRIORITIES.summarise };
result = selectPause(assessment);
if (result) return { ...result, priority: PRIORITIES.pause };
// Default — Continue
return {
behaviour: "continue",
confidence: "low",
reason: "No explicit rule matched — defaulting to continue (ask the next question).",
priority: PRIORITIES.continue
};
}
export const BEHAVIOUR_OPTIONS = BEHAVIOURS;
export default selectBehaviour;