@geminilight/mindos 0.6.19 → 0.6.20

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.
@@ -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
- // Auto-load skill + bootstrap context for each request.
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
- // Generate current time for the agent's context
394
- const now = new Date();
395
- const timeContext = `## Current Time Context
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
- const promptParts: string[] = [AGENT_SYSTEM_PROMPT];
403
-
404
- promptParts.push(`---\n\n${timeContext}`);
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
- if (initContextBlocks.length > 0) {
409
- promptParts.push(`---\n\nInitialization context:\n\n${initContextBlocks.join('\n\n---\n\n')}`);
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
- if (uploadedParts.length > 0) {
417
- promptParts.push(
418
- `---\n\n## ⚠️ USER-UPLOADED FILES (ACTIVE ATTACHMENTS)\n\n` +
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
- const systemPrompt = promptParts.join('\n\n');
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
- const useStreaming = agentConfig.useStreaming !== false;
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(async () => {
606
+ session.prompt(lastUserContent).then(() => {
616
607
  metrics.recordRequest(Date.now() - requestStartTime);
617
608
  if (!hasContent && lastModelError) {
618
- // Streaming failed auto-retry with non-streaming fallback.
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; streamingSupported?: boolean }> {
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, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
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
- // Streaming compatibility test `/api/ask` uses SSE streaming by default.
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
- const streamingSupported = json.streamingSupported !== false;
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' && (
@@ -21,7 +21,6 @@ export interface AgentSettings {
21
21
  thinkingBudget?: number;
22
22
  contextStrategy?: 'auto' | 'off';
23
23
  reconnectRetries?: number;
24
- useStreaming?: boolean;
25
24
  }
26
25
 
27
26
  export interface SettingsData {
@@ -281,6 +281,7 @@ export function useAiOrganize() {
281
281
  messages,
282
282
  uploadedFiles: truncatedFiles,
283
283
  maxSteps: 15,
284
+ mode: 'organize',
284
285
  }),
285
286
  signal: controller.signal,
286
287
  });
@@ -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) {
@@ -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.`;
@@ -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();
@@ -844,7 +844,6 @@ export const en = {
844
844
  testKeyUnknown: 'Test failed',
845
845
  listModels: 'Browse',
846
846
  noModelsFound: 'No models found',
847
- streamingFallback: '(will use standard mode)',
848
847
  },
849
848
  agent: {
850
849
  title: 'Agent Behavior',
@@ -868,7 +868,6 @@ export const zh = {
868
868
  testKeyUnknown: '测试失败',
869
869
  listModels: '选择模型',
870
870
  noModelsFound: '未找到可用模型',
871
- streamingFallback: '(将使用标准模式)',
872
871
  },
873
872
  agent: {
874
873
  title: 'Agent 行为',
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.6.19",
3
+ "version": "0.6.20",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",