@geminilight/mindos 0.6.19 → 0.6.21
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/app/app/api/ask/route.ts +147 -315
- package/app/app/api/settings/test-key/route.ts +3 -47
- package/app/components/settings/AiTab.tsx +1 -10
- package/app/components/settings/types.ts +0 -1
- package/app/hooks/useAiOrganize.ts +1 -0
- package/app/lib/agent/model.ts +2 -1
- package/app/lib/agent/prompt.ts +23 -0
- package/app/lib/agent/tools.ts +12 -0
- package/app/lib/i18n-en.ts +0 -1
- package/app/lib/i18n-zh.ts +0 -1
- package/app/lib/settings.ts +0 -2
- package/package.json +1 -1
package/app/app/api/ask/route.ts
CHANGED
|
@@ -16,8 +16,8 @@ import fs from 'fs';
|
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { getFileContent, getMindRoot } from '@/lib/fs';
|
|
18
18
|
import { getModelConfig } from '@/lib/agent/model';
|
|
19
|
-
import { getRequestScopedTools, WRITE_TOOLS, truncate } from '@/lib/agent/tools';
|
|
20
|
-
import { AGENT_SYSTEM_PROMPT } from '@/lib/agent/prompt';
|
|
19
|
+
import { getRequestScopedTools, getOrganizeTools, WRITE_TOOLS, truncate } from '@/lib/agent/tools';
|
|
20
|
+
import { AGENT_SYSTEM_PROMPT, ORGANIZE_SYSTEM_PROMPT } from '@/lib/agent/prompt';
|
|
21
21
|
import { toAgentMessages } from '@/lib/agent/to-agent-messages';
|
|
22
22
|
import { logAgentOp } from '@/lib/agent/log';
|
|
23
23
|
import { readSettings } from '@/lib/settings';
|
|
@@ -27,24 +27,6 @@ import { assertNotProtected } from '@/lib/core';
|
|
|
27
27
|
import { scanExtensionPaths } from '@/lib/pi-integration/extensions';
|
|
28
28
|
import type { Message as FrontendMessage } from '@/lib/types';
|
|
29
29
|
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Streaming blacklist — caches provider+model combos that don't support SSE.
|
|
32
|
-
// Auto-populated when streaming fails; entries expire after 10 minutes
|
|
33
|
-
// so transient proxy issues don't permanently lock out streaming.
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
const streamingBlacklist = new Map<string, number>();
|
|
36
|
-
const STREAMING_BLACKLIST_TTL = 10 * 60 * 1000;
|
|
37
|
-
|
|
38
|
-
function isStreamingBlacklisted(key: string): boolean {
|
|
39
|
-
const ts = streamingBlacklist.get(key);
|
|
40
|
-
if (ts === undefined) return false;
|
|
41
|
-
if (Date.now() - ts > STREAMING_BLACKLIST_TTL) {
|
|
42
|
-
streamingBlacklist.delete(key);
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
30
|
// ---------------------------------------------------------------------------
|
|
49
31
|
// MindOS SSE format — 6 event types (front-back contract)
|
|
50
32
|
// ---------------------------------------------------------------------------
|
|
@@ -270,6 +252,8 @@ export async function POST(req: NextRequest) {
|
|
|
270
252
|
attachedFiles?: string[];
|
|
271
253
|
uploadedFiles?: Array<{ name: string; content: string }>;
|
|
272
254
|
maxSteps?: number;
|
|
255
|
+
/** 'organize' = lean prompt for file import organize; default = full prompt */
|
|
256
|
+
mode?: 'organize' | 'default';
|
|
273
257
|
};
|
|
274
258
|
try {
|
|
275
259
|
body = await req.json();
|
|
@@ -278,6 +262,7 @@ export async function POST(req: NextRequest) {
|
|
|
278
262
|
}
|
|
279
263
|
|
|
280
264
|
const { messages, currentFile, attachedFiles, uploadedFiles } = body;
|
|
265
|
+
const isOrganizeMode = body.mode === 'organize';
|
|
281
266
|
|
|
282
267
|
// Read agent config from settings
|
|
283
268
|
const serverSettings = readSettings();
|
|
@@ -289,99 +274,7 @@ export async function POST(req: NextRequest) {
|
|
|
289
274
|
const thinkingBudget = agentConfig.thinkingBudget ?? 5000;
|
|
290
275
|
const contextStrategy = agentConfig.contextStrategy ?? 'auto';
|
|
291
276
|
|
|
292
|
-
//
|
|
293
|
-
// 1. SKILL.md — complete skill with operating rules (always loaded)
|
|
294
|
-
// 2. user-skill-rules.md — user's personalized rules from KB root (if exists)
|
|
295
|
-
const isZh = serverSettings.disabledSkills?.includes('mindos') ?? false;
|
|
296
|
-
const skillDirName = isZh ? 'mindos-zh' : 'mindos';
|
|
297
|
-
const appDir = process.env.MINDOS_PROJECT_ROOT
|
|
298
|
-
? path.join(process.env.MINDOS_PROJECT_ROOT, 'app')
|
|
299
|
-
: process.cwd();
|
|
300
|
-
const skillPath = path.join(appDir, `data/skills/${skillDirName}/SKILL.md`);
|
|
301
|
-
const skill = readAbsoluteFile(skillPath);
|
|
302
|
-
|
|
303
|
-
const mindRoot = getMindRoot();
|
|
304
|
-
const userSkillRules = readKnowledgeFile('user-skill-rules.md');
|
|
305
|
-
|
|
306
|
-
const targetDir = dirnameOf(currentFile);
|
|
307
|
-
const bootstrap = {
|
|
308
|
-
instruction: readKnowledgeFile('INSTRUCTION.md'),
|
|
309
|
-
index: readKnowledgeFile('README.md'),
|
|
310
|
-
config_json: readKnowledgeFile('CONFIG.json'),
|
|
311
|
-
config_md: readKnowledgeFile('CONFIG.md'),
|
|
312
|
-
target_readme: targetDir ? readKnowledgeFile(`${targetDir}/README.md`) : null,
|
|
313
|
-
target_instruction: targetDir ? readKnowledgeFile(`${targetDir}/INSTRUCTION.md`) : null,
|
|
314
|
-
target_config_json: targetDir ? readKnowledgeFile(`${targetDir}/CONFIG.json`) : null,
|
|
315
|
-
target_config_md: targetDir ? readKnowledgeFile(`${targetDir}/CONFIG.md`) : null,
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
// Only report failures + truncation warnings
|
|
319
|
-
const initFailures: string[] = [];
|
|
320
|
-
const truncationWarnings: string[] = [];
|
|
321
|
-
if (!skill.ok) initFailures.push(`skill.mindos: failed (${skill.error})`);
|
|
322
|
-
if (skill.ok && skill.truncated) truncationWarnings.push('skill.mindos was truncated');
|
|
323
|
-
if (userSkillRules.ok && userSkillRules.truncated) truncationWarnings.push('user-skill-rules.md was truncated');
|
|
324
|
-
if (!bootstrap.instruction.ok) initFailures.push(`bootstrap.instruction: failed (${bootstrap.instruction.error})`);
|
|
325
|
-
if (bootstrap.instruction.ok && bootstrap.instruction.truncated) truncationWarnings.push('bootstrap.instruction was truncated');
|
|
326
|
-
if (!bootstrap.index.ok) initFailures.push(`bootstrap.index: failed (${bootstrap.index.error})`);
|
|
327
|
-
if (bootstrap.index.ok && bootstrap.index.truncated) truncationWarnings.push('bootstrap.index was truncated');
|
|
328
|
-
if (!bootstrap.config_json.ok) initFailures.push(`bootstrap.config_json: failed (${bootstrap.config_json.error})`);
|
|
329
|
-
if (bootstrap.config_json.ok && bootstrap.config_json.truncated) truncationWarnings.push('bootstrap.config_json was truncated');
|
|
330
|
-
if (!bootstrap.config_md.ok) initFailures.push(`bootstrap.config_md: failed (${bootstrap.config_md.error})`);
|
|
331
|
-
if (bootstrap.config_md.ok && bootstrap.config_md.truncated) truncationWarnings.push('bootstrap.config_md was truncated');
|
|
332
|
-
if (bootstrap.target_readme && !bootstrap.target_readme.ok) initFailures.push(`bootstrap.target_readme: failed (${bootstrap.target_readme.error})`);
|
|
333
|
-
if (bootstrap.target_readme?.ok && bootstrap.target_readme.truncated) truncationWarnings.push('bootstrap.target_readme was truncated');
|
|
334
|
-
if (bootstrap.target_instruction && !bootstrap.target_instruction.ok) initFailures.push(`bootstrap.target_instruction: failed (${bootstrap.target_instruction.error})`);
|
|
335
|
-
if (bootstrap.target_instruction?.ok && bootstrap.target_instruction.truncated) truncationWarnings.push('bootstrap.target_instruction was truncated');
|
|
336
|
-
if (bootstrap.target_config_json && !bootstrap.target_config_json.ok) initFailures.push(`bootstrap.target_config_json: failed (${bootstrap.target_config_json.error})`);
|
|
337
|
-
if (bootstrap.target_config_json?.ok && bootstrap.target_config_json.truncated) truncationWarnings.push('bootstrap.target_config_json was truncated');
|
|
338
|
-
if (bootstrap.target_config_md && !bootstrap.target_config_md.ok) initFailures.push(`bootstrap.target_config_md: failed (${bootstrap.target_config_md.error})`);
|
|
339
|
-
if (bootstrap.target_config_md?.ok && bootstrap.target_config_md.truncated) truncationWarnings.push('bootstrap.target_config_md was truncated');
|
|
340
|
-
|
|
341
|
-
const initStatus = initFailures.length === 0
|
|
342
|
-
? `All initialization contexts loaded successfully. mind_root=${getMindRoot()}${targetDir ? `, target_dir=${targetDir}` : ''}${truncationWarnings.length > 0 ? ` ⚠️ ${truncationWarnings.length} files truncated` : ''}`
|
|
343
|
-
: `Initialization issues:\n${initFailures.join('\n')}\nmind_root=${getMindRoot()}${targetDir ? `, target_dir=${targetDir}` : ''}${truncationWarnings.length > 0 ? `\n⚠️ Warnings:\n${truncationWarnings.join('\n')}` : ''}`;
|
|
344
|
-
|
|
345
|
-
const initContextBlocks: string[] = [];
|
|
346
|
-
if (skill.ok) initContextBlocks.push(`## mindos_skill_md\n\n${skill.content}`);
|
|
347
|
-
// User personalization rules (from knowledge base root)
|
|
348
|
-
if (userSkillRules.ok && !userSkillRules.truncated && userSkillRules.content.trim()) {
|
|
349
|
-
initContextBlocks.push(`## user_skill_rules\n\nUser personalization rules (user-skill-rules.md):\n\n${userSkillRules.content}`);
|
|
350
|
-
}
|
|
351
|
-
if (bootstrap.instruction.ok) initContextBlocks.push(`## bootstrap_instruction\n\n${bootstrap.instruction.content}`);
|
|
352
|
-
if (bootstrap.index.ok) initContextBlocks.push(`## bootstrap_index\n\n${bootstrap.index.content}`);
|
|
353
|
-
if (bootstrap.config_json.ok) initContextBlocks.push(`## bootstrap_config_json\n\n${bootstrap.config_json.content}`);
|
|
354
|
-
if (bootstrap.config_md.ok) initContextBlocks.push(`## bootstrap_config_md\n\n${bootstrap.config_md.content}`);
|
|
355
|
-
if (bootstrap.target_readme?.ok) initContextBlocks.push(`## bootstrap_target_readme\n\n${bootstrap.target_readme.content}`);
|
|
356
|
-
if (bootstrap.target_instruction?.ok) initContextBlocks.push(`## bootstrap_target_instruction\n\n${bootstrap.target_instruction.content}`);
|
|
357
|
-
if (bootstrap.target_config_json?.ok) initContextBlocks.push(`## bootstrap_target_config_json\n\n${bootstrap.target_config_json.content}`);
|
|
358
|
-
if (bootstrap.target_config_md?.ok) initContextBlocks.push(`## bootstrap_target_config_md\n\n${bootstrap.target_config_md.content}`);
|
|
359
|
-
|
|
360
|
-
// Build initial context from attached/current files
|
|
361
|
-
const contextParts: string[] = [];
|
|
362
|
-
const seen = new Set<string>();
|
|
363
|
-
const hasAttached = Array.isArray(attachedFiles) && attachedFiles.length > 0;
|
|
364
|
-
|
|
365
|
-
if (hasAttached) {
|
|
366
|
-
for (const filePath of attachedFiles!) {
|
|
367
|
-
if (seen.has(filePath)) continue;
|
|
368
|
-
seen.add(filePath);
|
|
369
|
-
try {
|
|
370
|
-
const content = truncate(getFileContent(filePath));
|
|
371
|
-
contextParts.push(`## Attached: ${filePath}\n\n${content}`);
|
|
372
|
-
} catch { /* ignore missing files */ }
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (currentFile && !seen.has(currentFile)) {
|
|
377
|
-
seen.add(currentFile);
|
|
378
|
-
try {
|
|
379
|
-
const content = truncate(getFileContent(currentFile));
|
|
380
|
-
contextParts.push(`## Current file: ${currentFile}\n\n${content}`);
|
|
381
|
-
} catch { /* ignore */ }
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Uploaded files
|
|
277
|
+
// Uploaded files — shared by both modes
|
|
385
278
|
const uploadedParts: string[] = [];
|
|
386
279
|
if (Array.isArray(uploadedFiles) && uploadedFiles.length > 0) {
|
|
387
280
|
for (const f of uploadedFiles.slice(0, 8)) {
|
|
@@ -390,60 +283,158 @@ export async function POST(req: NextRequest) {
|
|
|
390
283
|
}
|
|
391
284
|
}
|
|
392
285
|
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Build system prompt — lean path for organize mode, full path otherwise
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
let systemPrompt: string;
|
|
290
|
+
|
|
291
|
+
if (isOrganizeMode) {
|
|
292
|
+
// Organize mode: minimal prompt — only KB structure + uploaded files
|
|
293
|
+
const promptParts: string[] = [ORGANIZE_SYSTEM_PROMPT];
|
|
294
|
+
|
|
295
|
+
promptParts.push(`---\n\nmind_root=${getMindRoot()}`);
|
|
296
|
+
|
|
297
|
+
// Only load root README.md for KB structure awareness (skip SKILL.md, configs, target dir, time, etc.)
|
|
298
|
+
const bootstrapIndex = readKnowledgeFile('README.md');
|
|
299
|
+
if (bootstrapIndex.ok) {
|
|
300
|
+
promptParts.push(`---\n\n## Knowledge Base Structure\n\n${bootstrapIndex.content}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (uploadedParts.length > 0) {
|
|
304
|
+
promptParts.push(
|
|
305
|
+
`---\n\n## ⚠️ USER-UPLOADED FILES\n\n` +
|
|
306
|
+
`Their FULL CONTENT is below. Use this directly — do NOT call read tools on them.\n\n` +
|
|
307
|
+
uploadedParts.join('\n\n---\n\n'),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
systemPrompt = promptParts.join('\n\n');
|
|
312
|
+
} else {
|
|
313
|
+
// Full mode: original prompt assembly
|
|
314
|
+
// Auto-load skill + bootstrap context for each request.
|
|
315
|
+
const isZh = serverSettings.disabledSkills?.includes('mindos') ?? false;
|
|
316
|
+
const skillDirName = isZh ? 'mindos-zh' : 'mindos';
|
|
317
|
+
const appDir = process.env.MINDOS_PROJECT_ROOT
|
|
318
|
+
? path.join(process.env.MINDOS_PROJECT_ROOT, 'app')
|
|
319
|
+
: process.cwd();
|
|
320
|
+
const skillPath = path.join(appDir, `data/skills/${skillDirName}/SKILL.md`);
|
|
321
|
+
const skill = readAbsoluteFile(skillPath);
|
|
322
|
+
|
|
323
|
+
const userSkillRules = readKnowledgeFile('user-skill-rules.md');
|
|
324
|
+
|
|
325
|
+
const targetDir = dirnameOf(currentFile);
|
|
326
|
+
const bootstrap = {
|
|
327
|
+
instruction: readKnowledgeFile('INSTRUCTION.md'),
|
|
328
|
+
index: readKnowledgeFile('README.md'),
|
|
329
|
+
config_json: readKnowledgeFile('CONFIG.json'),
|
|
330
|
+
config_md: readKnowledgeFile('CONFIG.md'),
|
|
331
|
+
target_readme: targetDir ? readKnowledgeFile(`${targetDir}/README.md`) : null,
|
|
332
|
+
target_instruction: targetDir ? readKnowledgeFile(`${targetDir}/INSTRUCTION.md`) : null,
|
|
333
|
+
target_config_json: targetDir ? readKnowledgeFile(`${targetDir}/CONFIG.json`) : null,
|
|
334
|
+
target_config_md: targetDir ? readKnowledgeFile(`${targetDir}/CONFIG.md`) : null,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Only report failures + truncation warnings
|
|
338
|
+
const initFailures: string[] = [];
|
|
339
|
+
const truncationWarnings: string[] = [];
|
|
340
|
+
if (!skill.ok) initFailures.push(`skill.mindos: failed (${skill.error})`);
|
|
341
|
+
if (skill.ok && skill.truncated) truncationWarnings.push('skill.mindos was truncated');
|
|
342
|
+
if (userSkillRules.ok && userSkillRules.truncated) truncationWarnings.push('user-skill-rules.md was truncated');
|
|
343
|
+
if (!bootstrap.instruction.ok) initFailures.push(`bootstrap.instruction: failed (${bootstrap.instruction.error})`);
|
|
344
|
+
if (bootstrap.instruction.ok && bootstrap.instruction.truncated) truncationWarnings.push('bootstrap.instruction was truncated');
|
|
345
|
+
if (!bootstrap.index.ok) initFailures.push(`bootstrap.index: failed (${bootstrap.index.error})`);
|
|
346
|
+
if (bootstrap.index.ok && bootstrap.index.truncated) truncationWarnings.push('bootstrap.index was truncated');
|
|
347
|
+
if (!bootstrap.config_json.ok) initFailures.push(`bootstrap.config_json: failed (${bootstrap.config_json.error})`);
|
|
348
|
+
if (bootstrap.config_json.ok && bootstrap.config_json.truncated) truncationWarnings.push('bootstrap.config_json was truncated');
|
|
349
|
+
if (!bootstrap.config_md.ok) initFailures.push(`bootstrap.config_md: failed (${bootstrap.config_md.error})`);
|
|
350
|
+
if (bootstrap.config_md.ok && bootstrap.config_md.truncated) truncationWarnings.push('bootstrap.config_md was truncated');
|
|
351
|
+
if (bootstrap.target_readme && !bootstrap.target_readme.ok) initFailures.push(`bootstrap.target_readme: failed (${bootstrap.target_readme.error})`);
|
|
352
|
+
if (bootstrap.target_readme?.ok && bootstrap.target_readme.truncated) truncationWarnings.push('bootstrap.target_readme was truncated');
|
|
353
|
+
if (bootstrap.target_instruction && !bootstrap.target_instruction.ok) initFailures.push(`bootstrap.target_instruction: failed (${bootstrap.target_instruction.error})`);
|
|
354
|
+
if (bootstrap.target_instruction?.ok && bootstrap.target_instruction.truncated) truncationWarnings.push('bootstrap.target_instruction was truncated');
|
|
355
|
+
if (bootstrap.target_config_json && !bootstrap.target_config_json.ok) initFailures.push(`bootstrap.target_config_json: failed (${bootstrap.target_config_json.error})`);
|
|
356
|
+
if (bootstrap.target_config_json?.ok && bootstrap.target_config_json.truncated) truncationWarnings.push('bootstrap.target_config_json was truncated');
|
|
357
|
+
if (bootstrap.target_config_md && !bootstrap.target_config_md.ok) initFailures.push(`bootstrap.target_config_md: failed (${bootstrap.target_config_md.error})`);
|
|
358
|
+
if (bootstrap.target_config_md?.ok && bootstrap.target_config_md.truncated) truncationWarnings.push('bootstrap.target_config_md was truncated');
|
|
359
|
+
|
|
360
|
+
const initStatus = initFailures.length === 0
|
|
361
|
+
? `All initialization contexts loaded successfully. mind_root=${getMindRoot()}${targetDir ? `, target_dir=${targetDir}` : ''}${truncationWarnings.length > 0 ? ` ⚠️ ${truncationWarnings.length} files truncated` : ''}`
|
|
362
|
+
: `Initialization issues:\n${initFailures.join('\n')}\nmind_root=${getMindRoot()}${targetDir ? `, target_dir=${targetDir}` : ''}${truncationWarnings.length > 0 ? `\n⚠️ Warnings:\n${truncationWarnings.join('\n')}` : ''}`;
|
|
363
|
+
|
|
364
|
+
const initContextBlocks: string[] = [];
|
|
365
|
+
if (skill.ok) initContextBlocks.push(`## mindos_skill_md\n\n${skill.content}`);
|
|
366
|
+
if (userSkillRules.ok && !userSkillRules.truncated && userSkillRules.content.trim()) {
|
|
367
|
+
initContextBlocks.push(`## user_skill_rules\n\nUser personalization rules (user-skill-rules.md):\n\n${userSkillRules.content}`);
|
|
368
|
+
}
|
|
369
|
+
if (bootstrap.instruction.ok) initContextBlocks.push(`## bootstrap_instruction\n\n${bootstrap.instruction.content}`);
|
|
370
|
+
if (bootstrap.index.ok) initContextBlocks.push(`## bootstrap_index\n\n${bootstrap.index.content}`);
|
|
371
|
+
if (bootstrap.config_json.ok) initContextBlocks.push(`## bootstrap_config_json\n\n${bootstrap.config_json.content}`);
|
|
372
|
+
if (bootstrap.config_md.ok) initContextBlocks.push(`## bootstrap_config_md\n\n${bootstrap.config_md.content}`);
|
|
373
|
+
if (bootstrap.target_readme?.ok) initContextBlocks.push(`## bootstrap_target_readme\n\n${bootstrap.target_readme.content}`);
|
|
374
|
+
if (bootstrap.target_instruction?.ok) initContextBlocks.push(`## bootstrap_target_instruction\n\n${bootstrap.target_instruction.content}`);
|
|
375
|
+
if (bootstrap.target_config_json?.ok) initContextBlocks.push(`## bootstrap_target_config_json\n\n${bootstrap.target_config_json.content}`);
|
|
376
|
+
if (bootstrap.target_config_md?.ok) initContextBlocks.push(`## bootstrap_target_config_md\n\n${bootstrap.target_config_md.content}`);
|
|
377
|
+
|
|
378
|
+
// Build initial context from attached/current files
|
|
379
|
+
const contextParts: string[] = [];
|
|
380
|
+
const seen = new Set<string>();
|
|
381
|
+
const hasAttached = Array.isArray(attachedFiles) && attachedFiles.length > 0;
|
|
382
|
+
|
|
383
|
+
if (hasAttached) {
|
|
384
|
+
for (const filePath of attachedFiles!) {
|
|
385
|
+
if (seen.has(filePath)) continue;
|
|
386
|
+
seen.add(filePath);
|
|
387
|
+
try {
|
|
388
|
+
const content = truncate(getFileContent(filePath));
|
|
389
|
+
contextParts.push(`## Attached: ${filePath}\n\n${content}`);
|
|
390
|
+
} catch { /* ignore missing files */ }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (currentFile && !seen.has(currentFile)) {
|
|
395
|
+
seen.add(currentFile);
|
|
396
|
+
try {
|
|
397
|
+
const content = truncate(getFileContent(currentFile));
|
|
398
|
+
contextParts.push(`## Current file: ${currentFile}\n\n${content}`);
|
|
399
|
+
} catch { /* ignore */ }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const now = new Date();
|
|
403
|
+
const timeContext = `## Current Time Context
|
|
396
404
|
- Current UTC Time: ${now.toISOString()}
|
|
397
405
|
- System Local Time: ${new Intl.DateTimeFormat('en-US', { dateStyle: 'full', timeStyle: 'long' }).format(now)}
|
|
398
406
|
- Unix Timestamp: ${Math.floor(now.getTime() / 1000)}
|
|
399
407
|
|
|
400
408
|
*Note: The times listed above represent "NOW". The user may have sent messages hours or days ago in this same conversation thread. Each user message in the history contains its own specific timestamp which you should refer to when understanding historical context.*`;
|
|
401
409
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
promptParts.push(`---\n\nInitialization status (auto-loaded at request start):\n\n${initStatus}`);
|
|
410
|
+
const promptParts: string[] = [AGENT_SYSTEM_PROMPT];
|
|
411
|
+
promptParts.push(`---\n\n${timeContext}`);
|
|
412
|
+
promptParts.push(`---\n\nInitialization status (auto-loaded at request start):\n\n${initStatus}`);
|
|
407
413
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (contextParts.length > 0) {
|
|
413
|
-
promptParts.push(`---\n\nThe user is currently viewing these files:\n\n${contextParts.join('\n\n---\n\n')}`);
|
|
414
|
-
}
|
|
414
|
+
if (initContextBlocks.length > 0) {
|
|
415
|
+
promptParts.push(`---\n\nInitialization context:\n\n${initContextBlocks.join('\n\n---\n\n')}`);
|
|
416
|
+
}
|
|
415
417
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
`The user has uploaded the following file(s) in this conversation. ` +
|
|
420
|
-
`Their FULL CONTENT is provided below. You MUST use this content directly when the user refers to these files. ` +
|
|
421
|
-
`Do NOT use read_file or search tools to find them — they exist only here, not in the knowledge base.\n\n` +
|
|
422
|
-
uploadedParts.join('\n\n---\n\n'),
|
|
423
|
-
);
|
|
424
|
-
}
|
|
418
|
+
if (contextParts.length > 0) {
|
|
419
|
+
promptParts.push(`---\n\nThe user is currently viewing these files:\n\n${contextParts.join('\n\n---\n\n')}`);
|
|
420
|
+
}
|
|
425
421
|
|
|
426
|
-
|
|
422
|
+
if (uploadedParts.length > 0) {
|
|
423
|
+
promptParts.push(
|
|
424
|
+
`---\n\n## ⚠️ USER-UPLOADED FILES (ACTIVE ATTACHMENTS)\n\n` +
|
|
425
|
+
`The user has uploaded the following file(s) in this conversation. ` +
|
|
426
|
+
`Their FULL CONTENT is provided below. You MUST use this content directly when the user refers to these files. ` +
|
|
427
|
+
`Do NOT use read_file or search tools to find them — they exist only here, not in the knowledge base.\n\n` +
|
|
428
|
+
uploadedParts.join('\n\n---\n\n'),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
427
431
|
|
|
428
|
-
|
|
432
|
+
systemPrompt = promptParts.join('\n\n');
|
|
433
|
+
}
|
|
429
434
|
|
|
430
435
|
try {
|
|
431
436
|
const { model, modelName, apiKey, provider } = getModelConfig();
|
|
432
437
|
|
|
433
|
-
// ── Non-streaming path (auto-detected or cached) ──
|
|
434
|
-
// When test-key detected streaming incompatibility, or a previous request
|
|
435
|
-
// failed and cached the result, go directly to non-streaming.
|
|
436
|
-
const cacheKey = `${provider}:${model.id}:${model.baseUrl ?? ''}`;
|
|
437
|
-
if (!useStreaming || isStreamingBlacklisted(cacheKey)) {
|
|
438
|
-
if (isStreamingBlacklisted(cacheKey)) {
|
|
439
|
-
console.log(`[ask] Using non-streaming mode (cached failure for ${cacheKey})`);
|
|
440
|
-
}
|
|
441
|
-
return await handleNonStreaming({
|
|
442
|
-
provider, apiKey, model, systemPrompt, messages, modelName,
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// ── Streaming path (default) ──
|
|
447
438
|
// Convert frontend messages to AgentMessage[]
|
|
448
439
|
const agentMessages = toAgentMessages(messages);
|
|
449
440
|
|
|
@@ -458,7 +449,7 @@ export async function POST(req: NextRequest) {
|
|
|
458
449
|
// Capture API key for this request — safe since each POST creates a new Agent instance.
|
|
459
450
|
const requestApiKey = apiKey;
|
|
460
451
|
const projectRoot = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
|
|
461
|
-
const requestTools = await getRequestScopedTools();
|
|
452
|
+
const requestTools = isOrganizeMode ? getOrganizeTools() : await getRequestScopedTools();
|
|
462
453
|
const customTools = toPiCustomToolDefinitions(requestTools);
|
|
463
454
|
|
|
464
455
|
const authStorage = AuthStorage.create();
|
|
@@ -612,27 +603,10 @@ export async function POST(req: NextRequest) {
|
|
|
612
603
|
}
|
|
613
604
|
});
|
|
614
605
|
|
|
615
|
-
session.prompt(lastUserContent).then(
|
|
606
|
+
session.prompt(lastUserContent).then(() => {
|
|
616
607
|
metrics.recordRequest(Date.now() - requestStartTime);
|
|
617
608
|
if (!hasContent && lastModelError) {
|
|
618
|
-
|
|
619
|
-
// Cache the failure so subsequent requests skip streaming entirely.
|
|
620
|
-
console.warn(`[ask] Streaming failed for ${modelName}, retrying non-streaming: ${lastModelError}`);
|
|
621
|
-
streamingBlacklist.set(cacheKey, Date.now());
|
|
622
|
-
// No visible hint needed — the fallback is transparent to the user
|
|
623
|
-
try {
|
|
624
|
-
const fallbackResult = await directNonStreamingCall({
|
|
625
|
-
provider, apiKey, model, systemPrompt, messages, modelName,
|
|
626
|
-
});
|
|
627
|
-
if (fallbackResult) {
|
|
628
|
-
send({ type: 'text_delta', delta: fallbackResult });
|
|
629
|
-
send({ type: 'done' });
|
|
630
|
-
} else {
|
|
631
|
-
send({ type: 'error', message: lastModelError });
|
|
632
|
-
}
|
|
633
|
-
} catch (fallbackErr) {
|
|
634
|
-
send({ type: 'error', message: lastModelError });
|
|
635
|
-
}
|
|
609
|
+
send({ type: 'error', message: lastModelError });
|
|
636
610
|
} else {
|
|
637
611
|
send({ type: 'done' });
|
|
638
612
|
}
|
|
@@ -663,145 +637,3 @@ export async function POST(req: NextRequest) {
|
|
|
663
637
|
}
|
|
664
638
|
}
|
|
665
639
|
|
|
666
|
-
// ---------------------------------------------------------------------------
|
|
667
|
-
// Non-streaming — direct /chat/completions call (no SSE, no tools)
|
|
668
|
-
// ---------------------------------------------------------------------------
|
|
669
|
-
|
|
670
|
-
interface NonStreamingOpts {
|
|
671
|
-
provider: 'anthropic' | 'openai';
|
|
672
|
-
apiKey: string;
|
|
673
|
-
model: { id: string; baseUrl?: string; maxTokens?: number };
|
|
674
|
-
systemPrompt: string;
|
|
675
|
-
messages: FrontendMessage[];
|
|
676
|
-
modelName: string;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/**
|
|
680
|
-
* Core non-streaming API call. Returns the response text or throws.
|
|
681
|
-
* Used by both the direct non-streaming path and the auto-fallback.
|
|
682
|
-
*/
|
|
683
|
-
async function directNonStreamingCall(opts: NonStreamingOpts): Promise<string> {
|
|
684
|
-
const { provider, apiKey, model, systemPrompt, messages, modelName } = opts;
|
|
685
|
-
const ctrl = new AbortController();
|
|
686
|
-
const timeout = setTimeout(() => ctrl.abort(), 120_000);
|
|
687
|
-
|
|
688
|
-
try {
|
|
689
|
-
if (provider === 'openai') {
|
|
690
|
-
const baseUrl = (model.baseUrl || 'https://api.openai.com/v1').replace(/\/+$/, '');
|
|
691
|
-
const url = `${baseUrl}/chat/completions`;
|
|
692
|
-
const apiMessages = [
|
|
693
|
-
{ role: 'system', content: systemPrompt },
|
|
694
|
-
...messages.map(m => ({ role: m.role, content: m.content })),
|
|
695
|
-
];
|
|
696
|
-
|
|
697
|
-
const res = await fetch(url, {
|
|
698
|
-
method: 'POST',
|
|
699
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
700
|
-
body: JSON.stringify({
|
|
701
|
-
model: model.id,
|
|
702
|
-
messages: apiMessages,
|
|
703
|
-
stream: false,
|
|
704
|
-
max_tokens: model.maxTokens ?? 16_384,
|
|
705
|
-
}),
|
|
706
|
-
signal: ctrl.signal,
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
if (!res.ok) {
|
|
710
|
-
const body = await res.text().catch(() => '');
|
|
711
|
-
throw new Error(`API returned ${res.status}: ${body.slice(0, 500)}`);
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
const json = await res.json();
|
|
715
|
-
return json?.choices?.[0]?.message?.content ?? '';
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// Anthropic
|
|
719
|
-
const url = 'https://api.anthropic.com/v1/messages';
|
|
720
|
-
const apiMessages = messages.map(m => ({ role: m.role, content: m.content }));
|
|
721
|
-
|
|
722
|
-
const res = await fetch(url, {
|
|
723
|
-
method: 'POST',
|
|
724
|
-
headers: {
|
|
725
|
-
'Content-Type': 'application/json',
|
|
726
|
-
'x-api-key': apiKey,
|
|
727
|
-
'anthropic-version': '2023-06-01',
|
|
728
|
-
},
|
|
729
|
-
body: JSON.stringify({
|
|
730
|
-
model: model.id,
|
|
731
|
-
system: systemPrompt,
|
|
732
|
-
messages: apiMessages,
|
|
733
|
-
max_tokens: model.maxTokens ?? 8_192,
|
|
734
|
-
}),
|
|
735
|
-
signal: ctrl.signal,
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
if (!res.ok) {
|
|
739
|
-
const body = await res.text().catch(() => '');
|
|
740
|
-
throw new Error(`API returned ${res.status}: ${body.slice(0, 500)}`);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
const json = await res.json();
|
|
744
|
-
const blocks = json?.content;
|
|
745
|
-
if (Array.isArray(blocks)) {
|
|
746
|
-
return blocks.filter((b: any) => b.type === 'text').map((b: any) => b.text).join('');
|
|
747
|
-
}
|
|
748
|
-
return '';
|
|
749
|
-
} finally {
|
|
750
|
-
clearTimeout(timeout);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
/**
|
|
755
|
-
* Full non-streaming response handler — wraps directNonStreamingCall
|
|
756
|
-
* and returns an SSE-formatted Response for the client.
|
|
757
|
-
*/
|
|
758
|
-
async function handleNonStreaming(opts: NonStreamingOpts): Promise<Response> {
|
|
759
|
-
const { modelName } = opts;
|
|
760
|
-
const requestStartTime = Date.now();
|
|
761
|
-
const encoder = new TextEncoder();
|
|
762
|
-
|
|
763
|
-
try {
|
|
764
|
-
const text = await directNonStreamingCall(opts);
|
|
765
|
-
metrics.recordRequest(Date.now() - requestStartTime);
|
|
766
|
-
|
|
767
|
-
if (!text) {
|
|
768
|
-
metrics.recordError();
|
|
769
|
-
return sseResponse(encoder, { type: 'error', message: `[non-streaming] ${modelName} returned empty response` });
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
console.log(`[ask] Non-streaming response from ${modelName}: ${text.length} chars`);
|
|
773
|
-
const stream = new ReadableStream({
|
|
774
|
-
start(controller) {
|
|
775
|
-
controller.enqueue(encoder.encode(`data:${JSON.stringify({ type: 'text_delta', delta: text })}\n\n`));
|
|
776
|
-
controller.enqueue(encoder.encode(`data:${JSON.stringify({ type: 'done' })}\n\n`));
|
|
777
|
-
controller.close();
|
|
778
|
-
},
|
|
779
|
-
});
|
|
780
|
-
return new Response(stream, { headers: sseHeaders() });
|
|
781
|
-
} catch (err) {
|
|
782
|
-
metrics.recordRequest(Date.now() - requestStartTime);
|
|
783
|
-
metrics.recordError();
|
|
784
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
785
|
-
console.error(`[ask] Non-streaming request failed:`, message);
|
|
786
|
-
return sseResponse(encoder, { type: 'error', message });
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function sseResponse(encoder: TextEncoder, event: MindOSSSEvent): Response {
|
|
791
|
-
const stream = new ReadableStream({
|
|
792
|
-
start(controller) {
|
|
793
|
-
controller.enqueue(encoder.encode(`data:${JSON.stringify(event)}\n\n`));
|
|
794
|
-
controller.close();
|
|
795
|
-
},
|
|
796
|
-
});
|
|
797
|
-
return new Response(stream, { headers: sseHeaders() });
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
function sseHeaders(): HeadersInit {
|
|
801
|
-
return {
|
|
802
|
-
'Content-Type': 'text/event-stream',
|
|
803
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
804
|
-
'Connection': 'keep-alive',
|
|
805
|
-
'X-Accel-Buffering': 'no',
|
|
806
|
-
};
|
|
807
|
-
}
|
|
@@ -46,7 +46,7 @@ async function testAnthropic(apiKey: string, model: string): Promise<{ ok: boole
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
async function testOpenAI(apiKey: string, model: string, baseUrl: string): Promise<{ ok: boolean; latency?: number; code?: ErrorCode; error?: string
|
|
49
|
+
async function testOpenAI(apiKey: string, model: string, baseUrl: string): Promise<{ ok: boolean; latency?: number; code?: ErrorCode; error?: string }> {
|
|
50
50
|
const start = Date.now();
|
|
51
51
|
const ctrl = new AbortController();
|
|
52
52
|
const timer = setTimeout(() => ctrl.abort(), TIMEOUT);
|
|
@@ -58,7 +58,7 @@ async function testOpenAI(apiKey: string, model: string, baseUrl: string): Promi
|
|
|
58
58
|
'Content-Type': 'application/json',
|
|
59
59
|
'Authorization': `Bearer ${apiKey}`,
|
|
60
60
|
},
|
|
61
|
-
body: JSON.stringify({ model,
|
|
61
|
+
body: JSON.stringify({ model, messages: [{ role: 'user', content: 'hi' }] }),
|
|
62
62
|
signal: ctrl.signal,
|
|
63
63
|
});
|
|
64
64
|
const latency = Date.now() - start;
|
|
@@ -76,7 +76,6 @@ async function testOpenAI(apiKey: string, model: string, baseUrl: string): Promi
|
|
|
76
76
|
},
|
|
77
77
|
body: JSON.stringify({
|
|
78
78
|
model,
|
|
79
|
-
max_tokens: 1,
|
|
80
79
|
messages: [
|
|
81
80
|
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
82
81
|
{ role: 'user', content: 'hi' },
|
|
@@ -107,50 +106,7 @@ async function testOpenAI(apiKey: string, model: string, baseUrl: string): Promi
|
|
|
107
106
|
};
|
|
108
107
|
}
|
|
109
108
|
|
|
110
|
-
|
|
111
|
-
// Many proxies pass non-streaming tests but fail at streaming.
|
|
112
|
-
// If streaming fails, we still report ok: true (basic chat works via non-streaming fallback).
|
|
113
|
-
let streamingSupported = true;
|
|
114
|
-
try {
|
|
115
|
-
const streamRes = await fetch(url, {
|
|
116
|
-
method: 'POST',
|
|
117
|
-
headers: {
|
|
118
|
-
'Content-Type': 'application/json',
|
|
119
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
120
|
-
},
|
|
121
|
-
body: JSON.stringify({
|
|
122
|
-
model,
|
|
123
|
-
max_tokens: 5,
|
|
124
|
-
stream: true,
|
|
125
|
-
messages: [{ role: 'user', content: 'Say OK' }],
|
|
126
|
-
}),
|
|
127
|
-
signal: ctrl.signal,
|
|
128
|
-
});
|
|
129
|
-
if (!streamRes.ok) {
|
|
130
|
-
streamingSupported = false;
|
|
131
|
-
} else {
|
|
132
|
-
const reader = streamRes.body?.getReader();
|
|
133
|
-
if (reader) {
|
|
134
|
-
const decoder = new TextDecoder();
|
|
135
|
-
let gotData = false;
|
|
136
|
-
try {
|
|
137
|
-
while (true) {
|
|
138
|
-
const { done, value } = await reader.read();
|
|
139
|
-
if (done) break;
|
|
140
|
-
const text = decoder.decode(value, { stream: true });
|
|
141
|
-
if (text.includes('data:')) { gotData = true; break; }
|
|
142
|
-
}
|
|
143
|
-
} finally {
|
|
144
|
-
reader.releaseLock();
|
|
145
|
-
}
|
|
146
|
-
if (!gotData) streamingSupported = false;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
} catch {
|
|
150
|
-
streamingSupported = false;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return { ok: true, latency, streamingSupported };
|
|
109
|
+
return { ok: true, latency };
|
|
154
110
|
} catch (e: unknown) {
|
|
155
111
|
if (e instanceof Error && e.name === 'AbortError') return { ok: false, code: 'network_error', error: 'Request timed out' };
|
|
156
112
|
return { ok: false, code: 'network_error', error: e instanceof Error ? e.message : 'Network error' };
|
|
@@ -14,7 +14,6 @@ interface TestResult {
|
|
|
14
14
|
latency?: number;
|
|
15
15
|
error?: string;
|
|
16
16
|
code?: ErrorCode;
|
|
17
|
-
streamingSupported?: boolean;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
function errorMessage(t: AiTabProps['t'], code?: ErrorCode): string {
|
|
@@ -73,12 +72,7 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
73
72
|
const json = await res.json();
|
|
74
73
|
|
|
75
74
|
if (json.ok) {
|
|
76
|
-
|
|
77
|
-
setTestResult(prev => ({ ...prev, [providerName]: { state: 'ok', latency: json.latency, streamingSupported } }));
|
|
78
|
-
// Auto-persist streaming capability so /api/ask uses the right path
|
|
79
|
-
if (providerName === data.ai.provider) {
|
|
80
|
-
updateAgent({ useStreaming: streamingSupported });
|
|
81
|
-
}
|
|
75
|
+
setTestResult(prev => ({ ...prev, [providerName]: { state: 'ok', latency: json.latency } }));
|
|
82
76
|
if (okTimerRef.current) clearTimeout(okTimerRef.current);
|
|
83
77
|
okTimerRef.current = setTimeout(() => {
|
|
84
78
|
setTestResult(prev => ({ ...prev, [providerName]: { state: 'idle' } }));
|
|
@@ -146,9 +140,6 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
146
140
|
{result.state === 'ok' && result.latency != null && (
|
|
147
141
|
<span className="text-xs text-success">
|
|
148
142
|
{t.settings.ai.testKeyOk(result.latency)}
|
|
149
|
-
{result.streamingSupported === false && (
|
|
150
|
-
<span className="text-muted-foreground ml-1.5">{t.settings.ai.streamingFallback}</span>
|
|
151
|
-
)}
|
|
152
143
|
</span>
|
|
153
144
|
)}
|
|
154
145
|
{result.state === 'error' && (
|
package/app/lib/agent/model.ts
CHANGED
|
@@ -57,6 +57,8 @@ export function getModelConfig(): {
|
|
|
57
57
|
// For custom proxy endpoints, set conservative compat flags.
|
|
58
58
|
// Most proxies (Azure, Bedrock relays, corporate gateways) only support
|
|
59
59
|
// a subset of OpenAI's features. These defaults prevent silent failures.
|
|
60
|
+
// NOTE: maxTokensField is NOT overridden — pi-ai auto-detects the correct
|
|
61
|
+
// field based on URL (defaults to max_completion_tokens for modern APIs).
|
|
60
62
|
if (hasCustomBase) {
|
|
61
63
|
model = {
|
|
62
64
|
...model,
|
|
@@ -68,7 +70,6 @@ export function getModelConfig(): {
|
|
|
68
70
|
supportsReasoningEffort: false,
|
|
69
71
|
supportsUsageInStreaming: false,
|
|
70
72
|
supportsStrictMode: false,
|
|
71
|
-
maxTokensField: 'max_tokens' as const,
|
|
72
73
|
},
|
|
73
74
|
};
|
|
74
75
|
if (customApiVariant) {
|
package/app/lib/agent/prompt.ts
CHANGED
|
@@ -35,3 +35,26 @@ Persona: Methodical, strictly objective, execution-oriented. Zero fluff. Never u
|
|
|
35
35
|
- Reply in the user's language.
|
|
36
36
|
- Use clean Markdown (tables, lists, bold).
|
|
37
37
|
- End with concrete next actions if the task is incomplete.`;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Lean system prompt for "organize uploaded files" mode.
|
|
41
|
+
*
|
|
42
|
+
* Design goal: ~200 tokens (vs ~600 for general). Strips everything the
|
|
43
|
+
* organize task doesn't need: anti-hallucination (no KB Q&A), cite sources,
|
|
44
|
+
* smart recovery, skills/MCP discovery, output formatting.
|
|
45
|
+
*
|
|
46
|
+
* The full SKILL.md is NOT loaded in organize mode — only the bootstrap
|
|
47
|
+
* README.md (for KB structure awareness) is injected by route.ts.
|
|
48
|
+
*/
|
|
49
|
+
export const ORGANIZE_SYSTEM_PROMPT = `You are MindOS Agent — an expert at organizing information into a local Markdown knowledge base.
|
|
50
|
+
|
|
51
|
+
Your ONLY job: read the user's uploaded files, extract key information, and save well-structured Markdown notes into the knowledge base using file tools.
|
|
52
|
+
|
|
53
|
+
Rules:
|
|
54
|
+
1. Read uploaded file content from the "USER-UPLOADED FILES" section below — do NOT call read tools on them.
|
|
55
|
+
2. Use \`list_files\` to understand the existing KB structure before deciding where to place notes.
|
|
56
|
+
3. Create new files or update existing ones. Prefer \`create_file\` for new content, \`update_section\` / \`append_to_file\` for additions to existing files.
|
|
57
|
+
4. Match the language of the source files when writing notes.
|
|
58
|
+
5. Batch parallel tool calls in a single turn for efficiency.
|
|
59
|
+
6. Do NOT write to the KB root directory — place files under the most fitting subdirectory.
|
|
60
|
+
7. After writing, provide a brief summary of what you created/updated.`;
|
package/app/lib/agent/tools.ts
CHANGED
|
@@ -173,6 +173,18 @@ export const WRITE_TOOLS = new Set([
|
|
|
173
173
|
'update_section', 'edit_lines', 'delete_file', 'rename_file', 'move_file', 'append_csv',
|
|
174
174
|
]);
|
|
175
175
|
|
|
176
|
+
/** Tool names sufficient for the "organize uploaded files" task. */
|
|
177
|
+
const ORGANIZE_TOOL_NAMES = new Set([
|
|
178
|
+
'list_files', 'read_file', 'search',
|
|
179
|
+
'create_file', 'batch_create_files', 'write_file',
|
|
180
|
+
'append_to_file', 'insert_after_heading', 'update_section',
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
/** Lean tool set for organize mode — skips MCP discovery, history, backlinks, etc. */
|
|
184
|
+
export function getOrganizeTools(): AgentTool<any>[] {
|
|
185
|
+
return knowledgeBaseTools.filter(t => ORGANIZE_TOOL_NAMES.has(t.name));
|
|
186
|
+
}
|
|
187
|
+
|
|
176
188
|
export async function getRequestScopedTools(): Promise<AgentTool<any>[]> {
|
|
177
189
|
try {
|
|
178
190
|
const result = await listMcporterServers();
|
package/app/lib/i18n-en.ts
CHANGED
package/app/lib/i18n-zh.ts
CHANGED
package/app/lib/settings.ts
CHANGED
|
@@ -24,7 +24,6 @@ export interface AgentConfig {
|
|
|
24
24
|
thinkingBudget?: number; // default 5000
|
|
25
25
|
contextStrategy?: 'auto' | 'off'; // default 'auto'
|
|
26
26
|
reconnectRetries?: number; // default 3, range 0-10 (0 = disabled)
|
|
27
|
-
useStreaming?: boolean; // default true; false = non-streaming fallback for proxy compat
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
export interface GuideState {
|
|
@@ -131,7 +130,6 @@ function parseAgent(raw: unknown): AgentConfig | undefined {
|
|
|
131
130
|
if (typeof obj.thinkingBudget === 'number') result.thinkingBudget = Math.min(50000, Math.max(1000, obj.thinkingBudget));
|
|
132
131
|
if (obj.contextStrategy === 'auto' || obj.contextStrategy === 'off') result.contextStrategy = obj.contextStrategy;
|
|
133
132
|
if (typeof obj.reconnectRetries === 'number') result.reconnectRetries = Math.min(10, Math.max(0, obj.reconnectRetries));
|
|
134
|
-
if (typeof obj.useStreaming === 'boolean') result.useStreaming = obj.useStreaming;
|
|
135
133
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
136
134
|
}
|
|
137
135
|
|
package/package.json
CHANGED