osborn 0.5.3 → 0.5.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/.claude/settings.local.json +9 -0
- package/.claude/skills/markdown-to-pdf/SKILL.md +29 -0
- package/.claude/skills/pdf-to-markdown/SKILL.md +28 -0
- package/.claude/skills/playwright-browser/SKILL.md +75 -0
- package/.claude/skills/youtube-transcript/SKILL.md +24 -0
- package/dist/claude-llm.d.ts +29 -1
- package/dist/claude-llm.js +334 -78
- package/dist/config.d.ts +5 -1
- package/dist/config.js +4 -1
- package/dist/fast-brain.d.ts +70 -16
- package/dist/fast-brain.js +662 -99
- package/dist/index-3-2-26-legacy.d.ts +1 -0
- package/dist/index-3-2-26-legacy.js +2233 -0
- package/dist/index.js +752 -423
- package/dist/jsonl-search.d.ts +66 -0
- package/dist/jsonl-search.js +274 -0
- package/dist/leagcyprompts2.d.ts +0 -0
- package/dist/leagcyprompts2.js +573 -0
- package/dist/pipeline-direct-llm.d.ts +77 -0
- package/dist/pipeline-direct-llm.js +216 -0
- package/dist/pipeline-fastbrain.d.ts +45 -0
- package/dist/pipeline-fastbrain.js +367 -0
- package/dist/prompts-2-25-26.d.ts +0 -0
- package/dist/prompts-2-25-26.js +518 -0
- package/dist/prompts-3-2-26.d.ts +78 -0
- package/dist/prompts-3-2-26.js +1319 -0
- package/dist/prompts.d.ts +83 -12
- package/dist/prompts.js +1991 -588
- package/dist/session-access.d.ts +24 -0
- package/dist/session-access.js +74 -0
- package/dist/summary-index.d.ts +87 -0
- package/dist/summary-index.js +570 -0
- package/dist/turn-detector-shim.d.ts +24 -0
- package/dist/turn-detector-shim.js +83 -0
- package/dist/voice-io.d.ts +9 -3
- package/dist/voice-io.js +39 -20
- package/package.json +13 -10
package/dist/fast-brain.js
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fast Brain
|
|
2
|
+
* Fast Brain — Central Orchestrator for the Voice AI System
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The sole intelligence layer between the user and all backend capabilities.
|
|
5
|
+
* The realtime voice model is a thin teleprompter — it speaks what this module returns.
|
|
6
6
|
*
|
|
7
7
|
* Capabilities:
|
|
8
8
|
* - Read/write session files (spec.md + library/)
|
|
9
9
|
* - Web search for quick factual lookups
|
|
10
10
|
* - Record user decisions and preferences into spec.md
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
11
|
+
* - Trigger deep research (via callbacks to index.ts)
|
|
12
|
+
* - Generate teleprompter scripts for ALL voice output
|
|
13
|
+
* - Post-research: synthesize findings from JSONL into spec.md + voice scripts
|
|
14
|
+
* - Generate visual documents (comparison, diagram, analysis, summary)
|
|
13
15
|
*
|
|
14
|
-
*
|
|
16
|
+
* Central function: askFastBrain() — ALL user questions route here.
|
|
17
|
+
* It returns a FastBrainResponse with a teleprompter script the voice model reads verbatim.
|
|
15
18
|
*
|
|
16
19
|
* Auth chain (tried in order):
|
|
17
20
|
* 1. ANTHROPIC_API_KEY env var → Anthropic SDK (Haiku)
|
|
18
21
|
* 2. ANTHROPIC_AUTH_TOKEN env var → Anthropic SDK (Haiku)
|
|
19
22
|
* 3. GOOGLE_API_KEY env var → Gemini Flash fallback
|
|
20
|
-
*
|
|
21
|
-
* Note: Claude Code OAuth (macOS Keychain) was tested but Anthropic's Messages API
|
|
22
|
-
* rejects OAuth tokens with 401 "OAuth authentication is currently not supported."
|
|
23
23
|
*/
|
|
24
24
|
import Anthropic from '@anthropic-ai/sdk';
|
|
25
|
+
import { query as sdkQuery, tool as sdkTool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
|
|
25
26
|
import { GoogleGenAI } from '@google/genai';
|
|
26
27
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
27
28
|
import { dirname, basename } from 'path';
|
|
29
|
+
import { z } from 'zod';
|
|
28
30
|
import { getSessionWorkspace, readSessionSpec, listLibraryFiles } from './config.js';
|
|
29
|
-
import { FAST_BRAIN_SYSTEM_PROMPT, CHUNK_PROCESS_SYSTEM, REFINEMENT_PROCESS_SYSTEM, AUGMENT_RESULT_SYSTEM, CONTEXTUALIZE_UPDATE_SYSTEM, PROACTIVE_PROMPT_SYSTEM, VISUAL_DOCUMENT_SYSTEM } from './prompts.js';
|
|
31
|
+
import { FAST_BRAIN_SYSTEM_PROMPT, CHUNK_PROCESS_SYSTEM, REFINEMENT_PROCESS_SYSTEM, AUGMENT_RESULT_SYSTEM, CONTEXTUALIZE_UPDATE_SYSTEM, PROACTIVE_PROMPT_SYSTEM, VISUAL_DOCUMENT_SYSTEM, RESEARCH_COMPLETION_SYSTEM, buildFastBrainSdkPrompt } from './prompts.js';
|
|
30
32
|
import { getRecentToolResults, readSessionHistory, getSubagentTranscripts, getConversationText, getSessionTranscripts, searchSessionJsonl, getSessionStats } from './session-access.js';
|
|
31
33
|
// ============================================================
|
|
32
34
|
// Content extraction — pulls useful snippets from tool responses
|
|
@@ -70,55 +72,81 @@ let initialized = false;
|
|
|
70
72
|
// Model IDs — configurable per provider
|
|
71
73
|
const ANTHROPIC_FAST_MODEL = 'claude-haiku-4-5-20251001';
|
|
72
74
|
const GEMINI_FAST_MODEL = 'gemini-2.0-flash';
|
|
73
|
-
|
|
75
|
+
// Agent SDK session tracking — resume across voice questions for context continuity
|
|
76
|
+
let fastBrainSessionId = null;
|
|
77
|
+
// Gemini Chat session — persists across voice questions for context continuity.
|
|
78
|
+
// The Chat object auto-manages full conversation history (messages + tool calls).
|
|
79
|
+
// Cleared on disconnect/reconnect/session switch via clearFastBrainSession().
|
|
80
|
+
let geminiChat = null;
|
|
81
|
+
const MAX_FAST_BRAIN_HISTORY = 30;
|
|
82
|
+
let fastBrainHistory = [];
|
|
83
|
+
/** Clear fast brain session state — call on disconnect/reconnect/session switch */
|
|
84
|
+
export function clearFastBrainSession() {
|
|
85
|
+
fastBrainSessionId = null;
|
|
86
|
+
geminiChat = null;
|
|
87
|
+
fastBrainHistory = [];
|
|
88
|
+
console.log('🧠 Fast brain: session cleared (SDK + Gemini chat + Anthropic history)');
|
|
89
|
+
}
|
|
90
|
+
/** @deprecated Use clearFastBrainSession() instead */
|
|
74
91
|
export function clearFastBrainHistory() {
|
|
75
|
-
|
|
92
|
+
clearFastBrainSession();
|
|
76
93
|
}
|
|
77
94
|
function initProvider() {
|
|
78
95
|
if (initialized)
|
|
79
96
|
return;
|
|
80
97
|
initialized = true;
|
|
81
|
-
//
|
|
98
|
+
// Initialize fallback clients (Gemini for fallback, Anthropic direct API if key available)
|
|
99
|
+
const googleKey = process.env.GOOGLE_API_KEY;
|
|
100
|
+
if (googleKey) {
|
|
101
|
+
geminiClient = new GoogleGenAI({ apiKey: googleKey });
|
|
102
|
+
}
|
|
82
103
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
83
104
|
if (apiKey) {
|
|
84
105
|
anthropicClient = new Anthropic({ apiKey });
|
|
85
|
-
provider = 'anthropic';
|
|
86
|
-
console.log('🧠 Fast brain: using Anthropic API (ANTHROPIC_API_KEY)');
|
|
87
|
-
return;
|
|
88
106
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
console.log('🧠 Fast brain: using Anthropic API (ANTHROPIC_AUTH_TOKEN)');
|
|
95
|
-
return;
|
|
107
|
+
else {
|
|
108
|
+
const authToken = process.env.ANTHROPIC_AUTH_TOKEN;
|
|
109
|
+
if (authToken) {
|
|
110
|
+
anthropicClient = new Anthropic({ authToken });
|
|
111
|
+
}
|
|
96
112
|
}
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
const googleKey = process.env.GOOGLE_API_KEY;
|
|
101
|
-
if (googleKey) {
|
|
102
|
-
geminiClient = new GoogleGenAI({ apiKey: googleKey });
|
|
113
|
+
// PRIMARY: Gemini Flash — fastest (~1-2s), handles 1M tokens, no cold start.
|
|
114
|
+
// Agent SDK Haiku is too slow (~10-15s) due to CLI process spawn + session overhead.
|
|
115
|
+
if (geminiClient) {
|
|
103
116
|
provider = 'gemini';
|
|
104
|
-
console.log(`🧠 Fast brain: using Gemini Flash
|
|
105
|
-
|
|
117
|
+
console.log(`🧠 Fast brain: using Gemini Flash (primary) — fastest response time`);
|
|
118
|
+
if (anthropicClient) {
|
|
119
|
+
console.log(`🧠 Fast brain: Direct Anthropic API available as fallback`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (anthropicClient) {
|
|
123
|
+
provider = 'anthropic';
|
|
124
|
+
console.log(`🧠 Fast brain: using Anthropic API (primary) — no Gemini key available`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Last resort: Agent SDK is slow but functional
|
|
128
|
+
provider = 'agent-sdk';
|
|
129
|
+
console.log(`🧠 Fast brain: using Claude Agent SDK (fallback) — no API keys available`);
|
|
106
130
|
}
|
|
107
|
-
// No provider available
|
|
108
|
-
provider = 'none';
|
|
109
|
-
console.error('⚠️ Fast brain: no API key available — fast brain disabled');
|
|
110
|
-
console.error(' Set ANTHROPIC_API_KEY or GOOGLE_API_KEY in agent/.env');
|
|
111
131
|
}
|
|
112
132
|
// ============================================================
|
|
113
133
|
// Tool execution (shared across providers)
|
|
114
134
|
// ============================================================
|
|
115
|
-
|
|
135
|
+
// Track whether send_to_chat was called during a fast brain conversation.
|
|
136
|
+
// If the LLM calls send_to_chat but returns no text, we use a fallback
|
|
137
|
+
// instead of "No answer found."
|
|
138
|
+
let sendToChatCalledThisTurn = false;
|
|
139
|
+
function executeTool(toolName, toolInput, workspace, sessionId, workingDir, sendToChat) {
|
|
116
140
|
try {
|
|
117
141
|
switch (toolName) {
|
|
118
142
|
case 'read_file': {
|
|
119
143
|
const relPath = toolInput.path;
|
|
120
144
|
if (relPath.includes('..'))
|
|
121
145
|
return 'Error: path traversal not allowed';
|
|
146
|
+
const ext = relPath.toLowerCase().split('.').pop() || '';
|
|
147
|
+
const BINARY_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'pdf', 'mp3', 'wav', 'mp4', 'mov'];
|
|
148
|
+
if (BINARY_EXTS.includes(ext))
|
|
149
|
+
return `Binary file (${ext}) — cannot read as text.`;
|
|
122
150
|
const fullPath = `${workspace}/${relPath}`;
|
|
123
151
|
if (!existsSync(fullPath))
|
|
124
152
|
return `File not found: ${relPath}`;
|
|
@@ -273,6 +301,18 @@ ${toolList}`;
|
|
|
273
301
|
}
|
|
274
302
|
return output;
|
|
275
303
|
}
|
|
304
|
+
case 'send_to_chat': {
|
|
305
|
+
const text = toolInput.text;
|
|
306
|
+
if (!text)
|
|
307
|
+
return 'Error: text is required';
|
|
308
|
+
if (sendToChat) {
|
|
309
|
+
console.log(`💬 [fast brain] send_to_chat: ${text.substring(0, 80)}...`);
|
|
310
|
+
sendToChat(text);
|
|
311
|
+
sendToChatCalledThisTurn = true;
|
|
312
|
+
return `Sent to chat successfully. Now return a brief spoken summary — do NOT repeat the content you just sent.`;
|
|
313
|
+
}
|
|
314
|
+
return 'Error: chat sending not available';
|
|
315
|
+
}
|
|
276
316
|
default:
|
|
277
317
|
return `Unknown tool: ${toolName}`;
|
|
278
318
|
}
|
|
@@ -389,6 +429,17 @@ function buildAnthropicTools() {
|
|
|
389
429
|
name: 'deep_read_text',
|
|
390
430
|
description: 'Read ALL agent reasoning and analysis across the ENTIRE session — not just recent messages. Returns every piece of thinking, synthesis, comparison, and recommendation the agent produced. Use this for generating comprehensive overviews or when the user asks for detailed explanations of what the agent found.',
|
|
391
431
|
input_schema: { type: 'object', properties: {} }
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: 'send_to_chat',
|
|
435
|
+
description: 'Send formatted content to the user\'s chat panel. Use for URLs, links, lists, prices, code snippets, or anything that\'s better read than spoken. The content appears as a chat message in the frontend. You should STILL speak a brief summary — use this tool for the detailed/visual content.',
|
|
436
|
+
input_schema: {
|
|
437
|
+
type: 'object',
|
|
438
|
+
properties: {
|
|
439
|
+
text: { type: 'string', description: 'The formatted text to display in chat. Supports markdown.' }
|
|
440
|
+
},
|
|
441
|
+
required: ['text']
|
|
442
|
+
}
|
|
392
443
|
}
|
|
393
444
|
];
|
|
394
445
|
}
|
|
@@ -521,6 +572,17 @@ function buildGeminiTools() {
|
|
|
521
572
|
name: 'deep_read_text',
|
|
522
573
|
description: 'Read ALL agent reasoning across the ENTIRE session. For comprehensive overviews or detailed explanations of what the agent found throughout the session.',
|
|
523
574
|
parameters: { type: 'object', properties: {} }
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
name: 'send_to_chat',
|
|
578
|
+
description: 'Send formatted content to the user\'s chat panel. Use for URLs, links, lists, prices, code snippets, or anything better read than spoken. Still speak a brief summary — use this for the detailed/visual content.',
|
|
579
|
+
parameters: {
|
|
580
|
+
type: 'object',
|
|
581
|
+
properties: {
|
|
582
|
+
text: { type: 'string', description: 'The formatted text to display in chat. Supports markdown.' }
|
|
583
|
+
},
|
|
584
|
+
required: ['text']
|
|
585
|
+
}
|
|
524
586
|
}
|
|
525
587
|
]
|
|
526
588
|
}
|
|
@@ -549,23 +611,150 @@ async function geminiWebSearch(query) {
|
|
|
549
611
|
}
|
|
550
612
|
}
|
|
551
613
|
// ============================================================
|
|
552
|
-
//
|
|
614
|
+
// Agent SDK Q&A implementation — replaces direct Anthropic API for Q&A
|
|
553
615
|
// ============================================================
|
|
554
|
-
|
|
616
|
+
/**
|
|
617
|
+
* Create an in-process MCP server with the send_to_chat tool for the Agent SDK fast brain.
|
|
618
|
+
*/
|
|
619
|
+
function createFastBrainMcpServer(sendToChat) {
|
|
620
|
+
const tools = [];
|
|
621
|
+
if (sendToChat) {
|
|
622
|
+
tools.push(sdkTool('send_to_chat', 'Send formatted content to the user\'s chat panel. Use for URLs, links, lists, prices, code snippets, tables, or anything better read than spoken. Supports markdown. You should STILL speak a brief summary — the chat content is supplementary.', { text: z.string().describe('The formatted text to display in chat. Supports markdown.') }, async ({ text }) => {
|
|
623
|
+
sendToChat(text);
|
|
624
|
+
sendToChatCalledThisTurn = true;
|
|
625
|
+
return { content: [{ type: 'text', text: 'Sent to chat. Now give a brief spoken summary of what you sent.' }] };
|
|
626
|
+
}));
|
|
627
|
+
}
|
|
628
|
+
return createSdkMcpServer({
|
|
629
|
+
name: 'osborn-fast-brain',
|
|
630
|
+
version: '1.0.0',
|
|
631
|
+
tools,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Ask via Claude Agent SDK — the agent traverses JSONL files natively using Read/Grep/Glob.
|
|
636
|
+
* Falls back to Gemini on timeout or error.
|
|
637
|
+
*/
|
|
638
|
+
async function askViaAgentSdk(question, workspace, researchContext, sessionId, workingDir, chatHistory, sendToChat, sessionBaseDir) {
|
|
639
|
+
sendToChatCalledThisTurn = false;
|
|
640
|
+
// Build the prompt with conversation context
|
|
641
|
+
let prompt = question;
|
|
642
|
+
if (researchContext) {
|
|
643
|
+
prompt += `\n\n[LIVE RESEARCH CONTEXT — the deep research agent is currently working]\n${researchContext}`;
|
|
644
|
+
}
|
|
645
|
+
if (chatHistory && chatHistory.length > 0) {
|
|
646
|
+
const historyStr = chatHistory.slice(-15).map(t => `${t.role}: ${t.text}`).join('\n');
|
|
647
|
+
prompt = `[Recent voice conversation]\n${historyStr}\n\n[Current question]\n${prompt}`;
|
|
648
|
+
}
|
|
649
|
+
// Create MCP server for send_to_chat
|
|
650
|
+
const mcpServer = createFastBrainMcpServer(sendToChat);
|
|
651
|
+
// Build system prompt with computed paths
|
|
652
|
+
const systemPrompt = buildFastBrainSdkPrompt(workingDir || workspace, sessionId || '', sessionBaseDir || workingDir || workspace);
|
|
653
|
+
// Tools: Read/Write/Edit for files, Grep/Glob for search, WebSearch/WebFetch for web
|
|
654
|
+
const toolNames = ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
|
|
655
|
+
const mcpToolPatterns = sendToChat ? ['mcp__osborn-fast-brain__*'] : [];
|
|
656
|
+
const options = {
|
|
657
|
+
model: ANTHROPIC_FAST_MODEL,
|
|
658
|
+
cwd: workingDir,
|
|
659
|
+
systemPrompt,
|
|
660
|
+
maxTurns: 8,
|
|
661
|
+
tools: toolNames,
|
|
662
|
+
allowedTools: [...toolNames, ...mcpToolPatterns],
|
|
663
|
+
mcpServers: { 'osborn-fast-brain': mcpServer },
|
|
664
|
+
};
|
|
665
|
+
if (fastBrainSessionId) {
|
|
666
|
+
options.resume = fastBrainSessionId;
|
|
667
|
+
}
|
|
668
|
+
// Run with 15s timeout — falls back to Gemini on timeout
|
|
669
|
+
const TIMEOUT_MS = 15000;
|
|
670
|
+
let timeoutHandle;
|
|
671
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
672
|
+
timeoutHandle = setTimeout(() => reject(new Error('fast-brain-timeout')), TIMEOUT_MS);
|
|
673
|
+
});
|
|
674
|
+
const queryPromise = (async () => {
|
|
675
|
+
let result = '';
|
|
676
|
+
try {
|
|
677
|
+
for await (const message of sdkQuery({ prompt, options })) {
|
|
678
|
+
if (message.type === 'result') {
|
|
679
|
+
result = message.result || '';
|
|
680
|
+
}
|
|
681
|
+
// Capture session ID eagerly — even if we timeout, next call can resume
|
|
682
|
+
if (message.type === 'assistant' && message.session_id) {
|
|
683
|
+
const sid = message.session_id;
|
|
684
|
+
if (sid !== fastBrainSessionId) {
|
|
685
|
+
fastBrainSessionId = sid;
|
|
686
|
+
console.log(`🧠 Fast brain session: ${sid.substring(0, 12)}... (${options.resume ? 'resumed' : 'new'})`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
console.error('❌ Agent SDK query error:', err);
|
|
693
|
+
throw err;
|
|
694
|
+
}
|
|
695
|
+
clearTimeout(timeoutHandle);
|
|
696
|
+
return result;
|
|
697
|
+
})();
|
|
698
|
+
try {
|
|
699
|
+
const result = await Promise.race([queryPromise, timeoutPromise]);
|
|
700
|
+
if (!result || result.trim().length === 0) {
|
|
701
|
+
if (sendToChatCalledThisTurn)
|
|
702
|
+
return "I've sent the details to your chat panel.";
|
|
703
|
+
return 'No answer found.';
|
|
704
|
+
}
|
|
705
|
+
console.log(`🧠 Agent SDK fast brain: ${result.length} chars (session: ${fastBrainSessionId?.substring(0, 8) || 'new'})`);
|
|
706
|
+
return result;
|
|
707
|
+
}
|
|
708
|
+
catch (err) {
|
|
709
|
+
clearTimeout(timeoutHandle);
|
|
710
|
+
if (err.message === 'fast-brain-timeout') {
|
|
711
|
+
console.log('⏱️ Agent SDK fast brain timed out (15s), falling back to Gemini');
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
console.error('❌ Agent SDK fast brain error:', err.message || err);
|
|
715
|
+
}
|
|
716
|
+
// Fall back to Gemini if available
|
|
717
|
+
if (geminiClient) {
|
|
718
|
+
console.log('🔄 Falling back to Gemini fast brain');
|
|
719
|
+
return askViaGemini(question, workspace, researchContext, sessionId, workingDir, chatHistory, sendToChat);
|
|
720
|
+
}
|
|
721
|
+
// Fall back to direct Anthropic API if no Gemini
|
|
722
|
+
if (anthropicClient) {
|
|
723
|
+
console.log('🔄 Falling back to direct Anthropic API');
|
|
724
|
+
return askViaAnthropic(question, workspace, researchContext, sessionId, workingDir, chatHistory, sendToChat);
|
|
725
|
+
}
|
|
726
|
+
return 'Fast brain unavailable. Try asking me to research it.';
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// ============================================================
|
|
730
|
+
// Direct Anthropic API Q&A — kept as fallback for Agent SDK failures
|
|
731
|
+
// ============================================================
|
|
732
|
+
async function askViaAnthropic(question, workspace, researchContext, sessionId, workingDir, chatHistory, sendToChat) {
|
|
555
733
|
const client = anthropicClient;
|
|
556
734
|
const tools = buildAnthropicTools();
|
|
735
|
+
sendToChatCalledThisTurn = false;
|
|
557
736
|
const userContent = researchContext
|
|
558
737
|
? `${question}\n\n[LIVE RESEARCH CONTEXT — the research agent is currently working]\n${researchContext}`
|
|
559
738
|
: question;
|
|
560
|
-
// Build messages
|
|
739
|
+
// Build messages: persistent fast brain history + live voice history + current question
|
|
561
740
|
const messages = [];
|
|
741
|
+
// 1. Inject persistent fast brain history (prior exchanges from this session)
|
|
742
|
+
for (const exchange of fastBrainHistory) {
|
|
743
|
+
messages.push({ role: 'user', content: exchange.question });
|
|
744
|
+
messages.push({ role: 'assistant', content: exchange.answer });
|
|
745
|
+
}
|
|
746
|
+
// 2. Inject live voice conversation history (from agent.chatCtx — what user/model actually said)
|
|
562
747
|
if (chatHistory && chatHistory.length > 0) {
|
|
563
748
|
for (const turn of chatHistory) {
|
|
564
749
|
messages.push({ role: turn.role, content: turn.text });
|
|
565
750
|
}
|
|
566
751
|
}
|
|
752
|
+
// 3. Current question
|
|
567
753
|
messages.push({ role: 'user', content: userContent });
|
|
568
754
|
const allTools = [...tools, ANTHROPIC_WEB_SEARCH];
|
|
755
|
+
const noAnswerFallback = () => sendToChatCalledThisTurn
|
|
756
|
+
? "I've sent the details to your chat panel."
|
|
757
|
+
: 'No answer found.';
|
|
569
758
|
for (let i = 0; i < 10; i++) {
|
|
570
759
|
const response = await client.messages.create({
|
|
571
760
|
model: ANTHROPIC_FAST_MODEL,
|
|
@@ -576,70 +765,97 @@ async function askViaAnthropic(question, workspace, researchContext, sessionId,
|
|
|
576
765
|
});
|
|
577
766
|
if (response.stop_reason === 'end_turn') {
|
|
578
767
|
const textBlock = response.content.find((b) => b.type === 'text');
|
|
579
|
-
|
|
768
|
+
const answer = textBlock?.text || noAnswerFallback();
|
|
769
|
+
// Persist this exchange for future calls
|
|
770
|
+
fastBrainHistory.push({ question: userContent, answer });
|
|
771
|
+
if (fastBrainHistory.length > MAX_FAST_BRAIN_HISTORY)
|
|
772
|
+
fastBrainHistory.shift();
|
|
773
|
+
return answer;
|
|
580
774
|
}
|
|
581
775
|
const toolUseBlocks = response.content.filter((b) => b.type === 'tool_use');
|
|
582
776
|
if (toolUseBlocks.length === 0 && response.stop_reason !== 'tool_use') {
|
|
583
777
|
const textBlock = response.content.find((b) => b.type === 'text');
|
|
584
|
-
|
|
778
|
+
const answer = textBlock?.text || noAnswerFallback();
|
|
779
|
+
fastBrainHistory.push({ question: userContent, answer });
|
|
780
|
+
if (fastBrainHistory.length > MAX_FAST_BRAIN_HISTORY)
|
|
781
|
+
fastBrainHistory.shift();
|
|
782
|
+
return answer;
|
|
585
783
|
}
|
|
586
784
|
messages.push({ role: 'assistant', content: response.content });
|
|
587
785
|
if (toolUseBlocks.length > 0) {
|
|
588
786
|
const toolResults = toolUseBlocks.map(toolUse => ({
|
|
589
787
|
type: 'tool_result',
|
|
590
788
|
tool_use_id: toolUse.id,
|
|
591
|
-
content: executeTool(toolUse.name, toolUse.input, workspace, sessionId, workingDir),
|
|
789
|
+
content: executeTool(toolUse.name, toolUse.input, workspace, sessionId, workingDir, sendToChat),
|
|
592
790
|
}));
|
|
593
791
|
messages.push({ role: 'user', content: toolResults });
|
|
594
792
|
}
|
|
595
793
|
}
|
|
794
|
+
if (sendToChatCalledThisTurn) {
|
|
795
|
+
const answer = "I've sent the full details to your chat. Let me know if you want to dive deeper into anything.";
|
|
796
|
+
fastBrainHistory.push({ question: userContent, answer });
|
|
797
|
+
if (fastBrainHistory.length > MAX_FAST_BRAIN_HISTORY)
|
|
798
|
+
fastBrainHistory.shift();
|
|
799
|
+
return answer;
|
|
800
|
+
}
|
|
596
801
|
return 'Fast brain reached maximum tool iterations. Try ask_agent for a deeper search.';
|
|
597
802
|
}
|
|
598
803
|
// ============================================================
|
|
599
804
|
// Gemini Q&A implementation
|
|
600
805
|
// ============================================================
|
|
601
|
-
async function askViaGemini(question, workspace, researchContext, sessionId, workingDir, chatHistory) {
|
|
806
|
+
async function askViaGemini(question, workspace, researchContext, sessionId, workingDir, chatHistory, sendToChat, sessionBaseDir) {
|
|
602
807
|
const ai = geminiClient;
|
|
603
808
|
const tools = buildGeminiTools();
|
|
809
|
+
sendToChatCalledThisTurn = false;
|
|
604
810
|
const userContent = researchContext
|
|
605
811
|
? `${question}\n\n[LIVE RESEARCH CONTEXT — the research agent is currently working]\n${researchContext}`
|
|
606
812
|
: question;
|
|
607
|
-
//
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
813
|
+
// Create or reuse persistent Gemini Chat session.
|
|
814
|
+
// The Chat object auto-manages full conversation history (messages + tool calls).
|
|
815
|
+
// Cleared on disconnect/reconnect/session switch via clearFastBrainSession().
|
|
816
|
+
if (!geminiChat) {
|
|
817
|
+
// Seed with live voice conversation history so Gemini knows what user/model said
|
|
818
|
+
const history = [];
|
|
819
|
+
if (chatHistory && chatHistory.length > 0) {
|
|
820
|
+
for (const turn of chatHistory) {
|
|
821
|
+
history.push({
|
|
822
|
+
role: turn.role === 'assistant' ? 'model' : 'user',
|
|
823
|
+
parts: [{ text: turn.text }],
|
|
824
|
+
});
|
|
825
|
+
}
|
|
615
826
|
}
|
|
616
|
-
|
|
617
|
-
contents.push({ role: 'user', parts: [{ text: userContent }] });
|
|
618
|
-
for (let i = 0; i < 10; i++) {
|
|
619
|
-
const response = await ai.models.generateContent({
|
|
827
|
+
geminiChat = ai.chats.create({
|
|
620
828
|
model: GEMINI_FAST_MODEL,
|
|
621
|
-
contents,
|
|
622
829
|
config: {
|
|
623
830
|
systemInstruction: FAST_BRAIN_SYSTEM_PROMPT,
|
|
624
831
|
tools,
|
|
625
|
-
}
|
|
832
|
+
},
|
|
833
|
+
history,
|
|
626
834
|
});
|
|
835
|
+
console.log(`🧠 Gemini fast brain: new chat session (history: ${history.length} turns)`);
|
|
836
|
+
}
|
|
837
|
+
// Send user message via the persistent chat — history accumulates automatically.
|
|
838
|
+
// The Chat object tracks all messages + tool calls internally.
|
|
839
|
+
let response = await geminiChat.sendMessage({ message: userContent });
|
|
840
|
+
// Tool call loop: execute tools and send results back, up to 10 rounds
|
|
841
|
+
for (let i = 0; i < 10; i++) {
|
|
627
842
|
const functionCalls = response.functionCalls;
|
|
628
843
|
if (!functionCalls || functionCalls.length === 0) {
|
|
629
|
-
|
|
844
|
+
const text = response.text;
|
|
845
|
+
if (text)
|
|
846
|
+
return text;
|
|
847
|
+
if (sendToChatCalledThisTurn)
|
|
848
|
+
return "I've sent the details to your chat panel.";
|
|
849
|
+
return 'No answer found.';
|
|
630
850
|
}
|
|
631
|
-
//
|
|
632
|
-
if (response.candidates?.[0]?.content) {
|
|
633
|
-
contents.push(response.candidates[0].content);
|
|
634
|
-
}
|
|
635
|
-
// Execute tools and send results back (web_search is async, others are sync)
|
|
851
|
+
// Execute tools
|
|
636
852
|
const functionResponses = await Promise.all(functionCalls.map(async (call) => {
|
|
637
853
|
let result;
|
|
638
854
|
if (call.name === 'web_search') {
|
|
639
855
|
result = await geminiWebSearch(call.args?.query || question);
|
|
640
856
|
}
|
|
641
857
|
else {
|
|
642
|
-
result = executeTool(call.name, call.args || {}, workspace, sessionId, workingDir);
|
|
858
|
+
result = executeTool(call.name, call.args || {}, workspace, sessionId, workingDir, sendToChat);
|
|
643
859
|
}
|
|
644
860
|
return {
|
|
645
861
|
functionResponse: {
|
|
@@ -648,7 +864,11 @@ async function askViaGemini(question, workspace, researchContext, sessionId, wor
|
|
|
648
864
|
}
|
|
649
865
|
};
|
|
650
866
|
}));
|
|
651
|
-
|
|
867
|
+
// Send tool results back — chat auto-tracks the full exchange
|
|
868
|
+
response = await geminiChat.sendMessage({ message: functionResponses });
|
|
869
|
+
}
|
|
870
|
+
if (sendToChatCalledThisTurn) {
|
|
871
|
+
return "I've sent the full details to your chat. Let me know if you want to dive deeper into anything.";
|
|
652
872
|
}
|
|
653
873
|
return 'Fast brain reached maximum tool iterations. Try ask_agent for a deeper search.';
|
|
654
874
|
}
|
|
@@ -664,19 +884,181 @@ async function askViaGemini(question, workspace, researchContext, sessionId, wor
|
|
|
664
884
|
* @param researchContext - Optional snapshot of the live research log.
|
|
665
885
|
* ~2 second response time for most queries.
|
|
666
886
|
*/
|
|
667
|
-
export async function askHaiku(workingDir, sessionId, question, researchContext, chatHistory) {
|
|
887
|
+
export async function askHaiku(workingDir, sessionId, question, researchContext, chatHistory, sendToChat, sessionBaseDir) {
|
|
668
888
|
initProvider();
|
|
669
|
-
|
|
670
|
-
|
|
889
|
+
// workspace uses sessionBaseDir (Osborn install dir) for spec.md/library
|
|
890
|
+
// workingDir is for JSONL access (matches Claude SDK cwd)
|
|
891
|
+
const wsDir = sessionBaseDir || workingDir;
|
|
892
|
+
const workspace = getSessionWorkspace(wsDir, sessionId);
|
|
893
|
+
// Primary: Gemini Flash (~1-2s) with pre-loaded JSONL context
|
|
894
|
+
// Fallback: Anthropic direct API or Agent SDK (slower but functional)
|
|
895
|
+
if (provider === 'gemini') {
|
|
896
|
+
return askViaGemini(question, workspace, researchContext, sessionId, workingDir, chatHistory, sendToChat, wsDir);
|
|
671
897
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
return askViaAnthropic(question, workspace, researchContext, sessionId, workingDir, chatHistory);
|
|
898
|
+
else if (provider === 'anthropic' || provider === 'agent-sdk') {
|
|
899
|
+
return askViaAgentSdk(question, workspace, researchContext, sessionId, workingDir, chatHistory, sendToChat, wsDir);
|
|
675
900
|
}
|
|
676
901
|
else {
|
|
677
|
-
return
|
|
902
|
+
return 'NEEDS_DEEPER_RESEARCH: Fast brain unavailable (no API key or CLI auth). Try ask_agent instead.';
|
|
678
903
|
}
|
|
679
904
|
}
|
|
905
|
+
let researchTaskCounter = 0;
|
|
906
|
+
/**
|
|
907
|
+
* Central orchestrator — ALL user questions from the realtime model come here.
|
|
908
|
+
* Routes to: direct answer, research triggering, decision recording, or document generation.
|
|
909
|
+
* Returns a teleprompter script the voice model reads verbatim.
|
|
910
|
+
*/
|
|
911
|
+
export async function askFastBrain(workingDir, sessionId, question, opts) {
|
|
912
|
+
const { chatHistory, researchContext, callbacks } = opts;
|
|
913
|
+
const wsDir = opts.sessionBaseDir || workingDir;
|
|
914
|
+
// Detect document generation requests
|
|
915
|
+
const docMatch = detectDocumentRequest(question);
|
|
916
|
+
if (docMatch) {
|
|
917
|
+
try {
|
|
918
|
+
const result = await generateVisualDocument(workingDir, sessionId, question, docMatch, wsDir);
|
|
919
|
+
if (result) {
|
|
920
|
+
const fullPath = `${wsDir}/.osborn/sessions/${sessionId}/library/${result.fileName}`;
|
|
921
|
+
callbacks.sendToFrontend({
|
|
922
|
+
type: 'research_artifact_updated',
|
|
923
|
+
filePath: fullPath,
|
|
924
|
+
fileName: result.fileName,
|
|
925
|
+
});
|
|
926
|
+
return {
|
|
927
|
+
script: `I've created a ${docMatch} document called ${result.fileName}. You can see it in the files panel.`,
|
|
928
|
+
type: 'answer',
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
catch (err) {
|
|
933
|
+
console.error('❌ askFastBrain: document generation failed:', err);
|
|
934
|
+
}
|
|
935
|
+
// Fall through to regular handling if document gen fails
|
|
936
|
+
}
|
|
937
|
+
// Create sendToChat wrapper that sends assistant_response to frontend
|
|
938
|
+
const sendToChat = (text) => {
|
|
939
|
+
callbacks.sendToFrontend({ type: 'assistant_response', text });
|
|
940
|
+
};
|
|
941
|
+
// Core: ask the fast brain LLM
|
|
942
|
+
const answer = await askHaiku(workingDir, sessionId, question, researchContext, chatHistory, sendToChat, wsDir);
|
|
943
|
+
// Parse the response to determine routing
|
|
944
|
+
if (answer.startsWith('RECORDED:') || answer.includes('\nRECORDED:')) {
|
|
945
|
+
// Decision was recorded — extract the confirmation
|
|
946
|
+
const recordedLine = answer.split('\n').find(l => l.startsWith('RECORDED:'));
|
|
947
|
+
const confirmation = recordedLine
|
|
948
|
+
? recordedLine.replace('RECORDED:', '').trim()
|
|
949
|
+
: 'Got it, noted.';
|
|
950
|
+
// Notify frontend about spec update
|
|
951
|
+
const specPath = `${wsDir}/.osborn/sessions/${sessionId}/spec.md`;
|
|
952
|
+
callbacks.sendToFrontend({
|
|
953
|
+
type: 'research_artifact_updated',
|
|
954
|
+
filePath: specPath,
|
|
955
|
+
fileName: 'spec.md',
|
|
956
|
+
});
|
|
957
|
+
return { script: confirmation, type: 'recorded' };
|
|
958
|
+
}
|
|
959
|
+
// Handle ASK_USER — questions directed at the user (not research tasks)
|
|
960
|
+
if (answer.startsWith('ASK_USER:') || answer.includes('\nASK_USER:')) {
|
|
961
|
+
const askLine = answer.split('\n').find(l => l.includes('ASK_USER:'));
|
|
962
|
+
const userQuestion = askLine
|
|
963
|
+
? askLine.replace(/^ASK_USER:\s*/, '').trim()
|
|
964
|
+
: answer.replace(/^ASK_USER:\s*/, '').trim();
|
|
965
|
+
return { script: userQuestion, type: 'question' };
|
|
966
|
+
}
|
|
967
|
+
if (answer.includes('NEEDS_DEEPER_RESEARCH')) {
|
|
968
|
+
// Extract the research task context
|
|
969
|
+
const needsLine = answer.split('\n').find(l => l.includes('NEEDS_DEEPER_RESEARCH'));
|
|
970
|
+
const contextLine = answer.split('\n').find(l => l.startsWith('CONTEXT:'));
|
|
971
|
+
const researchTask = needsLine
|
|
972
|
+
? needsLine.replace(/^(PARTIAL:\s*)?NEEDS_DEEPER_RESEARCH:\s*/, '').trim()
|
|
973
|
+
: question;
|
|
974
|
+
const contextStr = contextLine ? contextLine.replace('CONTEXT:', '').trim() : '';
|
|
975
|
+
// Safety check: if the "research task" looks like a question for the user
|
|
976
|
+
// (ends with ?, asks about preferences/needs, is very short), treat it as ASK_USER instead.
|
|
977
|
+
// This catches the common Gemini bug where clarification questions are formatted as research tasks.
|
|
978
|
+
const taskLower = researchTask.toLowerCase();
|
|
979
|
+
const looksLikeUserQuestion = (researchTask.endsWith('?') && (taskLower.includes('would you') ||
|
|
980
|
+
taskLower.includes('do you') ||
|
|
981
|
+
taskLower.includes('could you') ||
|
|
982
|
+
taskLower.includes('what kind of') ||
|
|
983
|
+
taskLower.includes('which') ||
|
|
984
|
+
taskLower.includes('your needs') ||
|
|
985
|
+
taskLower.includes('your preference') ||
|
|
986
|
+
taskLower.includes('more details') ||
|
|
987
|
+
taskLower.includes('clarif') ||
|
|
988
|
+
taskLower.includes('specify') ||
|
|
989
|
+
taskLower.includes('interested in') ||
|
|
990
|
+
researchTask.length < 80 // Very short "tasks" ending in ? are almost always user questions
|
|
991
|
+
));
|
|
992
|
+
if (looksLikeUserQuestion) {
|
|
993
|
+
console.log(`🧠 [fast brain] Caught question-as-research-task, redirecting to ASK_USER: "${researchTask.substring(0, 100)}"`);
|
|
994
|
+
return { script: researchTask, type: 'question' };
|
|
995
|
+
}
|
|
996
|
+
const fullTask = contextStr ? `${researchTask}\n\nContext: ${contextStr}` : researchTask;
|
|
997
|
+
// Extract any partial answer (spoken script before NEEDS_DEEPER_RESEARCH)
|
|
998
|
+
const partialMatch = answer.match(/^PARTIAL:\s*([\s\S]*?)(?=\nNEEDS_DEEPER_RESEARCH)/m);
|
|
999
|
+
const partialScript = partialMatch ? partialMatch[1].trim() : '';
|
|
1000
|
+
// Generate a task ID for frontend tracking
|
|
1001
|
+
researchTaskCounter++;
|
|
1002
|
+
const taskId = `research-${researchTaskCounter}-${Date.now()}`;
|
|
1003
|
+
// Trigger research in background
|
|
1004
|
+
callbacks.triggerResearch(fullTask);
|
|
1005
|
+
callbacks.sendToFrontend({
|
|
1006
|
+
type: 'research_task_started',
|
|
1007
|
+
task: researchTask.substring(0, 200),
|
|
1008
|
+
taskId,
|
|
1009
|
+
});
|
|
1010
|
+
// Generate acknowledgment script
|
|
1011
|
+
let script;
|
|
1012
|
+
if (partialScript) {
|
|
1013
|
+
script = `${partialScript} Let me dig deeper on the rest.`;
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
// Generate a contextual ack based on conversation flow
|
|
1017
|
+
script = generateResearchAck(question, chatHistory);
|
|
1018
|
+
}
|
|
1019
|
+
return { script, type: 'research_started' };
|
|
1020
|
+
}
|
|
1021
|
+
// Direct answer — the response IS the teleprompter script
|
|
1022
|
+
return { script: answer, type: 'answer' };
|
|
1023
|
+
}
|
|
1024
|
+
/** Detect if the user's question is an EXPLICIT document generation request.
|
|
1025
|
+
* Must be very specific — don't catch general questions about analysis or comparisons.
|
|
1026
|
+
* Only triggers when the user explicitly asks for a written document/artifact. */
|
|
1027
|
+
function detectDocumentRequest(question) {
|
|
1028
|
+
const q = question.toLowerCase();
|
|
1029
|
+
// Only match explicit document requests — "create a comparison", "make a diagram", "write a summary"
|
|
1030
|
+
// Do NOT match: "compare X and Y", "analyze the code", "give me an overview"
|
|
1031
|
+
const docVerbs = /(create|make|generate|write|build|produce|draw)\s+(a\s+|an\s+|the\s+)?/;
|
|
1032
|
+
if (!docVerbs.test(q))
|
|
1033
|
+
return null;
|
|
1034
|
+
if (q.includes('comparison') || q.includes('comparison table') || q.includes('comparison document'))
|
|
1035
|
+
return 'comparison';
|
|
1036
|
+
if (q.includes('diagram') || q.includes('flow chart') || q.includes('architecture diagram'))
|
|
1037
|
+
return 'diagram';
|
|
1038
|
+
if (q.includes('analysis document') || q.includes('tradeoff document'))
|
|
1039
|
+
return 'analysis';
|
|
1040
|
+
if (q.includes('summary document') || q.includes('overview document'))
|
|
1041
|
+
return 'summary';
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
/** Generate a natural research acknowledgment based on conversation context */
|
|
1045
|
+
function generateResearchAck(question, chatHistory) {
|
|
1046
|
+
// Use simple heuristics for a natural ack — no LLM call needed
|
|
1047
|
+
const q = question.toLowerCase();
|
|
1048
|
+
if (q.includes('how') && (q.includes('work') || q.includes('implement'))) {
|
|
1049
|
+
return "Let me look into how that works. I'll have the details for you shortly.";
|
|
1050
|
+
}
|
|
1051
|
+
if (q.includes('what') && (q.includes('option') || q.includes('available') || q.includes('choice'))) {
|
|
1052
|
+
return "Let me research the options for you.";
|
|
1053
|
+
}
|
|
1054
|
+
if (q.includes('why') || q.includes('explain')) {
|
|
1055
|
+
return "Good question. Let me dig into that.";
|
|
1056
|
+
}
|
|
1057
|
+
if (q.includes('find') || q.includes('search') || q.includes('look')) {
|
|
1058
|
+
return "On it. Give me a moment to look into that.";
|
|
1059
|
+
}
|
|
1060
|
+
return "Let me research that for you. I'll have findings shortly.";
|
|
1061
|
+
}
|
|
680
1062
|
// ============================================================
|
|
681
1063
|
// processResearchChunk — Incremental content processing during research
|
|
682
1064
|
// ============================================================
|
|
@@ -686,7 +1068,7 @@ export async function askHaiku(workingDir, sessionId, question, researchContext,
|
|
|
686
1068
|
*
|
|
687
1069
|
* @param isRefinement - true for the final post-research consolidation pass (higher token budget)
|
|
688
1070
|
*/
|
|
689
|
-
export async function processResearchChunk(workingDir, sessionId, task, contentChunks, isRefinement) {
|
|
1071
|
+
export async function processResearchChunk(workingDir, sessionId, task, contentChunks, isRefinement, sessionBaseDir) {
|
|
690
1072
|
initProvider();
|
|
691
1073
|
if (provider === 'none')
|
|
692
1074
|
return null;
|
|
@@ -698,8 +1080,9 @@ export async function processResearchChunk(workingDir, sessionId, task, contentC
|
|
|
698
1080
|
return null;
|
|
699
1081
|
}
|
|
700
1082
|
specUpdateInProgress = true;
|
|
1083
|
+
const wsDir = sessionBaseDir || workingDir;
|
|
701
1084
|
try {
|
|
702
|
-
const workspace = getSessionWorkspace(
|
|
1085
|
+
const workspace = getSessionWorkspace(wsDir, sessionId);
|
|
703
1086
|
const specPath = `${workspace}/spec.md`;
|
|
704
1087
|
if (!existsSync(specPath)) {
|
|
705
1088
|
console.log('⚠️ processResearchChunk: spec.md not found, skipping');
|
|
@@ -711,7 +1094,7 @@ export async function processResearchChunk(workingDir, sessionId, task, contentC
|
|
|
711
1094
|
// Mid-research: skip library entirely to stay fast and avoid file proliferation
|
|
712
1095
|
let existingSection = '';
|
|
713
1096
|
if (isRefinement) {
|
|
714
|
-
const existingFiles = listLibraryFiles(
|
|
1097
|
+
const existingFiles = listLibraryFiles(wsDir, sessionId);
|
|
715
1098
|
const existingContents = [];
|
|
716
1099
|
for (const file of existingFiles) {
|
|
717
1100
|
const filePath = `${libraryDir}/${file}`;
|
|
@@ -744,7 +1127,7 @@ ${chunksText}
|
|
|
744
1127
|
|
|
745
1128
|
Return ONLY valid JSON — no code fences, no explanation.`;
|
|
746
1129
|
let responseText = null;
|
|
747
|
-
if (
|
|
1130
|
+
if (anthropicClient) {
|
|
748
1131
|
const response = await anthropicClient.messages.create({
|
|
749
1132
|
model: ANTHROPIC_FAST_MODEL,
|
|
750
1133
|
max_tokens: isRefinement ? 20000 : 10000,
|
|
@@ -753,7 +1136,7 @@ Return ONLY valid JSON — no code fences, no explanation.`;
|
|
|
753
1136
|
});
|
|
754
1137
|
responseText = response.content[0].type === 'text' ? response.content[0].text : null;
|
|
755
1138
|
}
|
|
756
|
-
else {
|
|
1139
|
+
else if (geminiClient) {
|
|
757
1140
|
const response = await geminiClient.models.generateContent({
|
|
758
1141
|
model: GEMINI_FAST_MODEL,
|
|
759
1142
|
contents: userMessage,
|
|
@@ -886,7 +1269,7 @@ ${specSection}${libSection}
|
|
|
886
1269
|
|
|
887
1270
|
Augment the agent's findings with relevant context from the spec. Pass ALL details through verbatim.`;
|
|
888
1271
|
let responseText = null;
|
|
889
|
-
if (
|
|
1272
|
+
if (anthropicClient) {
|
|
890
1273
|
const response = await anthropicClient.messages.create({
|
|
891
1274
|
model: ANTHROPIC_FAST_MODEL,
|
|
892
1275
|
max_tokens: 16000,
|
|
@@ -895,7 +1278,7 @@ Augment the agent's findings with relevant context from the spec. Pass ALL detai
|
|
|
895
1278
|
});
|
|
896
1279
|
responseText = response.content[0].type === 'text' ? response.content[0].text : null;
|
|
897
1280
|
}
|
|
898
|
-
else {
|
|
1281
|
+
else if (geminiClient) {
|
|
899
1282
|
const response = await geminiClient.models.generateContent({
|
|
900
1283
|
model: GEMINI_FAST_MODEL,
|
|
901
1284
|
contents: userMessage,
|
|
@@ -931,7 +1314,7 @@ Augment the agent's findings with relevant context from the spec. Pass ALL detai
|
|
|
931
1314
|
*
|
|
932
1315
|
* Returns { spec, libraryFiles } or null if update failed.
|
|
933
1316
|
*/
|
|
934
|
-
export async function updateSpecFromJSONL(workingDir, sessionId, task, researchLog) {
|
|
1317
|
+
export async function updateSpecFromJSONL(workingDir, sessionId, task, researchLog, sessionBaseDir) {
|
|
935
1318
|
initProvider();
|
|
936
1319
|
if (provider === 'none')
|
|
937
1320
|
return null;
|
|
@@ -983,7 +1366,7 @@ export async function updateSpecFromJSONL(workingDir, sessionId, task, researchL
|
|
|
983
1366
|
const totalChars = contentChunks.reduce((sum, c) => sum + c.length, 0);
|
|
984
1367
|
console.log(`📖 updateSpecFromJSONL: read ${toolResults.length} tool results, ${agentTexts.length} agent messages, ${subagents.length} sub-agents (${totalChars} total chars)`);
|
|
985
1368
|
// 3. Pass to processResearchChunk with isRefinement=true
|
|
986
|
-
return processResearchChunk(workingDir, sessionId, task, contentChunks, true);
|
|
1369
|
+
return processResearchChunk(workingDir, sessionId, task, contentChunks, true, sessionBaseDir);
|
|
987
1370
|
}
|
|
988
1371
|
catch (err) {
|
|
989
1372
|
console.error('❌ updateSpecFromJSONL failed:', err);
|
|
@@ -1027,7 +1410,7 @@ Rules:
|
|
|
1027
1410
|
- Output ONLY the full spec.md content or the word SKIP — nothing else`;
|
|
1028
1411
|
const userMessage = `Current spec.md:\n\`\`\`\n${currentSpec}\n\`\`\`\n\nNew user question to track:\n"${question}"`;
|
|
1029
1412
|
let responseText = null;
|
|
1030
|
-
if (
|
|
1413
|
+
if (anthropicClient) {
|
|
1031
1414
|
const response = await anthropicClient.messages.create({
|
|
1032
1415
|
model: ANTHROPIC_FAST_MODEL,
|
|
1033
1416
|
max_tokens: 8000,
|
|
@@ -1036,7 +1419,7 @@ Rules:
|
|
|
1036
1419
|
});
|
|
1037
1420
|
responseText = response.content[0].type === 'text' ? response.content[0].text : null;
|
|
1038
1421
|
}
|
|
1039
|
-
else {
|
|
1422
|
+
else if (geminiClient) {
|
|
1040
1423
|
const response = await geminiClient.models.generateContent({
|
|
1041
1424
|
model: GEMINI_FAST_MODEL,
|
|
1042
1425
|
contents: userMessage,
|
|
@@ -1123,7 +1506,7 @@ Rules:
|
|
|
1123
1506
|
const truncatedOutput = output.length > 15000 ? output.substring(0, 15000) + '\n[... truncated]' : output;
|
|
1124
1507
|
const userMessage = `Current spec.md:\n\`\`\`\n${currentSpec}\n\`\`\`\n\nAgent output (${outputType}):\n\`\`\`\n${truncatedOutput}\n\`\`\``;
|
|
1125
1508
|
let responseText = null;
|
|
1126
|
-
if (
|
|
1509
|
+
if (anthropicClient) {
|
|
1127
1510
|
const response = await anthropicClient.messages.create({
|
|
1128
1511
|
model: ANTHROPIC_FAST_MODEL,
|
|
1129
1512
|
max_tokens: 8000,
|
|
@@ -1132,7 +1515,7 @@ Rules:
|
|
|
1132
1515
|
});
|
|
1133
1516
|
responseText = response.content[0].type === 'text' ? response.content[0].text : null;
|
|
1134
1517
|
}
|
|
1135
|
-
else {
|
|
1518
|
+
else if (geminiClient) {
|
|
1136
1519
|
const response = await geminiClient.models.generateContent({
|
|
1137
1520
|
model: GEMINI_FAST_MODEL,
|
|
1138
1521
|
contents: userMessage,
|
|
@@ -1168,12 +1551,13 @@ Rules:
|
|
|
1168
1551
|
* Returns a natural 1-2 sentence update, or null if nothing interesting to say.
|
|
1169
1552
|
* 3-second timeout — returns null if the LLM is too slow.
|
|
1170
1553
|
*/
|
|
1171
|
-
export async function contextualizeResearchUpdate(workingDir, sessionId, task, batchEvents, researchLog) {
|
|
1554
|
+
export async function contextualizeResearchUpdate(workingDir, sessionId, task, batchEvents, researchLog, chatHistory, sessionBaseDir) {
|
|
1172
1555
|
initProvider();
|
|
1173
1556
|
if (provider === 'none')
|
|
1174
1557
|
return null;
|
|
1558
|
+
const wsDir = sessionBaseDir || workingDir;
|
|
1175
1559
|
try {
|
|
1176
|
-
const specContent = readSessionSpec(
|
|
1560
|
+
const specContent = readSessionSpec(wsDir, sessionId);
|
|
1177
1561
|
const specTruncated = specContent ? specContent.substring(0, 1500) : '';
|
|
1178
1562
|
// Read last 5 tool results for what was just found
|
|
1179
1563
|
const recentResults = getRecentToolResults(sessionId, workingDir, 5);
|
|
@@ -1194,7 +1578,7 @@ ${resultsSummary}
|
|
|
1194
1578
|
${specTruncated ? `Spec context:\n${specTruncated}` : ''}`;
|
|
1195
1579
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 3000));
|
|
1196
1580
|
let responsePromise;
|
|
1197
|
-
if (
|
|
1581
|
+
if (anthropicClient) {
|
|
1198
1582
|
responsePromise = anthropicClient.messages.create({
|
|
1199
1583
|
model: ANTHROPIC_FAST_MODEL,
|
|
1200
1584
|
max_tokens: 200,
|
|
@@ -1202,13 +1586,16 @@ ${specTruncated ? `Spec context:\n${specTruncated}` : ''}`;
|
|
|
1202
1586
|
messages: [{ role: 'user', content: userMessage }]
|
|
1203
1587
|
}).then(r => r.content[0].type === 'text' ? r.content[0].text : null);
|
|
1204
1588
|
}
|
|
1205
|
-
else {
|
|
1589
|
+
else if (geminiClient) {
|
|
1206
1590
|
responsePromise = geminiClient.models.generateContent({
|
|
1207
1591
|
model: GEMINI_FAST_MODEL,
|
|
1208
1592
|
contents: userMessage,
|
|
1209
1593
|
config: { systemInstruction: CONTEXTUALIZE_UPDATE_SYSTEM }
|
|
1210
1594
|
}).then(r => r.text || null);
|
|
1211
1595
|
}
|
|
1596
|
+
else {
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1212
1599
|
const result = await Promise.race([responsePromise, timeoutPromise]);
|
|
1213
1600
|
if (!result || result.trim() === 'NOTHING')
|
|
1214
1601
|
return null;
|
|
@@ -1230,12 +1617,13 @@ ${specTruncated ? `Spec context:\n${specTruncated}` : ''}`;
|
|
|
1230
1617
|
* Returns null/NOTHING if nothing interesting to say.
|
|
1231
1618
|
* 3-second timeout.
|
|
1232
1619
|
*/
|
|
1233
|
-
export async function generateProactivePrompt(workingDir, sessionId, task, researchLog, previousPrompts) {
|
|
1620
|
+
export async function generateProactivePrompt(workingDir, sessionId, task, researchLog, previousPrompts, sessionBaseDir) {
|
|
1234
1621
|
initProvider();
|
|
1235
1622
|
if (provider === 'none')
|
|
1236
1623
|
return null;
|
|
1624
|
+
const wsDir = sessionBaseDir || workingDir;
|
|
1237
1625
|
try {
|
|
1238
|
-
const specContent = readSessionSpec(
|
|
1626
|
+
const specContent = readSessionSpec(wsDir, sessionId);
|
|
1239
1627
|
const specTruncated = specContent ? specContent.substring(0, 2000) : '';
|
|
1240
1628
|
// Read recent discoveries from JSONL
|
|
1241
1629
|
const recentResults = getRecentToolResults(sessionId, workingDir, 8);
|
|
@@ -1269,7 +1657,7 @@ Previous things already said (DO NOT repeat):
|
|
|
1269
1657
|
${previousPrompts.length > 0 ? previousPrompts.join('\n') : '(none yet)'}`;
|
|
1270
1658
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 3000));
|
|
1271
1659
|
let responsePromise;
|
|
1272
|
-
if (
|
|
1660
|
+
if (anthropicClient) {
|
|
1273
1661
|
responsePromise = anthropicClient.messages.create({
|
|
1274
1662
|
model: ANTHROPIC_FAST_MODEL,
|
|
1275
1663
|
max_tokens: 200,
|
|
@@ -1277,13 +1665,16 @@ ${previousPrompts.length > 0 ? previousPrompts.join('\n') : '(none yet)'}`;
|
|
|
1277
1665
|
messages: [{ role: 'user', content: userMessage }]
|
|
1278
1666
|
}).then(r => r.content[0].type === 'text' ? r.content[0].text : null);
|
|
1279
1667
|
}
|
|
1280
|
-
else {
|
|
1668
|
+
else if (geminiClient) {
|
|
1281
1669
|
responsePromise = geminiClient.models.generateContent({
|
|
1282
1670
|
model: GEMINI_FAST_MODEL,
|
|
1283
1671
|
contents: userMessage,
|
|
1284
1672
|
config: { systemInstruction: PROACTIVE_PROMPT_SYSTEM }
|
|
1285
1673
|
}).then(r => r.text || null);
|
|
1286
1674
|
}
|
|
1675
|
+
else {
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1287
1678
|
const result = await Promise.race([responsePromise, timeoutPromise]);
|
|
1288
1679
|
if (!result || result.trim() === 'NOTHING')
|
|
1289
1680
|
return null;
|
|
@@ -1304,14 +1695,15 @@ ${previousPrompts.length > 0 ? previousPrompts.join('\n') : '(none yet)'}`;
|
|
|
1304
1695
|
* Reads spec.md, JSONL results, and library for context.
|
|
1305
1696
|
* Writes the result to library/ and returns the filename + content.
|
|
1306
1697
|
*/
|
|
1307
|
-
export async function generateVisualDocument(workingDir, sessionId, request, documentType) {
|
|
1698
|
+
export async function generateVisualDocument(workingDir, sessionId, request, documentType, sessionBaseDir) {
|
|
1308
1699
|
initProvider();
|
|
1309
1700
|
if (provider === 'none')
|
|
1310
1701
|
return null;
|
|
1702
|
+
const wsDir = sessionBaseDir || workingDir;
|
|
1311
1703
|
try {
|
|
1312
|
-
const workspace = getSessionWorkspace(
|
|
1313
|
-
const specContent = readSessionSpec(
|
|
1314
|
-
const libraryFiles = listLibraryFiles(
|
|
1704
|
+
const workspace = getSessionWorkspace(wsDir, sessionId);
|
|
1705
|
+
const specContent = readSessionSpec(wsDir, sessionId) || '';
|
|
1706
|
+
const libraryFiles = listLibraryFiles(wsDir, sessionId);
|
|
1315
1707
|
// Read library contents for context
|
|
1316
1708
|
const libraryDir = `${workspace}/library`;
|
|
1317
1709
|
const libraryContents = [];
|
|
@@ -1344,7 +1736,7 @@ ${toolResultsSummary}
|
|
|
1344
1736
|
|
|
1345
1737
|
Return JSON: {"fileName": "descriptive-name.md", "content": "full markdown content"}`;
|
|
1346
1738
|
let responseText = null;
|
|
1347
|
-
if (
|
|
1739
|
+
if (anthropicClient) {
|
|
1348
1740
|
const response = await anthropicClient.messages.create({
|
|
1349
1741
|
model: ANTHROPIC_FAST_MODEL,
|
|
1350
1742
|
max_tokens: 16000,
|
|
@@ -1353,7 +1745,7 @@ Return JSON: {"fileName": "descriptive-name.md", "content": "full markdown conte
|
|
|
1353
1745
|
});
|
|
1354
1746
|
responseText = response.content[0].type === 'text' ? response.content[0].text : null;
|
|
1355
1747
|
}
|
|
1356
|
-
else {
|
|
1748
|
+
else if (geminiClient) {
|
|
1357
1749
|
const response = await geminiClient.models.generateContent({
|
|
1358
1750
|
model: GEMINI_FAST_MODEL,
|
|
1359
1751
|
contents: userMessage,
|
|
@@ -1402,3 +1794,174 @@ Return JSON: {"fileName": "descriptive-name.md", "content": "full markdown conte
|
|
|
1402
1794
|
return null;
|
|
1403
1795
|
}
|
|
1404
1796
|
}
|
|
1797
|
+
// ============================================================
|
|
1798
|
+
// processResearchCompletion — Generate teleprompter script from research results
|
|
1799
|
+
// ============================================================
|
|
1800
|
+
/**
|
|
1801
|
+
* Generate a complete teleprompter script from research results.
|
|
1802
|
+
* Replaces augmentResearchResult + extractPriorityContent.
|
|
1803
|
+
* Reads full JSONL and produces a spoken monologue.
|
|
1804
|
+
*/
|
|
1805
|
+
export async function processResearchCompletion(workingDir, sessionId, task, agentResult, chatHistory, sendToChat, sessionBaseDir) {
|
|
1806
|
+
initProvider();
|
|
1807
|
+
if (provider === 'none')
|
|
1808
|
+
return agentResult.substring(0, 500);
|
|
1809
|
+
const wsDir = sessionBaseDir || workingDir;
|
|
1810
|
+
try {
|
|
1811
|
+
// Read spec for context
|
|
1812
|
+
const specContent = readSessionSpec(wsDir, sessionId) || '';
|
|
1813
|
+
// Read FULL JSONL data — not truncated. The user waited for this research;
|
|
1814
|
+
// give the completion generator the complete picture.
|
|
1815
|
+
const toolResults = getRecentToolResults(sessionId, workingDir, 30);
|
|
1816
|
+
const toolSummary = toolResults.map(tr => {
|
|
1817
|
+
const inputPreview = JSON.stringify(tr.toolInput).substring(0, 200);
|
|
1818
|
+
return `[${tr.toolName}: ${inputPreview}]\n${tr.resultContent}`;
|
|
1819
|
+
}).join('\n\n---\n\n');
|
|
1820
|
+
// Also read agent reasoning for synthesis context
|
|
1821
|
+
const agentTexts = readSessionHistory(sessionId, workingDir, {
|
|
1822
|
+
lastN: 20,
|
|
1823
|
+
types: ['assistant']
|
|
1824
|
+
}).filter(m => m.text && m.text.length > 30)
|
|
1825
|
+
.map(m => m.text)
|
|
1826
|
+
.join('\n\n');
|
|
1827
|
+
// Read sub-agent findings if any
|
|
1828
|
+
const subagents = getSubagentTranscripts(sessionId, workingDir);
|
|
1829
|
+
const subagentSummary = subagents.length > 0
|
|
1830
|
+
? subagents.map(sa => {
|
|
1831
|
+
const texts = sa.messages.filter(m => m.text && m.text.length > 30).map(m => m.text);
|
|
1832
|
+
return `[Sub-agent ${sa.taskId}]\n${texts.join('\n')}`;
|
|
1833
|
+
}).join('\n\n')
|
|
1834
|
+
: '';
|
|
1835
|
+
const historyStr = chatHistory
|
|
1836
|
+
? chatHistory.slice(-10).map(t => `${t.role}: ${t.text}`).join('\n')
|
|
1837
|
+
: '';
|
|
1838
|
+
const userMessage = `Research task: "${task}"
|
|
1839
|
+
|
|
1840
|
+
Agent's headline findings:
|
|
1841
|
+
${agentResult}
|
|
1842
|
+
|
|
1843
|
+
Full tool outputs (${toolResults.length} results):
|
|
1844
|
+
${toolSummary}
|
|
1845
|
+
|
|
1846
|
+
${agentTexts ? `Agent reasoning and analysis:\n${agentTexts.substring(0, 8000)}` : ''}
|
|
1847
|
+
|
|
1848
|
+
${subagentSummary ? `Sub-agent findings:\n${subagentSummary.substring(0, 4000)}` : ''}
|
|
1849
|
+
|
|
1850
|
+
${specContent ? `Session spec (for context):\n${specContent.substring(0, 3000)}` : ''}
|
|
1851
|
+
|
|
1852
|
+
${historyStr ? `Recent conversation (match this vocabulary):\n${historyStr}` : ''}
|
|
1853
|
+
|
|
1854
|
+
Write the spoken monologue now. The user waited for this research — be comprehensive.${sendToChat ? ' If you have structured data (lists, URLs, code, steps), include a CHAT_CONTENT section at the end after a line "---CHAT---" with markdown content to send to the chat panel.' : ''}`;
|
|
1855
|
+
let script = null;
|
|
1856
|
+
if (anthropicClient) {
|
|
1857
|
+
const response = await anthropicClient.messages.create({
|
|
1858
|
+
model: ANTHROPIC_FAST_MODEL,
|
|
1859
|
+
max_tokens: 4000,
|
|
1860
|
+
system: RESEARCH_COMPLETION_SYSTEM,
|
|
1861
|
+
messages: [{ role: 'user', content: userMessage }]
|
|
1862
|
+
});
|
|
1863
|
+
script = response.content[0].type === 'text' ? response.content[0].text : null;
|
|
1864
|
+
}
|
|
1865
|
+
else if (geminiClient) {
|
|
1866
|
+
const response = await geminiClient.models.generateContent({
|
|
1867
|
+
model: GEMINI_FAST_MODEL,
|
|
1868
|
+
contents: userMessage,
|
|
1869
|
+
config: { systemInstruction: RESEARCH_COMPLETION_SYSTEM }
|
|
1870
|
+
});
|
|
1871
|
+
script = response.text || null;
|
|
1872
|
+
}
|
|
1873
|
+
if (!script)
|
|
1874
|
+
return agentResult.substring(0, 500);
|
|
1875
|
+
// Check for chat content section
|
|
1876
|
+
if (sendToChat && script.includes('---CHAT---')) {
|
|
1877
|
+
const parts = script.split('---CHAT---');
|
|
1878
|
+
const spokenPart = parts[0].trim();
|
|
1879
|
+
const chatPart = parts[1]?.trim();
|
|
1880
|
+
if (chatPart) {
|
|
1881
|
+
console.log(`💬 processResearchCompletion: sending ${chatPart.length} chars to chat`);
|
|
1882
|
+
sendToChat(chatPart);
|
|
1883
|
+
}
|
|
1884
|
+
console.log(`🎙️ processResearchCompletion: generated ${spokenPart.length} char script + ${chatPart?.length || 0} char chat content`);
|
|
1885
|
+
return spokenPart;
|
|
1886
|
+
}
|
|
1887
|
+
console.log(`🎙️ processResearchCompletion: generated ${script.length} char script`);
|
|
1888
|
+
return script;
|
|
1889
|
+
}
|
|
1890
|
+
catch (err) {
|
|
1891
|
+
console.error('❌ processResearchCompletion failed:', err);
|
|
1892
|
+
// Fallback: return truncated agent result as-is
|
|
1893
|
+
return agentResult.substring(0, 500);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
// ============================================================
|
|
1897
|
+
// handleResearchBatch — Decide whether research events are worth speaking
|
|
1898
|
+
// ============================================================
|
|
1899
|
+
/**
|
|
1900
|
+
* Process a batch of research events and decide whether to speak.
|
|
1901
|
+
* Replaces contextualizeResearchUpdate — but usually returns null (silent).
|
|
1902
|
+
* Only speaks when something genuinely critical is found.
|
|
1903
|
+
*/
|
|
1904
|
+
export async function handleResearchBatch(workingDir, sessionId, task, batchEvents, researchLog, chatHistory, sessionBaseDir) {
|
|
1905
|
+
// Usually: stay silent. The frontend spinner handles visual feedback.
|
|
1906
|
+
// Only speak if the batch contains something genuinely interesting.
|
|
1907
|
+
// Quick heuristic: if fewer than 5 research steps, too early to say anything useful
|
|
1908
|
+
if (researchLog.length < 5)
|
|
1909
|
+
return null;
|
|
1910
|
+
// Check if any event mentions something critical (error, user-impacting finding)
|
|
1911
|
+
const hasCritical = batchEvents.some(e => e.toLowerCase().includes('error') ||
|
|
1912
|
+
e.toLowerCase().includes('warning') ||
|
|
1913
|
+
e.toLowerCase().includes('breaking') ||
|
|
1914
|
+
e.toLowerCase().includes('deprecated'));
|
|
1915
|
+
if (!hasCritical)
|
|
1916
|
+
return null;
|
|
1917
|
+
// Something interesting — generate a brief spoken update via contextualizeResearchUpdate
|
|
1918
|
+
return contextualizeResearchUpdate(workingDir, sessionId, task, batchEvents, researchLog, chatHistory, sessionBaseDir);
|
|
1919
|
+
}
|
|
1920
|
+
// ============================================================
|
|
1921
|
+
// prepareBriefingScript — Session resume/switch spoken briefing
|
|
1922
|
+
// ============================================================
|
|
1923
|
+
/**
|
|
1924
|
+
* Generate a brief spoken script for session resume or switch.
|
|
1925
|
+
* Replaces buildContextBriefing + getSpecForVoiceModel.
|
|
1926
|
+
*/
|
|
1927
|
+
export async function prepareBriefingScript(workingDir, sessionId, conversationHistory, type = 'default') {
|
|
1928
|
+
initProvider();
|
|
1929
|
+
// Read spec for context
|
|
1930
|
+
const specContent = readSessionSpec(workingDir, sessionId);
|
|
1931
|
+
if (!specContent && (!conversationHistory || conversationHistory.length === 0)) {
|
|
1932
|
+
return type === 'switch'
|
|
1933
|
+
? 'Switched sessions. What would you like to work on?'
|
|
1934
|
+
: 'Welcome back. What would you like to work on?';
|
|
1935
|
+
}
|
|
1936
|
+
// Extract goal and last topic from spec
|
|
1937
|
+
const goalMatch = specContent?.match(/## Goal\s*\n([\s\S]*?)(?=\n##|$)/);
|
|
1938
|
+
const goal = goalMatch ? goalMatch[1].trim().substring(0, 200) : '';
|
|
1939
|
+
const prefix = type === 'switch' ? 'Switched over.' : 'Welcome back.';
|
|
1940
|
+
// If we have a goal, generate a brief spoken briefing
|
|
1941
|
+
if (goal) {
|
|
1942
|
+
const lastExchanges = conversationHistory
|
|
1943
|
+
? conversationHistory.slice(-3).map(e => `${e.role}: ${e.text.substring(0, 100)}`).join('. ')
|
|
1944
|
+
: '';
|
|
1945
|
+
if (lastExchanges) {
|
|
1946
|
+
return `${prefix} We were working on ${goal}. Last time we discussed ${lastExchanges.substring(0, 150)}. Where would you like to pick up?`;
|
|
1947
|
+
}
|
|
1948
|
+
return `${prefix} We were working on ${goal}. Where would you like to pick up?`;
|
|
1949
|
+
}
|
|
1950
|
+
return type === 'switch'
|
|
1951
|
+
? 'Switched sessions. What would you like to work on?'
|
|
1952
|
+
: 'Session resumed. What would you like to work on?';
|
|
1953
|
+
}
|
|
1954
|
+
// ============================================================
|
|
1955
|
+
// prepareRecoveryScript — Gemini crash recovery spoken script
|
|
1956
|
+
// ============================================================
|
|
1957
|
+
/**
|
|
1958
|
+
* Generate a spoken script after Gemini auto-recovery.
|
|
1959
|
+
* Replaces inline recovery logic in index.ts.
|
|
1960
|
+
*/
|
|
1961
|
+
export async function prepareRecoveryScript(conversationHistory) {
|
|
1962
|
+
if (conversationHistory && conversationHistory.length > 0) {
|
|
1963
|
+
const lastTopic = conversationHistory[conversationHistory.length - 1];
|
|
1964
|
+
return `Voice session was briefly interrupted but I'm back. We were talking about ${lastTopic.text.substring(0, 100)}. Where were we?`;
|
|
1965
|
+
}
|
|
1966
|
+
return 'Voice session was briefly interrupted but I\'m back. What were we working on?';
|
|
1967
|
+
}
|