kiku-agent 1.2.0 → 1.3.1

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.
Files changed (131) hide show
  1. package/README.md +459 -407
  2. package/package.json +32 -3
  3. package/server/agent/agentCore.js +635 -267
  4. package/server/agent/appPaths.js +2 -0
  5. package/server/agent/catalogEntityResolver.js +637 -0
  6. package/server/agent/citations.js +0 -21
  7. package/server/agent/clarificationAnalytics.js +1 -1
  8. package/server/agent/clarifyingQuestions.js +286 -216
  9. package/server/agent/cli.js +532 -73
  10. package/server/agent/commandFeatures.js +123 -24
  11. package/server/agent/commandRegistry.js +128 -62
  12. package/server/agent/commands.js +1410 -95
  13. package/server/agent/config.js +253 -163
  14. package/server/agent/contextCompactor.js +239 -25
  15. package/server/agent/dynamicCatalog.js +192 -0
  16. package/server/agent/envBootstrap.js +46 -0
  17. package/server/agent/envManager.js +317 -0
  18. package/server/agent/eventBus.js +3 -3
  19. package/server/agent/feedbackLearning.js +0 -8
  20. package/server/agent/gpuMetrics.js +188 -55
  21. package/server/agent/ingestionWorker.js +339 -0
  22. package/server/agent/ink/App.jsx +348 -0
  23. package/server/agent/ink/Panel.jsx +54 -0
  24. package/server/agent/ink/PromptInput.jsx +321 -0
  25. package/server/agent/ink/TaskPlanPanel.jsx +88 -0
  26. package/server/agent/ink/ThinkingPanel.jsx +68 -0
  27. package/server/agent/ink/ToolResult.jsx +133 -0
  28. package/server/agent/ink/main.jsx +195 -0
  29. package/server/agent/ink/useEventBus.js +218 -0
  30. package/server/agent/lanDiscovery.js +369 -0
  31. package/server/agent/llmClient.js +111 -39
  32. package/server/agent/lmStudioWakeup.js +144 -0
  33. package/server/agent/mcpClient.js +2 -2
  34. package/server/agent/memory/contextAllocator.js +214 -0
  35. package/server/agent/memory/hierarchicalStore.js +523 -0
  36. package/server/agent/memory/reflectionWorker.js +110 -0
  37. package/server/agent/memory.js +635 -494
  38. package/server/agent/modelConfig.js +487 -0
  39. package/server/agent/newsIntelligence.js +219 -0
  40. package/server/agent/newsWatch.js +85 -0
  41. package/server/agent/prompts.js +146 -0
  42. package/server/agent/publicTools.js +7 -0
  43. package/server/agent/queryDetails.js +62 -1
  44. package/server/agent/research.js +314 -35
  45. package/server/agent/routingInterceptor.js +77 -0
  46. package/server/agent/rumoredProjects.js +15 -0
  47. package/server/agent/sanitizer.js +117 -17
  48. package/server/agent/subagent.js +52 -9
  49. package/server/agent/suggestionTui.js +33 -13
  50. package/server/agent/suggestions.js +29 -13
  51. package/server/agent/telemetry/hardwareTiers.js +332 -0
  52. package/server/agent/telemetry/runtimeWatchdog.js +66 -0
  53. package/server/agent/telemetry/techPowerUpClient.js +682 -0
  54. package/server/agent/telemetry/techpowerup-mcp.json +22 -0
  55. package/server/agent/telemetry.js +0 -11
  56. package/server/agent/terminalImage.js +0 -1
  57. package/server/agent/textToolProtocol.js +190 -0
  58. package/server/agent/tokenTracker.js +220 -0
  59. package/server/agent/toolRouter.js +362 -216
  60. package/server/agent/tools.js +1155 -194
  61. package/server/agent/ui.js +144 -41
  62. package/server/agent/vramService.js +572 -0
  63. package/server/agent/webSearch.js +2279 -273
  64. package/server/agent/websearch/adapters/duckduckgo.js +97 -0
  65. package/server/agent/websearch/adapters/jina.js +77 -0
  66. package/server/agent/websearch/adapters/searxng.js +57 -0
  67. package/server/agent/websearch/core/canonical.js +89 -0
  68. package/server/agent/websearch/core/fusion.js +81 -0
  69. package/server/agent/websearch/core/types.js +29 -0
  70. package/server/agent/websearch/extract.js +605 -0
  71. package/server/agent/websearch/index.js +43 -0
  72. package/server/agent/websearch/orchestrator.js +266 -0
  73. package/server/agent/websearch/resilience/breaker.js +97 -0
  74. package/server/agent/websearch/resilience/cache.js +59 -0
  75. package/server/agent/websearch/resilience/limiter.js +93 -0
  76. package/server/agent/websearch/security/filter.js +36 -0
  77. package/server/agent/websearch/security/ssrf.js +289 -0
  78. package/server/agent/websearch/telemetry.js +71 -0
  79. package/server/agent/youtubeAuth.js +82 -73
  80. package/server/agent/youtubeTranscript.js +209 -0
  81. package/server/apiRouter.js +390 -190
  82. package/server/mcp/data/brands.js +5 -5
  83. package/server/mcp/data/discography.js +56 -45
  84. package/server/mcp/data/gallery.js +9 -0
  85. package/server/mcp/data/index.js +1 -0
  86. package/server/mcp/data/industryIndex.js +251 -0
  87. package/server/mcp/data/profile.js +5 -5
  88. package/server/mcp/data/timeline.js +2 -2
  89. package/server/mcp/data/works.js +2 -2
  90. package/server/mcp/handler.js +0 -2
  91. package/server/mcp/prompts.js +42 -2
  92. package/server/mcp/resources.js +5 -1
  93. package/server/mcp/security.js +25 -14
  94. package/server/mcp/server.js +92 -3
  95. package/server/mcp/tools.js +20 -11
  96. package/server/routes/agent.js +55 -0
  97. package/server/web-chat/core/connection.js +218 -194
  98. package/server/web-chat/core/conversationContext.js +1 -1
  99. package/server/web-chat/core/directCatalogEngine.js +190 -184
  100. package/server/web-chat/core/followUpEngine.js +88 -11
  101. package/server/web-chat/core/security.js +7 -4
  102. package/server/web-chat/router.js +54 -17
  103. package/server/web-chat/routes/bilibiliRoute.js +34 -0
  104. package/server/web-chat/routes/chatRoute.js +488 -397
  105. package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
  106. package/server/web-chat/routes/exportRoute.js +2 -1
  107. package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
  108. package/server/web-chat/routes/newsRoute.js +36 -6
  109. package/server/web-chat/routes/providerRoute.js +87 -0
  110. package/server/web-chat/routes/staticRoute.js +61 -13
  111. package/server/web-chat/routes/statusRoute.js +23 -2
  112. package/server/web-chat/routes/vramRoute.js +22 -0
  113. package/server/web-chat/server.js +135 -106
  114. package/server/web-chat/ui/htmlBuilder.js +8 -8
  115. package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
  116. package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
  117. package/server/web-chat/ui/scripts/client.js +10526 -8397
  118. package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
  119. package/server/web-chat/ui/styles/main.css +7471 -4246
  120. package/server/web-chat/ui/templates/body.html +305 -179
  121. package/src/data/brandsData.js +29 -6
  122. package/src/data/discographyData.js +918 -900
  123. package/src/data/galleryData.js +66 -0
  124. package/src/data/industryIndexData.js +292 -0
  125. package/src/data/musicVideoData.js +48 -5
  126. package/src/data/profileData.js +15 -1
  127. package/src/data/timelineData.js +4 -3
  128. package/src/data/worksData.js +536 -535
  129. package/src/lib/vram/estimator.js +464 -0
  130. package/src/lib/vram/modelRegistry.js +387 -0
  131. package/tokens.css +33 -2
