kiku-agent 1.0.5 → 1.0.7

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/README.md CHANGED
@@ -48,7 +48,24 @@ The same package also installs the stdio MCP server command:
48
48
  kiku-mcp
49
49
  ```
50
50
 
51
- For a local checkout before the package is published, use `npm pack` followed by `npm install -g ./kiku-agent-1.0.5.tgz`.
51
+ ### Updating the CLI and MCP server
52
+
53
+ Kiku uses the published `kiku-agent` package on npm as its update source. Inside an interactive session:
54
+
55
+ ```text
56
+ /update # Check the configured release channel
57
+ /update install # Install the stable `latest` release
58
+ /update install next # Install a preview release
59
+ /update install 1.2.3 # Install an exact published version
60
+ /update channel next # Persist the preferred channel
61
+ /update rollback # Restore the previously recorded version
62
+ ```
63
+
64
+ Automatic installation is available for global npm installs. Source checkouts, local dependencies, and `npx` executions receive a safe manual command instead of having their files replaced. Major updates require `--allow-major`; deprecated releases require `--allow-deprecated`. npm commands are executed without a shell and only allow known channel names or strict semantic versions.
65
+
66
+ After an update or rollback, restart `kiku-agent` and any application hosting `kiku-mcp`. Update settings and rollback history are stored in the normal Kiku user data directory as `update-state.json`; sessions, memory, `.env`, and YouTube cookies are not modified.
67
+
68
+ For a local checkout before the package is published, use `npm pack` followed by `npm install -g ./kiku-agent-1.0.7.tgz`.
52
69
 
53
70
  ### ✨ Features
54
71
  - **LM Studio Integration**: Connects seamlessly to LM Studio's OpenAI-compatible server at `http://127.0.0.1:1234/v1` (default model `google/gemma-4-e4b`, with auto-detection of loaded models).
@@ -84,10 +101,11 @@ npm run kiku-agent
84
101
 
85
102
  ### Launch with Specific Cloud Provider & Model
86
103
  ```bash
87
- npm run kiku-agent -- --provider openai --model gpt-4o-mini
88
- npm run kiku-agent -- --provider gemini --model gemini-2.5-flash
89
- npm run kiku-agent -- --provider groq --model llama-3.1-8b-instant
90
- npm run kiku-agent -- --provider openrouter --model google/gemini-2.5-flash
104
+ npm run kiku-agent -- --provider openai --model gpt-5.6-luna
105
+ npm run kiku-agent -- --provider gemini --model gemini-3.7-flash
106
+ npm run kiku-agent -- --provider groq --model openai/gpt-oss-120b
107
+ npm run kiku-agent -- --provider deepseek --model deepseek-v4-flash
108
+ npm run kiku-agent -- --provider openrouter --model openrouter/auto
91
109
  ```
92
110
 
93
111
  ### One-shot Question Query
@@ -104,9 +122,10 @@ npm run kiku-agent -- --model google/gemma-4-e4b-Q4_K_M --url http://127.0.0.1:1
104
122
  | Flag | Description |
105
123
  |---|---|
106
124
  | `-h`, `--help` | Show usage help |
125
+ | `-V`, `--version` | Print the installed package version and exit |
107
126
  | `-P`, `--provider <name>` | Select active provider (`lmstudio`, `openai`, `gemini`, `groq`, `deepseek`, `openrouter`, `custom`) |
108
127
  | `-k`, `--api-key <key>` | Specify API key for cloud provider |
109
- | `-m`, `--model <id>` | Override model ID (e.g. `gpt-4o-mini`, `google/gemma-4-e4b-Q4_K_M`) |
128
+ | `-m`, `--model <id>` | Override model ID (e.g. `gpt-5.6-luna`, `google/gemma-4-e4b-Q4_K_M`) |
110
129
  | `-u`, `--url <url>` | Override provider base URL |
111
130
  | `-t`, `--tools` | List all registered tools |
