/** * `reflection-extractor` — try to lift a self-reflection out of the * assistant text for free (no LLM required). * * The V7 spec defines a reflection as "the agent's own explanation of * why it made this decision". Hosts sometimes emit this inline: * - An OpenClaw assistant block containing `### Reasoning:` or * `I chose this because …`. * - A Hermes `` tag (legacy). * - A Chinese-language agent producing "我这样做是因为…" or "思考过程:". * * We recognise a handful of high-precision patterns and return the cleaned * snippet. Never throws, never invokes an LLM. * * If the step already has `rawReflection` set (from adapter-provided meta), * that wins unchanged. */ import type { NormalizedStep } from "./types.js"; const INLINE_PATTERNS: RegExp[] = [ // Markdown heading-style reasoning blocks. /^###?\s*(reasoning|rationale|why|思考(?:过程|过程如下)?|我的理由)[::]?\s*\n([\s\S]+?)(?=\n(?:###?\s|$))/im, // "..." legacy tags. /\s*([\s\S]+?)\s*<\/reflection>/i, // English inline phrase "Reflection: ..." / "Reasoning: ..." /\b(reflection|reasoning|rationale)\s*[::]\s*([\s\S]{20,})/i, // Chinese phrases. /(我(?:这么|这样)做的?(?:原因|理由)[是::]?)\s*([\s\S]{10,})/m, /(思考(?:过程|过程如下))\s*[::]?\s*([\s\S]{10,})/m, ]; /** * Extract a reflection from the step. Prefers the adapter-provided value; * falls back to parsing `agentText`. Returns `null` when no signal found. */ export function extractReflection(step: NormalizedStep): string | null { if (step.rawReflection && step.rawReflection.trim().length > 0) { return step.rawReflection.trim(); } const text = step.agentText ?? ""; if (text.length === 0) return null; for (const pat of INLINE_PATTERNS) { const m = pat.exec(text); if (m) { // The actual body is the last capturing group. const body = (m[m.length - 1] ?? "").trim(); if (body.length >= 10) { // Cap so a misbehaving pattern can't swallow the whole message. return body.slice(0, 1_500); } } } return null; }