{"version":3,"file":"nudges.d.ts","sourceRoot":"","sources":["../../../../src/extensions/core/prompt-reactive/nudges.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EAGX,YAAY,EAMZ,MAAM,mCAAmC,CAAC;AAe3C,MAAM,WAAW,2BAA2B;IAC3C;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACrC;AA+DD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,GAAE,2BAAgC,GAAG,IAAI,CAoE5G","sourcesContent":["/**\n * Runtime \"plugin reuse nudge\" — the reactive half of the plugin-productivity\n * guidance.\n *\n * The static `promptGuidelines` on the plugin tools are injected into the system\n * prompt once at session build and never re-surface when the work actually shows\n * a reusable-facing cue. This extension closes that gap: it watches tool output\n * and turn text for the cues in {@link REUSE_NUDGES}, and when one appears it\n * attaches a matching, plugin-facing note to the *next* turn's context — an\n * ephemeral message merged into the outgoing request via the `context` hook, so\n * nothing is written into persisted history.\n *\n * Policy (see ./policy.ts for the cue table — the single source of truth):\n *   - Cues are read from `before_agent_start` (user prompt), `tool_execution_start`\n *     (tool args, e.g. content being written) and `tool_execution_end` (tool\n *     output, e.g. content being read).\n *   - First-hit arming: a matching cue arms its nudge immediately.\n *   - One note per turn, and at most once per category per session — so a cue\n *     that keeps appearing never turns into a nag.\n *   - The whole thing is gated on the autonomous-plugin-system flag\n *     (`enablePluginTools`, default off) and never blocks normal flow.\n *\n * Wired once from hoo-core (the single default composition root); the guard below\n * makes double-registration a no-op for downstreams that compose extensions\n * differently, and the static import keeps it bundled in the compiled binary.\n */\n\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { TextContent } from \"@kolisachint/hoocode-ai\";\nimport type {\n\tBeforeAgentStartEvent,\n\tContextEvent,\n\tExtensionAPI,\n\tExtensionContext,\n\tSessionStartEvent,\n\tToolExecutionEndEvent,\n\tToolExecutionStartEvent,\n\tTurnStartEvent,\n} from \"../../../core/extensions/types.js\";\nimport {\n\tarmReuseNudge,\n\tclearArmedReuseNudges,\n\tisAutonomousPluginSystemEnabled,\n\tmatchReuseNudges,\n\ttype ReuseNudge,\n} from \"./policy.js\";\n\n/** Guards against double-registration when default extensions load more than once. */\nconst REGISTERED = Symbol.for(\"hoocode.promptReactiveNudges.registered\");\n\n/** Cap on how much tool text we scan per event — cues are short, files can be huge. */\nconst SCAN_CAP = 20_000;\n\nexport interface PromptReactiveNudgesOptions {\n\t/**\n\t * Enablement gate. Defaults to the shared autonomous-plugin-system flag so the\n\t * reactive nudge and the plugin tool surface flip together. Injectable for tests.\n\t */\n\tisEnabled?: (cwd: string) => boolean;\n}\n\n/**\n * Pull scannable text out of an arbitrary tool result or args object. Prefers\n * `content[].text` blocks (the tool-result convention) and falls back to a\n * bounded JSON stringify so cues in nested fields are still caught.\n */\nfunction extractText(value: unknown): string {\n\tif (value == null) return \"\";\n\tif (typeof value === \"string\") return value.slice(0, SCAN_CAP);\n\tif (typeof value === \"object\") {\n\t\tconst obj = value as { content?: unknown };\n\t\tif (Array.isArray(obj.content)) {\n\t\t\tconst text = obj.content\n\t\t\t\t.map((b) =>\n\t\t\t\t\tb && typeof b === \"object\" && typeof (b as { text?: unknown }).text === \"string\"\n\t\t\t\t\t\t? (b as { text: string }).text\n\t\t\t\t\t\t: \"\",\n\t\t\t\t)\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (text) return text.slice(0, SCAN_CAP);\n\t\t}\n\t\ttry {\n\t\t\treturn JSON.stringify(value).slice(0, SCAN_CAP);\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t}\n\treturn String(value).slice(0, SCAN_CAP);\n}\n\n/**\n * Merge an ephemeral reuse note into the outgoing messages. Appends it as a text\n * block on the final user turn (valid alongside tool-result blocks) so the model\n * sees it right before it responds; falls back to a fresh user message when the\n * last message isn't a user turn. The input array is never mutated.\n */\nfunction injectNote(messages: AgentMessage[], note: string): AgentMessage[] {\n\tconst block: TextContent = { type: \"text\", text: note };\n\tconst out = messages.slice();\n\tconst last = out[out.length - 1] as { role?: string; content?: unknown } | undefined;\n\tif (last && last.role === \"user\") {\n\t\tconst content = last.content;\n\t\tconst merged =\n\t\t\ttypeof content === \"string\"\n\t\t\t\t? [{ type: \"text\", text: content } as TextContent, block]\n\t\t\t\t: Array.isArray(content)\n\t\t\t\t\t? [...content, block]\n\t\t\t\t\t: null;\n\t\tif (merged) {\n\t\t\tout[out.length - 1] = { ...(last as object), content: merged } as AgentMessage;\n\t\t\treturn out;\n\t\t}\n\t}\n\tout.push({ role: \"user\", content: [block], timestamp: Date.now() } as AgentMessage);\n\treturn out;\n}\n\n/** Wrap a nudge snippet so it reads as a system aside rather than user text. */\nfunction formatNote(nudge: ReuseNudge): string {\n\treturn `[reuse-nudge] ${nudge.snippet}`;\n}\n\n/**\n * Install the runtime reuse-nudge extension. Idempotent — a second call on the\n * same `hoo` is a no-op, so composing default extensions twice is harmless.\n */\nexport function setupPromptReactiveNudges(hoo: ExtensionAPI, options: PromptReactiveNudgesOptions = {}): void {\n\tconst guarded = hoo as unknown as Record<symbol, boolean>;\n\tif (guarded[REGISTERED]) return;\n\tguarded[REGISTERED] = true;\n\n\tconst isEnabled = options.isEnabled ?? isAutonomousPluginSystemEnabled;\n\n\t// Session-scoped state. Categories fire once per session; the pending queue\n\t// holds armed-but-not-yet-injected nudges; injectedThisTurn caps one per turn.\n\tconst deliveredCategories = new Set<string>();\n\tconst pending: ReuseNudge[] = [];\n\tlet injectedThisTurn = false;\n\t// Cached enablement, recomputed lazily and reset on session start.\n\tlet enabledCache: boolean | undefined;\n\n\tconst enabled = (cwd: string): boolean => {\n\t\tif (enabledCache === undefined) enabledCache = isEnabled(cwd);\n\t\treturn enabledCache;\n\t};\n\n\tconst enqueue = (text: string, cwd: string): void => {\n\t\tif (!enabled(cwd)) return;\n\t\tfor (const nudge of matchReuseNudges(text)) {\n\t\t\tif (deliveredCategories.has(nudge.category)) continue;\n\t\t\tif (pending.some((p) => p.category === nudge.category)) continue;\n\t\t\tpending.push(nudge);\n\t\t}\n\t};\n\n\thoo.on(\"session_start\", (_event: SessionStartEvent) => {\n\t\tdeliveredCategories.clear();\n\t\tpending.length = 0;\n\t\tinjectedThisTurn = false;\n\t\tenabledCache = undefined;\n\t\tclearArmedReuseNudges();\n\t});\n\n\thoo.on(\"turn_start\", (_event: TurnStartEvent) => {\n\t\tinjectedThisTurn = false;\n\t});\n\n\thoo.on(\"before_agent_start\", (event: BeforeAgentStartEvent, ctx: ExtensionContext) => {\n\t\tenqueue(event.prompt, ctx.cwd);\n\t});\n\n\thoo.on(\"tool_execution_start\", (event: ToolExecutionStartEvent, ctx: ExtensionContext) => {\n\t\tenqueue(extractText(event.args), ctx.cwd);\n\t});\n\n\thoo.on(\"tool_execution_end\", (event: ToolExecutionEndEvent, ctx: ExtensionContext) => {\n\t\tif (event.isError) return;\n\t\tenqueue(extractText(event.result), ctx.cwd);\n\t});\n\n\t// The injection point: fires before each provider request. transformContext\n\t// output is request-scoped (never written back to agent state), so the note\n\t// is ephemeral by construction.\n\thoo.on(\"context\", (event: ContextEvent, ctx: ExtensionContext) => {\n\t\tif (!enabled(ctx.cwd) || injectedThisTurn) return undefined;\n\t\t// Drop any that raced to \"delivered\" via another path, then take the oldest.\n\t\twhile (pending.length > 0 && deliveredCategories.has(pending[0]!.category)) pending.shift();\n\t\tconst nudge = pending.shift();\n\t\tif (!nudge) return undefined;\n\t\tdeliveredCategories.add(nudge.category);\n\t\tarmReuseNudge(nudge);\n\t\tinjectedThisTurn = true;\n\t\treturn { messages: injectNote(event.messages, formatNote(nudge)) };\n\t});\n}\n"]}