kiku-agent 1.2.0 → 1.3.0

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 (100) hide show
  1. package/README.md +459 -407
  2. package/package.json +27 -3
  3. package/server/agent/agentCore.js +635 -267
  4. package/server/agent/appPaths.js +2 -0
  5. package/server/agent/catalogEntityResolver.js +629 -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 +528 -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/lanDiscovery.js +369 -0
  23. package/server/agent/llmClient.js +111 -39
  24. package/server/agent/lmStudioWakeup.js +144 -0
  25. package/server/agent/mcpClient.js +2 -2
  26. package/server/agent/memory.js +635 -494
  27. package/server/agent/modelConfig.js +487 -0
  28. package/server/agent/newsIntelligence.js +219 -0
  29. package/server/agent/newsWatch.js +85 -0
  30. package/server/agent/prompts.js +146 -0
  31. package/server/agent/publicTools.js +7 -0
  32. package/server/agent/queryDetails.js +62 -1
  33. package/server/agent/research.js +314 -35
  34. package/server/agent/routingInterceptor.js +77 -0
  35. package/server/agent/rumoredProjects.js +15 -0
  36. package/server/agent/sanitizer.js +117 -17
  37. package/server/agent/subagent.js +52 -9
  38. package/server/agent/suggestionTui.js +33 -13
  39. package/server/agent/suggestions.js +29 -13
  40. package/server/agent/telemetry.js +0 -11
  41. package/server/agent/terminalImage.js +0 -1
  42. package/server/agent/textToolProtocol.js +190 -0
  43. package/server/agent/tokenTracker.js +220 -0
  44. package/server/agent/toolRouter.js +362 -216
  45. package/server/agent/tools.js +1155 -194
  46. package/server/agent/ui.js +144 -41
  47. package/server/agent/vramService.js +572 -0
  48. package/server/agent/webSearch.js +2279 -273
  49. package/server/agent/youtubeAuth.js +82 -73
  50. package/server/agent/youtubeTranscript.js +209 -0
  51. package/server/apiRouter.js +383 -179
  52. package/server/mcp/data/brands.js +5 -5
  53. package/server/mcp/data/discography.js +56 -45
  54. package/server/mcp/data/gallery.js +9 -0
  55. package/server/mcp/data/index.js +1 -0
  56. package/server/mcp/data/industryIndex.js +251 -0
  57. package/server/mcp/data/profile.js +5 -5
  58. package/server/mcp/data/timeline.js +2 -2
  59. package/server/mcp/data/works.js +2 -2
  60. package/server/mcp/handler.js +0 -2
  61. package/server/mcp/prompts.js +42 -2
  62. package/server/mcp/resources.js +5 -1
  63. package/server/mcp/security.js +25 -14
  64. package/server/mcp/server.js +92 -3
  65. package/server/mcp/tools.js +20 -11
  66. package/server/web-chat/core/connection.js +218 -194
  67. package/server/web-chat/core/conversationContext.js +1 -1
  68. package/server/web-chat/core/directCatalogEngine.js +190 -184
  69. package/server/web-chat/core/followUpEngine.js +88 -11
  70. package/server/web-chat/core/security.js +7 -4
  71. package/server/web-chat/router.js +54 -17
  72. package/server/web-chat/routes/bilibiliRoute.js +34 -0
  73. package/server/web-chat/routes/chatRoute.js +488 -397
  74. package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
  75. package/server/web-chat/routes/exportRoute.js +2 -1
  76. package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
  77. package/server/web-chat/routes/newsRoute.js +36 -6
  78. package/server/web-chat/routes/providerRoute.js +87 -0
  79. package/server/web-chat/routes/staticRoute.js +61 -13
  80. package/server/web-chat/routes/statusRoute.js +23 -2
  81. package/server/web-chat/routes/vramRoute.js +22 -0
  82. package/server/web-chat/server.js +135 -106
  83. package/server/web-chat/ui/htmlBuilder.js +8 -8
  84. package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
  85. package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
  86. package/server/web-chat/ui/scripts/client.js +10526 -8397
  87. package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
  88. package/server/web-chat/ui/styles/main.css +7471 -4246
  89. package/server/web-chat/ui/templates/body.html +305 -179
  90. package/src/data/brandsData.js +29 -6
  91. package/src/data/discographyData.js +918 -900
  92. package/src/data/galleryData.js +66 -0
  93. package/src/data/industryIndexData.js +292 -0
  94. package/src/data/musicVideoData.js +48 -5
  95. package/src/data/profileData.js +15 -1
  96. package/src/data/timelineData.js +4 -3
  97. package/src/data/worksData.js +536 -535
  98. package/src/lib/vram/estimator.js +464 -0
  99. package/src/lib/vram/modelRegistry.js +387 -0
  100. package/tokens.css +33 -2