112
131
  | `-s`, `--stats` | Print catalog overview statistics |
@@ -131,8 +150,11 @@ Run `/help` inside a session for the full list. Commonly used:
131
150
  - `/model use <model-id>` — Switch runtime model without restarting the CLI
132
151
  - `/model reconnect` — Recheck server and leave catalog-only mode after a chat model is loaded
133
152
  - `/doctor` — Check Node, writable app data, LM Studio, model availability, FFmpeg, speaker, and yt-dlp
153
+ - `/update [check|install|channel|rollback]` — Manage npm-backed CLI and MCP package updates
134
154
  - `/tools` — View all tool names and parameter descriptions
135
- - `/details [Qn]` — Expand recorded thinking and tool results for a question
155
+ - `/details [Qn]` — Open a compact diagnostic overview for the latest or selected question (F2 opens latest)
156
+ - `/details [Qn] answer|thinking|tools` — Inspect one diagnostic section without dumping every raw result
157
+ - `/details [Qn] tool <n>` — Inspect bounded raw arguments and result for one tool call
136
158
  - `/stats` — Print summary counts of dramas, songs, brands, and awards
137
159
  - `/thinking [show|hide]` — Toggle reasoning process visibility
138
160
  - `/save` — Export session transcript to Markdown file
@@ -206,30 +228,33 @@ Run `/help` inside a session for the full list. Commonly used:
206
228
  | Provider | Recommended Active Models | Pricing / Cost Tier | Doc & API Reference | Required ENV Variable |
207
229
  |---|---|---|---|---|
