osborn 0.5.2 → 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 +346 -79
- package/dist/config.d.ts +6 -2
- package/dist/config.js +6 -1
- package/dist/fast-brain.d.ts +124 -12
- package/dist/fast-brain.js +1361 -96
- 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 +889 -394
- 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 -8
- package/dist/prompts.js +1990 -374
- package/dist/session-access.d.ts +60 -2
- package/dist/session-access.js +172 -2
- 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 +18 -11
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pipeline-fastbrain.ts — Pipeline Fast Brain (Agent with AFC)
|
|
3
|
+
*
|
|
4
|
+
* Uses Gemini Flash as an AGENT with Automatic Function Calling (AFC).
|
|
5
|
+
* One generateContent() call handles everything:
|
|
6
|
+
* - Gemini decides IF it needs to search (skips for greetings/follow-ups)
|
|
7
|
+
* - Gemini decides WHAT to search (smart phrase selection)
|
|
8
|
+
* - Gemini can multi-step: search → not enough → refine → search again
|
|
9
|
+
* - AFC handles the tool loop internally (up to 3 rounds)
|
|
10
|
+
*
|
|
11
|
+
* Tools:
|
|
12
|
+
* search_session — ripgrep the summary index + read full content via byte offsets
|
|
13
|
+
*
|
|
14
|
+
* No separate phrase extraction call. No manual tool loop. One API invocation.
|
|
15
|
+
*/
|
|
16
|
+
import { GoogleGenAI } from '@google/genai';
|
|
17
|
+
// ============================================================
|
|
18
|
+
// CONSTANTS
|
|
19
|
+
// ============================================================
|
|
20
|
+
const GEMINI_MODEL = 'gemini-2.0-flash';
|
|
21
|
+
const TIMEOUT_MS = 20_000; // AFC needs time for tool calls + processing + synthesis
|
|
22
|
+
const MAX_AFC_CALLS = 4;
|
|
23
|
+
// ============================================================
|
|
24
|
+
// PERSISTENT STATE
|
|
25
|
+
// ============================================================
|
|
26
|
+
let persistentContents = [];
|
|
27
|
+
let persistentSessionId = null;
|
|
28
|
+
/** Clear the pipeline fast brain session (call on disconnect/reconnect) */
|
|
29
|
+
export function clearPipelineFastBrainSession() {
|
|
30
|
+
persistentContents = [];
|
|
31
|
+
persistentSessionId = null;
|
|
32
|
+
}
|
|
33
|
+
/** No-op — kept for backward compatibility with index.ts import */
|
|
34
|
+
export async function prewarmBM25Index(_sessionId, _workingDir) { }
|
|
35
|
+
function createSearchTool(sessionId, workingDir, sessionBaseDir, agentControl) {
|
|
36
|
+
let searchCount = 0;
|
|
37
|
+
const callableTool = {
|
|
38
|
+
async tool() {
|
|
39
|
+
return {
|
|
40
|
+
functionDeclarations: [
|
|
41
|
+
{
|
|
42
|
+
name: 'search_session',
|
|
43
|
+
description: 'Search session history by keywords. Returns summaries + full untruncated content. Use for questions about what was discussed, decided, researched, or built.',
|
|
44
|
+
parameters: {
|
|
45
|
+
type: 'OBJECT',
|
|
46
|
+
properties: {
|
|
47
|
+
phrases: {
|
|
48
|
+
type: 'ARRAY',
|
|
49
|
+
items: { type: 'STRING' },
|
|
50
|
+
description: '2-3 word search phrases, lowercase. Include one phrase per topic.',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
required: ['phrases'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'get_recent',
|
|
58
|
+
description: 'Get the most recent session activity with full content. Use for: "where did we leave off?", "what just happened?", "what are we working on?", or any question about recent/current work.',
|
|
59
|
+
parameters: {
|
|
60
|
+
type: 'OBJECT',
|
|
61
|
+
properties: {
|
|
62
|
+
count: {
|
|
63
|
+
type: 'NUMBER',
|
|
64
|
+
description: 'Number of recent entries. Default 20, max 50.',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
...(agentControl ? [{
|
|
70
|
+
name: 'emergency_stop',
|
|
71
|
+
description: [
|
|
72
|
+
'Kill and restart the main agent with new instructions.',
|
|
73
|
+
'ONLY call this when BOTH conditions are met:',
|
|
74
|
+
' 1. The agent is performing a DESTRUCTIVE or ALTERING action (write, edit, delete, overwrite, install, deploy, push, drop, remove, modify files/data).',
|
|
75
|
+
' 2. The user signals they want it stopped (high intent: "stop", "don\'t", "cancel that", "wait no", "not that").',
|
|
76
|
+
'NEVER call for: research, reading, exploring, searching, fetching, or conversation.',
|
|
77
|
+
'Priority: how destructive/unrecoverable the action is > how strongly the user signals.',
|
|
78
|
+
].join(' '),
|
|
79
|
+
parameters: {
|
|
80
|
+
type: 'OBJECT',
|
|
81
|
+
properties: {
|
|
82
|
+
reason: {
|
|
83
|
+
type: 'STRING',
|
|
84
|
+
description: 'What destructive action is being stopped and what the user wants instead. Use their exact words.',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
required: ['reason'],
|
|
88
|
+
},
|
|
89
|
+
}] : []),
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
async callTool(functionCalls) {
|
|
94
|
+
const results = [];
|
|
95
|
+
for (const call of functionCalls) {
|
|
96
|
+
if (call.name === 'search_session') {
|
|
97
|
+
searchCount++;
|
|
98
|
+
const phrases = call.args?.phrases || [];
|
|
99
|
+
if (phrases.length === 0) {
|
|
100
|
+
results.push({ functionResponse: { name: 'search_session', response: { result: 'No phrases provided' } } });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
console.log(`🧠⚡ [pipeline-fb] AFC search: [${phrases.join(', ')}]`);
|
|
104
|
+
const searchResult = await executeSearch(phrases, sessionId, workingDir, sessionBaseDir);
|
|
105
|
+
results.push({ functionResponse: { name: 'search_session', response: { result: searchResult } } });
|
|
106
|
+
}
|
|
107
|
+
else if (call.name === 'get_recent') {
|
|
108
|
+
searchCount++;
|
|
109
|
+
const count = Math.min(Math.max(call.args?.count || 20, 5), 50);
|
|
110
|
+
console.log(`🧠⚡ [pipeline-fb] AFC get_recent: ${count}`);
|
|
111
|
+
const recent = await getRecentEntries(sessionId, workingDir, sessionBaseDir, count);
|
|
112
|
+
results.push({ functionResponse: { name: 'get_recent', response: { result: recent } } });
|
|
113
|
+
}
|
|
114
|
+
else if (call.name === 'emergency_stop' && agentControl) {
|
|
115
|
+
const reason = call.args?.reason || 'user requested stop';
|
|
116
|
+
console.log(`🧠⚡ [pipeline-fb] AFC emergency_stop: ${reason}`);
|
|
117
|
+
// Gather context
|
|
118
|
+
const recentUserMessages = agentControl.getRecentUserMessages(10);
|
|
119
|
+
const recentActivity = await getRecentEntries(sessionId, workingDir, sessionBaseDir, 10);
|
|
120
|
+
// Kill the destructive process and restart with new instructions
|
|
121
|
+
agentControl.abort();
|
|
122
|
+
const restartPrompt = [
|
|
123
|
+
`[EMERGENCY STOP] A destructive action was stopped by the user.`,
|
|
124
|
+
``,
|
|
125
|
+
`Reason: ${reason}`,
|
|
126
|
+
``,
|
|
127
|
+
`Recent user messages:`,
|
|
128
|
+
...recentUserMessages.map((m, i) => ` ${i + 1}. ${m}`),
|
|
129
|
+
``,
|
|
130
|
+
`What was happening before the stop:`,
|
|
131
|
+
recentActivity.substring(0, 2000),
|
|
132
|
+
``,
|
|
133
|
+
`Review any changes already made. The user wants to change course.`,
|
|
134
|
+
].join('\n');
|
|
135
|
+
agentControl.sendPrompt(restartPrompt);
|
|
136
|
+
results.push({ functionResponse: { name: 'emergency_stop', response: { result: `Agent stopped and restarted. Reason: ${reason}` } } });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return results;
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
return { tool: callableTool, searchCount, getSearchCount: () => searchCount };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Execute a search: ripgrep the summary index, then read full content via byte offsets.
|
|
146
|
+
*/
|
|
147
|
+
async function executeSearch(phrases, sessionId, workingDir, sessionBaseDir) {
|
|
148
|
+
const { ripgrepSearch } = await import('./jsonl-search.js');
|
|
149
|
+
const { getIndexPath, readFullContent } = await import('./summary-index.js');
|
|
150
|
+
const indexPath = getIndexPath(sessionId, sessionBaseDir);
|
|
151
|
+
if (indexPath) {
|
|
152
|
+
// ── Fast path: search summary index + targeted byte-offset reads ──
|
|
153
|
+
const sections = [];
|
|
154
|
+
const matchedRefs = [];
|
|
155
|
+
const seenLines = new Set();
|
|
156
|
+
let totalMatches = 0;
|
|
157
|
+
for (const phrase of phrases.slice(0, 6)) {
|
|
158
|
+
const results = ripgrepSearch(indexPath, phrase, {
|
|
159
|
+
maxResults: 8,
|
|
160
|
+
fromEnd: true,
|
|
161
|
+
contextLines: 0,
|
|
162
|
+
});
|
|
163
|
+
const newResults = results.filter((r) => {
|
|
164
|
+
const key = `${r.lineNumber}`;
|
|
165
|
+
if (seenLines.has(key))
|
|
166
|
+
return false;
|
|
167
|
+
seenLines.add(key);
|
|
168
|
+
return true;
|
|
169
|
+
});
|
|
170
|
+
if (newResults.length > 0) {
|
|
171
|
+
sections.push(`["${phrase}" — ${newResults.length} matches]`);
|
|
172
|
+
for (const r of newResults) {
|
|
173
|
+
const parts = r.content.split('|');
|
|
174
|
+
if (parts.length >= 6) {
|
|
175
|
+
matchedRefs.push({
|
|
176
|
+
lineNum: parseInt(parts[0], 10),
|
|
177
|
+
byteOffset: parseInt(parts[1], 10),
|
|
178
|
+
source: parts[3],
|
|
179
|
+
});
|
|
180
|
+
sections.push(r.content);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
totalMatches += newResults.length;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Read full content for matched entries (byte-offset reads, ~0.5ms each)
|
|
187
|
+
if (matchedRefs.length > 0) {
|
|
188
|
+
try {
|
|
189
|
+
const fullTexts = readFullContent(matchedRefs, sessionId, workingDir, sessionBaseDir, 2000);
|
|
190
|
+
if (fullTexts.length > 0) {
|
|
191
|
+
sections.push('', `[FULL CONTENT — ${fullTexts.length} entries]`, ...fullTexts);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch { }
|
|
195
|
+
}
|
|
196
|
+
if (sections.length === 0) {
|
|
197
|
+
return `No matches for: ${phrases.join(', ')}`;
|
|
198
|
+
}
|
|
199
|
+
return sections.join('\n');
|
|
200
|
+
}
|
|
201
|
+
// ── Fallback: raw JSONL search ──
|
|
202
|
+
const { getSessionPaths } = await import('./session-access.js');
|
|
203
|
+
const paths = getSessionPaths(sessionId, workingDir);
|
|
204
|
+
if (!paths.exists)
|
|
205
|
+
return 'No session files found';
|
|
206
|
+
const sections = [];
|
|
207
|
+
for (const phrase of phrases.slice(0, 4)) {
|
|
208
|
+
const results = ripgrepSearch(paths.conversation, phrase, {
|
|
209
|
+
maxResults: 5,
|
|
210
|
+
fromEnd: true,
|
|
211
|
+
contextLines: 0,
|
|
212
|
+
});
|
|
213
|
+
if (results.length > 0) {
|
|
214
|
+
sections.push(`["${phrase}" — ${results.length} matches]`);
|
|
215
|
+
sections.push(...results.map((r) => `L${r.lineNumber}: ${r.content}`));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return sections.length > 0 ? sections.join('\n') : `No matches for: ${phrases.join(', ')}`;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get the most recent N entries from the index + their full content.
|
|
222
|
+
* Reads last N lines of search-index.txt, then byte-offset reads for full text.
|
|
223
|
+
*/
|
|
224
|
+
async function getRecentEntries(sessionId, workingDir, sessionBaseDir, count) {
|
|
225
|
+
const { readFileSync } = await import('fs');
|
|
226
|
+
const { getIndexPath, readFullContent } = await import('./summary-index.js');
|
|
227
|
+
const indexPath = getIndexPath(sessionId, sessionBaseDir);
|
|
228
|
+
if (!indexPath)
|
|
229
|
+
return 'Index not built yet.';
|
|
230
|
+
// Read last N lines
|
|
231
|
+
const content = readFileSync(indexPath, 'utf-8');
|
|
232
|
+
const allLines = content.split('\n').filter(Boolean);
|
|
233
|
+
const recentLines = allLines.slice(-count);
|
|
234
|
+
// Parse refs for full content reads
|
|
235
|
+
const refs = [];
|
|
236
|
+
const summaries = [`[RECENT — last ${recentLines.length} entries]`];
|
|
237
|
+
for (const line of recentLines) {
|
|
238
|
+
summaries.push(line);
|
|
239
|
+
const parts = line.split('|');
|
|
240
|
+
if (parts.length >= 6) {
|
|
241
|
+
refs.push({
|
|
242
|
+
lineNum: parseInt(parts[0], 10),
|
|
243
|
+
byteOffset: parseInt(parts[1], 10),
|
|
244
|
+
source: parts[3],
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Read full content for each entry
|
|
249
|
+
if (refs.length > 0) {
|
|
250
|
+
try {
|
|
251
|
+
const fullTexts = readFullContent(refs, sessionId, workingDir, sessionBaseDir, 1500);
|
|
252
|
+
if (fullTexts.length > 0) {
|
|
253
|
+
summaries.push('', `[FULL CONTENT — ${fullTexts.length} entries]`, ...fullTexts);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch { }
|
|
257
|
+
}
|
|
258
|
+
return summaries.join('\n');
|
|
259
|
+
}
|
|
260
|
+
// ============================================================
|
|
261
|
+
// SYSTEM PROMPT
|
|
262
|
+
// ============================================================
|
|
263
|
+
function buildSystemPrompt(chatHistory, researchContext) {
|
|
264
|
+
const parts = [];
|
|
265
|
+
parts.push(
|
|
266
|
+
// CONTEXT
|
|
267
|
+
`You are a fast memory recall agent for a voice AI assistant called Osborn.`, `You search the user's conversation history — their questions, the assistant's answers,`, `tool calls, research findings, and decisions — stored as indexed session files.`, `Tools: search_session (keyword search) and get_recent (latest activity).`, ``,
|
|
268
|
+
// OBJECTIVE
|
|
269
|
+
`== OBJECTIVE ==`, `Answer from session history. Search first for any recall question.`, `Greetings/thanks/confirmations: respond directly, no search.`, `Tasks needing live code analysis or new research: respond with [RESEARCH_NEEDED]`, ``,
|
|
270
|
+
// STYLE
|
|
271
|
+
`== STYLE ==`, `1-3 sentences. Grounded in results. Never fabricate.`, `If not found after thorough searching: "I didn't find that in the session history."`, ``,
|
|
272
|
+
// AUDIENCE
|
|
273
|
+
`== AUDIENCE ==`, `A user having a conversation and asking questions based on past context and research/task intentions via voice. Questions may be casual, rambling,`, `or use vague references ("that thing", "the error"). Interpret intent, not just words.`, ``,
|
|
274
|
+
// RESULTS FORMAT
|
|
275
|
+
`== RESULTS FORMAT ==`, `Each line: lineNum|byteOffset|timestamp|source|msgType|summary`, ` source: "main" = conversation, "agent-XXXX" = sub-agent research`, `Full content sections have complete untruncated text.`, ``,
|
|
276
|
+
// SEARCH STRATEGY
|
|
277
|
+
`== HOW TO SEARCH ==`, `You are searching a CONVERSATION, not a database. Think about what words people`, `ACTUALLY USED when this topic came up — not how the user is phrasing it now.`, ``, `PHRASES: 1-4 words each, multiple phrases per call.`, ` Short precise terms beat long phrases. "error" finds more than "error we got".`, ` Single words work great: "BM25", "latency", "crash", "watcher".`, ` Longer user questions = more clues. Mine them for specific nouns and names.`, ` e.g. "can you check the file sizes and see if the watcher is running"`, ` → ["file size", "watcher", "indexer", "running"]`, ``, `RETRIES (4 rounds — use them before giving up):`, ` 1: Specific terms from the question.`, ` 2: Think about how the conversation would READ when this was discussed.`, ` What would the assistant have said? What would the user have asked?`, ` 3: Related terms — names, tools, files that would appear near the topic.`, ` 4: Broad single words — cast a wide net.`, ` Only say "didn't find" after 3+ failed rounds.`, ``, `FOLLOW-UPS: "why?", "what about that?", "the other one?" — check your recent`, ` conversation to find the topic, then search for THAT topic specifically.`, ``, `⚠ Your own prior answers may have errors. Trust search results over your memory.`);
|
|
278
|
+
if (chatHistory && chatHistory.length > 0) {
|
|
279
|
+
parts.push(``, `== RECENT CONVERSATION ==`);
|
|
280
|
+
for (const turn of chatHistory.slice(-6)) {
|
|
281
|
+
parts.push(`${turn.role}: ${turn.content.substring(0, 200)}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (researchContext) {
|
|
285
|
+
parts.push(``, `== ACTIVE RESEARCH ==`, researchContext);
|
|
286
|
+
}
|
|
287
|
+
return parts.join('\n');
|
|
288
|
+
}
|
|
289
|
+
// ============================================================
|
|
290
|
+
// MAIN FUNCTION
|
|
291
|
+
// ============================================================
|
|
292
|
+
export async function askPipelineFastBrain(workingDir, sessionId, question, opts) {
|
|
293
|
+
// Skip when no real session yet
|
|
294
|
+
if (!sessionId || sessionId === 'pending') {
|
|
295
|
+
return { script: 'Session is still initializing.', type: 'acknowledgment', toolsUsed: [] };
|
|
296
|
+
}
|
|
297
|
+
const apiKey = process.env.GOOGLE_API_KEY;
|
|
298
|
+
if (!apiKey) {
|
|
299
|
+
return { script: "Search system not available right now.", type: 'acknowledgment', toolsUsed: [] };
|
|
300
|
+
}
|
|
301
|
+
// Reset persistent state if session changed
|
|
302
|
+
if (persistentSessionId !== sessionId) {
|
|
303
|
+
persistentContents = [];
|
|
304
|
+
persistentSessionId = sessionId;
|
|
305
|
+
console.log(`🧠⚡ [pipeline-fb] New session: ${sessionId.substring(0, 8)}`);
|
|
306
|
+
}
|
|
307
|
+
// Prune persistent history (keep last 12)
|
|
308
|
+
if (persistentContents.length > 12) {
|
|
309
|
+
persistentContents = persistentContents.slice(-12);
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
313
|
+
const systemPrompt = buildSystemPrompt(opts?.chatHistory, opts?.researchContext);
|
|
314
|
+
const sessionBaseDir = opts?.sessionBaseDir || workingDir;
|
|
315
|
+
// Create the search tool for this session
|
|
316
|
+
const { tool: searchTool, getSearchCount } = createSearchTool(sessionId, workingDir, sessionBaseDir, opts?.agentControl);
|
|
317
|
+
// Add question to persistent history
|
|
318
|
+
persistentContents.push({ role: 'user', parts: [{ text: question }] });
|
|
319
|
+
// Single generateContent call — AFC handles the tool loop automatically
|
|
320
|
+
const apiCall = ai.models.generateContent({
|
|
321
|
+
model: GEMINI_MODEL,
|
|
322
|
+
contents: persistentContents,
|
|
323
|
+
config: {
|
|
324
|
+
systemInstruction: systemPrompt,
|
|
325
|
+
tools: [searchTool],
|
|
326
|
+
automaticFunctionCalling: { maximumRemoteCalls: MAX_AFC_CALLS },
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
// Real timeout via Promise.race
|
|
330
|
+
const timeoutRace = new Promise((resolve) => setTimeout(() => resolve(null), TIMEOUT_MS));
|
|
331
|
+
const response = await Promise.race([apiCall, timeoutRace]);
|
|
332
|
+
if (!response) {
|
|
333
|
+
persistentContents.pop();
|
|
334
|
+
console.warn(`Pipeline fast brain: timed out after ${TIMEOUT_MS}ms`);
|
|
335
|
+
return { script: 'Search took too long.', type: 'error', toolsUsed: [] };
|
|
336
|
+
}
|
|
337
|
+
const text = response.text;
|
|
338
|
+
if (text) {
|
|
339
|
+
persistentContents.push({ role: 'model', parts: [{ text }] });
|
|
340
|
+
}
|
|
341
|
+
const toolsUsed = getSearchCount() > 0 ? ['search_session'] : [];
|
|
342
|
+
console.log(`🧠⚡ [pipeline-fb] AFC: ${getSearchCount()} searches, answer: "${(text || '').substring(0, 80)}"`);
|
|
343
|
+
if (!text?.trim()) {
|
|
344
|
+
return {
|
|
345
|
+
script: "I didn't find that in the session history.",
|
|
346
|
+
type: 'answer',
|
|
347
|
+
toolsUsed,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
if (text.includes('[RESEARCH_NEEDED]')) {
|
|
351
|
+
return {
|
|
352
|
+
script: text.replace('[RESEARCH_NEEDED]', '').trim() || 'This needs deeper research.',
|
|
353
|
+
type: 'research_needed',
|
|
354
|
+
toolsUsed,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return { script: text.trim(), type: 'answer', toolsUsed };
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
if (err?.status === 429 || err?.message?.includes('429') || err?.message?.includes('RESOURCE_EXHAUSTED')) {
|
|
361
|
+
console.warn('Pipeline fast brain: 429 rate limited');
|
|
362
|
+
return { script: 'Memory search is cooling down.', type: 'error', toolsUsed: [] };
|
|
363
|
+
}
|
|
364
|
+
console.error('Pipeline fast brain error:', err?.message);
|
|
365
|
+
return { script: 'Search error occurred.', type: 'error', toolsUsed: [] };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
File without changes
|