@@ -0,0 +1,190 @@
1
+ export function extractTopLevelJsonObjects(text) {
2
+ const objects = [];
3
+ let depth = 0;
4
+ let start = -1;
5
+ let inString = false;
6
+ let escaped = false;
7
+
8
+ for (let i = 0; i < text.length; i++) {
9
+ const ch = text[i];
10
+ if (inString) {
11
+ if (escaped) escaped = false;
12
+ else if (ch === '\\') escaped = true;
13
+ else if (ch === '"') inString = false;
14
+ continue;
15
+ }
16
+ if (ch === '"') {
17
+ if (depth > 0) inString = true;
18
+ continue;
19
+ }
20
+ if (ch === '{') {
21
+ if (depth === 0) start = i;
22
+ depth++;
23
+ } else if (ch === '}') {
24
+ if (depth > 0) {
25
+ depth--;
26
+ if (depth === 0 && start >= 0) {
27
+ objects.push({ raw: text.slice(start, i + 1), start, end: i + 1 });
28
+ start = -1;
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ const unbalanced = depth > 0 && start >= 0 ? text.slice(start) : null;
35
+ return { objects, unbalanced };
36
+ }
37
+
38
+ const JUNK_PARAM_REGEX = /(kiku_vip_keys|kiku_api_keys|api_key|apikey|secret|password|token|credentials)/i;
39
+
40
+ export function sanitizeToolArguments(toolName, rawArgs = {}, toolsSchema = []) {
41
+ if (!rawArgs || typeof rawArgs !== 'object' || Array.isArray(rawArgs)) return {};
42
+ const toolDef = (toolsSchema || []).find(t => (t.function?.name || t.name) === toolName);
43
+ const properties = toolDef?.function?.parameters?.properties || toolDef?.parameters?.properties || {};
44
+
45
+ const cleanArgs = {};
46
+ for (const [key, value] of Object.entries(rawArgs)) {
47
+ if (JUNK_PARAM_REGEX.test(key)) continue;
48
+
49
+ const schema = properties[key];
50
+ let val = value;
51
+ if (schema) {
52
+ if (schema.type === 'number' || schema.type === 'integer') {
53
+ if (typeof val === 'string' && /^-?\d+(\.\d+)?$/.test(val.trim())) {
54
+ val = schema.type === 'integer' ? parseInt(val.trim(), 10) : parseFloat(val.trim());
55
+ }
56
+ } else if (schema.type === 'boolean') {
57
+ if (typeof val === 'string') {
58
+ if (val.toLowerCase() === 'true' || val === '1') val = true;
59
+ else if (val.toLowerCase() === 'false' || val === '0') val = false;
60
+ }
61
+ } else if (schema.type === 'string') {
62
+ if (typeof val === 'number' || typeof val === 'boolean') {
63
+ val = String(val);
64
+ }
65
+ }
66
+
67
+ if (schema.enum && Array.isArray(schema.enum)) {
68
+ const strVal = String(val).toLowerCase().trim();
69
+ const matched = schema.enum.find(e => String(e).toLowerCase() === strVal);
70
+ if (matched !== undefined) {
71
+ val = matched;
72
+ } else if (schema.enum.includes('all')) {
73
+ val = 'all';
74
+ }
75
+ }
76
+ }
77
+
78
+ cleanArgs[key] = val;
79
+ }
80
+ return cleanArgs;
81
+ }
82
+
83
+ /**
84
+ * Parse text-emitted tool calls (fenced JSON or ReAct "Action:" blocks).
85
+ * Returns { calls, malformed } — malformed counts blocks that LOOK like tool
86
+ * calls but could not be parsed, so the caller can feed that back to the model
87
+ * instead of silently treating the message as the final answer.
88
+ */
89
+ export function parseTextToolCallsFromContent(content, toolsSchema) {
90
+ const result = { calls: [], malformed: 0 };
91
+ if (!content) return result;
92
+ const allTools = toolsSchema || [];
93
+ const isValidTool = (name) => allTools.some(t => t.function?.name === name);
94
+
95
+ const pushCall = (prefix, toolName, args) => {
96
+ const cleanArgs = sanitizeToolArguments(toolName, args, allTools);
97
+ result.calls.push({
98
+ id: `${prefix}_${Date.now()}_${result.calls.length}`,
99
+ function: { name: toolName, arguments: JSON.stringify(cleanArgs) }
100
+ });
101
+ };
102
+
103
+ const fenceRegex = /```(?:json)?\s*([\s\S]*?)```/g;
104
+ for (const match of content.matchAll(fenceRegex)) {
105
+ const body = match[1];
106
+ if (!body.includes('{')) continue;
107
+ const { objects, unbalanced } = extractTopLevelJsonObjects(body);
108
+ if (unbalanced) result.malformed++;
109
+ for (const obj of objects) {
110
+ let parsed = null;
111
+ try {
112
+ parsed = JSON.parse(obj.raw);
113
+ } catch {
114
+ parsed = null;
115
+ }
116
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
117
+ result.malformed++;
118
+ continue;
119
+ }
120
+ const toolName = parsed.action || parsed.tool || parsed.name || parsed.function;
121
+ if (!toolName) continue;
122
+ let toolArgs = parsed.action_input ?? parsed.parameters ?? parsed.arguments ?? parsed.args ?? {};
123
+ if (toolArgs && typeof toolArgs === 'string') {
124
+ try { toolArgs = JSON.parse(toolArgs); } catch { toolArgs = { query: toolArgs }; }
125
+ }
126
+ if (!toolArgs || typeof toolArgs !== 'object' || Array.isArray(toolArgs)) toolArgs = {};
127
+ if (isValidTool(String(toolName))) {
128
+ pushCall('call_txt', String(toolName), toolArgs);
129
+ }
130
+ }
131
+ }
132
+
133
+ const actionRegex = /Action:\s*([a-zA-Z0-9_]+)\s*\nAction Input:\s*([\s\S]*?)(?=\nAction:|\nObservation:|$)/g;
134
+ for (const match of content.matchAll(actionRegex)) {
135
+ const toolName = match[1].trim();
136
+ const rawInput = match[2].trim();
137
+ if (!isValidTool(toolName)) continue;
138
+ if (rawInput.startsWith('{')) {
139
+ const { objects, unbalanced } = extractTopLevelJsonObjects(rawInput);
140
+ const candidate = objects.length > 0 ? objects[0].raw : unbalanced;
141
+ let parsed = null;
142
+ if (candidate) {
143
+ try { parsed = JSON.parse(candidate); } catch { parsed = null; }
144
+ }
145
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
146
+ result.malformed++;
147
+ continue;
148
+ }
149
+ pushCall('call_act', toolName, parsed);
150
+ } else {
151
+ pushCall('call_act', toolName, { query: rawInput });
152
+ }
153
+ }
154
+
155
+ return result;
156
+ }
157
+
158
+ export const MALFORMED_TOOL_CALL_FEEDBACK = '[SYSTEM PARSE ERROR] Your previous message contained a tool-call block that could not be parsed (most likely invalid or truncated JSON). Re-emit the tool call using EXACTLY ONE valid format:\n1. Fenced JSON: ```json {"action": "tool_name", "parameters": { ... }} ``` (complete, strictly valid JSON)\n2. ReAct: "Action: tool_name" followed by "Action Input: {valid JSON object}"\nDo not add any text inside the JSON block and make sure all braces are closed.';
159
+
160
+ export function buildTextToolProtocolDirective(toolsSchema) {
161
+ const toolNames = (toolsSchema || []).map(t => t.function?.name).filter(Boolean);
162
+ const sample = toolNames.includes('web_search') ? 'web_search' : (toolNames[0] || 'web_search');
163
+ return [
164
+ '=== TEXT TOOL PROTOCOL (ACTIVE — native function calling is unavailable for this model/provider) ===',
165
+ 'You cannot emit native tool_calls. To use a tool, END your reply with EXACTLY ONE tool call in one of these formats:',
166
+ '',
167
+ 'Option A (fenced JSON):',
168
+ '```json',
169
+ `{"action": "${sample}", "parameters": { "query": "example" }}`,
170
+ '```',
171
+ '',
172
+ `Option B (ReAct):\nAction: ${sample}\nAction Input: {"query": "example"}`,
173
+ '',
174
+ 'Rules:',
175
+ `- Use ONLY these tool names: ${toolNames.join(', ')}.`,
176
+ '- The JSON must be strictly valid: no comments, no trailing commas, escape every quote inside string values.',
177
+ '- Write the complete JSON argument object — every "{" must have its matching "}".',
178
+ '- Emit the block and stop; the system executes it and returns the results in the next message.',
179
+ '- When you have enough verified data, reply with the final answer WITHOUT any tool-call block.',
180
+ ].join('\n');
181
+ }
182
+
183
+ /** Strip the emitted call blocks from assistant content so they never leak to
184
+ * the user (the "NO SYSTEM LEAK" rule). */
185
+ export function stripToolCallBlocks(content) {
186
+ let cleaned = String(content || '');
187
+ cleaned = cleaned.replace(/```(?:json)?\s*[\s\S]*?```/g, '');
188
+ cleaned = cleaned.replace(/Action:\s*[a-zA-Z0-9_]+\s*\nAction Input:[\s\S]*?(?=\nAction:|\nObservation:|$)/g, '');
189
+ return cleaned.trim();
190
+ }
@@ -0,0 +1,220 @@
1
+ import { EventEmitter } from 'node:events';
2
+
3
+ export function computePercentile(sortedValues, percentile) {
4
+ if (!Array.isArray(sortedValues) || sortedValues.length === 0) return null;
5
+ const sorted = [...sortedValues].sort((a, b) => a - b);
6
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(percentile * sorted.length) - 1));
7
+ return sorted[index];
8
+ }
9
+
10
+ function round(value, digits = 2) {
11
+ if (value === null || value === undefined || !Number.isFinite(value)) return 0;
12
+ const factor = 10 ** digits;
13
+ return Math.round(value * factor) / factor;
14
+ }
15
+
16
+ export class TokenTracker extends EventEmitter {
17
+ /**
18
+ * @param {object} options
19
+ * @param {string} [options.sessionId]
20
+ * @param {string} [options.modelId]
21
+ * @param {number} [options.contextLimit] Maximum context window (0 = unknown).
22
+ */
23
+ constructor({ sessionId = null, modelId = 'unknown', contextLimit = 0 } = {}) {
24
+ super();
25
+ this.sessionId = sessionId;
26
+ this.modelId = modelId;
27
+ this.contextLimit = Number(contextLimit) || 0;
28
+ this.startTime = null;
29
+ this.endTime = null;
30
+ this.segments = [];
31
+ this.currentSegment = null;
32
+ this.finished = false;
33
+ }
34
+
35
+
36
+ start({ promptTokens = 0, contextLimit } = {}) {
37
+ if (contextLimit !== undefined && contextLimit !== null) this.contextLimit = Number(contextLimit) || 0;
38
+ if (this.startTime === null) this.startTime = Date.now();
39
+ if (promptTokens > 0 && this.segments.length === 0) {
40
+ this.pendingInitialPromptTokens = Number(promptTokens);
41
+ }
42
+ return this.startTime;
43
+ }
44
+
45
+
46
+ beginSegment(label = null) {
47
+ this.start();
48
+ const segment = {
49
+ index: this.segments.length,
50
+ label,
51
+ tStart: Date.now(),
52
+ tFirst: null,
53
+ tokenTimestamps: [],
54
+ promptTokens: null,
55
+ completionTokens: null,
56
+ };
57
+ this.currentSegment = segment;
58
+ this.emit('segment-start', { index: segment.index, tStart: segment.tStart });
59
+ return segment;
60
+ }
61
+
62
+
63
+ markFirstToken(segment = this.currentSegment) {
64
+ if (!segment || segment.tFirst !== null) return;
65
+ segment.tFirst = Date.now();
66
+ this.emit('first-token', { index: segment.index, ttft: segment.tFirst - this.startTime });
67
+ }
68
+
69
+
70
+ markToken(segment = this.currentSegment) {
71
+ if (!segment) return;
72
+ const now = Date.now();
73
+ if (segment.tFirst === null) {
74
+ segment.tFirst = now;
75
+ this.emit('first-token', { index: segment.index, ttft: now - this.startTime });
76
+ }
77
+ segment.tokenTimestamps.push(now);
78
+ this.emit('token', { index: segment.index, at: now, tokens: segment.tokenTimestamps.length });
79
+ }
80
+
81
+
82
+ endSegment({ promptTokens = null, completionTokens = null } = {}, segment = this.currentSegment) {
83
+ if (!segment) return null;
84
+ segment.tEnd = Date.now();
85
+ segment.promptTokens = Number.isFinite(Number(promptTokens)) && Number(promptTokens) > 0
86
+ ? Number(promptTokens)
87
+ : null;
88
+ segment.completionTokens = Number.isFinite(Number(completionTokens)) && Number(completionTokens) > 0
89
+ ? Number(completionTokens)
90
+ : segment.tokenTimestamps.length || null;
91
+ this.segments.push(segment);
92
+ if (this.currentSegment === segment) this.currentSegment = null;
93
+ this.emit('segment-end', {
94
+ index: segment.index,
95
+ durationMs: segment.tEnd - segment.tStart,
96
+ completionTokens: segment.completionTokens,
97
+ });
98
+ return segment;
99
+ }
100
+
101
+
102
+ updateUsage({ promptTokens, completionTokens, contextLimit } = {}) {
103
+ if (contextLimit !== undefined && contextLimit !== null) this.contextLimit = Number(contextLimit) || 0;
104
+ if (this.segments.length > 0) {
105
+ const last = this.segments[this.segments.length - 1];
106
+ if (Number.isFinite(Number(promptTokens)) && Number(promptTokens) > 0) last.promptTokens = Number(promptTokens);
107
+ if (Number.isFinite(Number(completionTokens)) && Number(completionTokens) > 0) last.completionTokens = Number(completionTokens);
108
+ }
109
+ }
110
+
111
+
112
+ finish({ promptTokens = null, completionTokens = null, contextLimit } = {}) {
113
+ if (contextLimit !== undefined && contextLimit !== null) this.contextLimit = Number(contextLimit) || 0;
114
+ if (this.currentSegment && this.currentSegment.tEnd === undefined) {
115
+ this.endSegment({}, this.currentSegment);
116
+ }
117
+ this.endTime = Date.now();
118
+ this.finished = true;
119
+ const payload = this.getPayload({ promptTokens, completionTokens });
120
+ this.emit('complete', payload);
121
+ return payload;
122
+ }
123
+
124
+
125
+ getPayload({ promptTokens = null, completionTokens = null } = {}) {
126
+ const segments = this.segments;
127
+ if (this.startTime === null) return null;
128
+
129
+ const endMs = this.endTime ?? Date.now();
130
+ const totalLatencyMs = Math.max(0, endMs - this.startTime);
131
+
132
+ const countedTokens = segments.reduce((sum, s) => sum + s.tokenTimestamps.length, 0);
133
+ const segmentPromptSum = segments.reduce((sum, s) => sum + (s.promptTokens ?? 0), 0);
134
+ const segmentCompletionSum = segments.reduce((sum, s) => sum + (s.completionTokens ?? 0), 0);
135
+
136
+ const prompt = Number(promptTokens) > 0 ? Number(promptTokens)
137
+ : segmentPromptSum > 0 ? segmentPromptSum
138
+ : (this.pendingInitialPromptTokens || 0);
139
+ const completion = Number(completionTokens) > 0 ? Number(completionTokens)
140
+ : segmentCompletionSum > 0 ? segmentCompletionSum
141
+ : countedTokens;
142
+
143
+ const totalTokens = prompt + completion;
144
+
145
+ const firstSegment = segments[0] || null;
146
+ const ttft = firstSegment && firstSegment.tFirst !== null
147
+ ? Math.max(0, firstSegment.tFirst - this.startTime)
148
+ : null;
149
+ const prefillDuration = ttft;
150
+
151
+ const decodeDuration = segments.reduce((sum, s) => {
152
+ if (s.tFirst === null || s.tEnd === undefined) return sum;
153
+ return sum + Math.max(0, s.tEnd - s.tFirst);
154
+ }, 0);
155
+
156
+ const prefillPrompt = firstSegment?.promptTokens ?? prompt;
157
+ const prefillTps = ttft > 0 && prefillPrompt > 0
158
+ ? prefillPrompt / (ttft / 1000)
159
+ : 0;
160
+ const decodeTps = decodeDuration > 0 && completion > 1
161
+ ? (completion - 1) / (decodeDuration / 1000)
162
+ : 0;
163
+ const e2eTps = totalLatencyMs > 0 && totalTokens > 0
164
+ ? totalTokens / (totalLatencyMs / 1000)
165
+ : 0;
166
+
167
+ const interTokenDeltas = [];
168
+ for (const segment of segments) {
169
+ for (let i = 1; i < segment.tokenTimestamps.length; i++) {
170
+ interTokenDeltas.push(segment.tokenTimestamps[i] - segment.tokenTimestamps[i - 1]);
171
+ }
172
+ }
173
+ const itlMean = interTokenDeltas.length > 0
174
+ ? interTokenDeltas.reduce((a, b) => a + b, 0) / interTokenDeltas.length
175
+ : null;
176
+ const itlP95 = computePercentile(interTokenDeltas, 0.95);
177
+
178
+ const contextLimit = this.contextLimit > 0 ? this.contextLimit : null;
179
+ const contextUsagePct = contextLimit && contextLimit > 0
180
+ ? round((totalTokens / contextLimit) * 100, 2)
181
+ : null;
182
+
183
+ return {
184
+ session_id: this.sessionId,
185
+ model_id: this.modelId,
186
+ counts: {
187
+ prompt_tokens: Math.round(prompt),
188
+ completion_tokens: Math.round(completion),
189
+ total_tokens: Math.round(totalTokens),
190
+ context_limit: contextLimit,
191
+ remaining_tokens: contextLimit ? Math.max(0, contextLimit - Math.round(totalTokens)) : null,
192
+ },
193
+ timing_ms: {
194
+ start_time: this.startTime,
195
+ ttft: round(ttft, 1),
196
+ prefill_duration: round(prefillDuration, 1),
197
+ decode_duration: round(decodeDuration, 1),
198
+ total_latency: round(totalLatencyMs, 1),
199
+ },
200
+ rates: {
201
+ prefill_tps: round(prefillTps, 1),
202
+ decode_tps: round(decodeTps, 1),
203
+ e2e_tps: round(e2eTps, 1),
204
+ itl_mean_ms: itlMean === null ? null : round(itlMean, 1),
205
+ itl_p95_ms: itlP95 === null ? null : round(itlP95, 1),
206
+ },
207
+ context_usage_pct: contextUsagePct,
208
+ stream_segments: segments.length,
209
+ };
210
+ }
211
+
212
+ reset() {
213
+ this.startTime = null;
214
+ this.endTime = null;
215
+ this.segments = [];
216
+ this.currentSegment = null;
217
+ this.finished = false;
218
+ this.pendingInitialPromptTokens = 0;
219
+ }
220
+ }