@dreb/coding-agent 2.4.3 → 2.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +1 -0
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/core/agent-session.d.ts +13 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +69 -10
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/buddy/buddy-controller.d.ts.map +1 -1
- package/dist/core/buddy/buddy-controller.js +13 -4
- package/dist/core/buddy/buddy-controller.js.map +1 -1
- package/dist/core/buddy/buddy-manager.d.ts.map +1 -1
- package/dist/core/buddy/buddy-manager.js +2 -0
- package/dist/core/buddy/buddy-manager.js.map +1 -1
- package/dist/core/daily-cost-tracker.d.ts.map +1 -1
- package/dist/core/daily-cost-tracker.js +2 -0
- package/dist/core/daily-cost-tracker.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +2 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +3 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +1 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +24 -8
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +2 -0
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/resolve-config-value.d.ts +3 -1
- package/dist/core/resolve-config-value.d.ts.map +1 -1
- package/dist/core/resolve-config-value.js +11 -3
- package/dist/core/resolve-config-value.js.map +1 -1
- package/dist/core/resource-loader.d.ts +3 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +69 -29
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +17 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +6 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +5 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/core/tools/dreb-paths.d.ts.map +1 -1
- package/dist/core/tools/dreb-paths.js +1 -0
- package/dist/core/tools/dreb-paths.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +1 -0
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +1 -0
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +1 -0
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +8 -0
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +1 -0
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/web.d.ts.map +1 -1
- package/dist/core/tools/web.js +1 -0
- package/dist/core/tools/web.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +1 -0
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +2 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +5 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +4 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/utils/changelog.d.ts.map +1 -1
- package/dist/utils/changelog.js +2 -2
- package/dist/utils/changelog.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +3 -0
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts.map +1 -1
- package/dist/utils/clipboard-native.js +1 -0
- package/dist/utils/clipboard-native.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +5 -3
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +2 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +1 -0
- package/dist/utils/image-resize.js.map +1 -1
- package/dist/utils/photon.d.ts.map +1 -1
- package/dist/utils/photon.js +3 -0
- package/dist/utils/photon.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +1 -0
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/custom-provider.md +20 -0
- package/docs/sdk.md +5 -0
- package/examples/sdk/12-full-control.ts +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buddy-controller.d.ts","sourceRoot":"","sources":["../../../src/core/buddy/buddy-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BuddyController — Frontend-agnostic controller for the buddy companion.\n *\n * Owns: context buffer, idle timer, reaction throttle, name-call detection.\n * Extracted from InteractiveMode so both TUI and Telegram can compose it\n * without duplicating ~150 lines of buddy wiring logic.\n *\n * The host (TUI or Telegram) provides callbacks for frontend-specific rendering:\n * - onSpeech(text) — display a speech bubble / message\n * - onThinkingStart() / onThinkingEnd() — show/hide thinking indicator\n *\n * Policies are configurable via BuddyControllerConfig so the TUI (no limits)\n * and Telegram (activity gating + reaction budget) can use different strategies.\n */\n\nimport { type BuddyManager, checkOllama } from \"./buddy-manager.js\";\nimport type { BuddyState } from \"./buddy-types.js\";\n\n/** Frontend-provided callbacks for buddy rendering */\nexport interface BuddyCallbacks {\n\t/** Display a speech/reaction message from the buddy */\n\tonSpeech: (text: string) => void;\n\t/** Show a thinking/loading indicator */\n\tonThinkingStart: () => void;\n\t/** Hide the thinking/loading indicator */\n\tonThinkingEnd: () => void;\n\t/** Hatch a new buddy — frontend resolves API key and calls manager.hatch() */\n\tonHatch: (manager: BuddyManager) => Promise<BuddyState>;\n\t/** Reroll the buddy — frontend resolves API key and calls manager.reroll() */\n\tonReroll: (manager: BuddyManager) => Promise<BuddyState>;\n}\n\n/** Configuration for buddy behavior — differs between TUI and Telegram */\nexport interface BuddyControllerConfig {\n\t/** Max entries in the context buffer (default: 20) */\n\tcontextMaxEntries?: number;\n\t/** Idle timeout in ms before buddy reacts to silence (default: 30000) */\n\tidleTimeoutMs?: number;\n\t/** Minimum ms between reactions (default: 60000) */\n\treactionCooldownMs?: number;\n\t/** If set, pause idle timer when user has been inactive this many ms */\n\tactivityGateMs?: number;\n\t/** If set, cap reactions to this many per hour */\n\treactionsPerHour?: number;\n}\n\n/** Subcommand result for frontend to render */\nexport type BuddyCommandResult =\n\t| { type: \"hatch\"; state: BuddyState }\n\t| { type: \"show\"; state: BuddyState }\n\t| { type: \"reroll\"; state: BuddyState }\n\t| { type: \"pet\" }\n\t| { type: \"stats\"; state: BuddyState }\n\t| { type: \"off\" }\n\t| { type: \"model\"; message: string }\n\t| { type: \"warning\"; message: string }\n\t| { type: \"error\"; message: string };\n\nexport class BuddyController {\n\tprivate contextBuffer: string[] = [];\n\tprivate lastReactionTime = 0;\n\tprivate idleTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate lastActivityTime = 0;\n\tprivate reactionTimestamps: number[] = []; // for budget tracking\n\t/** When false, all active functionality is disabled: no reactions, name-calls,\n\t * idle timer, or Ollama calls. Context capture (passive) still happens. */\n\tenabled = true;\n\n\treadonly manager: BuddyManager;\n\tprivate readonly callbacks: BuddyCallbacks;\n\tprivate readonly config: Required<BuddyControllerConfig>;\n\n\tconstructor(manager: BuddyManager, callbacks: BuddyCallbacks, config?: BuddyControllerConfig) {\n\t\tthis.manager = manager;\n\t\tthis.callbacks = callbacks;\n\t\tthis.config = {\n\t\t\tcontextMaxEntries: config?.contextMaxEntries ?? 20,\n\t\t\tidleTimeoutMs: config?.idleTimeoutMs ?? 30_000,\n\t\t\treactionCooldownMs: config?.reactionCooldownMs ?? 60_000,\n\t\t\tactivityGateMs: config?.activityGateMs ?? 0, // 0 = no gating (TUI default)\n\t\t\treactionsPerHour: config?.reactionsPerHour ?? 0, // 0 = unlimited (TUI default)\n\t\t};\n\t}\n\n\t// =========================================================================\n\t// Context buffer\n\t// =========================================================================\n\n\t/** Append an entry to the buddy context buffer (evicts oldest if at capacity) */\n\tappendContext(entry: string): void {\n\t\tthis.contextBuffer.push(entry);\n\t\tif (this.contextBuffer.length > this.config.contextMaxEntries) {\n\t\t\tthis.contextBuffer.shift();\n\t\t}\n\t}\n\n\t/** Build the context buffer into a string for LLM prompts */\n\tbuildContext(): string {\n\t\tif (this.contextBuffer.length === 0) {\n\t\t\treturn \"No recent activity.\";\n\t\t}\n\t\treturn this.contextBuffer.join(\"\\n\");\n\t}\n\n\t// =========================================================================\n\t// Activity & idle timer\n\t// =========================================================================\n\n\t/** Mark that user activity occurred (for activity gating) */\n\tmarkActivity(): void {\n\t\tthis.lastActivityTime = Date.now();\n\t}\n\n\t/** Reset the idle timer — called on every user message */\n\tresetIdleTimer(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t}\n\n\t\tif (!this.enabled) return;\n\n\t\tif (!this.manager.getState()) return; // No buddy loaded, skip idle timer\n\n\t\t// Activity gating: skip idle timer if user has been inactive too long\n\t\tif (this.config.activityGateMs > 0 && this.lastActivityTime > 0) {\n\t\t\tconst elapsed = Date.now() - this.lastActivityTime;\n\t\t\tif (elapsed > this.config.activityGateMs) {\n\t\t\t\tthis.idleTimer = null;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tthis.idleTimer = setTimeout(() => {\n\t\t\tconst ctx = this.buildContext();\n\t\t\tthis.triggerReaction(`It's been quiet for a moment. Recent activity:\\n${ctx}`).catch(() => {});\n\t\t}, this.config.idleTimeoutMs);\n\t}\n\n\t// =========================================================================\n\t// Reactions\n\t// =========================================================================\n\n\t/** Check if a reaction is allowed under current throttle and budget */\n\tprivate canReact(): boolean {\n\t\tif (!this.enabled) return false;\n\n\t\tconst now = Date.now();\n\n\t\t// Cooldown throttle\n\t\tif (now - this.lastReactionTime < this.config.reactionCooldownMs) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Reaction budget (per hour)\n\t\tif (this.config.reactionsPerHour > 0) {\n\t\t\tconst oneHourAgo = now - 3_600_000;\n\t\t\tthis.reactionTimestamps = this.reactionTimestamps.filter((t) => t > oneHourAgo);\n\t\t\tif (this.reactionTimestamps.length >= this.config.reactionsPerHour) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Trigger a buddy reaction. Throttled by cooldown and budget.\n\t * Calls onThinkingStart/End and onSpeech callbacks.\n\t * No-op if disabled.\n\t */\n\tasync triggerReaction(event: string): Promise<void> {\n\t\tif (!this.canReact()) return;\n\n\t\tthis.callbacks.onThinkingStart();\n\t\ttry {\n\t\t\tconst quip = await this.manager.react(event);\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (quip) {\n\t\t\t\tthis.lastReactionTime = Date.now();\n\t\t\t\tthis.reactionTimestamps.push(Date.now());\n\t\t\t\tthis.callbacks.onSpeech(quip);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tconsole.error(\"[buddy] triggerReaction failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/**\n\t * Handle a name-call from the user.\n\t * No-op if disabled — returns immediately without calling Ollama.\n\t */\n\tasync handleNameCall(userMessage: string): Promise<void> {\n\t\tif (!this.enabled) return;\n\t\tlet state = this.manager.getState();\n\t\tif (!state) {\n\t\t\t// Try loading from disk (buddy may have been hatched in another frontend)\n\t\t\tstate = this.manager.load();\n\t\t}\n\t\tif (!state) return;\n\n\t\tthis.callbacks.onThinkingStart();\n\t\ttry {\n\t\t\tconst response = await this.manager.respondToNameCall(userMessage, this.buildContext());\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (response) {\n\t\t\t\tthis.callbacks.onSpeech(response);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tconsole.error(\"[buddy] handleNameCall failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/** Check if a message contains the buddy's name (word-boundary matching).\n\t * Returns false if disabled. */\n\tdetectNameCall(text: string): boolean {\n\t\tif (!this.enabled) return false;\n\t\tconst name = this.manager.getName();\n\t\tif (!name) return false;\n\t\ttry {\n\t\t\tconst escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t\t\tconst regex = new RegExp(`\\\\b${escaped}\\\\b`, \"i\");\n\t\t\treturn regex.test(text);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Event handling\n\t// =========================================================================\n\n\t/**\n\t * Process an agent event for buddy context capture and reaction triggers.\n\t * The host calls this from its event handler.\n\t *\n\t * Context capture always happens. Reactions are gated by `enabled`.\n\t */\n\thandleEvent(event: { type: string; [key: string]: any }): void {\n\t\tconst state = this.manager.getState();\n\t\tif (!state) return; // No buddy loaded\n\n\t\tswitch (event.type) {\n\t\t\tcase \"message_end\": {\n\t\t\t\tif (event.message?.role === \"assistant\") {\n\t\t\t\t\tconst textParts = event.message.content\n\t\t\t\t\t\t?.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t?.map((c: any) => c.text)\n\t\t\t\t\t\t?.join(\"\")\n\t\t\t\t\t\t?.slice(0, 200);\n\t\t\t\t\tif (textParts) {\n\t\t\t\t\t\tthis.appendContext(`Assistant: ${textParts}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst toolCalls = event.message.content?.filter((c: any) => c.type === \"toolCall\") ?? [];\n\t\t\t\t\tif (toolCalls.length > 0) {\n\t\t\t\t\t\tconst tools = toolCalls.map((c: any) => c.name).join(\", \");\n\t\t\t\t\t\tthis.appendContext(`Called tools: ${tools}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t// Context capture (always)\n\t\t\t\tconst status = event.isError ? \"failed\" : \"completed\";\n\t\t\t\tconst output = event.result?.output || event.result?.content;\n\t\t\t\tconst outputText =\n\t\t\t\t\ttypeof output === \"string\"\n\t\t\t\t\t\t? output.slice(0, 100)\n\t\t\t\t\t\t: Array.isArray(output)\n\t\t\t\t\t\t\t? output\n\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t\t\t.slice(0, 100)\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\tthis.appendContext(`Tool ${event.toolName} ${status}${outputText ? `: ${outputText}` : \"\"}`);\n\n\t\t\t\t// Reaction on error (gated by enabled)\n\t\t\t\tif (event.isError && this.enabled) {\n\t\t\t\t\tlet errorText = \"unknown error\";\n\t\t\t\t\tconst result = event.result;\n\t\t\t\t\tif (result?.content && Array.isArray(result.content)) {\n\t\t\t\t\t\terrorText = result.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t.slice(0, 200);\n\t\t\t\t\t} else if (typeof result?.error === \"string\") {\n\t\t\t\t\t\terrorText = result.error.slice(0, 200);\n\t\t\t\t\t}\n\t\t\t\t\tif (!errorText) errorText = \"unknown error\";\n\t\t\t\t\tthis.triggerReaction(`Tool \"${event.toolName}\" failed: ${errorText}`).catch(() => {});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"agent_end\": {\n\t\t\t\tif (this.enabled) {\n\t\t\t\t\tconst ctx = this.buildContext();\n\t\t\t\t\tthis.triggerReaction(`The agent finished responding. Recent activity:\\n${ctx}`).catch(() => {});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Process a user message — captures context, resets idle, checks name-call.\n\t * Context capture always happens. Active features gated by `enabled`.\n\t * Returns true if a name-call was detected and is being handled.\n\t */\n\tprocessUserMessage(text: string): boolean {\n\t\tthis.appendContext(`User: ${text}`);\n\t\tthis.markActivity();\n\t\tthis.resetIdleTimer();\n\n\t\t// Name-call detection (gated by enabled via detectNameCall)\n\t\tif (this.detectNameCall(text)) {\n\t\t\tthis.handleNameCall(text).catch(() => {});\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t// =========================================================================\n\t// Command handling\n\t// =========================================================================\n\n\t/**\n\t * Handle a /buddy command. Returns a result object for the frontend to render.\n\t * Hatch/reroll are delegated to the frontend via onHatch/onReroll callbacks.\n\t */\n\tasync handleCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tswitch (subcommand) {\n\t\t\tcase \"pet\": {\n\t\t\t\tif (!this.manager.getState()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to pet! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"pet\" };\n\t\t\t}\n\t\t\tcase \"reroll\": {\n\t\t\t\tif (!this.manager.hasStoredBuddy()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to reroll! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst state = await this.callbacks.onReroll(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"reroll\", state };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Reroll failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tconst state = this.manager.getState();\n\t\t\t\tif (!state) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to show stats for! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"stats\", state };\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tthis.enabled = false;\n\t\t\t\tthis.manager.setHidden(true);\n\t\t\t\tthis.stop();\n\t\t\t\treturn { type: \"off\" };\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\t// Handle \"/buddy model\" and \"/buddy model <name>\"\n\t\t\t\tif (subcommand === \"model\" || subcommand.startsWith(\"model \")) {\n\t\t\t\t\treturn this.handleModelCommand(subcommand);\n\t\t\t\t}\n\n\t\t\t\t// No subcommand: hatch or show\n\t\t\t\tconst current = this.manager.getState();\n\t\t\t\tif (current) {\n\t\t\t\t\t// Already showing — just enable and return\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: current };\n\t\t\t\t}\n\n\t\t\t\t// Try to load existing buddy\n\t\t\t\tconst existing = this.manager.load();\n\t\t\t\tif (existing) {\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: existing };\n\t\t\t\t}\n\n\t\t\t\t// Hatch new buddy\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst hatchState = await this.callbacks.onHatch(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\treturn { type: \"hatch\", state: hatchState };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Hatch failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Model selection\n\t// =========================================================================\n\n\t/** Handle /buddy model [name] — show current model or set a new one */\n\tprivate async handleModelCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tconst modelArg = subcommand.slice(\"model\".length).trim();\n\n\t\tif (!modelArg) {\n\t\t\t// \"/buddy model\" with no argument — show current + available\n\t\t\tconst current = this.manager.getOllamaModel();\n\t\t\tconst status = await checkOllama();\n\t\t\tif (!status.available) {\n\t\t\t\treturn { type: \"model\", message: status.error ?? \"Ollama is not available.\" };\n\t\t\t}\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\tif (current) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"model\",\n\t\t\t\t\tmessage: `Current model: ${current}\\n\\nAvailable models:\\n${available}\\n\\nChange with: /buddy model <name>`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\ttype: \"model\",\n\t\t\t\tmessage: `No model set. Choose one with: /buddy model <name>\\n\\nAvailable models:\\n${available}`,\n\t\t\t};\n\t\t}\n\n\t\t// \"/buddy model <name>\" — set the model\n\t\tif (!this.manager.getState() && !this.manager.hasStoredBuddy()) {\n\t\t\treturn { type: \"warning\", message: \"No buddy yet — hatch one first with /buddy, then set a model.\" };\n\t\t}\n\n\t\tconst status = await checkOllama();\n\t\tif (!status.available) {\n\t\t\treturn { type: \"error\", message: status.error ?? \"Ollama is not available.\" };\n\t\t}\n\n\t\t// Check if the model is installed\n\t\tconst match = status.models.find((m) => m === modelArg || m.startsWith(`${modelArg}:`));\n\t\tif (!match) {\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\treturn {\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: `Model \"${modelArg}\" not found. Available models:\\n${available}\\n\\nPull it first with: ollama pull ${modelArg}`,\n\t\t\t};\n\t\t}\n\n\t\tthis.manager.setOllamaModel(match);\n\t\treturn { type: \"model\", message: `Buddy model set to: ${match}` };\n\t}\n\n\t/** Check if an Ollama model is configured, return a nudge message if not */\n\tgetModelNudge(): string | null {\n\t\tif (this.manager.getOllamaModel()) return null;\n\t\treturn \"No Ollama model set — reactions are disabled. Run /buddy model to choose one.\";\n\t}\n\n\t// =========================================================================\n\t// Lifecycle\n\t// =========================================================================\n\n\t/** Start the controller — auto-load buddy if one exists */\n\tstart(): BuddyState | null {\n\t\tconst existing = this.manager.load();\n\t\tif (existing) {\n\t\t\t// If buddy was hidden (via /buddy off), keep it loaded but disabled\n\t\t\tthis.enabled = !existing.hidden;\n\t\t\treturn existing;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/** Stop the controller — clear timers */\n\tstop(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t\tthis.idleTimer = null;\n\t\t}\n\t}\n\n\t/** Full reset — clear context buffer, idle timer, reaction budget.\n\t * Respects persisted hidden state so bridge reconnects don't undo /buddy off. */\n\treset(): void {\n\t\tthis.stop();\n\t\tthis.contextBuffer = [];\n\t\tthis.lastReactionTime = 0;\n\t\tthis.reactionTimestamps = [];\n\t\t// Re-enable unless buddy was explicitly hidden via /buddy off\n\t\tconst state = this.manager.getState() ?? this.manager.load();\n\t\tthis.enabled = state ? !state.hidden : true;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buddy-controller.d.ts","sourceRoot":"","sources":["../../../src/core/buddy/buddy-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BuddyController — Frontend-agnostic controller for the buddy companion.\n *\n * Owns: context buffer, idle timer, reaction throttle, name-call detection.\n * Extracted from InteractiveMode so both TUI and Telegram can compose it\n * without duplicating ~150 lines of buddy wiring logic.\n *\n * The host (TUI or Telegram) provides callbacks for frontend-specific rendering:\n * - onSpeech(text) — display a speech bubble / message\n * - onThinkingStart() / onThinkingEnd() — show/hide thinking indicator\n *\n * Policies are configurable via BuddyControllerConfig so the TUI (no limits)\n * and Telegram (activity gating + reaction budget) can use different strategies.\n */\n\nimport { type BuddyManager, checkOllama } from \"./buddy-manager.js\";\nimport type { BuddyState } from \"./buddy-types.js\";\n\n/** Frontend-provided callbacks for buddy rendering */\nexport interface BuddyCallbacks {\n\t/** Display a speech/reaction message from the buddy */\n\tonSpeech: (text: string) => void;\n\t/** Show a thinking/loading indicator */\n\tonThinkingStart: () => void;\n\t/** Hide the thinking/loading indicator */\n\tonThinkingEnd: () => void;\n\t/** Hatch a new buddy — frontend resolves API key and calls manager.hatch() */\n\tonHatch: (manager: BuddyManager) => Promise<BuddyState>;\n\t/** Reroll the buddy — frontend resolves API key and calls manager.reroll() */\n\tonReroll: (manager: BuddyManager) => Promise<BuddyState>;\n}\n\n/** Configuration for buddy behavior — differs between TUI and Telegram */\nexport interface BuddyControllerConfig {\n\t/** Max entries in the context buffer (default: 20) */\n\tcontextMaxEntries?: number;\n\t/** Idle timeout in ms before buddy reacts to silence (default: 30000) */\n\tidleTimeoutMs?: number;\n\t/** Minimum ms between reactions (default: 60000) */\n\treactionCooldownMs?: number;\n\t/** If set, pause idle timer when user has been inactive this many ms */\n\tactivityGateMs?: number;\n\t/** If set, cap reactions to this many per hour */\n\treactionsPerHour?: number;\n}\n\n/** Subcommand result for frontend to render */\nexport type BuddyCommandResult =\n\t| { type: \"hatch\"; state: BuddyState }\n\t| { type: \"show\"; state: BuddyState }\n\t| { type: \"reroll\"; state: BuddyState }\n\t| { type: \"pet\" }\n\t| { type: \"stats\"; state: BuddyState }\n\t| { type: \"off\" }\n\t| { type: \"model\"; message: string }\n\t| { type: \"warning\"; message: string }\n\t| { type: \"error\"; message: string };\n\nexport class BuddyController {\n\tprivate contextBuffer: string[] = [];\n\tprivate lastReactionTime = 0;\n\tprivate idleTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate lastActivityTime = 0;\n\tprivate reactionTimestamps: number[] = []; // for budget tracking\n\t/** When false, all active functionality is disabled: no reactions, name-calls,\n\t * idle timer, or Ollama calls. Context capture (passive) still happens. */\n\tenabled = true;\n\n\treadonly manager: BuddyManager;\n\tprivate readonly callbacks: BuddyCallbacks;\n\tprivate readonly config: Required<BuddyControllerConfig>;\n\n\tconstructor(manager: BuddyManager, callbacks: BuddyCallbacks, config?: BuddyControllerConfig) {\n\t\tthis.manager = manager;\n\t\tthis.callbacks = callbacks;\n\t\tthis.config = {\n\t\t\tcontextMaxEntries: config?.contextMaxEntries ?? 20,\n\t\t\tidleTimeoutMs: config?.idleTimeoutMs ?? 30_000,\n\t\t\treactionCooldownMs: config?.reactionCooldownMs ?? 60_000,\n\t\t\tactivityGateMs: config?.activityGateMs ?? 0, // 0 = no gating (TUI default)\n\t\t\treactionsPerHour: config?.reactionsPerHour ?? 0, // 0 = unlimited (TUI default)\n\t\t};\n\t}\n\n\t// =========================================================================\n\t// Context buffer\n\t// =========================================================================\n\n\t/** Append an entry to the buddy context buffer (evicts oldest if at capacity) */\n\tappendContext(entry: string): void {\n\t\tthis.contextBuffer.push(entry);\n\t\tif (this.contextBuffer.length > this.config.contextMaxEntries) {\n\t\t\tthis.contextBuffer.shift();\n\t\t}\n\t}\n\n\t/** Build the context buffer into a string for LLM prompts */\n\tbuildContext(): string {\n\t\tif (this.contextBuffer.length === 0) {\n\t\t\treturn \"No recent activity.\";\n\t\t}\n\t\treturn this.contextBuffer.join(\"\\n\");\n\t}\n\n\t// =========================================================================\n\t// Activity & idle timer\n\t// =========================================================================\n\n\t/** Mark that user activity occurred (for activity gating) */\n\tmarkActivity(): void {\n\t\tthis.lastActivityTime = Date.now();\n\t}\n\n\t/** Reset the idle timer — called on every user message */\n\tresetIdleTimer(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t}\n\n\t\tif (!this.enabled) return;\n\n\t\tif (!this.manager.getState()) return; // No buddy loaded, skip idle timer\n\n\t\t// Activity gating: skip idle timer if user has been inactive too long\n\t\tif (this.config.activityGateMs > 0 && this.lastActivityTime > 0) {\n\t\t\tconst elapsed = Date.now() - this.lastActivityTime;\n\t\t\tif (elapsed > this.config.activityGateMs) {\n\t\t\t\tthis.idleTimer = null;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tthis.idleTimer = setTimeout(() => {\n\t\t\tconst ctx = this.buildContext();\n\t\t\tthis.triggerReaction(`It's been quiet for a moment. Recent activity:\\n${ctx}`).catch(() => {\n\t\t\t\t/* triggerReaction() logs errors internally — prevents unhandled rejection */\n\t\t\t});\n\t\t}, this.config.idleTimeoutMs);\n\t}\n\n\t// =========================================================================\n\t// Reactions\n\t// =========================================================================\n\n\t/** Check if a reaction is allowed under current throttle and budget */\n\tprivate canReact(): boolean {\n\t\tif (!this.enabled) return false;\n\n\t\tconst now = Date.now();\n\n\t\t// Cooldown throttle\n\t\tif (now - this.lastReactionTime < this.config.reactionCooldownMs) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Reaction budget (per hour)\n\t\tif (this.config.reactionsPerHour > 0) {\n\t\t\tconst oneHourAgo = now - 3_600_000;\n\t\t\tthis.reactionTimestamps = this.reactionTimestamps.filter((t) => t > oneHourAgo);\n\t\t\tif (this.reactionTimestamps.length >= this.config.reactionsPerHour) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Trigger a buddy reaction. Throttled by cooldown and budget.\n\t * Calls onThinkingStart/End and onSpeech callbacks.\n\t * No-op if disabled.\n\t */\n\tasync triggerReaction(event: string): Promise<void> {\n\t\tif (!this.canReact()) return;\n\n\t\tthis.callbacks.onThinkingStart();\n\t\ttry {\n\t\t\tconst quip = await this.manager.react(event);\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (quip) {\n\t\t\t\tthis.lastReactionTime = Date.now();\n\t\t\t\tthis.reactionTimestamps.push(Date.now());\n\t\t\t\tthis.callbacks.onSpeech(quip);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tconsole.error(\"[buddy] triggerReaction failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/**\n\t * Handle a name-call from the user.\n\t * No-op if disabled — returns immediately without calling Ollama.\n\t */\n\tasync handleNameCall(userMessage: string): Promise<void> {\n\t\tif (!this.enabled) return;\n\t\tlet state = this.manager.getState();\n\t\tif (!state) {\n\t\t\t// Try loading from disk (buddy may have been hatched in another frontend)\n\t\t\tstate = this.manager.load();\n\t\t}\n\t\tif (!state) return;\n\n\t\tthis.callbacks.onThinkingStart();\n\t\ttry {\n\t\t\tconst response = await this.manager.respondToNameCall(userMessage, this.buildContext());\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (response) {\n\t\t\t\tthis.callbacks.onSpeech(response);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tconsole.error(\"[buddy] handleNameCall failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/** Check if a message contains the buddy's name (word-boundary matching).\n\t * Returns false if disabled. */\n\tdetectNameCall(text: string): boolean {\n\t\tif (!this.enabled) return false;\n\t\tconst name = this.manager.getName();\n\t\tif (!name) return false;\n\t\ttry {\n\t\t\tconst escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t\t\tconst regex = new RegExp(`\\\\b${escaped}\\\\b`, \"i\");\n\t\t\treturn regex.test(text);\n\t\t} catch {\n\t\t\t/* Invalid regex from buddy name — safe to return false */\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Event handling\n\t// =========================================================================\n\n\t/**\n\t * Process an agent event for buddy context capture and reaction triggers.\n\t * The host calls this from its event handler.\n\t *\n\t * Context capture always happens. Reactions are gated by `enabled`.\n\t */\n\thandleEvent(event: { type: string; [key: string]: any }): void {\n\t\tconst state = this.manager.getState();\n\t\tif (!state) return; // No buddy loaded\n\n\t\tswitch (event.type) {\n\t\t\tcase \"message_end\": {\n\t\t\t\tif (event.message?.role === \"assistant\") {\n\t\t\t\t\tconst textParts = event.message.content\n\t\t\t\t\t\t?.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t?.map((c: any) => c.text)\n\t\t\t\t\t\t?.join(\"\")\n\t\t\t\t\t\t?.slice(0, 200);\n\t\t\t\t\tif (textParts) {\n\t\t\t\t\t\tthis.appendContext(`Assistant: ${textParts}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst toolCalls = event.message.content?.filter((c: any) => c.type === \"toolCall\") ?? [];\n\t\t\t\t\tif (toolCalls.length > 0) {\n\t\t\t\t\t\tconst tools = toolCalls.map((c: any) => c.name).join(\", \");\n\t\t\t\t\t\tthis.appendContext(`Called tools: ${tools}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t// Context capture (always)\n\t\t\t\tconst status = event.isError ? \"failed\" : \"completed\";\n\t\t\t\tconst output = event.result?.output || event.result?.content;\n\t\t\t\tconst outputText =\n\t\t\t\t\ttypeof output === \"string\"\n\t\t\t\t\t\t? output.slice(0, 100)\n\t\t\t\t\t\t: Array.isArray(output)\n\t\t\t\t\t\t\t? output\n\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t\t\t.slice(0, 100)\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\tthis.appendContext(`Tool ${event.toolName} ${status}${outputText ? `: ${outputText}` : \"\"}`);\n\n\t\t\t\t// Reaction on error (gated by enabled)\n\t\t\t\tif (event.isError && this.enabled) {\n\t\t\t\t\tlet errorText = \"unknown error\";\n\t\t\t\t\tconst result = event.result;\n\t\t\t\t\tif (result?.content && Array.isArray(result.content)) {\n\t\t\t\t\t\terrorText = result.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t.slice(0, 200);\n\t\t\t\t\t} else if (typeof result?.error === \"string\") {\n\t\t\t\t\t\terrorText = result.error.slice(0, 200);\n\t\t\t\t\t}\n\t\t\t\t\tif (!errorText) errorText = \"unknown error\";\n\t\t\t\t\tthis.triggerReaction(`Tool \"${event.toolName}\" failed: ${errorText}`).catch(() => {\n\t\t\t\t\t\t/* triggerReaction() logs errors internally */\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"agent_end\": {\n\t\t\t\tif (this.enabled) {\n\t\t\t\t\tconst ctx = this.buildContext();\n\t\t\t\t\tthis.triggerReaction(`The agent finished responding. Recent activity:\\n${ctx}`).catch(() => {\n\t\t\t\t\t\t/* triggerReaction() logs errors internally */\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Process a user message — captures context, resets idle, checks name-call.\n\t * Context capture always happens. Active features gated by `enabled`.\n\t * Returns true if a name-call was detected and is being handled.\n\t */\n\tprocessUserMessage(text: string): boolean {\n\t\tthis.appendContext(`User: ${text}`);\n\t\tthis.markActivity();\n\t\tthis.resetIdleTimer();\n\n\t\t// Name-call detection (gated by enabled via detectNameCall)\n\t\tif (this.detectNameCall(text)) {\n\t\t\tthis.handleNameCall(text).catch(() => {\n\t\t\t\t/* handleNameCall() logs errors internally — prevents unhandled rejection */\n\t\t\t});\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t// =========================================================================\n\t// Command handling\n\t// =========================================================================\n\n\t/**\n\t * Handle a /buddy command. Returns a result object for the frontend to render.\n\t * Hatch/reroll are delegated to the frontend via onHatch/onReroll callbacks.\n\t */\n\tasync handleCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tswitch (subcommand) {\n\t\t\tcase \"pet\": {\n\t\t\t\tif (!this.manager.getState()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to pet! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"pet\" };\n\t\t\t}\n\t\t\tcase \"reroll\": {\n\t\t\t\tif (!this.manager.hasStoredBuddy()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to reroll! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst state = await this.callbacks.onReroll(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"reroll\", state };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Reroll failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tconst state = this.manager.getState();\n\t\t\t\tif (!state) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to show stats for! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"stats\", state };\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tthis.enabled = false;\n\t\t\t\tthis.manager.setHidden(true);\n\t\t\t\tthis.stop();\n\t\t\t\treturn { type: \"off\" };\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\t// Handle \"/buddy model\" and \"/buddy model <name>\"\n\t\t\t\tif (subcommand === \"model\" || subcommand.startsWith(\"model \")) {\n\t\t\t\t\treturn this.handleModelCommand(subcommand);\n\t\t\t\t}\n\n\t\t\t\t// No subcommand: hatch or show\n\t\t\t\tconst current = this.manager.getState();\n\t\t\t\tif (current) {\n\t\t\t\t\t// Already showing — just enable and return\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: current };\n\t\t\t\t}\n\n\t\t\t\t// Try to load existing buddy\n\t\t\t\tconst existing = this.manager.load();\n\t\t\t\tif (existing) {\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: existing };\n\t\t\t\t}\n\n\t\t\t\t// Hatch new buddy\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst hatchState = await this.callbacks.onHatch(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\treturn { type: \"hatch\", state: hatchState };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Hatch failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Model selection\n\t// =========================================================================\n\n\t/** Handle /buddy model [name] — show current model or set a new one */\n\tprivate async handleModelCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tconst modelArg = subcommand.slice(\"model\".length).trim();\n\n\t\tif (!modelArg) {\n\t\t\t// \"/buddy model\" with no argument — show current + available\n\t\t\tconst current = this.manager.getOllamaModel();\n\t\t\tconst status = await checkOllama();\n\t\t\tif (!status.available) {\n\t\t\t\treturn { type: \"model\", message: status.error ?? \"Ollama is not available.\" };\n\t\t\t}\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\tif (current) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"model\",\n\t\t\t\t\tmessage: `Current model: ${current}\\n\\nAvailable models:\\n${available}\\n\\nChange with: /buddy model <name>`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\ttype: \"model\",\n\t\t\t\tmessage: `No model set. Choose one with: /buddy model <name>\\n\\nAvailable models:\\n${available}`,\n\t\t\t};\n\t\t}\n\n\t\t// \"/buddy model <name>\" — set the model\n\t\tif (!this.manager.getState() && !this.manager.hasStoredBuddy()) {\n\t\t\treturn { type: \"warning\", message: \"No buddy yet — hatch one first with /buddy, then set a model.\" };\n\t\t}\n\n\t\tconst status = await checkOllama();\n\t\tif (!status.available) {\n\t\t\treturn { type: \"error\", message: status.error ?? \"Ollama is not available.\" };\n\t\t}\n\n\t\t// Check if the model is installed\n\t\tconst match = status.models.find((m) => m === modelArg || m.startsWith(`${modelArg}:`));\n\t\tif (!match) {\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\treturn {\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: `Model \"${modelArg}\" not found. Available models:\\n${available}\\n\\nPull it first with: ollama pull ${modelArg}`,\n\t\t\t};\n\t\t}\n\n\t\tthis.manager.setOllamaModel(match);\n\t\treturn { type: \"model\", message: `Buddy model set to: ${match}` };\n\t}\n\n\t/** Check if an Ollama model is configured, return a nudge message if not */\n\tgetModelNudge(): string | null {\n\t\tif (this.manager.getOllamaModel()) return null;\n\t\treturn \"No Ollama model set — reactions are disabled. Run /buddy model to choose one.\";\n\t}\n\n\t// =========================================================================\n\t// Lifecycle\n\t// =========================================================================\n\n\t/** Start the controller — auto-load buddy if one exists */\n\tstart(): BuddyState | null {\n\t\tconst existing = this.manager.load();\n\t\tif (existing) {\n\t\t\t// If buddy was hidden (via /buddy off), keep it loaded but disabled\n\t\t\tthis.enabled = !existing.hidden;\n\t\t\treturn existing;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/** Stop the controller — clear timers */\n\tstop(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t\tthis.idleTimer = null;\n\t\t}\n\t}\n\n\t/** Full reset — clear context buffer, idle timer, reaction budget.\n\t * Respects persisted hidden state so bridge reconnects don't undo /buddy off. */\n\treset(): void {\n\t\tthis.stop();\n\t\tthis.contextBuffer = [];\n\t\tthis.lastReactionTime = 0;\n\t\tthis.reactionTimestamps = [];\n\t\t// Re-enable unless buddy was explicitly hidden via /buddy off\n\t\tconst state = this.manager.getState() ?? this.manager.load();\n\t\tthis.enabled = state ? !state.hidden : true;\n\t}\n}\n"]}
|
|
@@ -79,7 +79,9 @@ export class BuddyController {
|
|
|
79
79
|
}
|
|
80
80
|
this.idleTimer = setTimeout(() => {
|
|
81
81
|
const ctx = this.buildContext();
|
|
82
|
-
this.triggerReaction(`It's been quiet for a moment. Recent activity:\n${ctx}`).catch(() => {
|
|
82
|
+
this.triggerReaction(`It's been quiet for a moment. Recent activity:\n${ctx}`).catch(() => {
|
|
83
|
+
/* triggerReaction() logs errors internally — prevents unhandled rejection */
|
|
84
|
+
});
|
|
83
85
|
}, this.config.idleTimeoutMs);
|
|
84
86
|
}
|
|
85
87
|
// =========================================================================
|
|
@@ -168,6 +170,7 @@ export class BuddyController {
|
|
|
168
170
|
return regex.test(text);
|
|
169
171
|
}
|
|
170
172
|
catch {
|
|
173
|
+
/* Invalid regex from buddy name — safe to return false */
|
|
171
174
|
return false;
|
|
172
175
|
}
|
|
173
176
|
}
|
|
@@ -233,14 +236,18 @@ export class BuddyController {
|
|
|
233
236
|
}
|
|
234
237
|
if (!errorText)
|
|
235
238
|
errorText = "unknown error";
|
|
236
|
-
this.triggerReaction(`Tool "${event.toolName}" failed: ${errorText}`).catch(() => {
|
|
239
|
+
this.triggerReaction(`Tool "${event.toolName}" failed: ${errorText}`).catch(() => {
|
|
240
|
+
/* triggerReaction() logs errors internally */
|
|
241
|
+
});
|
|
237
242
|
}
|
|
238
243
|
break;
|
|
239
244
|
}
|
|
240
245
|
case "agent_end": {
|
|
241
246
|
if (this.enabled) {
|
|
242
247
|
const ctx = this.buildContext();
|
|
243
|
-
this.triggerReaction(`The agent finished responding. Recent activity:\n${ctx}`).catch(() => {
|
|
248
|
+
this.triggerReaction(`The agent finished responding. Recent activity:\n${ctx}`).catch(() => {
|
|
249
|
+
/* triggerReaction() logs errors internally */
|
|
250
|
+
});
|
|
244
251
|
}
|
|
245
252
|
break;
|
|
246
253
|
}
|
|
@@ -257,7 +264,9 @@ export class BuddyController {
|
|
|
257
264
|
this.resetIdleTimer();
|
|
258
265
|
// Name-call detection (gated by enabled via detectNameCall)
|
|
259
266
|
if (this.detectNameCall(text)) {
|
|
260
|
-
this.handleNameCall(text).catch(() => {
|
|
267
|
+
this.handleNameCall(text).catch(() => {
|
|
268
|
+
/* handleNameCall() logs errors internally — prevents unhandled rejection */
|
|
269
|
+
});
|
|
261
270
|
return true;
|
|
262
271
|
}
|
|
263
272
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buddy-controller.js","sourceRoot":"","sources":["../../../src/core/buddy/buddy-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BuddyController — Frontend-agnostic controller for the buddy companion.\n *\n * Owns: context buffer, idle timer, reaction throttle, name-call detection.\n * Extracted from InteractiveMode so both TUI and Telegram can compose it\n * without duplicating ~150 lines of buddy wiring logic.\n *\n * The host (TUI or Telegram) provides callbacks for frontend-specific rendering:\n * - onSpeech(text) — display a speech bubble / message\n * - onThinkingStart() / onThinkingEnd() — show/hide thinking indicator\n *\n * Policies are configurable via BuddyControllerConfig so the TUI (no limits)\n * and Telegram (activity gating + reaction budget) can use different strategies.\n */\n\nimport { type BuddyManager, checkOllama } from \"./buddy-manager.js\";\nimport type { BuddyState } from \"./buddy-types.js\";\n\n/** Frontend-provided callbacks for buddy rendering */\nexport interface BuddyCallbacks {\n\t/** Display a speech/reaction message from the buddy */\n\tonSpeech: (text: string) => void;\n\t/** Show a thinking/loading indicator */\n\tonThinkingStart: () => void;\n\t/** Hide the thinking/loading indicator */\n\tonThinkingEnd: () => void;\n\t/** Hatch a new buddy — frontend resolves API key and calls manager.hatch() */\n\tonHatch: (manager: BuddyManager) => Promise<BuddyState>;\n\t/** Reroll the buddy — frontend resolves API key and calls manager.reroll() */\n\tonReroll: (manager: BuddyManager) => Promise<BuddyState>;\n}\n\n/** Configuration for buddy behavior — differs between TUI and Telegram */\nexport interface BuddyControllerConfig {\n\t/** Max entries in the context buffer (default: 20) */\n\tcontextMaxEntries?: number;\n\t/** Idle timeout in ms before buddy reacts to silence (default: 30000) */\n\tidleTimeoutMs?: number;\n\t/** Minimum ms between reactions (default: 60000) */\n\treactionCooldownMs?: number;\n\t/** If set, pause idle timer when user has been inactive this many ms */\n\tactivityGateMs?: number;\n\t/** If set, cap reactions to this many per hour */\n\treactionsPerHour?: number;\n}\n\n/** Subcommand result for frontend to render */\nexport type BuddyCommandResult =\n\t| { type: \"hatch\"; state: BuddyState }\n\t| { type: \"show\"; state: BuddyState }\n\t| { type: \"reroll\"; state: BuddyState }\n\t| { type: \"pet\" }\n\t| { type: \"stats\"; state: BuddyState }\n\t| { type: \"off\" }\n\t| { type: \"model\"; message: string }\n\t| { type: \"warning\"; message: string }\n\t| { type: \"error\"; message: string };\n\nexport class BuddyController {\n\tprivate contextBuffer: string[] = [];\n\tprivate lastReactionTime = 0;\n\tprivate idleTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate lastActivityTime = 0;\n\tprivate reactionTimestamps: number[] = []; // for budget tracking\n\t/** When false, all active functionality is disabled: no reactions, name-calls,\n\t * idle timer, or Ollama calls. Context capture (passive) still happens. */\n\tenabled = true;\n\n\treadonly manager: BuddyManager;\n\tprivate readonly callbacks: BuddyCallbacks;\n\tprivate readonly config: Required<BuddyControllerConfig>;\n\n\tconstructor(manager: BuddyManager, callbacks: BuddyCallbacks, config?: BuddyControllerConfig) {\n\t\tthis.manager = manager;\n\t\tthis.callbacks = callbacks;\n\t\tthis.config = {\n\t\t\tcontextMaxEntries: config?.contextMaxEntries ?? 20,\n\t\t\tidleTimeoutMs: config?.idleTimeoutMs ?? 30_000,\n\t\t\treactionCooldownMs: config?.reactionCooldownMs ?? 60_000,\n\t\t\tactivityGateMs: config?.activityGateMs ?? 0, // 0 = no gating (TUI default)\n\t\t\treactionsPerHour: config?.reactionsPerHour ?? 0, // 0 = unlimited (TUI default)\n\t\t};\n\t}\n\n\t// =========================================================================\n\t// Context buffer\n\t// =========================================================================\n\n\t/** Append an entry to the buddy context buffer (evicts oldest if at capacity) */\n\tappendContext(entry: string): void {\n\t\tthis.contextBuffer.push(entry);\n\t\tif (this.contextBuffer.length > this.config.contextMaxEntries) {\n\t\t\tthis.contextBuffer.shift();\n\t\t}\n\t}\n\n\t/** Build the context buffer into a string for LLM prompts */\n\tbuildContext(): string {\n\t\tif (this.contextBuffer.length === 0) {\n\t\t\treturn \"No recent activity.\";\n\t\t}\n\t\treturn this.contextBuffer.join(\"\\n\");\n\t}\n\n\t// =========================================================================\n\t// Activity & idle timer\n\t// =========================================================================\n\n\t/** Mark that user activity occurred (for activity gating) */\n\tmarkActivity(): void {\n\t\tthis.lastActivityTime = Date.now();\n\t}\n\n\t/** Reset the idle timer — called on every user message */\n\tresetIdleTimer(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t}\n\n\t\tif (!this.enabled) return;\n\n\t\tif (!this.manager.getState()) return; // No buddy loaded, skip idle timer\n\n\t\t// Activity gating: skip idle timer if user has been inactive too long\n\t\tif (this.config.activityGateMs > 0 && this.lastActivityTime > 0) {\n\t\t\tconst elapsed = Date.now() - this.lastActivityTime;\n\t\t\tif (elapsed > this.config.activityGateMs) {\n\t\t\t\tthis.idleTimer = null;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tthis.idleTimer = setTimeout(() => {\n\t\t\tconst ctx = this.buildContext();\n\t\t\tthis.triggerReaction(`It's been quiet for a moment. Recent activity:\\n${ctx}`).catch(() => {});\n\t\t}, this.config.idleTimeoutMs);\n\t}\n\n\t// =========================================================================\n\t// Reactions\n\t// =========================================================================\n\n\t/** Check if a reaction is allowed under current throttle and budget */\n\tprivate canReact(): boolean {\n\t\tif (!this.enabled) return false;\n\n\t\tconst now = Date.now();\n\n\t\t// Cooldown throttle\n\t\tif (now - this.lastReactionTime < this.config.reactionCooldownMs) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Reaction budget (per hour)\n\t\tif (this.config.reactionsPerHour > 0) {\n\t\t\tconst oneHourAgo = now - 3_600_000;\n\t\t\tthis.reactionTimestamps = this.reactionTimestamps.filter((t) => t > oneHourAgo);\n\t\t\tif (this.reactionTimestamps.length >= this.config.reactionsPerHour) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Trigger a buddy reaction. Throttled by cooldown and budget.\n\t * Calls onThinkingStart/End and onSpeech callbacks.\n\t * No-op if disabled.\n\t */\n\tasync triggerReaction(event: string): Promise<void> {\n\t\tif (!this.canReact()) return;\n\n\t\tthis.callbacks.onThinkingStart();\n\t\ttry {\n\t\t\tconst quip = await this.manager.react(event);\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (quip) {\n\t\t\t\tthis.lastReactionTime = Date.now();\n\t\t\t\tthis.reactionTimestamps.push(Date.now());\n\t\t\t\tthis.callbacks.onSpeech(quip);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tconsole.error(\"[buddy] triggerReaction failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/**\n\t * Handle a name-call from the user.\n\t * No-op if disabled — returns immediately without calling Ollama.\n\t */\n\tasync handleNameCall(userMessage: string): Promise<void> {\n\t\tif (!this.enabled) return;\n\t\tlet state = this.manager.getState();\n\t\tif (!state) {\n\t\t\t// Try loading from disk (buddy may have been hatched in another frontend)\n\t\t\tstate = this.manager.load();\n\t\t}\n\t\tif (!state) return;\n\n\t\tthis.callbacks.onThinkingStart();\n\t\ttry {\n\t\t\tconst response = await this.manager.respondToNameCall(userMessage, this.buildContext());\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (response) {\n\t\t\t\tthis.callbacks.onSpeech(response);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tconsole.error(\"[buddy] handleNameCall failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/** Check if a message contains the buddy's name (word-boundary matching).\n\t * Returns false if disabled. */\n\tdetectNameCall(text: string): boolean {\n\t\tif (!this.enabled) return false;\n\t\tconst name = this.manager.getName();\n\t\tif (!name) return false;\n\t\ttry {\n\t\t\tconst escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t\t\tconst regex = new RegExp(`\\\\b${escaped}\\\\b`, \"i\");\n\t\t\treturn regex.test(text);\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Event handling\n\t// =========================================================================\n\n\t/**\n\t * Process an agent event for buddy context capture and reaction triggers.\n\t * The host calls this from its event handler.\n\t *\n\t * Context capture always happens. Reactions are gated by `enabled`.\n\t */\n\thandleEvent(event: { type: string; [key: string]: any }): void {\n\t\tconst state = this.manager.getState();\n\t\tif (!state) return; // No buddy loaded\n\n\t\tswitch (event.type) {\n\t\t\tcase \"message_end\": {\n\t\t\t\tif (event.message?.role === \"assistant\") {\n\t\t\t\t\tconst textParts = event.message.content\n\t\t\t\t\t\t?.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t?.map((c: any) => c.text)\n\t\t\t\t\t\t?.join(\"\")\n\t\t\t\t\t\t?.slice(0, 200);\n\t\t\t\t\tif (textParts) {\n\t\t\t\t\t\tthis.appendContext(`Assistant: ${textParts}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst toolCalls = event.message.content?.filter((c: any) => c.type === \"toolCall\") ?? [];\n\t\t\t\t\tif (toolCalls.length > 0) {\n\t\t\t\t\t\tconst tools = toolCalls.map((c: any) => c.name).join(\", \");\n\t\t\t\t\t\tthis.appendContext(`Called tools: ${tools}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t// Context capture (always)\n\t\t\t\tconst status = event.isError ? \"failed\" : \"completed\";\n\t\t\t\tconst output = event.result?.output || event.result?.content;\n\t\t\t\tconst outputText =\n\t\t\t\t\ttypeof output === \"string\"\n\t\t\t\t\t\t? output.slice(0, 100)\n\t\t\t\t\t\t: Array.isArray(output)\n\t\t\t\t\t\t\t? output\n\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t\t\t.slice(0, 100)\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\tthis.appendContext(`Tool ${event.toolName} ${status}${outputText ? `: ${outputText}` : \"\"}`);\n\n\t\t\t\t// Reaction on error (gated by enabled)\n\t\t\t\tif (event.isError && this.enabled) {\n\t\t\t\t\tlet errorText = \"unknown error\";\n\t\t\t\t\tconst result = event.result;\n\t\t\t\t\tif (result?.content && Array.isArray(result.content)) {\n\t\t\t\t\t\terrorText = result.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t.slice(0, 200);\n\t\t\t\t\t} else if (typeof result?.error === \"string\") {\n\t\t\t\t\t\terrorText = result.error.slice(0, 200);\n\t\t\t\t\t}\n\t\t\t\t\tif (!errorText) errorText = \"unknown error\";\n\t\t\t\t\tthis.triggerReaction(`Tool \"${event.toolName}\" failed: ${errorText}`).catch(() => {});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"agent_end\": {\n\t\t\t\tif (this.enabled) {\n\t\t\t\t\tconst ctx = this.buildContext();\n\t\t\t\t\tthis.triggerReaction(`The agent finished responding. Recent activity:\\n${ctx}`).catch(() => {});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Process a user message — captures context, resets idle, checks name-call.\n\t * Context capture always happens. Active features gated by `enabled`.\n\t * Returns true if a name-call was detected and is being handled.\n\t */\n\tprocessUserMessage(text: string): boolean {\n\t\tthis.appendContext(`User: ${text}`);\n\t\tthis.markActivity();\n\t\tthis.resetIdleTimer();\n\n\t\t// Name-call detection (gated by enabled via detectNameCall)\n\t\tif (this.detectNameCall(text)) {\n\t\t\tthis.handleNameCall(text).catch(() => {});\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t// =========================================================================\n\t// Command handling\n\t// =========================================================================\n\n\t/**\n\t * Handle a /buddy command. Returns a result object for the frontend to render.\n\t * Hatch/reroll are delegated to the frontend via onHatch/onReroll callbacks.\n\t */\n\tasync handleCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tswitch (subcommand) {\n\t\t\tcase \"pet\": {\n\t\t\t\tif (!this.manager.getState()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to pet! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"pet\" };\n\t\t\t}\n\t\t\tcase \"reroll\": {\n\t\t\t\tif (!this.manager.hasStoredBuddy()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to reroll! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst state = await this.callbacks.onReroll(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"reroll\", state };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Reroll failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tconst state = this.manager.getState();\n\t\t\t\tif (!state) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to show stats for! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"stats\", state };\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tthis.enabled = false;\n\t\t\t\tthis.manager.setHidden(true);\n\t\t\t\tthis.stop();\n\t\t\t\treturn { type: \"off\" };\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\t// Handle \"/buddy model\" and \"/buddy model <name>\"\n\t\t\t\tif (subcommand === \"model\" || subcommand.startsWith(\"model \")) {\n\t\t\t\t\treturn this.handleModelCommand(subcommand);\n\t\t\t\t}\n\n\t\t\t\t// No subcommand: hatch or show\n\t\t\t\tconst current = this.manager.getState();\n\t\t\t\tif (current) {\n\t\t\t\t\t// Already showing — just enable and return\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: current };\n\t\t\t\t}\n\n\t\t\t\t// Try to load existing buddy\n\t\t\t\tconst existing = this.manager.load();\n\t\t\t\tif (existing) {\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: existing };\n\t\t\t\t}\n\n\t\t\t\t// Hatch new buddy\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst hatchState = await this.callbacks.onHatch(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\treturn { type: \"hatch\", state: hatchState };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Hatch failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Model selection\n\t// =========================================================================\n\n\t/** Handle /buddy model [name] — show current model or set a new one */\n\tprivate async handleModelCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tconst modelArg = subcommand.slice(\"model\".length).trim();\n\n\t\tif (!modelArg) {\n\t\t\t// \"/buddy model\" with no argument — show current + available\n\t\t\tconst current = this.manager.getOllamaModel();\n\t\t\tconst status = await checkOllama();\n\t\t\tif (!status.available) {\n\t\t\t\treturn { type: \"model\", message: status.error ?? \"Ollama is not available.\" };\n\t\t\t}\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\tif (current) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"model\",\n\t\t\t\t\tmessage: `Current model: ${current}\\n\\nAvailable models:\\n${available}\\n\\nChange with: /buddy model <name>`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\ttype: \"model\",\n\t\t\t\tmessage: `No model set. Choose one with: /buddy model <name>\\n\\nAvailable models:\\n${available}`,\n\t\t\t};\n\t\t}\n\n\t\t// \"/buddy model <name>\" — set the model\n\t\tif (!this.manager.getState() && !this.manager.hasStoredBuddy()) {\n\t\t\treturn { type: \"warning\", message: \"No buddy yet — hatch one first with /buddy, then set a model.\" };\n\t\t}\n\n\t\tconst status = await checkOllama();\n\t\tif (!status.available) {\n\t\t\treturn { type: \"error\", message: status.error ?? \"Ollama is not available.\" };\n\t\t}\n\n\t\t// Check if the model is installed\n\t\tconst match = status.models.find((m) => m === modelArg || m.startsWith(`${modelArg}:`));\n\t\tif (!match) {\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\treturn {\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: `Model \"${modelArg}\" not found. Available models:\\n${available}\\n\\nPull it first with: ollama pull ${modelArg}`,\n\t\t\t};\n\t\t}\n\n\t\tthis.manager.setOllamaModel(match);\n\t\treturn { type: \"model\", message: `Buddy model set to: ${match}` };\n\t}\n\n\t/** Check if an Ollama model is configured, return a nudge message if not */\n\tgetModelNudge(): string | null {\n\t\tif (this.manager.getOllamaModel()) return null;\n\t\treturn \"No Ollama model set — reactions are disabled. Run /buddy model to choose one.\";\n\t}\n\n\t// =========================================================================\n\t// Lifecycle\n\t// =========================================================================\n\n\t/** Start the controller — auto-load buddy if one exists */\n\tstart(): BuddyState | null {\n\t\tconst existing = this.manager.load();\n\t\tif (existing) {\n\t\t\t// If buddy was hidden (via /buddy off), keep it loaded but disabled\n\t\t\tthis.enabled = !existing.hidden;\n\t\t\treturn existing;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/** Stop the controller — clear timers */\n\tstop(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t\tthis.idleTimer = null;\n\t\t}\n\t}\n\n\t/** Full reset — clear context buffer, idle timer, reaction budget.\n\t * Respects persisted hidden state so bridge reconnects don't undo /buddy off. */\n\treset(): void {\n\t\tthis.stop();\n\t\tthis.contextBuffer = [];\n\t\tthis.lastReactionTime = 0;\n\t\tthis.reactionTimestamps = [];\n\t\t// Re-enable unless buddy was explicitly hidden via /buddy off\n\t\tconst state = this.manager.getState() ?? this.manager.load();\n\t\tthis.enabled = state ? !state.hidden : true;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buddy-controller.js","sourceRoot":"","sources":["../../../src/core/buddy/buddy-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BuddyController — Frontend-agnostic controller for the buddy companion.\n *\n * Owns: context buffer, idle timer, reaction throttle, name-call detection.\n * Extracted from InteractiveMode so both TUI and Telegram can compose it\n * without duplicating ~150 lines of buddy wiring logic.\n *\n * The host (TUI or Telegram) provides callbacks for frontend-specific rendering:\n * - onSpeech(text) — display a speech bubble / message\n * - onThinkingStart() / onThinkingEnd() — show/hide thinking indicator\n *\n * Policies are configurable via BuddyControllerConfig so the TUI (no limits)\n * and Telegram (activity gating + reaction budget) can use different strategies.\n */\n\nimport { type BuddyManager, checkOllama } from \"./buddy-manager.js\";\nimport type { BuddyState } from \"./buddy-types.js\";\n\n/** Frontend-provided callbacks for buddy rendering */\nexport interface BuddyCallbacks {\n\t/** Display a speech/reaction message from the buddy */\n\tonSpeech: (text: string) => void;\n\t/** Show a thinking/loading indicator */\n\tonThinkingStart: () => void;\n\t/** Hide the thinking/loading indicator */\n\tonThinkingEnd: () => void;\n\t/** Hatch a new buddy — frontend resolves API key and calls manager.hatch() */\n\tonHatch: (manager: BuddyManager) => Promise<BuddyState>;\n\t/** Reroll the buddy — frontend resolves API key and calls manager.reroll() */\n\tonReroll: (manager: BuddyManager) => Promise<BuddyState>;\n}\n\n/** Configuration for buddy behavior — differs between TUI and Telegram */\nexport interface BuddyControllerConfig {\n\t/** Max entries in the context buffer (default: 20) */\n\tcontextMaxEntries?: number;\n\t/** Idle timeout in ms before buddy reacts to silence (default: 30000) */\n\tidleTimeoutMs?: number;\n\t/** Minimum ms between reactions (default: 60000) */\n\treactionCooldownMs?: number;\n\t/** If set, pause idle timer when user has been inactive this many ms */\n\tactivityGateMs?: number;\n\t/** If set, cap reactions to this many per hour */\n\treactionsPerHour?: number;\n}\n\n/** Subcommand result for frontend to render */\nexport type BuddyCommandResult =\n\t| { type: \"hatch\"; state: BuddyState }\n\t| { type: \"show\"; state: BuddyState }\n\t| { type: \"reroll\"; state: BuddyState }\n\t| { type: \"pet\" }\n\t| { type: \"stats\"; state: BuddyState }\n\t| { type: \"off\" }\n\t| { type: \"model\"; message: string }\n\t| { type: \"warning\"; message: string }\n\t| { type: \"error\"; message: string };\n\nexport class BuddyController {\n\tprivate contextBuffer: string[] = [];\n\tprivate lastReactionTime = 0;\n\tprivate idleTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate lastActivityTime = 0;\n\tprivate reactionTimestamps: number[] = []; // for budget tracking\n\t/** When false, all active functionality is disabled: no reactions, name-calls,\n\t * idle timer, or Ollama calls. Context capture (passive) still happens. */\n\tenabled = true;\n\n\treadonly manager: BuddyManager;\n\tprivate readonly callbacks: BuddyCallbacks;\n\tprivate readonly config: Required<BuddyControllerConfig>;\n\n\tconstructor(manager: BuddyManager, callbacks: BuddyCallbacks, config?: BuddyControllerConfig) {\n\t\tthis.manager = manager;\n\t\tthis.callbacks = callbacks;\n\t\tthis.config = {\n\t\t\tcontextMaxEntries: config?.contextMaxEntries ?? 20,\n\t\t\tidleTimeoutMs: config?.idleTimeoutMs ?? 30_000,\n\t\t\treactionCooldownMs: config?.reactionCooldownMs ?? 60_000,\n\t\t\tactivityGateMs: config?.activityGateMs ?? 0, // 0 = no gating (TUI default)\n\t\t\treactionsPerHour: config?.reactionsPerHour ?? 0, // 0 = unlimited (TUI default)\n\t\t};\n\t}\n\n\t// =========================================================================\n\t// Context buffer\n\t// =========================================================================\n\n\t/** Append an entry to the buddy context buffer (evicts oldest if at capacity) */\n\tappendContext(entry: string): void {\n\t\tthis.contextBuffer.push(entry);\n\t\tif (this.contextBuffer.length > this.config.contextMaxEntries) {\n\t\t\tthis.contextBuffer.shift();\n\t\t}\n\t}\n\n\t/** Build the context buffer into a string for LLM prompts */\n\tbuildContext(): string {\n\t\tif (this.contextBuffer.length === 0) {\n\t\t\treturn \"No recent activity.\";\n\t\t}\n\t\treturn this.contextBuffer.join(\"\\n\");\n\t}\n\n\t// =========================================================================\n\t// Activity & idle timer\n\t// =========================================================================\n\n\t/** Mark that user activity occurred (for activity gating) */\n\tmarkActivity(): void {\n\t\tthis.lastActivityTime = Date.now();\n\t}\n\n\t/** Reset the idle timer — called on every user message */\n\tresetIdleTimer(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t}\n\n\t\tif (!this.enabled) return;\n\n\t\tif (!this.manager.getState()) return; // No buddy loaded, skip idle timer\n\n\t\t// Activity gating: skip idle timer if user has been inactive too long\n\t\tif (this.config.activityGateMs > 0 && this.lastActivityTime > 0) {\n\t\t\tconst elapsed = Date.now() - this.lastActivityTime;\n\t\t\tif (elapsed > this.config.activityGateMs) {\n\t\t\t\tthis.idleTimer = null;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tthis.idleTimer = setTimeout(() => {\n\t\t\tconst ctx = this.buildContext();\n\t\t\tthis.triggerReaction(`It's been quiet for a moment. Recent activity:\\n${ctx}`).catch(() => {\n\t\t\t\t/* triggerReaction() logs errors internally — prevents unhandled rejection */\n\t\t\t});\n\t\t}, this.config.idleTimeoutMs);\n\t}\n\n\t// =========================================================================\n\t// Reactions\n\t// =========================================================================\n\n\t/** Check if a reaction is allowed under current throttle and budget */\n\tprivate canReact(): boolean {\n\t\tif (!this.enabled) return false;\n\n\t\tconst now = Date.now();\n\n\t\t// Cooldown throttle\n\t\tif (now - this.lastReactionTime < this.config.reactionCooldownMs) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Reaction budget (per hour)\n\t\tif (this.config.reactionsPerHour > 0) {\n\t\t\tconst oneHourAgo = now - 3_600_000;\n\t\t\tthis.reactionTimestamps = this.reactionTimestamps.filter((t) => t > oneHourAgo);\n\t\t\tif (this.reactionTimestamps.length >= this.config.reactionsPerHour) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Trigger a buddy reaction. Throttled by cooldown and budget.\n\t * Calls onThinkingStart/End and onSpeech callbacks.\n\t * No-op if disabled.\n\t */\n\tasync triggerReaction(event: string): Promise<void> {\n\t\tif (!this.canReact()) return;\n\n\t\tthis.callbacks.onThinkingStart();\n\t\ttry {\n\t\t\tconst quip = await this.manager.react(event);\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (quip) {\n\t\t\t\tthis.lastReactionTime = Date.now();\n\t\t\t\tthis.reactionTimestamps.push(Date.now());\n\t\t\t\tthis.callbacks.onSpeech(quip);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tconsole.error(\"[buddy] triggerReaction failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/**\n\t * Handle a name-call from the user.\n\t * No-op if disabled — returns immediately without calling Ollama.\n\t */\n\tasync handleNameCall(userMessage: string): Promise<void> {\n\t\tif (!this.enabled) return;\n\t\tlet state = this.manager.getState();\n\t\tif (!state) {\n\t\t\t// Try loading from disk (buddy may have been hatched in another frontend)\n\t\t\tstate = this.manager.load();\n\t\t}\n\t\tif (!state) return;\n\n\t\tthis.callbacks.onThinkingStart();\n\t\ttry {\n\t\t\tconst response = await this.manager.respondToNameCall(userMessage, this.buildContext());\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (response) {\n\t\t\t\tthis.callbacks.onSpeech(response);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tconsole.error(\"[buddy] handleNameCall failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/** Check if a message contains the buddy's name (word-boundary matching).\n\t * Returns false if disabled. */\n\tdetectNameCall(text: string): boolean {\n\t\tif (!this.enabled) return false;\n\t\tconst name = this.manager.getName();\n\t\tif (!name) return false;\n\t\ttry {\n\t\t\tconst escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t\t\tconst regex = new RegExp(`\\\\b${escaped}\\\\b`, \"i\");\n\t\t\treturn regex.test(text);\n\t\t} catch {\n\t\t\t/* Invalid regex from buddy name — safe to return false */\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Event handling\n\t// =========================================================================\n\n\t/**\n\t * Process an agent event for buddy context capture and reaction triggers.\n\t * The host calls this from its event handler.\n\t *\n\t * Context capture always happens. Reactions are gated by `enabled`.\n\t */\n\thandleEvent(event: { type: string; [key: string]: any }): void {\n\t\tconst state = this.manager.getState();\n\t\tif (!state) return; // No buddy loaded\n\n\t\tswitch (event.type) {\n\t\t\tcase \"message_end\": {\n\t\t\t\tif (event.message?.role === \"assistant\") {\n\t\t\t\t\tconst textParts = event.message.content\n\t\t\t\t\t\t?.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t?.map((c: any) => c.text)\n\t\t\t\t\t\t?.join(\"\")\n\t\t\t\t\t\t?.slice(0, 200);\n\t\t\t\t\tif (textParts) {\n\t\t\t\t\t\tthis.appendContext(`Assistant: ${textParts}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst toolCalls = event.message.content?.filter((c: any) => c.type === \"toolCall\") ?? [];\n\t\t\t\t\tif (toolCalls.length > 0) {\n\t\t\t\t\t\tconst tools = toolCalls.map((c: any) => c.name).join(\", \");\n\t\t\t\t\t\tthis.appendContext(`Called tools: ${tools}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t// Context capture (always)\n\t\t\t\tconst status = event.isError ? \"failed\" : \"completed\";\n\t\t\t\tconst output = event.result?.output || event.result?.content;\n\t\t\t\tconst outputText =\n\t\t\t\t\ttypeof output === \"string\"\n\t\t\t\t\t\t? output.slice(0, 100)\n\t\t\t\t\t\t: Array.isArray(output)\n\t\t\t\t\t\t\t? output\n\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t\t\t.slice(0, 100)\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\tthis.appendContext(`Tool ${event.toolName} ${status}${outputText ? `: ${outputText}` : \"\"}`);\n\n\t\t\t\t// Reaction on error (gated by enabled)\n\t\t\t\tif (event.isError && this.enabled) {\n\t\t\t\t\tlet errorText = \"unknown error\";\n\t\t\t\t\tconst result = event.result;\n\t\t\t\t\tif (result?.content && Array.isArray(result.content)) {\n\t\t\t\t\t\terrorText = result.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t.slice(0, 200);\n\t\t\t\t\t} else if (typeof result?.error === \"string\") {\n\t\t\t\t\t\terrorText = result.error.slice(0, 200);\n\t\t\t\t\t}\n\t\t\t\t\tif (!errorText) errorText = \"unknown error\";\n\t\t\t\t\tthis.triggerReaction(`Tool \"${event.toolName}\" failed: ${errorText}`).catch(() => {\n\t\t\t\t\t\t/* triggerReaction() logs errors internally */\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"agent_end\": {\n\t\t\t\tif (this.enabled) {\n\t\t\t\t\tconst ctx = this.buildContext();\n\t\t\t\t\tthis.triggerReaction(`The agent finished responding. Recent activity:\\n${ctx}`).catch(() => {\n\t\t\t\t\t\t/* triggerReaction() logs errors internally */\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Process a user message — captures context, resets idle, checks name-call.\n\t * Context capture always happens. Active features gated by `enabled`.\n\t * Returns true if a name-call was detected and is being handled.\n\t */\n\tprocessUserMessage(text: string): boolean {\n\t\tthis.appendContext(`User: ${text}`);\n\t\tthis.markActivity();\n\t\tthis.resetIdleTimer();\n\n\t\t// Name-call detection (gated by enabled via detectNameCall)\n\t\tif (this.detectNameCall(text)) {\n\t\t\tthis.handleNameCall(text).catch(() => {\n\t\t\t\t/* handleNameCall() logs errors internally — prevents unhandled rejection */\n\t\t\t});\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t// =========================================================================\n\t// Command handling\n\t// =========================================================================\n\n\t/**\n\t * Handle a /buddy command. Returns a result object for the frontend to render.\n\t * Hatch/reroll are delegated to the frontend via onHatch/onReroll callbacks.\n\t */\n\tasync handleCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tswitch (subcommand) {\n\t\t\tcase \"pet\": {\n\t\t\t\tif (!this.manager.getState()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to pet! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"pet\" };\n\t\t\t}\n\t\t\tcase \"reroll\": {\n\t\t\t\tif (!this.manager.hasStoredBuddy()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to reroll! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst state = await this.callbacks.onReroll(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"reroll\", state };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Reroll failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tconst state = this.manager.getState();\n\t\t\t\tif (!state) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to show stats for! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"stats\", state };\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tthis.enabled = false;\n\t\t\t\tthis.manager.setHidden(true);\n\t\t\t\tthis.stop();\n\t\t\t\treturn { type: \"off\" };\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\t// Handle \"/buddy model\" and \"/buddy model <name>\"\n\t\t\t\tif (subcommand === \"model\" || subcommand.startsWith(\"model \")) {\n\t\t\t\t\treturn this.handleModelCommand(subcommand);\n\t\t\t\t}\n\n\t\t\t\t// No subcommand: hatch or show\n\t\t\t\tconst current = this.manager.getState();\n\t\t\t\tif (current) {\n\t\t\t\t\t// Already showing — just enable and return\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: current };\n\t\t\t\t}\n\n\t\t\t\t// Try to load existing buddy\n\t\t\t\tconst existing = this.manager.load();\n\t\t\t\tif (existing) {\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: existing };\n\t\t\t\t}\n\n\t\t\t\t// Hatch new buddy\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst hatchState = await this.callbacks.onHatch(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\treturn { type: \"hatch\", state: hatchState };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Hatch failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Model selection\n\t// =========================================================================\n\n\t/** Handle /buddy model [name] — show current model or set a new one */\n\tprivate async handleModelCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tconst modelArg = subcommand.slice(\"model\".length).trim();\n\n\t\tif (!modelArg) {\n\t\t\t// \"/buddy model\" with no argument — show current + available\n\t\t\tconst current = this.manager.getOllamaModel();\n\t\t\tconst status = await checkOllama();\n\t\t\tif (!status.available) {\n\t\t\t\treturn { type: \"model\", message: status.error ?? \"Ollama is not available.\" };\n\t\t\t}\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\tif (current) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"model\",\n\t\t\t\t\tmessage: `Current model: ${current}\\n\\nAvailable models:\\n${available}\\n\\nChange with: /buddy model <name>`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\ttype: \"model\",\n\t\t\t\tmessage: `No model set. Choose one with: /buddy model <name>\\n\\nAvailable models:\\n${available}`,\n\t\t\t};\n\t\t}\n\n\t\t// \"/buddy model <name>\" — set the model\n\t\tif (!this.manager.getState() && !this.manager.hasStoredBuddy()) {\n\t\t\treturn { type: \"warning\", message: \"No buddy yet — hatch one first with /buddy, then set a model.\" };\n\t\t}\n\n\t\tconst status = await checkOllama();\n\t\tif (!status.available) {\n\t\t\treturn { type: \"error\", message: status.error ?? \"Ollama is not available.\" };\n\t\t}\n\n\t\t// Check if the model is installed\n\t\tconst match = status.models.find((m) => m === modelArg || m.startsWith(`${modelArg}:`));\n\t\tif (!match) {\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\treturn {\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: `Model \"${modelArg}\" not found. Available models:\\n${available}\\n\\nPull it first with: ollama pull ${modelArg}`,\n\t\t\t};\n\t\t}\n\n\t\tthis.manager.setOllamaModel(match);\n\t\treturn { type: \"model\", message: `Buddy model set to: ${match}` };\n\t}\n\n\t/** Check if an Ollama model is configured, return a nudge message if not */\n\tgetModelNudge(): string | null {\n\t\tif (this.manager.getOllamaModel()) return null;\n\t\treturn \"No Ollama model set — reactions are disabled. Run /buddy model to choose one.\";\n\t}\n\n\t// =========================================================================\n\t// Lifecycle\n\t// =========================================================================\n\n\t/** Start the controller — auto-load buddy if one exists */\n\tstart(): BuddyState | null {\n\t\tconst existing = this.manager.load();\n\t\tif (existing) {\n\t\t\t// If buddy was hidden (via /buddy off), keep it loaded but disabled\n\t\t\tthis.enabled = !existing.hidden;\n\t\t\treturn existing;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/** Stop the controller — clear timers */\n\tstop(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t\tthis.idleTimer = null;\n\t\t}\n\t}\n\n\t/** Full reset — clear context buffer, idle timer, reaction budget.\n\t * Respects persisted hidden state so bridge reconnects don't undo /buddy off. */\n\treset(): void {\n\t\tthis.stop();\n\t\tthis.contextBuffer = [];\n\t\tthis.lastReactionTime = 0;\n\t\tthis.reactionTimestamps = [];\n\t\t// Re-enable unless buddy was explicitly hidden via /buddy off\n\t\tconst state = this.manager.getState() ?? this.manager.load();\n\t\tthis.enabled = state ? !state.hidden : true;\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buddy-manager.d.ts","sourceRoot":"","sources":["../../../src/core/buddy/buddy-manager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAW,KAAK,EAAE,MAAM,UAAU,CAAC;AAQ/C,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,kBAAkB,CAAC;AA4DpF,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC,CAezD;AAaD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIvE;AAiID,qBAAa,YAAY;IACxB,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,YAAY,CAA6B;IAEjD,mDAAmD;IACnD,QAAQ,IAAI,UAAU,GAAG,IAAI,CAE5B;IAED,oCAAoC;IACpC,cAAc,IAAI,OAAO,CAExB;IAED;;;;OAIG;IACH,IAAI,IAAI,UAAU,GAAG,IAAI,CAOxB;IAED;;;OAGG;IACG,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmBzF;IAED;;OAEG;IACG,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmB1F;IAED,iDAAiD;IACjD,OAAO,IAAI,MAAM,GAAG,IAAI,CAEvB;YAMa,UAAU;IAmDxB;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAejD;IAED;;;OAGG;IACG,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgB1F;IAED,+DAA+D;IAC/D,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED,kEAAkE;IAClE,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAWtC;IAED,2EAA2E;IAC3E,gBAAgB,IAAI,IAAI,CAEvB;IAED,kDAAkD;IAClD,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAU/B;CACD","sourcesContent":["/**\n * BuddyManager — Core state machine for the buddy companion.\n *\n * Handles: bone rolling, soul generation, persistence, Ollama availability checks.\n * Bones are deterministic from hash(username + hostname + salt + rerollCount).\n * Soul is LLM-generated once on first hatch and persisted to buddy.json.\n */\n\nimport type { Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { hostname } from \"os\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { createBuddyRng } from \"./buddy-prng.js\";\nimport { rollEyes, rollHat, rollSpecies, rollStats } from \"./buddy-species.js\";\nimport type { BuddyState, CompanionBones, StoredCompanion } from \"./buddy-types.js\";\nimport { STAT_NAMES } from \"./buddy-types.js\";\n\nconst BUDDY_SALT = \"dreb-buddy-v1\";\nconst BUDDY_FILENAME = \"buddy.json\";\nconst DEFAULT_BACKSTORY = \"A mysterious past shrouded in legend.\";\n\n/** Base Ollama model config — id/name are set dynamically from available models */\nconst OLLAMA_MODEL_BASE: Omit<Model<\"openai-completions\">, \"id\" | \"name\"> = {\n\tapi: \"openai-completions\",\n\tprovider: \"ollama\",\n\tbaseUrl: \"http://localhost:11434/v1\",\n\treasoning: false,\n\tinput: [\"text\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 2048,\n\tcompat: {\n\t\tsupportsDeveloperRole: false,\n\t\tsupportsReasoningEffort: false,\n\t},\n};\n\n/** Max words for buddy response before truncation */\nconst MAX_RESPONSE_WORDS = 60;\n\n/** Prompt for soul generation (uses parent LLM, not Ollama) */\nconst SOUL_GENERATION_PROMPT = `You are generating a companion character for a coding assistant terminal app. Based on the species, rarity, and stats below, generate a creative name, a one-sentence personality description, and a funny fictional backstory.\n\nSpecies: {species}\nRarity: {rarity}\nStats: {stats}\nShiny: {shiny}\n\nThe name must NOT be a common English word, programming keyword, tool name, or command. It should be unique and distinctive — a proper noun that won't appear in normal conversation. The name must be 4-8 characters and easy to type on a QWERTY keyboard — use only common letters (a-z, avoid q, x, z, j). Do not use species name as the name.\n\nRespond in EXACTLY this format:\nNAME: <name>\nPERSONALITY: <one sentence personality>\nBACKSTORY: <2-3 sentence elaborate fictional backstory — funny, absurd, or dramatic. Include specific events, places, former occupations>`;\n\n/** Prompt for buddy reactions via Ollama */\nconst REACTION_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nSomething just happened. React with a short, in-character quip based on the context below. Be specific — reference what actually happened, not just that something happened. Max 20 words. No quotes, no prefixes, just the quip.\n\nContext:\n{event}`;\n\nconst NAME_CALL_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nThe user just said: \"{message}\"\nRecent context: {context}\n\nRespond to what the user said directly. Be in-character, reference your backstory occasionally. Max 30 words. No quotes, no prefixes, just your response.`;\n\n// =============================================================================\n// Ollama availability\n// =============================================================================\n\nexport interface OllamaStatus {\n\tavailable: boolean;\n\tmodels: string[];\n\terror?: string;\n}\n\n/**\n * Check if Ollama is running and has models available.\n * Uses the /api/tags endpoint.\n */\nexport async function checkOllama(): Promise<OllamaStatus> {\n\ttry {\n\t\tconst res = await fetch(\"http://localhost:11434/api/tags\", { signal: AbortSignal.timeout(3000) });\n\t\tif (!res.ok) {\n\t\t\treturn { available: false, models: [], error: `Ollama returned ${res.status}` };\n\t\t}\n\t\tconst data = (await res.json()) as { models?: { name: string }[] };\n\t\tconst models = (data.models ?? []).map((m) => m.name);\n\t\tif (models.length === 0) {\n\t\t\treturn { available: false, models: [], error: \"No models installed. Run: ollama pull llama3.2\" };\n\t\t}\n\t\treturn { available: true, models };\n\t} catch {\n\t\treturn { available: false, models: [], error: \"Ollama is not running. Start it with: ollama serve\" };\n\t}\n}\n\n/**\n * Pick the Ollama model for the buddy.\n * Returns the stored model name if it's available, otherwise null.\n */\nfunction pickOllamaModel(storedModel: string | undefined, availableModels: string[]): string | null {\n\tif (!storedModel) return null;\n\t// Check if the stored model is installed (exact match or prefix match without tag)\n\tconst match = availableModels.find((m) => m === storedModel || m.startsWith(`${storedModel}:`));\n\treturn match ?? null;\n}\n\n/**\n * Truncate response to a maximum word count, appending \"...[truncated]\" if exceeded.\n */\nexport function truncateResponse(text: string, maxWords: number): string {\n\tconst words = text.split(/\\s+/);\n\tif (words.length <= maxWords) return text;\n\treturn `${words.slice(0, maxWords).join(\" \")} ...[truncated]`;\n}\n\n// =============================================================================\n// Persistence\n// =============================================================================\n\nfunction getBuddyPath(): string {\n\treturn join(getAgentDir(), BUDDY_FILENAME);\n}\n\nfunction loadStored(): StoredCompanion | null {\n\tconst path = getBuddyPath();\n\tif (!existsSync(path)) return null;\n\ttry {\n\t\tconst data = JSON.parse(readFileSync(path, \"utf-8\"));\n\t\t// Validate required fields\n\t\tif (\n\t\t\ttypeof data.rerollCount === \"number\" &&\n\t\t\ttypeof data.name === \"string\" &&\n\t\t\ttypeof data.personality === \"string\"\n\t\t) {\n\t\t\treturn {\n\t\t\t\trerollCount: data.rerollCount,\n\t\t\t\tname: data.name,\n\t\t\t\tpersonality: data.personality,\n\t\t\t\tbackstory: typeof data.backstory === \"string\" ? data.backstory : DEFAULT_BACKSTORY,\n\t\t\t\thatchedAt: data.hatchedAt ?? new Date().toISOString(),\n\t\t\t\t...(data.hidden !== undefined ? { hidden: data.hidden } : {}),\n\t\t\t\t...(typeof data.ollamaModel === \"string\" ? { ollamaModel: data.ollamaModel } : {}),\n\t\t\t};\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction saveStored(stored: StoredCompanion): void {\n\tconst path = getBuddyPath();\n\tconst dir = join(path, \"..\");\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\twriteFileSync(path, JSON.stringify(stored, null, 2));\n}\n\n// =============================================================================\n// Bone rolling\n// =============================================================================\n\nfunction rollBones(rerollCount: number): CompanionBones {\n\tconst username = process.env.USER ?? process.env.LOGNAME ?? \"user\";\n\tconst host = hostname();\n\tconst rng = createBuddyRng(username, host, BUDDY_SALT, rerollCount);\n\n\t// Roll species + rarity\n\tconst { species, rarity } = rollSpecies(rng);\n\n\t// Roll shiny (1% chance)\n\tconst shiny = rng() < 0.01;\n\n\t// Roll eyes and hat\n\tconst eyes = rollEyes(rng);\n\tconst hat = rollHat(rng);\n\n\t// Roll stats\n\tconst stats = rollStats(rng, rarity);\n\n\treturn { species, rarity, shiny, stats, eyeStyle: eyes, hat };\n}\n\n// =============================================================================\n// Soul generation\n// =============================================================================\n\n/**\n * Generate a soul (name + personality + backstory) using the parent LLM.\n * Only called on first hatch or reroll.\n */\nasync function generateSoul(\n\tbones: CompanionBones,\n\tparentModel: Model<\"openai-completions\">,\n\tapiKey: string,\n): Promise<{ name: string; personality: string; backstory: string }> {\n\tconst statsStr = STAT_NAMES.map((s) => `${s}: ${bones.stats[s]}`).join(\", \");\n\tconst prompt = SOUL_GENERATION_PROMPT.replace(\"{species}\", bones.species)\n\t\t.replace(\"{rarity}\", bones.rarity)\n\t\t.replace(\"{stats}\", statsStr)\n\t\t.replace(\"{shiny}\", bones.shiny ? \"YES ✨\" : \"no\");\n\n\tconst context: Context = {\n\t\tsystemPrompt: \"Generate a companion character. Respond in the exact format requested.\",\n\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t};\n\n\ttry {\n\t\tconst response = await completeSimple(parentModel, context, { apiKey });\n\t\tconst text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\n\t\t// Parse NAME: ... and PERSONALITY: ... and BACKSTORY: ...\n\t\tconst nameMatch = text.match(/NAME:\\s*(.+)/i);\n\t\tconst personalityMatch = text.match(/PERSONALITY:\\s*(.+)/i);\n\t\tconst backstoryMatch = text.match(/BACKSTORY:\\s*([\\s\\S]+)/i);\n\n\t\tlet name = nameMatch?.[1]?.trim() ?? bones.species;\n\t\tconst personality = personalityMatch?.[1]?.trim() ?? `A ${bones.rarity} ${bones.species} companion.`;\n\t\tconst backstory = backstoryMatch?.[1]?.trim() ?? DEFAULT_BACKSTORY;\n\n\t\t// Enforce name length\n\t\tif (name.length > 8) name = name.slice(0, 8);\n\n\t\treturn { name, personality, backstory };\n\t} catch {\n\t\t// Fallback if LLM fails\n\t\treturn {\n\t\t\tname: bones.species,\n\t\t\tpersonality: `A ${bones.rarity} ${bones.species} companion.`,\n\t\t\tbackstory: DEFAULT_BACKSTORY,\n\t\t};\n\t}\n}\n\n// =============================================================================\n// BuddyManager\n// =============================================================================\n\nexport class BuddyManager {\n\tprivate state: BuddyState | null = null;\n\tprivate ollamaStatus: OllamaStatus | null = null;\n\n\t/** Get current buddy state (null if not loaded) */\n\tgetState(): BuddyState | null {\n\t\treturn this.state;\n\t}\n\n\t/** Check if buddy exists on disk */\n\thasStoredBuddy(): boolean {\n\t\treturn loadStored() !== null;\n\t}\n\n\t/**\n\t * Load or create buddy state.\n\t * If stored buddy exists, loads soul and re-rolls bones.\n\t * If no stored buddy, returns null (need to hatch first).\n\t */\n\tload(): BuddyState | null {\n\t\tconst stored = loadStored();\n\t\tif (!stored) return null;\n\n\t\tconst bones = rollBones(stored.rerollCount);\n\t\tthis.state = { ...bones, ...stored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Hatch a new buddy. Generates bones, then uses parent LLM for soul.\n\t * Returns the new state.\n\t */\n\tasync hatch(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst rerollCount = stored?.rerollCount ?? 0;\n\n\t\tconst bones = rollBones(rerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Reroll the buddy — new bones + new soul.\n\t */\n\tasync reroll(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst newRerollCount = (stored?.rerollCount ?? 0) + 1;\n\n\t\tconst bones = rollBones(newRerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount: newRerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/** Get buddy's name (for name-call detection) */\n\tgetName(): string | null {\n\t\treturn this.state?.name ?? loadStored()?.name ?? null;\n\t}\n\n\t/**\n\t * Shared Ollama chat helper. Checks availability, picks model, runs completion.\n\t * Returns the response text, or null if Ollama is unavailable or no model configured.\n\t */\n\tprivate async ollamaChat(context: Context): Promise<string | null> {\n\t\t// Check Ollama lazily, retry if previously unavailable\n\t\tif (!this.ollamaStatus || !this.ollamaStatus.available) {\n\t\t\tthis.ollamaStatus = await checkOllama();\n\t\t}\n\t\tif (!this.ollamaStatus.available) return null;\n\n\t\tconst modelName = pickOllamaModel(this.state?.ollamaModel, this.ollamaStatus.models);\n\t\tif (!modelName) return null;\n\t\tconst model: Model<\"openai-completions\"> = {\n\t\t\t...OLLAMA_MODEL_BASE,\n\t\t\tid: modelName,\n\t\t\tname: `${modelName} (Ollama)`,\n\t\t};\n\n\t\tlet response: import(\"@dreb/ai\").AssistantMessage;\n\t\ttry {\n\t\t\tresponse = await completeSimple(model, context, {\n\t\t\t\tapiKey: \"ollama\",\n\t\t\t\tsignal: AbortSignal.timeout(120000),\n\t\t\t});\n\t\t} catch {\n\t\t\t// Safety net for unexpected sync errors (e.g. provider not found).\n\t\t\t// Normal runtime errors (timeout, connection) are handled via stopReason below.\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Connection error — invalidate cache so next attempt re-checks Ollama\n\t\tif (response.stopReason === \"error\") {\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Timeout or abort — preserve cache (model is just slow, Ollama is fine)\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\")\n\t\t\t.trim();\n\n\t\t// Truncate overly long responses\n\t\ttext = truncateResponse(text, MAX_RESPONSE_WORDS);\n\n\t\treturn text || null;\n\t}\n\n\t/**\n\t * Generate a reaction to an event using Ollama.\n\t * Returns null if Ollama is unavailable.\n\t */\n\tasync react(event: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = REACTION_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{event}\", event);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short in-character quip. Max 20 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/**\n\t * Respond to the user calling the buddy's name.\n\t * Uses Ollama for the response.\n\t */\n\tasync respondToNameCall(userMessage: string, recentContext: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = NAME_CALL_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{message}\", userMessage)\n\t\t\t.replace(\"{context}\", recentContext);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short friendly greeting. Max 30 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/** Get the configured Ollama model name, or null if not set */\n\tgetOllamaModel(): string | null {\n\t\treturn this.state?.ollamaModel ?? loadStored()?.ollamaModel ?? null;\n\t}\n\n\t/** Set the Ollama model for buddy reactions. Persists to disk. */\n\tsetOllamaModel(modelName: string): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.ollamaModel = modelName;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\tif (this.state) {\n\t\t\tthis.state.ollamaModel = modelName;\n\t\t}\n\t\t// Invalidate Ollama status cache so next call picks up the new model\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Reset Ollama status cache (e.g. after detecting it became available) */\n\tresetOllamaCache(): void {\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Update the hidden flag in persisted storage */\n\tsetHidden(hidden: boolean): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.hidden = hidden;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\t// Keep in-memory state in sync so reset() reads current hidden flag\n\t\tif (this.state) {\n\t\t\tthis.state.hidden = hidden;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buddy-manager.d.ts","sourceRoot":"","sources":["../../../src/core/buddy/buddy-manager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAW,KAAK,EAAE,MAAM,UAAU,CAAC;AAQ/C,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,kBAAkB,CAAC;AA4DpF,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC,CAgBzD;AAaD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIvE;AAkID,qBAAa,YAAY;IACxB,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,YAAY,CAA6B;IAEjD,mDAAmD;IACnD,QAAQ,IAAI,UAAU,GAAG,IAAI,CAE5B;IAED,oCAAoC;IACpC,cAAc,IAAI,OAAO,CAExB;IAED;;;;OAIG;IACH,IAAI,IAAI,UAAU,GAAG,IAAI,CAOxB;IAED;;;OAGG;IACG,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmBzF;IAED;;OAEG;IACG,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmB1F;IAED,iDAAiD;IACjD,OAAO,IAAI,MAAM,GAAG,IAAI,CAEvB;YAMa,UAAU;IAmDxB;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAejD;IAED;;;OAGG;IACG,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgB1F;IAED,+DAA+D;IAC/D,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED,kEAAkE;IAClE,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAWtC;IAED,2EAA2E;IAC3E,gBAAgB,IAAI,IAAI,CAEvB;IAED,kDAAkD;IAClD,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAU/B;CACD","sourcesContent":["/**\n * BuddyManager — Core state machine for the buddy companion.\n *\n * Handles: bone rolling, soul generation, persistence, Ollama availability checks.\n * Bones are deterministic from hash(username + hostname + salt + rerollCount).\n * Soul is LLM-generated once on first hatch and persisted to buddy.json.\n */\n\nimport type { Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { hostname } from \"os\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { createBuddyRng } from \"./buddy-prng.js\";\nimport { rollEyes, rollHat, rollSpecies, rollStats } from \"./buddy-species.js\";\nimport type { BuddyState, CompanionBones, StoredCompanion } from \"./buddy-types.js\";\nimport { STAT_NAMES } from \"./buddy-types.js\";\n\nconst BUDDY_SALT = \"dreb-buddy-v1\";\nconst BUDDY_FILENAME = \"buddy.json\";\nconst DEFAULT_BACKSTORY = \"A mysterious past shrouded in legend.\";\n\n/** Base Ollama model config — id/name are set dynamically from available models */\nconst OLLAMA_MODEL_BASE: Omit<Model<\"openai-completions\">, \"id\" | \"name\"> = {\n\tapi: \"openai-completions\",\n\tprovider: \"ollama\",\n\tbaseUrl: \"http://localhost:11434/v1\",\n\treasoning: false,\n\tinput: [\"text\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 2048,\n\tcompat: {\n\t\tsupportsDeveloperRole: false,\n\t\tsupportsReasoningEffort: false,\n\t},\n};\n\n/** Max words for buddy response before truncation */\nconst MAX_RESPONSE_WORDS = 60;\n\n/** Prompt for soul generation (uses parent LLM, not Ollama) */\nconst SOUL_GENERATION_PROMPT = `You are generating a companion character for a coding assistant terminal app. Based on the species, rarity, and stats below, generate a creative name, a one-sentence personality description, and a funny fictional backstory.\n\nSpecies: {species}\nRarity: {rarity}\nStats: {stats}\nShiny: {shiny}\n\nThe name must NOT be a common English word, programming keyword, tool name, or command. It should be unique and distinctive — a proper noun that won't appear in normal conversation. The name must be 4-8 characters and easy to type on a QWERTY keyboard — use only common letters (a-z, avoid q, x, z, j). Do not use species name as the name.\n\nRespond in EXACTLY this format:\nNAME: <name>\nPERSONALITY: <one sentence personality>\nBACKSTORY: <2-3 sentence elaborate fictional backstory — funny, absurd, or dramatic. Include specific events, places, former occupations>`;\n\n/** Prompt for buddy reactions via Ollama */\nconst REACTION_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nSomething just happened. React with a short, in-character quip based on the context below. Be specific — reference what actually happened, not just that something happened. Max 20 words. No quotes, no prefixes, just the quip.\n\nContext:\n{event}`;\n\nconst NAME_CALL_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nThe user just said: \"{message}\"\nRecent context: {context}\n\nRespond to what the user said directly. Be in-character, reference your backstory occasionally. Max 30 words. No quotes, no prefixes, just your response.`;\n\n// =============================================================================\n// Ollama availability\n// =============================================================================\n\nexport interface OllamaStatus {\n\tavailable: boolean;\n\tmodels: string[];\n\terror?: string;\n}\n\n/**\n * Check if Ollama is running and has models available.\n * Uses the /api/tags endpoint.\n */\nexport async function checkOllama(): Promise<OllamaStatus> {\n\ttry {\n\t\tconst res = await fetch(\"http://localhost:11434/api/tags\", { signal: AbortSignal.timeout(3000) });\n\t\tif (!res.ok) {\n\t\t\treturn { available: false, models: [], error: `Ollama returned ${res.status}` };\n\t\t}\n\t\tconst data = (await res.json()) as { models?: { name: string }[] };\n\t\tconst models = (data.models ?? []).map((m) => m.name);\n\t\tif (models.length === 0) {\n\t\t\treturn { available: false, models: [], error: \"No models installed. Run: ollama pull llama3.2\" };\n\t\t}\n\t\treturn { available: true, models };\n\t} catch {\n\t\t/* Ollama not running or unreachable — report as unavailable */\n\t\treturn { available: false, models: [], error: \"Ollama is not running. Start it with: ollama serve\" };\n\t}\n}\n\n/**\n * Pick the Ollama model for the buddy.\n * Returns the stored model name if it's available, otherwise null.\n */\nfunction pickOllamaModel(storedModel: string | undefined, availableModels: string[]): string | null {\n\tif (!storedModel) return null;\n\t// Check if the stored model is installed (exact match or prefix match without tag)\n\tconst match = availableModels.find((m) => m === storedModel || m.startsWith(`${storedModel}:`));\n\treturn match ?? null;\n}\n\n/**\n * Truncate response to a maximum word count, appending \"...[truncated]\" if exceeded.\n */\nexport function truncateResponse(text: string, maxWords: number): string {\n\tconst words = text.split(/\\s+/);\n\tif (words.length <= maxWords) return text;\n\treturn `${words.slice(0, maxWords).join(\" \")} ...[truncated]`;\n}\n\n// =============================================================================\n// Persistence\n// =============================================================================\n\nfunction getBuddyPath(): string {\n\treturn join(getAgentDir(), BUDDY_FILENAME);\n}\n\nfunction loadStored(): StoredCompanion | null {\n\tconst path = getBuddyPath();\n\tif (!existsSync(path)) return null;\n\ttry {\n\t\tconst data = JSON.parse(readFileSync(path, \"utf-8\"));\n\t\t// Validate required fields\n\t\tif (\n\t\t\ttypeof data.rerollCount === \"number\" &&\n\t\t\ttypeof data.name === \"string\" &&\n\t\t\ttypeof data.personality === \"string\"\n\t\t) {\n\t\t\treturn {\n\t\t\t\trerollCount: data.rerollCount,\n\t\t\t\tname: data.name,\n\t\t\t\tpersonality: data.personality,\n\t\t\t\tbackstory: typeof data.backstory === \"string\" ? data.backstory : DEFAULT_BACKSTORY,\n\t\t\t\thatchedAt: data.hatchedAt ?? new Date().toISOString(),\n\t\t\t\t...(data.hidden !== undefined ? { hidden: data.hidden } : {}),\n\t\t\t\t...(typeof data.ollamaModel === \"string\" ? { ollamaModel: data.ollamaModel } : {}),\n\t\t\t};\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\t/* Corrupt or missing companion file — return null to trigger fresh creation */\n\t\treturn null;\n\t}\n}\n\nfunction saveStored(stored: StoredCompanion): void {\n\tconst path = getBuddyPath();\n\tconst dir = join(path, \"..\");\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\twriteFileSync(path, JSON.stringify(stored, null, 2));\n}\n\n// =============================================================================\n// Bone rolling\n// =============================================================================\n\nfunction rollBones(rerollCount: number): CompanionBones {\n\tconst username = process.env.USER ?? process.env.LOGNAME ?? \"user\";\n\tconst host = hostname();\n\tconst rng = createBuddyRng(username, host, BUDDY_SALT, rerollCount);\n\n\t// Roll species + rarity\n\tconst { species, rarity } = rollSpecies(rng);\n\n\t// Roll shiny (1% chance)\n\tconst shiny = rng() < 0.01;\n\n\t// Roll eyes and hat\n\tconst eyes = rollEyes(rng);\n\tconst hat = rollHat(rng);\n\n\t// Roll stats\n\tconst stats = rollStats(rng, rarity);\n\n\treturn { species, rarity, shiny, stats, eyeStyle: eyes, hat };\n}\n\n// =============================================================================\n// Soul generation\n// =============================================================================\n\n/**\n * Generate a soul (name + personality + backstory) using the parent LLM.\n * Only called on first hatch or reroll.\n */\nasync function generateSoul(\n\tbones: CompanionBones,\n\tparentModel: Model<\"openai-completions\">,\n\tapiKey: string,\n): Promise<{ name: string; personality: string; backstory: string }> {\n\tconst statsStr = STAT_NAMES.map((s) => `${s}: ${bones.stats[s]}`).join(\", \");\n\tconst prompt = SOUL_GENERATION_PROMPT.replace(\"{species}\", bones.species)\n\t\t.replace(\"{rarity}\", bones.rarity)\n\t\t.replace(\"{stats}\", statsStr)\n\t\t.replace(\"{shiny}\", bones.shiny ? \"YES ✨\" : \"no\");\n\n\tconst context: Context = {\n\t\tsystemPrompt: \"Generate a companion character. Respond in the exact format requested.\",\n\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t};\n\n\ttry {\n\t\tconst response = await completeSimple(parentModel, context, { apiKey });\n\t\tconst text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\n\t\t// Parse NAME: ... and PERSONALITY: ... and BACKSTORY: ...\n\t\tconst nameMatch = text.match(/NAME:\\s*(.+)/i);\n\t\tconst personalityMatch = text.match(/PERSONALITY:\\s*(.+)/i);\n\t\tconst backstoryMatch = text.match(/BACKSTORY:\\s*([\\s\\S]+)/i);\n\n\t\tlet name = nameMatch?.[1]?.trim() ?? bones.species;\n\t\tconst personality = personalityMatch?.[1]?.trim() ?? `A ${bones.rarity} ${bones.species} companion.`;\n\t\tconst backstory = backstoryMatch?.[1]?.trim() ?? DEFAULT_BACKSTORY;\n\n\t\t// Enforce name length\n\t\tif (name.length > 8) name = name.slice(0, 8);\n\n\t\treturn { name, personality, backstory };\n\t} catch {\n\t\t// Fallback if LLM fails\n\t\treturn {\n\t\t\tname: bones.species,\n\t\t\tpersonality: `A ${bones.rarity} ${bones.species} companion.`,\n\t\t\tbackstory: DEFAULT_BACKSTORY,\n\t\t};\n\t}\n}\n\n// =============================================================================\n// BuddyManager\n// =============================================================================\n\nexport class BuddyManager {\n\tprivate state: BuddyState | null = null;\n\tprivate ollamaStatus: OllamaStatus | null = null;\n\n\t/** Get current buddy state (null if not loaded) */\n\tgetState(): BuddyState | null {\n\t\treturn this.state;\n\t}\n\n\t/** Check if buddy exists on disk */\n\thasStoredBuddy(): boolean {\n\t\treturn loadStored() !== null;\n\t}\n\n\t/**\n\t * Load or create buddy state.\n\t * If stored buddy exists, loads soul and re-rolls bones.\n\t * If no stored buddy, returns null (need to hatch first).\n\t */\n\tload(): BuddyState | null {\n\t\tconst stored = loadStored();\n\t\tif (!stored) return null;\n\n\t\tconst bones = rollBones(stored.rerollCount);\n\t\tthis.state = { ...bones, ...stored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Hatch a new buddy. Generates bones, then uses parent LLM for soul.\n\t * Returns the new state.\n\t */\n\tasync hatch(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst rerollCount = stored?.rerollCount ?? 0;\n\n\t\tconst bones = rollBones(rerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Reroll the buddy — new bones + new soul.\n\t */\n\tasync reroll(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst newRerollCount = (stored?.rerollCount ?? 0) + 1;\n\n\t\tconst bones = rollBones(newRerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount: newRerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/** Get buddy's name (for name-call detection) */\n\tgetName(): string | null {\n\t\treturn this.state?.name ?? loadStored()?.name ?? null;\n\t}\n\n\t/**\n\t * Shared Ollama chat helper. Checks availability, picks model, runs completion.\n\t * Returns the response text, or null if Ollama is unavailable or no model configured.\n\t */\n\tprivate async ollamaChat(context: Context): Promise<string | null> {\n\t\t// Check Ollama lazily, retry if previously unavailable\n\t\tif (!this.ollamaStatus || !this.ollamaStatus.available) {\n\t\t\tthis.ollamaStatus = await checkOllama();\n\t\t}\n\t\tif (!this.ollamaStatus.available) return null;\n\n\t\tconst modelName = pickOllamaModel(this.state?.ollamaModel, this.ollamaStatus.models);\n\t\tif (!modelName) return null;\n\t\tconst model: Model<\"openai-completions\"> = {\n\t\t\t...OLLAMA_MODEL_BASE,\n\t\t\tid: modelName,\n\t\t\tname: `${modelName} (Ollama)`,\n\t\t};\n\n\t\tlet response: import(\"@dreb/ai\").AssistantMessage;\n\t\ttry {\n\t\t\tresponse = await completeSimple(model, context, {\n\t\t\t\tapiKey: \"ollama\",\n\t\t\t\tsignal: AbortSignal.timeout(120000),\n\t\t\t});\n\t\t} catch {\n\t\t\t// Safety net for unexpected sync errors (e.g. provider not found).\n\t\t\t// Normal runtime errors (timeout, connection) are handled via stopReason below.\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Connection error — invalidate cache so next attempt re-checks Ollama\n\t\tif (response.stopReason === \"error\") {\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Timeout or abort — preserve cache (model is just slow, Ollama is fine)\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\")\n\t\t\t.trim();\n\n\t\t// Truncate overly long responses\n\t\ttext = truncateResponse(text, MAX_RESPONSE_WORDS);\n\n\t\treturn text || null;\n\t}\n\n\t/**\n\t * Generate a reaction to an event using Ollama.\n\t * Returns null if Ollama is unavailable.\n\t */\n\tasync react(event: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = REACTION_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{event}\", event);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short in-character quip. Max 20 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/**\n\t * Respond to the user calling the buddy's name.\n\t * Uses Ollama for the response.\n\t */\n\tasync respondToNameCall(userMessage: string, recentContext: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = NAME_CALL_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{message}\", userMessage)\n\t\t\t.replace(\"{context}\", recentContext);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short friendly greeting. Max 30 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/** Get the configured Ollama model name, or null if not set */\n\tgetOllamaModel(): string | null {\n\t\treturn this.state?.ollamaModel ?? loadStored()?.ollamaModel ?? null;\n\t}\n\n\t/** Set the Ollama model for buddy reactions. Persists to disk. */\n\tsetOllamaModel(modelName: string): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.ollamaModel = modelName;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\tif (this.state) {\n\t\t\tthis.state.ollamaModel = modelName;\n\t\t}\n\t\t// Invalidate Ollama status cache so next call picks up the new model\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Reset Ollama status cache (e.g. after detecting it became available) */\n\tresetOllamaCache(): void {\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Update the hidden flag in persisted storage */\n\tsetHidden(hidden: boolean): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.hidden = hidden;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\t// Keep in-memory state in sync so reset() reads current hidden flag\n\t\tif (this.state) {\n\t\t\tthis.state.hidden = hidden;\n\t\t}\n\t}\n}\n"]}
|
|
@@ -78,6 +78,7 @@ export async function checkOllama() {
|
|
|
78
78
|
return { available: true, models };
|
|
79
79
|
}
|
|
80
80
|
catch {
|
|
81
|
+
/* Ollama not running or unreachable — report as unavailable */
|
|
81
82
|
return { available: false, models: [], error: "Ollama is not running. Start it with: ollama serve" };
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -130,6 +131,7 @@ function loadStored() {
|
|
|
130
131
|
return null;
|
|
131
132
|
}
|
|
132
133
|
catch {
|
|
134
|
+
/* Corrupt or missing companion file — return null to trigger fresh creation */
|
|
133
135
|
return null;
|
|
134
136
|
}
|
|
135
137
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buddy-manager.js","sourceRoot":"","sources":["../../../src/core/buddy/buddy-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BuddyManager — Core state machine for the buddy companion.\n *\n * Handles: bone rolling, soul generation, persistence, Ollama availability checks.\n * Bones are deterministic from hash(username + hostname + salt + rerollCount).\n * Soul is LLM-generated once on first hatch and persisted to buddy.json.\n */\n\nimport type { Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { hostname } from \"os\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { createBuddyRng } from \"./buddy-prng.js\";\nimport { rollEyes, rollHat, rollSpecies, rollStats } from \"./buddy-species.js\";\nimport type { BuddyState, CompanionBones, StoredCompanion } from \"./buddy-types.js\";\nimport { STAT_NAMES } from \"./buddy-types.js\";\n\nconst BUDDY_SALT = \"dreb-buddy-v1\";\nconst BUDDY_FILENAME = \"buddy.json\";\nconst DEFAULT_BACKSTORY = \"A mysterious past shrouded in legend.\";\n\n/** Base Ollama model config — id/name are set dynamically from available models */\nconst OLLAMA_MODEL_BASE: Omit<Model<\"openai-completions\">, \"id\" | \"name\"> = {\n\tapi: \"openai-completions\",\n\tprovider: \"ollama\",\n\tbaseUrl: \"http://localhost:11434/v1\",\n\treasoning: false,\n\tinput: [\"text\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 2048,\n\tcompat: {\n\t\tsupportsDeveloperRole: false,\n\t\tsupportsReasoningEffort: false,\n\t},\n};\n\n/** Max words for buddy response before truncation */\nconst MAX_RESPONSE_WORDS = 60;\n\n/** Prompt for soul generation (uses parent LLM, not Ollama) */\nconst SOUL_GENERATION_PROMPT = `You are generating a companion character for a coding assistant terminal app. Based on the species, rarity, and stats below, generate a creative name, a one-sentence personality description, and a funny fictional backstory.\n\nSpecies: {species}\nRarity: {rarity}\nStats: {stats}\nShiny: {shiny}\n\nThe name must NOT be a common English word, programming keyword, tool name, or command. It should be unique and distinctive — a proper noun that won't appear in normal conversation. The name must be 4-8 characters and easy to type on a QWERTY keyboard — use only common letters (a-z, avoid q, x, z, j). Do not use species name as the name.\n\nRespond in EXACTLY this format:\nNAME: <name>\nPERSONALITY: <one sentence personality>\nBACKSTORY: <2-3 sentence elaborate fictional backstory — funny, absurd, or dramatic. Include specific events, places, former occupations>`;\n\n/** Prompt for buddy reactions via Ollama */\nconst REACTION_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nSomething just happened. React with a short, in-character quip based on the context below. Be specific — reference what actually happened, not just that something happened. Max 20 words. No quotes, no prefixes, just the quip.\n\nContext:\n{event}`;\n\nconst NAME_CALL_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nThe user just said: \"{message}\"\nRecent context: {context}\n\nRespond to what the user said directly. Be in-character, reference your backstory occasionally. Max 30 words. No quotes, no prefixes, just your response.`;\n\n// =============================================================================\n// Ollama availability\n// =============================================================================\n\nexport interface OllamaStatus {\n\tavailable: boolean;\n\tmodels: string[];\n\terror?: string;\n}\n\n/**\n * Check if Ollama is running and has models available.\n * Uses the /api/tags endpoint.\n */\nexport async function checkOllama(): Promise<OllamaStatus> {\n\ttry {\n\t\tconst res = await fetch(\"http://localhost:11434/api/tags\", { signal: AbortSignal.timeout(3000) });\n\t\tif (!res.ok) {\n\t\t\treturn { available: false, models: [], error: `Ollama returned ${res.status}` };\n\t\t}\n\t\tconst data = (await res.json()) as { models?: { name: string }[] };\n\t\tconst models = (data.models ?? []).map((m) => m.name);\n\t\tif (models.length === 0) {\n\t\t\treturn { available: false, models: [], error: \"No models installed. Run: ollama pull llama3.2\" };\n\t\t}\n\t\treturn { available: true, models };\n\t} catch {\n\t\treturn { available: false, models: [], error: \"Ollama is not running. Start it with: ollama serve\" };\n\t}\n}\n\n/**\n * Pick the Ollama model for the buddy.\n * Returns the stored model name if it's available, otherwise null.\n */\nfunction pickOllamaModel(storedModel: string | undefined, availableModels: string[]): string | null {\n\tif (!storedModel) return null;\n\t// Check if the stored model is installed (exact match or prefix match without tag)\n\tconst match = availableModels.find((m) => m === storedModel || m.startsWith(`${storedModel}:`));\n\treturn match ?? null;\n}\n\n/**\n * Truncate response to a maximum word count, appending \"...[truncated]\" if exceeded.\n */\nexport function truncateResponse(text: string, maxWords: number): string {\n\tconst words = text.split(/\\s+/);\n\tif (words.length <= maxWords) return text;\n\treturn `${words.slice(0, maxWords).join(\" \")} ...[truncated]`;\n}\n\n// =============================================================================\n// Persistence\n// =============================================================================\n\nfunction getBuddyPath(): string {\n\treturn join(getAgentDir(), BUDDY_FILENAME);\n}\n\nfunction loadStored(): StoredCompanion | null {\n\tconst path = getBuddyPath();\n\tif (!existsSync(path)) return null;\n\ttry {\n\t\tconst data = JSON.parse(readFileSync(path, \"utf-8\"));\n\t\t// Validate required fields\n\t\tif (\n\t\t\ttypeof data.rerollCount === \"number\" &&\n\t\t\ttypeof data.name === \"string\" &&\n\t\t\ttypeof data.personality === \"string\"\n\t\t) {\n\t\t\treturn {\n\t\t\t\trerollCount: data.rerollCount,\n\t\t\t\tname: data.name,\n\t\t\t\tpersonality: data.personality,\n\t\t\t\tbackstory: typeof data.backstory === \"string\" ? data.backstory : DEFAULT_BACKSTORY,\n\t\t\t\thatchedAt: data.hatchedAt ?? new Date().toISOString(),\n\t\t\t\t...(data.hidden !== undefined ? { hidden: data.hidden } : {}),\n\t\t\t\t...(typeof data.ollamaModel === \"string\" ? { ollamaModel: data.ollamaModel } : {}),\n\t\t\t};\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction saveStored(stored: StoredCompanion): void {\n\tconst path = getBuddyPath();\n\tconst dir = join(path, \"..\");\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\twriteFileSync(path, JSON.stringify(stored, null, 2));\n}\n\n// =============================================================================\n// Bone rolling\n// =============================================================================\n\nfunction rollBones(rerollCount: number): CompanionBones {\n\tconst username = process.env.USER ?? process.env.LOGNAME ?? \"user\";\n\tconst host = hostname();\n\tconst rng = createBuddyRng(username, host, BUDDY_SALT, rerollCount);\n\n\t// Roll species + rarity\n\tconst { species, rarity } = rollSpecies(rng);\n\n\t// Roll shiny (1% chance)\n\tconst shiny = rng() < 0.01;\n\n\t// Roll eyes and hat\n\tconst eyes = rollEyes(rng);\n\tconst hat = rollHat(rng);\n\n\t// Roll stats\n\tconst stats = rollStats(rng, rarity);\n\n\treturn { species, rarity, shiny, stats, eyeStyle: eyes, hat };\n}\n\n// =============================================================================\n// Soul generation\n// =============================================================================\n\n/**\n * Generate a soul (name + personality + backstory) using the parent LLM.\n * Only called on first hatch or reroll.\n */\nasync function generateSoul(\n\tbones: CompanionBones,\n\tparentModel: Model<\"openai-completions\">,\n\tapiKey: string,\n): Promise<{ name: string; personality: string; backstory: string }> {\n\tconst statsStr = STAT_NAMES.map((s) => `${s}: ${bones.stats[s]}`).join(\", \");\n\tconst prompt = SOUL_GENERATION_PROMPT.replace(\"{species}\", bones.species)\n\t\t.replace(\"{rarity}\", bones.rarity)\n\t\t.replace(\"{stats}\", statsStr)\n\t\t.replace(\"{shiny}\", bones.shiny ? \"YES ✨\" : \"no\");\n\n\tconst context: Context = {\n\t\tsystemPrompt: \"Generate a companion character. Respond in the exact format requested.\",\n\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t};\n\n\ttry {\n\t\tconst response = await completeSimple(parentModel, context, { apiKey });\n\t\tconst text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\n\t\t// Parse NAME: ... and PERSONALITY: ... and BACKSTORY: ...\n\t\tconst nameMatch = text.match(/NAME:\\s*(.+)/i);\n\t\tconst personalityMatch = text.match(/PERSONALITY:\\s*(.+)/i);\n\t\tconst backstoryMatch = text.match(/BACKSTORY:\\s*([\\s\\S]+)/i);\n\n\t\tlet name = nameMatch?.[1]?.trim() ?? bones.species;\n\t\tconst personality = personalityMatch?.[1]?.trim() ?? `A ${bones.rarity} ${bones.species} companion.`;\n\t\tconst backstory = backstoryMatch?.[1]?.trim() ?? DEFAULT_BACKSTORY;\n\n\t\t// Enforce name length\n\t\tif (name.length > 8) name = name.slice(0, 8);\n\n\t\treturn { name, personality, backstory };\n\t} catch {\n\t\t// Fallback if LLM fails\n\t\treturn {\n\t\t\tname: bones.species,\n\t\t\tpersonality: `A ${bones.rarity} ${bones.species} companion.`,\n\t\t\tbackstory: DEFAULT_BACKSTORY,\n\t\t};\n\t}\n}\n\n// =============================================================================\n// BuddyManager\n// =============================================================================\n\nexport class BuddyManager {\n\tprivate state: BuddyState | null = null;\n\tprivate ollamaStatus: OllamaStatus | null = null;\n\n\t/** Get current buddy state (null if not loaded) */\n\tgetState(): BuddyState | null {\n\t\treturn this.state;\n\t}\n\n\t/** Check if buddy exists on disk */\n\thasStoredBuddy(): boolean {\n\t\treturn loadStored() !== null;\n\t}\n\n\t/**\n\t * Load or create buddy state.\n\t * If stored buddy exists, loads soul and re-rolls bones.\n\t * If no stored buddy, returns null (need to hatch first).\n\t */\n\tload(): BuddyState | null {\n\t\tconst stored = loadStored();\n\t\tif (!stored) return null;\n\n\t\tconst bones = rollBones(stored.rerollCount);\n\t\tthis.state = { ...bones, ...stored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Hatch a new buddy. Generates bones, then uses parent LLM for soul.\n\t * Returns the new state.\n\t */\n\tasync hatch(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst rerollCount = stored?.rerollCount ?? 0;\n\n\t\tconst bones = rollBones(rerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Reroll the buddy — new bones + new soul.\n\t */\n\tasync reroll(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst newRerollCount = (stored?.rerollCount ?? 0) + 1;\n\n\t\tconst bones = rollBones(newRerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount: newRerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/** Get buddy's name (for name-call detection) */\n\tgetName(): string | null {\n\t\treturn this.state?.name ?? loadStored()?.name ?? null;\n\t}\n\n\t/**\n\t * Shared Ollama chat helper. Checks availability, picks model, runs completion.\n\t * Returns the response text, or null if Ollama is unavailable or no model configured.\n\t */\n\tprivate async ollamaChat(context: Context): Promise<string | null> {\n\t\t// Check Ollama lazily, retry if previously unavailable\n\t\tif (!this.ollamaStatus || !this.ollamaStatus.available) {\n\t\t\tthis.ollamaStatus = await checkOllama();\n\t\t}\n\t\tif (!this.ollamaStatus.available) return null;\n\n\t\tconst modelName = pickOllamaModel(this.state?.ollamaModel, this.ollamaStatus.models);\n\t\tif (!modelName) return null;\n\t\tconst model: Model<\"openai-completions\"> = {\n\t\t\t...OLLAMA_MODEL_BASE,\n\t\t\tid: modelName,\n\t\t\tname: `${modelName} (Ollama)`,\n\t\t};\n\n\t\tlet response: import(\"@dreb/ai\").AssistantMessage;\n\t\ttry {\n\t\t\tresponse = await completeSimple(model, context, {\n\t\t\t\tapiKey: \"ollama\",\n\t\t\t\tsignal: AbortSignal.timeout(120000),\n\t\t\t});\n\t\t} catch {\n\t\t\t// Safety net for unexpected sync errors (e.g. provider not found).\n\t\t\t// Normal runtime errors (timeout, connection) are handled via stopReason below.\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Connection error — invalidate cache so next attempt re-checks Ollama\n\t\tif (response.stopReason === \"error\") {\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Timeout or abort — preserve cache (model is just slow, Ollama is fine)\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\")\n\t\t\t.trim();\n\n\t\t// Truncate overly long responses\n\t\ttext = truncateResponse(text, MAX_RESPONSE_WORDS);\n\n\t\treturn text || null;\n\t}\n\n\t/**\n\t * Generate a reaction to an event using Ollama.\n\t * Returns null if Ollama is unavailable.\n\t */\n\tasync react(event: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = REACTION_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{event}\", event);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short in-character quip. Max 20 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/**\n\t * Respond to the user calling the buddy's name.\n\t * Uses Ollama for the response.\n\t */\n\tasync respondToNameCall(userMessage: string, recentContext: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = NAME_CALL_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{message}\", userMessage)\n\t\t\t.replace(\"{context}\", recentContext);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short friendly greeting. Max 30 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/** Get the configured Ollama model name, or null if not set */\n\tgetOllamaModel(): string | null {\n\t\treturn this.state?.ollamaModel ?? loadStored()?.ollamaModel ?? null;\n\t}\n\n\t/** Set the Ollama model for buddy reactions. Persists to disk. */\n\tsetOllamaModel(modelName: string): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.ollamaModel = modelName;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\tif (this.state) {\n\t\t\tthis.state.ollamaModel = modelName;\n\t\t}\n\t\t// Invalidate Ollama status cache so next call picks up the new model\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Reset Ollama status cache (e.g. after detecting it became available) */\n\tresetOllamaCache(): void {\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Update the hidden flag in persisted storage */\n\tsetHidden(hidden: boolean): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.hidden = hidden;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\t// Keep in-memory state in sync so reset() reads current hidden flag\n\t\tif (this.state) {\n\t\t\tthis.state.hidden = hidden;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buddy-manager.js","sourceRoot":"","sources":["../../../src/core/buddy/buddy-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BuddyManager — Core state machine for the buddy companion.\n *\n * Handles: bone rolling, soul generation, persistence, Ollama availability checks.\n * Bones are deterministic from hash(username + hostname + salt + rerollCount).\n * Soul is LLM-generated once on first hatch and persisted to buddy.json.\n */\n\nimport type { Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { hostname } from \"os\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { createBuddyRng } from \"./buddy-prng.js\";\nimport { rollEyes, rollHat, rollSpecies, rollStats } from \"./buddy-species.js\";\nimport type { BuddyState, CompanionBones, StoredCompanion } from \"./buddy-types.js\";\nimport { STAT_NAMES } from \"./buddy-types.js\";\n\nconst BUDDY_SALT = \"dreb-buddy-v1\";\nconst BUDDY_FILENAME = \"buddy.json\";\nconst DEFAULT_BACKSTORY = \"A mysterious past shrouded in legend.\";\n\n/** Base Ollama model config — id/name are set dynamically from available models */\nconst OLLAMA_MODEL_BASE: Omit<Model<\"openai-completions\">, \"id\" | \"name\"> = {\n\tapi: \"openai-completions\",\n\tprovider: \"ollama\",\n\tbaseUrl: \"http://localhost:11434/v1\",\n\treasoning: false,\n\tinput: [\"text\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 2048,\n\tcompat: {\n\t\tsupportsDeveloperRole: false,\n\t\tsupportsReasoningEffort: false,\n\t},\n};\n\n/** Max words for buddy response before truncation */\nconst MAX_RESPONSE_WORDS = 60;\n\n/** Prompt for soul generation (uses parent LLM, not Ollama) */\nconst SOUL_GENERATION_PROMPT = `You are generating a companion character for a coding assistant terminal app. Based on the species, rarity, and stats below, generate a creative name, a one-sentence personality description, and a funny fictional backstory.\n\nSpecies: {species}\nRarity: {rarity}\nStats: {stats}\nShiny: {shiny}\n\nThe name must NOT be a common English word, programming keyword, tool name, or command. It should be unique and distinctive — a proper noun that won't appear in normal conversation. The name must be 4-8 characters and easy to type on a QWERTY keyboard — use only common letters (a-z, avoid q, x, z, j). Do not use species name as the name.\n\nRespond in EXACTLY this format:\nNAME: <name>\nPERSONALITY: <one sentence personality>\nBACKSTORY: <2-3 sentence elaborate fictional backstory — funny, absurd, or dramatic. Include specific events, places, former occupations>`;\n\n/** Prompt for buddy reactions via Ollama */\nconst REACTION_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nSomething just happened. React with a short, in-character quip based on the context below. Be specific — reference what actually happened, not just that something happened. Max 20 words. No quotes, no prefixes, just the quip.\n\nContext:\n{event}`;\n\nconst NAME_CALL_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nThe user just said: \"{message}\"\nRecent context: {context}\n\nRespond to what the user said directly. Be in-character, reference your backstory occasionally. Max 30 words. No quotes, no prefixes, just your response.`;\n\n// =============================================================================\n// Ollama availability\n// =============================================================================\n\nexport interface OllamaStatus {\n\tavailable: boolean;\n\tmodels: string[];\n\terror?: string;\n}\n\n/**\n * Check if Ollama is running and has models available.\n * Uses the /api/tags endpoint.\n */\nexport async function checkOllama(): Promise<OllamaStatus> {\n\ttry {\n\t\tconst res = await fetch(\"http://localhost:11434/api/tags\", { signal: AbortSignal.timeout(3000) });\n\t\tif (!res.ok) {\n\t\t\treturn { available: false, models: [], error: `Ollama returned ${res.status}` };\n\t\t}\n\t\tconst data = (await res.json()) as { models?: { name: string }[] };\n\t\tconst models = (data.models ?? []).map((m) => m.name);\n\t\tif (models.length === 0) {\n\t\t\treturn { available: false, models: [], error: \"No models installed. Run: ollama pull llama3.2\" };\n\t\t}\n\t\treturn { available: true, models };\n\t} catch {\n\t\t/* Ollama not running or unreachable — report as unavailable */\n\t\treturn { available: false, models: [], error: \"Ollama is not running. Start it with: ollama serve\" };\n\t}\n}\n\n/**\n * Pick the Ollama model for the buddy.\n * Returns the stored model name if it's available, otherwise null.\n */\nfunction pickOllamaModel(storedModel: string | undefined, availableModels: string[]): string | null {\n\tif (!storedModel) return null;\n\t// Check if the stored model is installed (exact match or prefix match without tag)\n\tconst match = availableModels.find((m) => m === storedModel || m.startsWith(`${storedModel}:`));\n\treturn match ?? null;\n}\n\n/**\n * Truncate response to a maximum word count, appending \"...[truncated]\" if exceeded.\n */\nexport function truncateResponse(text: string, maxWords: number): string {\n\tconst words = text.split(/\\s+/);\n\tif (words.length <= maxWords) return text;\n\treturn `${words.slice(0, maxWords).join(\" \")} ...[truncated]`;\n}\n\n// =============================================================================\n// Persistence\n// =============================================================================\n\nfunction getBuddyPath(): string {\n\treturn join(getAgentDir(), BUDDY_FILENAME);\n}\n\nfunction loadStored(): StoredCompanion | null {\n\tconst path = getBuddyPath();\n\tif (!existsSync(path)) return null;\n\ttry {\n\t\tconst data = JSON.parse(readFileSync(path, \"utf-8\"));\n\t\t// Validate required fields\n\t\tif (\n\t\t\ttypeof data.rerollCount === \"number\" &&\n\t\t\ttypeof data.name === \"string\" &&\n\t\t\ttypeof data.personality === \"string\"\n\t\t) {\n\t\t\treturn {\n\t\t\t\trerollCount: data.rerollCount,\n\t\t\t\tname: data.name,\n\t\t\t\tpersonality: data.personality,\n\t\t\t\tbackstory: typeof data.backstory === \"string\" ? data.backstory : DEFAULT_BACKSTORY,\n\t\t\t\thatchedAt: data.hatchedAt ?? new Date().toISOString(),\n\t\t\t\t...(data.hidden !== undefined ? { hidden: data.hidden } : {}),\n\t\t\t\t...(typeof data.ollamaModel === \"string\" ? { ollamaModel: data.ollamaModel } : {}),\n\t\t\t};\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\t/* Corrupt or missing companion file — return null to trigger fresh creation */\n\t\treturn null;\n\t}\n}\n\nfunction saveStored(stored: StoredCompanion): void {\n\tconst path = getBuddyPath();\n\tconst dir = join(path, \"..\");\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\twriteFileSync(path, JSON.stringify(stored, null, 2));\n}\n\n// =============================================================================\n// Bone rolling\n// =============================================================================\n\nfunction rollBones(rerollCount: number): CompanionBones {\n\tconst username = process.env.USER ?? process.env.LOGNAME ?? \"user\";\n\tconst host = hostname();\n\tconst rng = createBuddyRng(username, host, BUDDY_SALT, rerollCount);\n\n\t// Roll species + rarity\n\tconst { species, rarity } = rollSpecies(rng);\n\n\t// Roll shiny (1% chance)\n\tconst shiny = rng() < 0.01;\n\n\t// Roll eyes and hat\n\tconst eyes = rollEyes(rng);\n\tconst hat = rollHat(rng);\n\n\t// Roll stats\n\tconst stats = rollStats(rng, rarity);\n\n\treturn { species, rarity, shiny, stats, eyeStyle: eyes, hat };\n}\n\n// =============================================================================\n// Soul generation\n// =============================================================================\n\n/**\n * Generate a soul (name + personality + backstory) using the parent LLM.\n * Only called on first hatch or reroll.\n */\nasync function generateSoul(\n\tbones: CompanionBones,\n\tparentModel: Model<\"openai-completions\">,\n\tapiKey: string,\n): Promise<{ name: string; personality: string; backstory: string }> {\n\tconst statsStr = STAT_NAMES.map((s) => `${s}: ${bones.stats[s]}`).join(\", \");\n\tconst prompt = SOUL_GENERATION_PROMPT.replace(\"{species}\", bones.species)\n\t\t.replace(\"{rarity}\", bones.rarity)\n\t\t.replace(\"{stats}\", statsStr)\n\t\t.replace(\"{shiny}\", bones.shiny ? \"YES ✨\" : \"no\");\n\n\tconst context: Context = {\n\t\tsystemPrompt: \"Generate a companion character. Respond in the exact format requested.\",\n\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t};\n\n\ttry {\n\t\tconst response = await completeSimple(parentModel, context, { apiKey });\n\t\tconst text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\n\t\t// Parse NAME: ... and PERSONALITY: ... and BACKSTORY: ...\n\t\tconst nameMatch = text.match(/NAME:\\s*(.+)/i);\n\t\tconst personalityMatch = text.match(/PERSONALITY:\\s*(.+)/i);\n\t\tconst backstoryMatch = text.match(/BACKSTORY:\\s*([\\s\\S]+)/i);\n\n\t\tlet name = nameMatch?.[1]?.trim() ?? bones.species;\n\t\tconst personality = personalityMatch?.[1]?.trim() ?? `A ${bones.rarity} ${bones.species} companion.`;\n\t\tconst backstory = backstoryMatch?.[1]?.trim() ?? DEFAULT_BACKSTORY;\n\n\t\t// Enforce name length\n\t\tif (name.length > 8) name = name.slice(0, 8);\n\n\t\treturn { name, personality, backstory };\n\t} catch {\n\t\t// Fallback if LLM fails\n\t\treturn {\n\t\t\tname: bones.species,\n\t\t\tpersonality: `A ${bones.rarity} ${bones.species} companion.`,\n\t\t\tbackstory: DEFAULT_BACKSTORY,\n\t\t};\n\t}\n}\n\n// =============================================================================\n// BuddyManager\n// =============================================================================\n\nexport class BuddyManager {\n\tprivate state: BuddyState | null = null;\n\tprivate ollamaStatus: OllamaStatus | null = null;\n\n\t/** Get current buddy state (null if not loaded) */\n\tgetState(): BuddyState | null {\n\t\treturn this.state;\n\t}\n\n\t/** Check if buddy exists on disk */\n\thasStoredBuddy(): boolean {\n\t\treturn loadStored() !== null;\n\t}\n\n\t/**\n\t * Load or create buddy state.\n\t * If stored buddy exists, loads soul and re-rolls bones.\n\t * If no stored buddy, returns null (need to hatch first).\n\t */\n\tload(): BuddyState | null {\n\t\tconst stored = loadStored();\n\t\tif (!stored) return null;\n\n\t\tconst bones = rollBones(stored.rerollCount);\n\t\tthis.state = { ...bones, ...stored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Hatch a new buddy. Generates bones, then uses parent LLM for soul.\n\t * Returns the new state.\n\t */\n\tasync hatch(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst rerollCount = stored?.rerollCount ?? 0;\n\n\t\tconst bones = rollBones(rerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Reroll the buddy — new bones + new soul.\n\t */\n\tasync reroll(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst newRerollCount = (stored?.rerollCount ?? 0) + 1;\n\n\t\tconst bones = rollBones(newRerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount: newRerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/** Get buddy's name (for name-call detection) */\n\tgetName(): string | null {\n\t\treturn this.state?.name ?? loadStored()?.name ?? null;\n\t}\n\n\t/**\n\t * Shared Ollama chat helper. Checks availability, picks model, runs completion.\n\t * Returns the response text, or null if Ollama is unavailable or no model configured.\n\t */\n\tprivate async ollamaChat(context: Context): Promise<string | null> {\n\t\t// Check Ollama lazily, retry if previously unavailable\n\t\tif (!this.ollamaStatus || !this.ollamaStatus.available) {\n\t\t\tthis.ollamaStatus = await checkOllama();\n\t\t}\n\t\tif (!this.ollamaStatus.available) return null;\n\n\t\tconst modelName = pickOllamaModel(this.state?.ollamaModel, this.ollamaStatus.models);\n\t\tif (!modelName) return null;\n\t\tconst model: Model<\"openai-completions\"> = {\n\t\t\t...OLLAMA_MODEL_BASE,\n\t\t\tid: modelName,\n\t\t\tname: `${modelName} (Ollama)`,\n\t\t};\n\n\t\tlet response: import(\"@dreb/ai\").AssistantMessage;\n\t\ttry {\n\t\t\tresponse = await completeSimple(model, context, {\n\t\t\t\tapiKey: \"ollama\",\n\t\t\t\tsignal: AbortSignal.timeout(120000),\n\t\t\t});\n\t\t} catch {\n\t\t\t// Safety net for unexpected sync errors (e.g. provider not found).\n\t\t\t// Normal runtime errors (timeout, connection) are handled via stopReason below.\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Connection error — invalidate cache so next attempt re-checks Ollama\n\t\tif (response.stopReason === \"error\") {\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Timeout or abort — preserve cache (model is just slow, Ollama is fine)\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\")\n\t\t\t.trim();\n\n\t\t// Truncate overly long responses\n\t\ttext = truncateResponse(text, MAX_RESPONSE_WORDS);\n\n\t\treturn text || null;\n\t}\n\n\t/**\n\t * Generate a reaction to an event using Ollama.\n\t * Returns null if Ollama is unavailable.\n\t */\n\tasync react(event: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = REACTION_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{event}\", event);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short in-character quip. Max 20 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/**\n\t * Respond to the user calling the buddy's name.\n\t * Uses Ollama for the response.\n\t */\n\tasync respondToNameCall(userMessage: string, recentContext: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = NAME_CALL_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{message}\", userMessage)\n\t\t\t.replace(\"{context}\", recentContext);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short friendly greeting. Max 30 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/** Get the configured Ollama model name, or null if not set */\n\tgetOllamaModel(): string | null {\n\t\treturn this.state?.ollamaModel ?? loadStored()?.ollamaModel ?? null;\n\t}\n\n\t/** Set the Ollama model for buddy reactions. Persists to disk. */\n\tsetOllamaModel(modelName: string): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.ollamaModel = modelName;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\tif (this.state) {\n\t\t\tthis.state.ollamaModel = modelName;\n\t\t}\n\t\t// Invalidate Ollama status cache so next call picks up the new model\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Reset Ollama status cache (e.g. after detecting it became available) */\n\tresetOllamaCache(): void {\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Update the hidden flag in persisted storage */\n\tsetHidden(hidden: boolean): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.hidden = hidden;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\t// Keep in-memory state in sync so reset() reads current hidden flag\n\t\tif (this.state) {\n\t\t\tthis.state.hidden = hidden;\n\t\t}\n\t}\n}\n"]}
|