@@ -1,7 +1,7 @@
1
1
  import { timingSafeEqual } from 'node:crypto';
2
2
 
3
3
  const SECURITY_CONFIG = {
4
- apiKeyRequired: !!process.env.MCP_API_KEY,
4
+ apiKeyRequired: Boolean(process.env.MCP_API_KEY || process.env.NODE_ENV === 'production'),
5
5
 
6
6
  rateLimitWindowMs: 60_000,
7
7
  rateLimitMaxRequests: 30,
@@ -10,7 +10,7 @@ const SECURITY_CONFIG = {
10
10
  sessionTtlMs: 30 * 60_000,
11
11
  maxMessagesPerSession: 500,
12
12
 
13
- corsAllowedOrigins: (process.env.MCP_CORS_ORIGINS || '').split(',').map(s => s.trim()).filter(Boolean),
13
+ corsAllowedOrigins: (process.env.MCP_CORS_ORIGINS || 'https://jujingyi-kikuuu.vercel.app').split(',').map(s => s.trim()).filter(Boolean),
14
14
  maxRequestBodyBytes: 1_048_576,
15
15
  };
16
16
 
@@ -19,13 +19,16 @@ export const MAX_REQUEST_BODY_SIZE = SECURITY_CONFIG.maxRequestBodyBytes;
19
19
  export function validateApiKey(req) {
20
20
  if (!SECURITY_CONFIG.apiKeyRequired) return { valid: true };
21
21
 
22
- let key = req.headers['x-api-key'];
22
+ let key = req?.headers?.['x-api-key'];
23
23
  if (!key) {
24
- const authHeader = req.headers['authorization'];
24
+ const authHeader = req?.headers?.['authorization'];
25
25
  if (authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
26
26
  key = authHeader.slice(7).trim();
27
27
  }
28
28
  }
29
+ if (!process.env.MCP_API_KEY) {
30
+ return { valid: false, error: 'MCP API key is not configured on the server. Please set MCP_API_KEY.' };
31
+ }
29
32
  if (!key) return { valid: false, error: 'Missing API key. Set X-API-Key header or Authorization: Bearer <key>.' };
30
33
  const expected = Buffer.from(process.env.MCP_API_KEY || '');
31
34
  const received = Buffer.from(String(key));
@@ -88,16 +91,24 @@ export function validateSessionId(sessionId) {
88
91
  return /^[a-zA-Z0-9_-]{1,100}$/.test(sessionId);
89
92
  }
90
93
 
91
- export function setCorsHeaders(res, origin) {
92
- const allowed = SECURITY_CONFIG.corsAllowedOrigins;
93
- const isLocalhost = process.env.NODE_ENV !== 'production' &&
94
- (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:') || origin === 'http://localhost' || origin === 'http://127.0.0.1');
95
- const isAllowed = !origin || allowed.includes(origin) || isLocalhost;
96
-
97
- if (origin && isAllowed) {
98
- res.setHeader('Access-Control-Allow-Origin', origin);
99
- res.setHeader('Vary', 'Origin');
100
- }
94
+ export function setCorsHeaders(res, origin) {
95
+ const allowed = SECURITY_CONFIG.corsAllowedOrigins;
96
+ let isLocalhost = false;
97
+ if (process.env.NODE_ENV !== 'production' && origin) {
98
+ try {
99
+ const parsed = new URL(origin);
100
+ isLocalhost = (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') &&
101
+ (parsed.protocol === 'http:' || parsed.protocol === 'https:');
102
+ } catch {
103
+ isLocalhost = false;
104
+ }
105
+ }
106
+ const isAllowed = !origin || allowed.includes(origin) || isLocalhost;
107
+
108
+ if (origin && isAllowed) {
109
+ res.setHeader('Access-Control-Allow-Origin', origin);
110
+ res.setHeader('Vary', 'Origin');
111
+ }
101
112
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
102
113
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-API-Key, Accept');
103
114
  res.setHeader('Access-Control-Max-Age', '86400');
@@ -5,6 +5,27 @@ import { toolDefinitions, handleToolCall } from './tools.js';
5
5
  import { resourceDefinitions, readResource } from './resources.js';
6
6
  import { getPrompt } from './prompts.js';
7
7
 
8
+ const WRITE_TOOLS = new Set([
9
+ 'manage_skill',
10
+ 'play_music', 'stop_music', 'pause_music', 'resume_music',
11
+ 'mute_music', 'unmute_music', 'next_track', 'prev_track',
12
+ 'add_to_queue', 'remove_from_queue', 'clear_queue',
13
+ ]);
14
+
15
+
16
+ const NON_IDEMPOTENT_TOOLS = new Set([
17
+ 'manage_skill', 'play_music', 'add_to_queue', 'remove_from_queue',
18
+ 'clear_queue', 'next_track', 'prev_track', 'run_quiz',
19
+ ]);
20
+
21
+ const OPEN_WORLD_TOOLS = new Set([
22
+ 'web_search', 'web_extract', 'fetch_webpage', 'fetch_api', 'fetch_json',
23
+ 'screenshot_webpage', 'search_news', 'fetch_rss', 'search_encyclopedia',
24
+ 'search_music_platforms', 'search_chinese_web', 'search_bilibili',
25
+ 'search_youtube', 'search_web_images', 'run_quiz', 'run_subagent',
26
+ 'run_deep_research', 'verify_claim', 'suggest_topics',
27
+ ]);
28
+
8
29
  export function createMcpServer(options = {}) {
9
30
  const toolOptions = {
10
31
  transport: options.transport || 'stdio',
@@ -52,12 +73,19 @@ export function createMcpServer(options = {}) {
52
73
  });
53
74
 
54
75
  for (const tool of toolDefinitions) {
76
+ const description = tool.name === 'get_api_key'
77
+ ? `${tool.description} (SENSITIVE: returns a stored Developer VIP API key — do not expose it publicly.)`
78
+ : tool.description;
55
79
  server.registerTool(
56
80
  tool.name,
57
81
  {
58
- description: tool.description,
82
+ description,
59
83
  inputSchema: tool.inputSchema,
60
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
84
+ annotations: {
85
+ readOnlyHint: !WRITE_TOOLS.has(tool.name),
86
+ idempotentHint: !NON_IDEMPOTENT_TOOLS.has(tool.name),
87
+ openWorldHint: OPEN_WORLD_TOOLS.has(tool.name),
88
+ },
61
89
  },
62
90
  async (args) => {
63
91
  try {
@@ -85,11 +113,13 @@ export function createMcpServer(options = {}) {
85
113
  'kiku://discography': 'kiku-discography',
86
114
  'kiku://brands': 'kiku-brands',
87
115
  'kiku://social': 'kiku-social',
116
+ 'kiku://industry-index': 'kiku-industry-index',
88
117
  };
89
118
 
90
119
  for (const def of resourceDefinitions) {
120
+ const resourceName = resourceMap[def.uri] || def.uri.replace('kiku://', 'kiku-');
91
121
  server.registerResource(
92
- resourceMap[def.uri],
122
+ resourceName,
93
123
  def.uri,
94
124
  { description: def.description, mimeType: def.mimeType },
95
125
  async (uri) => readResource(uri.href)
@@ -131,5 +161,64 @@ export function createMcpServer(options = {}) {
131
161
  async (args) => getPrompt('career_overview', args)
132
162
  );
133
163
 
164
+ server.registerPrompt(
165
+ 'draft_fan_fiction',
166
+ {
167
+ description: 'Draft creative fan fiction scenarios incorporating authentic Ju Jingyi characters and lore.',
168
+ argsSchema: {
169
+ character: z.string().min(1).describe('Character name (e.g. "Yang Caiwei", "Han Yunxi", "Fu Rong")'),
170
+ theme: z.string().optional().describe('Story theme or mood (mystery, romance, fantasy adventure)'),
171
+ setting: z.string().optional().describe('Story backdrop or scenario'),
172
+ },
173
+ },
174
+ async (args) => getPrompt('draft_fan_fiction', args)
175
+ );
176
+
177
+ server.registerPrompt(
178
+ 'analyze_drama_rating',
179
+ {
180
+ description: 'Provide an analytical overview of a drama\'s reception, milestones, and iconic performances.',
181
+ argsSchema: {
182
+ title: z.string().min(1).describe('Drama title (e.g. "In Blossom", "Legend of Yunxi")'),
183
+ },
184
+ },
185
+ async (args) => getPrompt('analyze_drama_rating', args)
186
+ );
187
+
188
+ server.registerPrompt(
189
+ 'compare_discography_era',
190
+ {
191
+ description: 'Compare musical styles and evolution across Ju Jingyi\'s idol, solo, and studio eras.',
192
+ argsSchema: {
193
+ era_a: z.string().optional().describe('First era (e.g. "snh48", "solo_early", "studio_era")'),
194
+ era_b: z.string().optional().describe('Second era (e.g. "modern", "solo")'),
195
+ },
196
+ },
197
+ async (args) => getPrompt('compare_discography_era', args)
198
+ );
199
+
200
+ server.registerPrompt(
201
+ 'investigate_viral_claim',
202
+ {
203
+ description: 'Investigate a viral claim, set leak, casting rumor, or news report about Ju Jingyi using multi-tool evidence and catalog cross-referencing.',
204
+ argsSchema: {
205
+ claim: z.string().min(1).describe('The viral rumor, set leak, or statement to investigate'),
206
+ platform_hint: z.string().optional().describe('Platform where the claim went viral (e.g. "weibo", "douyin", "baidu")'),
207
+ },
208
+ },
209
+ async (args) => getPrompt('investigate_viral_claim', args)
210
+ );
211
+
212
+ server.registerPrompt(
213
+ 'deep_research_topic',
214
+ {
215
+ description: 'Perform an exhaustive deep research investigation on any Ju Jingyi entertainment topic across catalog, live Chinese media, news, and images.',
216
+ argsSchema: {
217
+ topic: z.string().min(1).describe('The topic, milestone, or controversy to research in depth'),
218
+ },
219
+ },
220
+ async (args) => getPrompt('deep_research_topic', args)
221
+ );
222
+
134
223
  return server;
135
224
  }
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { executeTool, toolsSchema } from '../agent/tools.js';
3
+ import { sanitizeInput } from './security.js';
3
4
 
4
- // Rich catalog/database tools — safe to expose over public MCP.
5
5
  const PUBLIC_TOOL_NAMES = new Set([
6
6
  'get_profile_and_fandom',
7
7
  'get_quotes',
@@ -18,26 +18,34 @@ const PUBLIC_TOOL_NAMES = new Set([
18
18
  'get_gallery_photos',
19
19
  'global_search',
20
20
  'get_system_stats',
21
+ 'get_current_datetime',
22
+ 'get_youtube_transcript',
23
+ 'search_news',
24
+ 'fetch_rss',
25
+ 'search_encyclopedia',
26
+ 'search_music_platforms',
27
+ 'get_entertainment_metrics',
28
+ 'get_industry_index',
21
29
  ]);
22
30
 
23
- // Web tools — gated behind KIKU_MCP_ENABLE_WEB_TOOLS flag.
24
- const OPTIONAL_WEB_TOOL_NAMES = new Set(['web_search', 'fetch_webpage', 'fetch_api', 'web_extract']);
31
+ const OPTIONAL_WEB_TOOL_NAMES = new Set(['web_search', 'fetch_webpage', 'fetch_api', 'web_extract', 'search_web_images']);
25
32
  const webToolsEnabled = process.env.KIKU_MCP_ENABLE_WEB_TOOLS === 'true';
26
33
 
27
- // Anonymous callers may search public music metadata, but cannot mutate playback state.
34
+ const OPTIONAL_CN_TOOL_NAMES = new Set(['search_chinese_web', 'search_bilibili', 'search_web_images']);
35
+ const cnToolsEnabled = process.env.KIKU_ENABLE_CN_WEB_TOOLS === 'true';
36
+
28
37
  const MUSIC_TOOL_NAMES = new Set([
29
38
  'search_youtube',
30
39
  ]);
31
40
 
32
- // Stateless engagement tools are safe to expose.
33
41
  const RESEARCH_TOOL_NAMES = new Set([
34
42
  'suggest_topics',
35
43
  'run_quiz',
44
+ 'checkpoint_step',
45
+ 'record_failure',
46
+ 'load_active_run',
36
47
  ]);
37
48
 
38
- // Privileged tools — they touch the filesystem, server-local resources, secrets, or
39
- // mutated server state. Exposed over MCP but REQUIRE authorization (a valid MCP_API_KEY
40
- // / X-API-Key / Bearer header). Never callable anonymously on the public HTTP endpoint.
41
49
  const PRIVILEGED_TOOL_NAMES = new Set([
42
50
  'read_local_file',
43
51
  'list_directory',
@@ -64,12 +72,13 @@ const PRIVILEGED_TOOL_NAMES = new Set([
64
72
  'fetch_json',
65
73
  ]);
66
74
 
67
- // Any tool that is not privileged is public.
68
75
  const isPrivileged = (name) => PRIVILEGED_TOOL_NAMES.has(name);
69
76
 
70
77
  function isAllowedTool(name) {
71
78
  if (PUBLIC_TOOL_NAMES.has(name) || MUSIC_TOOL_NAMES.has(name) || RESEARCH_TOOL_NAMES.has(name)) return true;
72
- if (OPTIONAL_WEB_TOOL_NAMES.has(name)) return webToolsEnabled;
79
+ if (OPTIONAL_WEB_TOOL_NAMES.has(name) && webToolsEnabled) return true;
80
+ if (OPTIONAL_CN_TOOL_NAMES.has(name) && cnToolsEnabled) return true;
81
+ if (OPTIONAL_WEB_TOOL_NAMES.has(name) || OPTIONAL_CN_TOOL_NAMES.has(name)) return false;
73
82
  return PRIVILEGED_TOOL_NAMES.has(name);
74
83
  }
75
84
 
@@ -164,7 +173,7 @@ export async function handleToolCall(toolName, args = {}, options = {}) {
164
173
  }
165
174
  }
166
175
 
167
- const adaptedArgs = { ...(args || {}) };
176
+ const adaptedArgs = { ...sanitizeInput(args || {}) };
168
177
  if (toolName === 'search_gallery') delete adaptedArgs.query;
169
178
  if (adaptedArgs.category && definition.targetName === 'search_discography' && !adaptedArgs.type) {
170
179
  adaptedArgs.type = adaptedArgs.category;
@@ -0,0 +1,55 @@
1
+ import { KikuAgent } from '../agent/agentCore.js';
2
+ import { AgentMemory } from '../agent/memory.js';
3
+ import { PUBLIC_REMOTE_TOOL_NAMES } from '../agent/publicTools.js';
4
+
5
+ export function getSharedAgent(options = {}) {
6
+ return new KikuAgent({
7
+ provider: 'lmstudio',
8
+ apiKey: '',
9
+ ...options,
10
+ memory: new AgentMemory({ persistence: false }),
11
+ sessionPersistence: false,
12
+ allowedToolNames: PUBLIC_REMOTE_TOOL_NAMES,
13
+ });
14
+ }
15
+
16
+ export async function handleAgentChatStream(req, res, queryPrompt, agentOptions = {}) {
17
+ res.statusCode = 200;
18
+ res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
19
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
20
+ res.setHeader('Connection', 'keep-alive');
21
+ res.setHeader('X-Accel-Buffering', 'no');
22
+ res.setHeader('X-Content-Type-Options', 'nosniff');
23
+ res.setHeader('X-Frame-Options', 'DENY');
24
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
25
+
26
+ const sendEvent = (event, data) => {
27
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
28
+ };
29
+
30
+ const agent = getSharedAgent(agentOptions);
31
+ await agent.ready();
32
+
33
+ try {
34
+ sendEvent('start', { prompt: queryPrompt, timestamp: Date.now() });
35
+
36
+ await agent.ask(queryPrompt, {
37
+ onReasoningStart: () => sendEvent('thought_start', {}),
38
+ onReasoningDelta: (chunk) => sendEvent('thought_delta', { chunk }),
39
+ onReasoningEnd: () => sendEvent('thought_end', {}),
40
+ onToolStart: (name, args) => sendEvent('tool_start', { name, args }),
41
+ onToolResult: (name, result, durationMs) => sendEvent('tool_result', { name, result, durationMs }),
42
+ onTokenDelta: (chunk) => sendEvent('token', { chunk }),
43
+ onFinalAnswer: (answer, meta) => sendEvent('done', { answer, meta }),
44
+ onError: (err) => {
45
+ console.error('Agent stream error:', err);
46
+ sendEvent('error', { error: process.env.NODE_ENV === 'development' ? err.message : 'Agent stream execution encountered an issue.' });
47
+ }
48
+ });
49
+ } catch (error) {
50
+ console.error('Agent chat stream error:', error);
51
+ sendEvent('error', { error: process.env.NODE_ENV === 'development' ? error.message : 'Agent stream execution failed.' });
52
+ } finally {
53
+ res.end();
54
+ }
55
+ }