208
230
  | **LM Studio** (Local) | `google/gemma-4-e4b` *(Q4_K_M, Q6_K, Q8_0)* | **100% Free / Local** (Zero token cost) | [Docs](https://lmstudio.ai/docs) • [API Ref](https://lmstudio.ai/docs/api/local-server) | `LMSTUDIO_BASE_URL` (default: `http://127.0.0.1:1234/v1`) |
209
- | **OpenAI** | `gpt-4o-mini` *(default)*, `gpt-4o`, `o3-mini` | **Ultra-Cheap** (~$0.15 / 1M input) | [Docs](https://platform.openai.com/docs) • [API Ref](https://platform.openai.com/docs/api-reference) | `OPENAI_API_KEY` |
210
- | **Google Gemini** | `gemini-2.5-flash` *(default)*, `gemini-2.5-flash-lite`, `gemini-2.5-pro`, `gemini-3.1-pro` | **Ultra-Cheap** ($0.10 / 1M in, 1M-2M context) | [Docs](https://ai.google.dev/docs) • [API Ref](https://ai.google.dev/api) | `GEMINI_API_KEY` |
211
- | **Groq** | `llama-3.3-70b-versatile` *(default)*, `llama-3.1-8b-instant`, `gpt-oss-120b` | **LPU Ultra-Fast** ($0.05 - $0.59 / 1M in) | [Docs](https://console.groq.com/docs/quickstart) • [API Ref](https://console.groq.com/docs/api-reference) | `GROQ_API_KEY` |
212
- | **DeepSeek** | `deepseek-chat` *(default, V3)*, `deepseek-reasoner` *(R1 CoT)* | **Ultra-Low Cost** ($0.14 - $0.27 / 1M in) | [Docs](https://api-docs.deepseek.com) • [API Ref](https://api-docs.deepseek.com/api/create-chat-completion) | `DEEPSEEK_API_KEY` |
213
- | **OpenRouter** | `google/gemini-2.5-flash`, `deepseek/deepseek-chat`, `deepseek/deepseek-r1`, `meta-llama/llama-3.3-70b-instruct`, `qwen/qwen-2.5-72b-instruct`, `:free` | **Flexible Aggregator** (Free & Paid) | [Docs](https://openrouter.ai/docs/quick-start) • [API Ref](https://openrouter.ai/docs/api_reference/overview) | `OPENROUTER_API_KEY` |
231
+ | **OpenAI** | `gpt-5.6-luna` *(default)*, `gpt-5.6-terra`, `gpt-5.6-sol` | **Cost-sensitive → Flagship** ($0.20 - $4.00 / 1M input) | [Docs](https://developers.openai.com/api/docs/models) • [API Ref](https://developers.openai.com/api/reference) | `OPENAI_API_KEY` |
232
+ | **Google Gemini** | `gemini-3.7-flash` *(default)*, `gemini-3.5-flash-lite`, `gemini-3.6-flash` | **Free tier / Stable Flash** ($0.30 - $0.75 / 1M input) | [Docs](https://ai.google.dev/gemini-api/docs/models) • [API Ref](https://ai.google.dev/api/generate-content) | `GEMINI_API_KEY` |
233
+ | **Groq** | `openai/gpt-oss-120b` *(default)*, `openai/gpt-oss-20b` | **LPU Ultra-Fast** ($0.075 - $0.15 / 1M input) | [Docs](https://console.groq.com/docs/quickstart) • [API Ref](https://console.groq.com/docs/api-reference) | `GROQ_API_KEY` |
234
+ | **DeepSeek** | `deepseek-v4-flash` *(default)*, `deepseek-v4-pro` | **V4 / 1M context** ($0.22 - $1.32 / 1M uncached input) | [Docs](https://api-docs.deepseek.com/quick_start/pricing/) • [API Ref](https://api-docs.deepseek.com/api/create-chat-completion) | `DEEPSEEK_API_KEY` |
235
+ | **OpenRouter** | `openrouter/auto` *(default, dynamic catalog)* | **Automatic routing** (billed at routed model rate) | [Docs](https://openrouter.ai/docs/guides/routing/routers/auto-router) • [API Ref](https://openrouter.ai/docs/api/reference/overview) | `OPENROUTER_API_KEY` |
214
236
  | **Custom Gateway** | Any OpenAI-compatible server (vLLM, Ollama, TGI, Azure) | **Custom** | [OpenAI Spec](https://platform.openai.com/docs/api-reference) | `LLM_BASE_URL`, `LLM_API_KEY` |
215
237
 
216
238
  ```bash
217
239
  # Launch with a specific cloud provider and model
218
- npm run kiku-agent -- --provider openai --model gpt-4o-mini
219
- npm run kiku-agent -- --provider gemini --model gemini-2.5-flash
220
- npm run kiku-agent -- --provider groq --model llama-3.1-8b-instant
221
- npm run kiku-agent -- --provider openrouter --model google/gemini-2.5-flash
240
+ npm run kiku-agent -- --provider openai --model gpt-5.6-luna
241
+ npm run kiku-agent -- --provider gemini --model gemini-3.7-flash
242
+ npm run kiku-agent -- --provider groq --model openai/gpt-oss-120b
243
+ npm run kiku-agent -- --provider deepseek --model deepseek-v4-flash
244
+ npm run kiku-agent -- --provider openrouter --model openrouter/auto
222
245
 
223
246
  # Manage providers interactively within the agent CLI
224
247
  /provider list # View all 7 providers and key status
225
- /provider use groq llama-3.1-8b-instant # Switch provider and model in one command
248
+ /provider use groq openai/gpt-oss-120b # Switch provider and model in one command
226
249
  /provider models gemini # List curated cost-effective models with pricing & context
227
250
  /provider info openai # Show developer documentation & API details
228
251
  /provider test groq # Test live endpoint connection and measure latency
229
252
  /provider setkey groq gsk-... # Set or update API key dynamically at runtime
230
253
  /model recommend # View recommended models for active provider
231
- /model use gpt-4o-mini # Switch active model on current provider
232
- ```
254
+ /model use gpt-5.6-luna # Switch active model on current provider
255
+ ```
256
+
257
+ At interactive startup, provider switches, and before a one-shot query, the CLI checks the provider's live `/models` catalog. If a configured model has been retired, it selects the first available model from that provider's maintained recommendation list and reports the fallback. Legacy IDs for Groq, DeepSeek, and selected renamed models are normalized automatically.
233
258
 
234
259
  ---
235
260
 
@@ -252,6 +277,8 @@ The production deployment exposes a stateless Streamable HTTP endpoint at `https
252
277
  /mcp tools
253
278
  ```
254
279
 
280
+ `/mcp` shows the connection summary plus every available MCP tool command in a compact list. `/mcp tools` expands the same list with parameters and descriptions. Long command panels open at their first page in the classic TUI; use Page Down or End to continue through the result.
281
+
255
282
  ---
256
283
 
257
284
  ## ⚙️ Environment Variables
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiku-agent",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Kiku AI terminal agent and MCP server for Ju Jingyi knowledge, media, and career data.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,16 @@
23
23
  "engines": {
24
24
  "node": ">=20.0.0"
25
25
  },
26
+ "kikuUpdate": {
27
+ "channel": "stable",
28
+ "releaseSummary": [
29
+ "Updated production model defaults and live fallback resolution across cloud providers",
30
+ "Hardened REST API key validation and isolated rate-limit probes",
31
+ "Fixed discography filtering, MCP configuration isolation, and environment loading order",
32
+ "Secured YouTube iframe messaging and expanded provider and API regression coverage"
33
+ ],
34
+ "releaseUrl": "https://www.npmjs.com/package/kiku-agent"
35
+ },
26
36
  "keywords": [
27
37
  "kiku",
28
38
  "ju-jingyi",
@@ -9,6 +9,7 @@ import { sanitizeQuery, isLikelySafe } from './sanitizer.js';
9
9
  import { createAbortError, isAbortError } from './errors.js';
10
10
  import { ResearchSubagent } from './subagent.js';
11
11
  import { ResearchOrchestrator } from './research.js';
12
+ import { SuggestionEngine } from './suggestions.js';
12
13
  import { McpClientManager } from './mcpClient.js';
13
14
  import { DynamicToolRouter } from './toolRouter.js';
14
15
  import { contextCompactor } from './contextCompactor.js';
@@ -62,6 +63,7 @@ export class KikuAgent {
62
63
  this.mcpManager = options.mcpManager || new McpClientManager();
63
64
  this.router = new DynamicToolRouter(this.getAllToolsSchema());
64
65
  this.research = new ResearchOrchestrator({ client: this.client });
66
+ this.suggestions = new SuggestionEngine({ client: this.client, memory: this.memory });
65
67
  this.systemPrompt = this.buildSystemPrompt();
66
68
  this.initHistory();
67
69
  this.restoredMessageCount = 0;
@@ -88,7 +90,7 @@ export class KikuAgent {
88
90
  await this.mcpManager.loadPersistedServers();
89
91
  this.router.setTools(this.getAllToolsSchema());
90
92
  this.systemPrompt = this.buildSystemPrompt();
91
- } catch {}
93
+ } catch { }
92
94
  }
93
95
  if (!this.conversationHistory || this.conversationHistory.length === 0) {
94
96
  this.initHistory();
@@ -128,6 +130,20 @@ export class KikuAgent {
128
130
  return this.research.cancel();
129
131
  }
130
132
 
133
+ async generateSuggestions(options = {}) {
134
+ await this._ready;
135
+ return this.suggestions.generateSuggestions(options);
136
+ }
137
+
138
+ async runSuggestionBatch(targets, options = {}) {
139
+ await this._ready;
140
+ return this.suggestions.runBatchSubagents(targets, options);
141
+ }
142
+
143
+ getLastSuggestions() {
144
+ return this.suggestions.getLastSuggestions();
145
+ }
146
+
131
147
  initHistory() {
132
148
  this.conversationHistory = [
133
149
  {
@@ -139,12 +155,12 @@ export class KikuAgent {
139
155
 
140
156
  clearMemory() {
141
157
  if (this.session.messages.length > 0) {
142
- this.memory.archiveSession(this.session.toSnapshot()).catch(() => {});
143
- this.memory.consolidateSession(this.session.toSnapshot()).catch(() => {});
158
+ this.memory.archiveSession(this.session.toSnapshot()).catch(() => { });
159
+ this.memory.consolidateSession(this.session.toSnapshot()).catch(() => { });
144
160
  }
145
161
  this.initHistory();
146
162
  this.session.reset();
147
- this.session.clearDisk().catch(() => {});
163
+ this.session.clearDisk().catch(() => { });
148
164
  }
149
165
 
150
166
  async loadSession() {
@@ -156,7 +172,7 @@ export class KikuAgent {
156
172
  content: this.systemPrompt
157
173
  }
158
174
  ];
159
-
175
+
160
176
  for (const msg of this.session.messages) {
161
177
  if (msg.role === 'user') {
162
178
  const textPart = msg.parts.find(p => p.type === 'text');
@@ -224,6 +240,47 @@ export class KikuAgent {
224
240
  return result;
225
241
  }
226
242
 
243
+ async removeMcp(name) {
244
+ const result = await this.mcpManager.remove(name);
245
+ this.router.setTools(this.getAllToolsSchema());
246
+ this.systemPrompt = this.buildSystemPrompt();
247
+ if (this.conversationHistory && this.conversationHistory[0]?.role === 'system') {
248
+ this.conversationHistory[0].content = this.getEnrichedSystemPrompt();
249
+ }
250
+ return result;
251
+ }
252
+
253
+ async reconnectMcp(name) {
254
+ const result = name && name !== 'all'
255
+ ? await this.mcpManager.reconnect(name)
256
+ : await this.mcpManager.reconnectAll();
257
+ this.router.setTools(this.getAllToolsSchema());
258
+ this.systemPrompt = this.buildSystemPrompt();
259
+ if (this.conversationHistory && this.conversationHistory[0]?.role === 'system') {
260
+ this.conversationHistory[0].content = this.getEnrichedSystemPrompt();
261
+ }
262
+ return result;
263
+ }
264
+
265
+ async saveMcpConfig(target = 'global') {
266
+ if (target === 'workspace' || target === 'local') {
267
+ return this.mcpManager.saveToWorkspace();
268
+ }
269
+ return this.mcpManager.savePersistedConfig();
270
+ }
271
+
272
+ async loadMcpConfig(customPath) {
273
+ const results = customPath
274
+ ? await this.mcpManager.loadConfigFile(customPath)
275
+ : await this.mcpManager.loadPersistedServers();
276
+ this.router.setTools(this.getAllToolsSchema());
277
+ this.systemPrompt = this.buildSystemPrompt();
278
+ if (this.conversationHistory && this.conversationHistory[0]?.role === 'system') {
279
+ this.conversationHistory[0].content = this.getEnrichedSystemPrompt();
280
+ }
281
+ return results;
282
+ }
283
+
227
284
  buildSystemPrompt() {
228
285
  const allTools = this.getAllToolsSchema();
229
286
  const toolsSummary = allTools.map(t => {
@@ -280,9 +337,9 @@ ${toolsSummary}
280
337
  - Directory listing ➔ list_directory
281
338
  - Screenshot ➔ screenshot_webpage
282
339
 
283
- 6. AGGRESSIVE WEB SEARCH FALLBACK:
284
- - If local tools return insufficient results, IMMEDIATELY call web_search or web_extract.
285
- - Combine local data WITH web search for complete, accurate answers.
340
+ 6. AGGRESSIVE WEB SEARCH FALLBACK (CRITICAL):
341
+ - If a local catalog tool (e.g. search_works, search_discography, get_brand_endorsements, get_awards) returns not_found, empty, or 0 results, DO NOT give up or guess. You MUST IMMEDIATELY call web_search or web_extract in the next step to look up live external facts online.
342
+ - Combine local catalog data WITH live web search for complete, accurate, and up-to-date answers.
286
343
  - Use web_extract for comprehensive research on any topic.
287
344
 
288
345
  7. LEGAL & CAREER KNOWLEDGE:
@@ -367,18 +424,18 @@ ${toolsSummary}
367
424
  for (const topic of topics) {
368
425
  this.memory.trackTopicInterest(topic);
369
426
  }
370
-
427
+
371
428
  const {
372
- onStepStart = () => {},
373
- onReasoningStart = () => {},
374
- onReasoningDelta = () => {},
375
- onReasoningEnd = () => {},
376
- onToolStart = () => {},
377
- onToolResult = () => {},
378
- onTokenStart = () => {},
379
- onTokenDelta = () => {},
380
- onFinalAnswer = () => {},
381
- onError = () => {},
429
+ onStepStart = () => { },
430
+ onReasoningStart = () => { },
431
+ onReasoningDelta = () => { },
432
+ onReasoningEnd = () => { },
433
+ onToolStart = () => { },
434
+ onToolResult = () => { },
435
+ onTokenStart = () => { },
436
+ onTokenDelta = () => { },
437
+ onFinalAnswer = () => { },
438
+ onError = () => { },
382
439
  signal = null
383
440
  } = callbacks;
384
441
 
@@ -509,6 +566,15 @@ ${toolsSummary}
509
566
  let toolCalls = assistantMessage.tool_calls || [];
510
567
  if (toolCalls.length === 0 && assistantMessage.content) {
511
568
  toolCalls = this.parseTextToolCalls(assistantMessage.content);
569
+
570
+ if (toolCalls.length > 0) {
571
+ let cleaned = assistantMessage.content;
572
+
573
+ cleaned = cleaned.replace(/```(?:json)?\s*\{[\s\S]*?\}\s*```/g, '');
574
+
575
+ cleaned = cleaned.replace(/Action:\s*[a-zA-Z0-9_]+\s*\nAction Input:\s*(?:\{[\s\S]*?\}|[^\n]+)/g, '');
576
+ assistantMessage.content = cleaned.trim();
577
+ }
512
578
  }
513
579
 
514
580
  this.conversationHistory.push(assistantMessage);
@@ -519,8 +585,8 @@ ${toolsSummary}
519
585
 
520
586
  const finalAnswer = assistantMessage.content || (
521
587
  dominantLang === 'zh' ? "抱歉,无法处理您的回答。" :
522
- dominantLang === 'en' ? "Sorry, unable to process a response." :
523
- "ขออภัยค่ะ ไม่สามารถประมวลผลคำตอบได้"
588
+ dominantLang === 'en' ? "Sorry, unable to process a response." :
589
+ "ขออภัยค่ะ ไม่สามารถประมวลผลคำตอบได้"
524
590
  );
525
591
  onFinalAnswer(finalAnswer, { durationMs: totalDurationMs, language: dominantLang });
526
592
  this._extractFactsFromFinalAnswer(finalAnswer);
@@ -532,11 +598,8 @@ ${toolsSummary}
532
598
  return finalAnswer;
533
599
  }
534
600
 
535
- for (const [toolCallIndex, tc] of toolCalls.entries()) {
536
- if (signal?.aborted) {
537
- this.stateMachine.reset();
538
- throw createAbortError();
539
- }
601
+ const executionPromises = toolCalls.map(async (tc, toolCallIndex) => {
602
+ if (signal?.aborted) throw createAbortError();
540
603
 
541
604
  const toolName = tc?.function?.name || '';
542
605
  const toolCallId = tc?.id || `call_${queryId}_${currentIteration}_${toolCallIndex}`;
@@ -560,20 +623,21 @@ ${toolsSummary}
560
623
  validationWarnings: prepared.warnings,
561
624
  };
562
625
  eventBus.emit(Events.AGENT_TOOL_ERROR, {
563
- queryId, toolName, args: tc.function.arguments,
626
+ queryId, toolName, args: tc?.function?.arguments,
564
627
  error: prepared.error,
565
628
  });
566
629
  onToolStart(toolName, {});
567
630
  onToolResult(toolName, rejectedResult, 0);
568
- const toolMessage = {
569
- role: 'tool',
570
- tool_call_id: toolCallId,
571
- name: toolName,
572
- content: JSON.stringify(rejectedResult)
631
+ return {
632
+ toolMessage: {
633
+ role: 'tool',
634
+ tool_call_id: toolCallId,
635
+ name: toolName,
636
+ content: JSON.stringify(rejectedResult)
637
+ }
573
638
  };
574
- this.conversationHistory.push(toolMessage);
575
- continue;
576
639
  }
640
+
577
641
  if (prepared.warnings.length > 0) {
578
642
  eventBus.emit(Events.AGENT_TOOL_ERROR, {
579
643
  queryId, toolName, warnings: prepared.warnings,
@@ -581,7 +645,6 @@ ${toolsSummary}
581
645
  }
582
646
 
583
647
  const finalArgs = prepared.args;
584
-
585
648
  this.stateMachine.transition(AgentStates.FETCHING_TOOL, { toolName });
586
649
 
587
650
  const toolPart = this.session.addToolPart(currentAssistantSessionMsg, toolName, finalArgs);
@@ -610,30 +673,40 @@ ${toolsSummary}
610
673
 
611
674
  this._extractFactsFromToolResult(toolName, finalArgs, result);
612
675
 
613
- const toolMessage = {
614
- role: 'tool',
615
- tool_call_id: toolCallId,
616
- name: toolName,
617
- content: JSON.stringify({
618
- authoritative: true,
619
- result,
620
- instruction: result.status === 'not_found'
621
- ? 'No matching data was found. Do not invent records; say that the catalog has no match or use an appropriate fallback tool.'
622
- : result.status === 'error'
623
- ? 'The tool failed. Do not claim that the requested facts were verified.'
624
- : 'Use only facts present in this result when answering.'
625
- })
676
+
677
+ const cappedResult = this._capToolResultForHistory(result);
678
+
679
+ return {
680
+ toolMessage: {
681
+ role: 'tool',
682
+ tool_call_id: toolCallId,
683
+ name: toolName,
684
+ content: JSON.stringify({
685
+ authoritative: true,
686
+ result: cappedResult,
687
+ instruction: result.status === 'not_found'
688
+ ? 'No matching data was found. Do not invent records; say that the catalog has no match or use an appropriate fallback tool.'
689
+ : result.status === 'error'
690
+ ? 'The tool failed. Do not claim that the requested facts were verified.'
691
+ : 'Use only facts present in this result when answering. Do not echo raw JSON data — summarize in natural language.'
692
+ })
693
+ }
626
694
  };
695
+ });
627
696
 
628
- this.conversationHistory.push(toolMessage);
697
+ const toolResults = await Promise.all(executionPromises);
698
+ for (const item of toolResults) {
699
+ if (item?.toolMessage) {
700
+ this.conversationHistory.push(item.toolMessage);
701
+ }
629
702
  }
630
703
  }
631
704
 
632
- const langPrompt = dominantLang === 'zh'
705
+ const langPrompt = dominantLang === 'zh'
633
706
  ? "请根据上述工具查询结果,直接回答(不要开头问候语),用流畅优雅的中文,先1-2句概述,再列要点。包含具体的 YouTube 真实链接(如 Direct Watch: https://www.youtube.com/watch?v=... หรือ Search: https://www.youtube.com/results?search_query=...)及事实数据。"
634
707
  : dominantLang === 'en'
635
- ? "Based on the tool results above, answer directly (no opening greeting or template phrase). Give a 1-2 sentence summary, then concise bullet points in polished English. Include verified YouTube direct and search links (https://www.youtube.com/results?search_query=...)."
636
- : "สรุปคำตอบจากผลลัพธ์เครื่องมือข้างต้น โดยไม่ต้องทักทาย ตอบตรงประเด็น 1-2 ประโยค แล้วตามด้วย bullet points สั้นๆ เป็นภาษาไทย พร้อมแสดงลิงก์ YouTube จริง (Direct Watch Link และ https://www.youtube.com/results?search_query=...) และข้อมูลผลงานที่ถูกต้องแม่นยำ";
708
+ ? "Based on the tool results above, answer directly (no opening greeting or template phrase). Give a 1-2 sentence summary, then concise bullet points in polished English. Include verified YouTube direct and search links (https://www.youtube.com/results?search_query=...)."
709
+ : "สรุปคำตอบจากผลลัพธ์เครื่องมือข้างต้น โดยไม่ต้องทักทาย ตอบตรงประเด็น 1-2 ประโยค แล้วตามด้วย bullet points สั้นๆ เป็นภาษาไทย พร้อมแสดงลิงก์ YouTube จริง (Direct Watch Link และ https://www.youtube.com/results?search_query=...) และข้อมูลผลงานที่ถูกต้องแม่นยำ";
637
710
 
638
711
  this.conversationHistory.push({ role: 'user', content: langPrompt });
639
712
 
@@ -677,19 +750,55 @@ ${toolsSummary}
677
750
  const defaultDone = dominantLang === 'zh' ? "查询完毕" : dominantLang === 'en' ? "Search complete." : "เสร็จสิ้นการค้นหาข้อมูล";
678
751
  const output = finalRes?.content || defaultDone;
679
752
  onFinalAnswer(output, { durationMs: totalDurationMs, language: dominantLang });
680
-
753
+
681
754
  this._extractFactsFromFinalAnswer(output);
682
755
 
683
756
  this.stateMachine.transition(AgentStates.COMPLETE);
684
757
  eventBus.emit(Events.AGENT_FINAL_ANSWER, { queryId, answer: output, durationMs: totalDurationMs });
685
758
  eventBus.emit(Events.AGENT_QUERY_END, { queryId, durationMs: totalDurationMs });
686
759
  this.stateMachine.reset();
687
-
760
+
688
761
  this.saveSession().catch(err => console.error('Session save failed:', err));
689
-
762
+
690
763
  return output;
691
764
  }
692
765
 
766
+ _capToolResultForHistory(result) {
767
+ if (!result || typeof result !== 'object') return result;
768
+
769
+ const MAX_RESULT_BYTES = 4096;
770
+ const serialized = JSON.stringify(result);
771
+ if (serialized.length <= MAX_RESULT_BYTES) return result;
772
+
773
+
774
+ const truncate = (obj) => {
775
+ if (Array.isArray(obj)) {
776
+ if (obj.length > 8) {
777
+ const kept = obj.slice(0, 8).map(truncate);
778
+ kept.push({ _truncated: true, totalItems: obj.length, showing: 8 });
779
+ return kept;
780
+ }
781
+ return obj.map(truncate);
782
+ }
783
+ if (obj && typeof obj === 'object') {
784
+ const out = {};
785
+ for (const [key, val] of Object.entries(obj)) {
786
+ out[key] = truncate(val);
787
+ }
788
+ return out;
789
+ }
790
+
791
+ if (typeof obj === 'string' && obj.length > 500) {
792
+ return obj.slice(0, 500) + '…[truncated]';
793
+ }
794
+ return obj;
795
+ };
796
+
797
+ const capped = truncate(result);
798
+ capped._cappedForHistory = true;
799
+ return capped;
800
+ }
801
+
693
802
  _extractFactsFromToolResult(toolName, toolArgs, result) {
694
803
  if (!result || result.status === 'error' || result.status === 'not_found') return;
695
804