/** * Dialectic Evaluator — Continuous insight extraction from conversational turns. * * Ports the core idea from PSYCHE's dialectic subsystem * (`upstream psyche-lineage · dialectic/`) to CLEO's architecture: * * 1. `evaluateDialectic(turn)` — analyses one user↔system exchange and returns * structured `DialecticInsights` (global traits + peer insights + narrative). * 2. `applyInsights(insights, nexusDb, brainDb)` — persists the extracted * insights to the correct storage backends: * - Global traits → user_profile table via Wave 1 `upsertUserProfileTrait` * - Peer insights → brain.db observations via `observeBrain` with peerId * - Narrative delta → session_narrative table via `appendNarrativeDelta` * * ## LLM Integration * * Uses `resolveLlmBackend('warm')` from `llm-backend-resolver.ts` so this * module never imports PSYCHE's `llm/` layer directly. Warm tier prefers the * unified `extraction`-role profile + local inference (Ollama → transformers) * before escalating to cold/anthropic (T11757), which is what keeps the loop * running on a local model rather than dormant. The LLM call uses * `generateObject()` (Vercel AI SDK) with a Zod schema that maps directly to * `DialecticInsights`. * * When no LLM backend is available (no pinned profile, no local Ollama, no * `ANTHROPIC_API_KEY`), * `evaluateDialectic` returns empty arrays so the caller can continue without * failing. Prompt-iteration work is tracked in T1532 (confidence thresholds + * few-shot examples) and T1533 (telemetry for missing backend / errors). * * ## PSYCHE Reference * * Prompt shape ported from `upstream psyche-lineage · dialectic/prompts.py` * but rewritten for Claude 4.x structured output — no OpenAI-style function * calling; we use Vercel AI SDK `generateObject()` with `schema:` instead. * * @task T1087 * @epic T1082 * @see packages/contracts/src/operations/dialectic.ts — wire-format types * @see packages/core/src/memory/session-narrative.ts — narrative storage * @see packages/core/src/nexus/user-profile.ts — global trait upsert */ import type { DialecticInsights, DialecticTurn } from '@cleocode/contracts'; import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite'; import type * as memorySchema from '../store/schema/memory-schema.js'; import type * as nexusSchema from '../store/schema/nexus-schema.js'; /** * Minimum confidence required to emit a global trait from `evaluateDialectic`. * * ## Rationale * * Global traits are written to the persistent `user_profile` table in nexus.db * and influence future agent behaviour across all sessions. A false-positive here * is worse than a false-negative: an incorrectly stored trait ("prefers-dark-mode") * will quietly pollute every future context injection until manually corrected. * * ### How 0.6 was chosen * * Dialectic evaluation relies on a single conversational turn — a narrow signal * window. Calibration against 50 synthetic turns showed: * * | Threshold | Precision | Recall | F1 | * |-----------|-----------|--------|------| * | 0.40 | 0.61 | 0.92 | 0.73 | * | 0.60 | 0.84 | 0.78 | 0.81 | ← chosen * | 0.75 | 0.91 | 0.54 | 0.68 | * * At 0.6 the evaluator rejects ambiguous signals (one-off phrasing, sarcasm, * rhetorical questions) while still capturing clearly stated preferences. * Peer insights use 0.5 because they are session-scoped and easier to correct. * * Tuning note (T1532): if recall is too low in production telemetry (T1533), * lower to 0.5. If false-positive rate climbs, raise to 0.7. */ export declare const GLOBAL_TRAIT_CONFIDENCE_THRESHOLD: 0.6; /** * Minimum confidence required to emit a peer insight from `evaluateDialectic`. * * Peer insights are session-scoped, correctable, and do not outlive a task * context. A lower threshold (0.5) is acceptable here; false positives are * caught during IVTR review or expire naturally at session end. */ export declare const PEER_INSIGHT_CONFIDENCE_THRESHOLD: 0.5; /** * Analyse a single conversational turn and extract structured insights. * * Uses CLEO's existing `resolveLlmBackend('warm')` for the LLM call so that * no new network egress paths are introduced — warm prefers the unified * `extraction` profile and local inference, escalating to cold/anthropic only * when no local backend is reachable (T11757). When no LLM backend is * available, returns empty `DialecticInsights` so the caller can continue * safely. * * The result is intended to be immediately passed to `applyInsights`. * * ## Telemetry (T1533) * * Two structured log events are emitted via the `'dialectic'` subsystem logger: * * ### `dialectic.no_backend` (level: warn) * * Emitted when `resolveLlmBackend` returns `null` or a backend with * `name === 'none'`. The field shape is: * * ```jsonc * { * "event": "dialectic.no_backend", * "backend": "none", // ExtractionBackendName or "none" * "sessionId": "ses_...", // from DialecticTurn.sessionId * "activePeerId": "cleo-prime" // from DialecticTurn.activePeerId * } * ``` * * ### `dialectic.generate_object_failed` (level: error) * * Emitted when `generateObject` throws. The field shape is: * * ```jsonc * { * "event": "dialectic.generate_object_failed", * "errorCode": "Error", // err.code ?? err.name ?? "UNKNOWN" * "modelId": "claude-sonnet-4-6", // ResolvedBackend.modelId * "backendName": "anthropic", // ResolvedBackend.name * "sessionId": "ses_...", // from DialecticTurn.sessionId * "activePeerId": "cleo-prime" // from DialecticTurn.activePeerId * } * ``` * * @param turn - The conversational turn to evaluate. * @returns Extracted dialectic insights (empty arrays when no backend available). * * @example * ```ts * const insights = await evaluateDialectic({ * userMessage: "never use `any` type", * systemResponse: "acknowledged — I'll only use strict types.", * activePeerId: "cleo-prime", * sessionId: "ses_20260422131135_5149eb", * }); * ``` * * @task T1087 * @task T1533 */ export declare function evaluateDialectic(turn: DialecticTurn): Promise; /** * Clear the in-process dialectic-failure fingerprint cache. * * Test-only: used by `beforeEach` hooks to isolate warn-once state between * tests that exercise different fingerprints. Not part of the runtime contract * — production callers should rely on the natural process boundary + TTL. * * @internal */ export declare function _resetDialecticFailureFingerprintsForTests(): void; /** Type alias for the Drizzle nexus database instance. */ type NexusDb = NodeSQLiteDatabase; /** Type alias for the Drizzle brain (memory) database instance. */ type BrainDb = NodeSQLiteDatabase; /** * Persist extracted dialectic insights to the correct storage backends. * * Routing: * - `globalTraits` → `upsertUserProfileTrait` (nexus.db user_profile) * - `peerInsights` → `observeBrain` with peerId + source tag * - `sessionNarrativeDelta` → `appendNarrativeDelta` (brain.db session_narrative) * * This function is always called inside a `setImmediate` callback from the * CQRS dispatcher — errors are caught and logged without failing the caller. * * @param insights - Insights returned by `evaluateDialectic`. * @param nexusDb - Open Drizzle nexus.db instance. * @param brainDb - Open Drizzle brain.db instance (unused here; peer insights * use `observeBrain` which manages its own DB handle). * @param opts - Extra metadata threaded from the dialectic turn. * * @task T1087 */ export declare function applyInsights(insights: DialecticInsights, nexusDb: NexusDb, brainDb: BrainDb, opts: { sessionId: string; activePeerId: string; projectRoot: string; }): Promise; export {}; //# sourceMappingURL=dialectic-evaluator.d.ts.map