wtt-connect 0.2.61 → 0.2.63

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtt-connect",
3
- "version": "0.2.61",
3
+ "version": "0.2.63",
4
4
  "private": false,
5
5
  "description": "WTT-native connector daemon for Codex, Claude Code, Cursor, Gemini, ACP, and other coding agent surfaces.",
6
6
  "type": "module",
package/src/runner.js CHANGED
@@ -21,6 +21,7 @@ import { runShellCommand } from './shell-runner.js';
21
21
  import { TerminalSessionManager } from './terminal-session.js';
22
22
  import { isModelSwitcherEnabled, switchModelProvider } from './model-switcher.js';
23
23
  import { formatSlashCommandHelp, isAgentSlashCommand, parseSlashCommand, slashCommandsForAdapter } from './slash.js';
24
+ import { createUsageCollector, taskUsagePatch } from './usage.js';
24
25
 
25
26
  const TERMINAL_STATUSES = new Set(['review', 'done', 'approved', 'cancelled']);
26
27
  const GENERATED_FILE_EXTENSIONS = new Set([
@@ -82,7 +83,8 @@ export class Runner {
82
83
  if (!meta || meta.kb_mode !== true) return '';
83
84
  const kbTaskId = String(meta.kb_task_id || meta.kbTaskId || '').trim();
84
85
  if (!kbTaskId) return '';
85
- const result = await this.api.searchKnowledgeContext(kbTaskId, query, { limit: 8 });
86
+ const explicitQuery = String(meta.kb_query || meta.kbQuery || '').trim();
87
+ const result = await this.api.searchKnowledgeContext(kbTaskId, explicitQuery || query, { limit: 8 });
86
88
  return renderKnowledgeContextBlock(result?.results || [], {
87
89
  taskId: kbTaskId,
88
90
  scope: meta.kb_scope || meta.kbScope || 'personal',
@@ -183,12 +185,14 @@ export class Runner {
183
185
  await this.wtt.typing(topicId, 'start', { statusText: 'Agent 已接收消息,正在准备执行', statusKind: 'queued', ttlMs: 30000 });
184
186
  let runtimeSelection = null;
185
187
  let chatStream = null;
188
+ let usageCollector = null;
186
189
  try {
187
190
  const transcripts = await this.transcribeAttachments(staged.files);
188
191
  const modelConfig = modelConfigFromMessage(m);
189
192
  const adapter = this.registry.select({ ...m, content, model_config: modelConfig });
193
+ usageCollector = createUsageCollector(adapter.name, modelConfig);
190
194
  runtimeSelection = { adapter: adapter.name, modelConfig };
191
- chatStream = this.createChatStream(topicId, adapter.name, modelConfig);
195
+ chatStream = this.createChatStream(topicId, adapter.name, modelConfig, () => usageCollector?.summary());
192
196
  await chatStream.start();
193
197
  this.recordRuntimeSelection(adapter.name, modelConfig, 'running');
194
198
  await this.wtt.typing(topicId, 'start', { statusText: `${adapterDisplayName(adapter.name)} 正在执行${modelConfig.model ? ` · ${modelConfig.model}` : ''}`, statusKind: 'running', adapter: adapter.name, model: modelConfig.model || undefined, ttlMs: 30000 });
@@ -210,12 +214,16 @@ export class Runner {
210
214
  modelConfig,
211
215
  files: staged.files,
212
216
  images: staged.images,
213
- onProgress: (event) => this.handleChatProgress(topicId, event, adapter.name, chatStream),
217
+ onProgress: (event) => {
218
+ usageCollector?.observe(event);
219
+ return this.handleChatProgress(topicId, event, adapter.name, chatStream);
220
+ },
214
221
  });
215
222
  if (localSlash !== null) {
216
223
  const reply = stripHiddenContextLeak(localSlash || '(empty response)') || '(empty response)';
217
224
  await chatStream.finish(reply);
218
225
  await this.wtt.publish(topicId, reply, 'CHAT_REPLY', chatStream.finalMetadata());
226
+ await this.reportUsageBestEffort(usageCollector?.summary(), { runId: chatStream.id, topicId, messageId: m.id });
219
227
  log('info', 'slash command replied', { topicId, adapter: adapter.name, chars: reply.length });
220
228
  return;
221
229
  }
@@ -254,13 +262,17 @@ export class Runner {
254
262
  modelConfig,
255
263
  files: staged.files,
256
264
  images: staged.images,
257
- onProgress: (event) => this.handleChatProgress(topicId, event, adapter.name, chatStream),
265
+ onProgress: (event) => {
266
+ usageCollector?.observe(event);
267
+ return this.handleChatProgress(topicId, event, adapter.name, chatStream);
268
+ },
258
269
  });
259
270
  const prepared = prepareGeneratedFileArtifacts(stripHiddenContextLeak(output || '(empty response)') || '(empty response)');
260
271
  const reply = prepared.text || '(empty response)';
261
272
  await chatStream.finish(reply);
262
273
  await this.maybeAttachSpeech(topicId, reply, `chat-${topicId}`);
263
274
  await this.wtt.publish(topicId, reply, 'CHAT_REPLY', chatStream.finalMetadata());
275
+ await this.reportUsageBestEffort(usageCollector?.summary(), { runId: chatStream.id, topicId, messageId: m.id });
264
276
  await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'chat', sourceId: topicId, adapter: adapter.name });
265
277
  log('info', 'chat replied', { topicId, chars: reply.length });
266
278
  } catch (err) {
@@ -283,6 +295,7 @@ export class Runner {
283
295
  const transcripts = await this.transcribeAttachments(staged.files);
284
296
  const modelConfig = modelConfigFromMessage(task);
285
297
  const adapter = this.registry.select({ ...task, model_config: modelConfig });
298
+ const usageCollector = createUsageCollector(adapter.name, modelConfig);
286
299
  this.recordRuntimeSelection(adapter.name, modelConfig, 'running');
287
300
  const agentProfile = await this.getAgentProfile();
288
301
  if (topicId) await this.wtt.typing(topicId, 'start', { statusText: `${adapterDisplayName(adapter.name)} 正在执行任务${modelConfig.model ? ` · ${modelConfig.model}` : ''}`, statusKind: 'running', adapter: adapter.name, model: modelConfig.model || undefined, ttlMs: 30000 });
@@ -313,14 +326,18 @@ export class Runner {
313
326
  modelConfig,
314
327
  files: staged.files,
315
328
  images: staged.images,
316
- onProgress: (ev) => topicId ? this.maybePublishProgress(topicId, ev, adapter.name) : Promise.resolve(),
329
+ onProgress: (ev) => {
330
+ usageCollector.observe(ev);
331
+ return topicId ? this.maybePublishProgress(topicId, ev, adapter.name) : Promise.resolve();
332
+ },
317
333
  });
318
334
  const prepared = prepareGeneratedFileArtifacts(output || '(empty response)');
319
335
  const summary = prepared.text || '(empty response)';
320
336
  await this.maybeRelayArenaOpenCLResult(task, summary);
321
337
  const artifact = await this.materializeTaskArtifact(taskId, summary);
322
338
  const commitIds = extractCommitIds(summary);
323
- const patch = { status: 'review', progress: 100, output: summary, summary, usage_source: `wtt-connect/${adapter.name}` };
339
+ const usage = usageCollector.summary();
340
+ const patch = { status: 'review', progress: 100, output: summary, summary, usage_source: `wtt-connect/${adapter.name}`, ...taskUsagePatch(usage) };
324
341
  if (commitIds.length) patch.commit_id = commitIds[0];
325
342
  await this.api.patchTask(taskId, patch);
326
343
  if (topicId) {
@@ -328,6 +345,7 @@ export class Runner {
328
345
  await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'task', sourceId: taskId, adapter: adapter.name });
329
346
  if (artifact?.asset?.url) await this.wtt.publish(topicId, `Artifact: ${artifact.asset.url}`, 'TASK_ARTIFACT');
330
347
  }
348
+ await this.reportUsageBestEffort(usage, { runId: `task-${taskId}-${Date.now().toString(36)}`, topicId, taskId });
331
349
  log('info', 'task completed', { taskId, chars: summary.length });
332
350
  } catch (err) {
333
351
  await this.api.patchTask(taskId, { status: 'blocked', output: String(err.message || err) });
@@ -625,7 +643,7 @@ export class Runner {
625
643
  }
626
644
  }
627
645
 
628
- createChatStream(topicId, adapterName = 'agent', modelConfig = {}) {
646
+ createChatStream(topicId, adapterName = 'agent', modelConfig = {}, usageProvider = null) {
629
647
  const enabled = this.config.messageStream !== false && Boolean(topicId);
630
648
  const streamId = `wtt-${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
631
649
  let seq = 0;
@@ -648,11 +666,13 @@ export class Runner {
648
666
  return {
649
667
  id: streamId,
650
668
  finalMetadata() {
669
+ const usage = typeof usageProvider === 'function' ? usageProvider() : null;
651
670
  return {
652
671
  stream_id: streamId,
653
672
  streamId,
654
673
  adapter: adapterName,
655
674
  model: modelConfig.model || modelConfig.model_id || modelConfig.modelId || '',
675
+ ...(usage ? { usage } : {}),
656
676
  };
657
677
  },
658
678
  async start() {
@@ -698,6 +718,17 @@ export class Runner {
698
718
  ]);
699
719
  }
700
720
 
721
+ async reportUsageBestEffort(usage, { runId = '', topicId = '', taskId = '', messageId = '' } = {}) {
722
+ if (!usage) return;
723
+ await this.api.reportAgentUsage({
724
+ ...usage,
725
+ run_id: runId,
726
+ topic_id: topicId,
727
+ task_id: taskId,
728
+ message_id: messageId,
729
+ });
730
+ }
731
+
701
732
  async maybePublishProgress(topicId, event, adapterName = 'agent') {
702
733
  if (!topicId) return;
703
734
  const normalized = normalizeAgentEvent(adapterName, event);
package/src/usage.js ADDED
@@ -0,0 +1,207 @@
1
+ export function createUsageCollector(adapter = 'agent', modelConfig = {}) {
2
+ let best = null;
3
+ let responseModel = '';
4
+ let rawUsage = null;
5
+
6
+ function observe(event) {
7
+ if (!event || typeof event !== 'object') return;
8
+ responseModel = extractModel(event) || responseModel;
9
+ const usage = extractUsage(event);
10
+ if (!usage) return;
11
+ rawUsage = usage.raw_usage || usage;
12
+ if (!best || scoreUsage(usage) >= scoreUsage(best)) best = usage;
13
+ }
14
+
15
+ function summary() {
16
+ if (!best) return null;
17
+ const inputTokens = safeInt(best.input_tokens);
18
+ const outputTokens = safeInt(best.output_tokens);
19
+ const cacheReadTokens = safeInt(best.cache_read_tokens);
20
+ const cacheWriteTokens = safeInt(best.cache_write_tokens);
21
+ const cacheTokens = safeInt(best.cache_tokens) + cacheReadTokens + cacheWriteTokens;
22
+ const totalTokens = safeInt(best.total_tokens) || inputTokens + outputTokens + cacheTokens;
23
+ if (!totalTokens && !inputTokens && !outputTokens && !cacheTokens) return null;
24
+ return {
25
+ adapter,
26
+ provider: providerForAdapter(adapter),
27
+ request_model: modelFromConfig(modelConfig),
28
+ response_model: responseModel,
29
+ input_tokens: inputTokens,
30
+ output_tokens: outputTokens,
31
+ cache_read_tokens: cacheReadTokens,
32
+ cache_write_tokens: cacheWriteTokens,
33
+ cache_tokens: cacheTokens,
34
+ total_tokens: totalTokens,
35
+ source: sourceForAdapter(adapter),
36
+ raw_usage: compactRawUsage(rawUsage),
37
+ };
38
+ }
39
+
40
+ return { observe, summary };
41
+ }
42
+
43
+ export function taskUsagePatch(usage) {
44
+ if (!usage) return {};
45
+ return {
46
+ usage_prompt_tokens: safeInt(usage.input_tokens),
47
+ usage_completion_tokens: safeInt(usage.output_tokens),
48
+ usage_cache_read_tokens: safeInt(usage.cache_read_tokens) + safeInt(usage.cache_tokens),
49
+ usage_cache_write_tokens: safeInt(usage.cache_write_tokens),
50
+ usage_total_tokens: safeInt(usage.total_tokens),
51
+ usage_source: usage.source || 'wtt-connect',
52
+ };
53
+ }
54
+
55
+ function extractUsage(event) {
56
+ const direct = findUsageObject(event);
57
+ if (direct) return normalizeUsage(direct);
58
+ const stats = event?.stats;
59
+ if (stats && typeof stats === 'object') {
60
+ const models = stats.models;
61
+ if (models && typeof models === 'object' && !Array.isArray(models)) {
62
+ return normalizeUsage(aggregateModelStats(models));
63
+ }
64
+ return normalizeUsage(stats);
65
+ }
66
+ return null;
67
+ }
68
+
69
+ function findUsageObject(value, depth = 0) {
70
+ if (!value || typeof value !== 'object' || depth > 5) return null;
71
+ for (const key of ['usage', 'usage_metadata', 'usageMetadata', 'token_usage', 'tokenUsage']) {
72
+ const candidate = value[key];
73
+ if (candidate && typeof candidate === 'object') return candidate;
74
+ }
75
+ if (looksLikeUsage(value)) return value;
76
+ for (const key of ['message', 'response', 'result', 'item', 'delta', 'event']) {
77
+ const found = findUsageObject(value[key], depth + 1);
78
+ if (found) return found;
79
+ }
80
+ return null;
81
+ }
82
+
83
+ function looksLikeUsage(value) {
84
+ if (!value || typeof value !== 'object') return false;
85
+ return [
86
+ 'input_tokens',
87
+ 'prompt_tokens',
88
+ 'output_tokens',
89
+ 'completion_tokens',
90
+ 'total_tokens',
91
+ 'promptTokenCount',
92
+ 'candidatesTokenCount',
93
+ 'totalTokenCount',
94
+ ].some((key) => key in value);
95
+ }
96
+
97
+ function normalizeUsage(raw) {
98
+ if (!raw || typeof raw !== 'object') return null;
99
+ const inputTokens = firstInt(raw, [
100
+ 'input_tokens',
101
+ 'prompt_tokens',
102
+ 'promptTokens',
103
+ 'prompt_token_count',
104
+ 'promptTokenCount',
105
+ 'inputTokenCount',
106
+ ]);
107
+ const candidateTokens = firstInt(raw, [
108
+ 'output_tokens',
109
+ 'completion_tokens',
110
+ 'completionTokens',
111
+ 'completion_token_count',
112
+ 'candidatesTokenCount',
113
+ 'outputTokenCount',
114
+ ]);
115
+ const thoughtsTokens = firstInt(raw, ['thoughtsTokenCount', 'reasoning_tokens', 'reasoningTokens']);
116
+ const outputTokens = candidateTokens + thoughtsTokens;
117
+ const cacheReadTokens = firstInt(raw, [
118
+ 'cache_read_input_tokens',
119
+ 'cached_tokens',
120
+ 'cachedTokens',
121
+ 'cachedContentTokenCount',
122
+ ]);
123
+ const cacheWriteTokens = firstInt(raw, ['cache_creation_input_tokens', 'cache_write_input_tokens']);
124
+ const cacheTokens = firstInt(raw, ['cache_tokens', 'cacheTokens']);
125
+ const totalTokens = firstInt(raw, ['total_tokens', 'totalTokens', 'totalTokenCount'])
126
+ || inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens + cacheTokens;
127
+ if (!inputTokens && !outputTokens && !cacheReadTokens && !cacheWriteTokens && !cacheTokens && !totalTokens) return null;
128
+ return {
129
+ input_tokens: inputTokens,
130
+ output_tokens: outputTokens,
131
+ cache_read_tokens: cacheReadTokens,
132
+ cache_write_tokens: cacheWriteTokens,
133
+ cache_tokens: cacheTokens,
134
+ total_tokens: totalTokens,
135
+ raw_usage: raw,
136
+ };
137
+ }
138
+
139
+ function aggregateModelStats(models) {
140
+ const totals = {};
141
+ for (const value of Object.values(models || {})) {
142
+ if (!value || typeof value !== 'object') continue;
143
+ const usage = normalizeUsage(value);
144
+ if (!usage) continue;
145
+ for (const key of ['input_tokens', 'output_tokens', 'cache_read_tokens', 'cache_write_tokens', 'cache_tokens', 'total_tokens']) {
146
+ totals[key] = safeInt(totals[key]) + safeInt(usage[key]);
147
+ }
148
+ }
149
+ return totals;
150
+ }
151
+
152
+ function extractModel(event) {
153
+ if (!event || typeof event !== 'object') return '';
154
+ for (const key of ['model', 'model_id', 'modelId', 'current_model', 'response_model']) {
155
+ const value = event[key];
156
+ if (typeof value === 'string' && value.trim()) return value.trim();
157
+ }
158
+ return extractModel(event.message) || extractModel(event.response) || '';
159
+ }
160
+
161
+ function modelFromConfig(modelConfig = {}) {
162
+ return String(modelConfig.model || modelConfig.model_id || modelConfig.modelId || '').trim();
163
+ }
164
+
165
+ function firstInt(raw, keys) {
166
+ for (const key of keys) {
167
+ const value = raw?.[key];
168
+ const num = safeInt(value);
169
+ if (num) return num;
170
+ }
171
+ return 0;
172
+ }
173
+
174
+ function safeInt(value) {
175
+ const num = Number(value || 0);
176
+ if (!Number.isFinite(num) || num <= 0) return 0;
177
+ return Math.round(num);
178
+ }
179
+
180
+ function scoreUsage(usage) {
181
+ return safeInt(usage?.total_tokens) || safeInt(usage?.input_tokens) + safeInt(usage?.output_tokens) + safeInt(usage?.cache_tokens);
182
+ }
183
+
184
+ function providerForAdapter(adapter) {
185
+ if (adapter === 'claude-code') return 'anthropic-compatible';
186
+ if (adapter === 'codex') return 'openai-compatible';
187
+ if (adapter === 'gemini') return 'gemini';
188
+ return adapter || 'agent';
189
+ }
190
+
191
+ function sourceForAdapter(adapter) {
192
+ if (adapter === 'claude-code') return 'claude_code_stream_json';
193
+ if (adapter === 'codex') return 'codex_json';
194
+ if (adapter === 'gemini') return 'gemini_stream_json';
195
+ return `wtt-connect/${adapter || 'agent'}`;
196
+ }
197
+
198
+ function compactRawUsage(value) {
199
+ if (!value || typeof value !== 'object') return undefined;
200
+ try {
201
+ const json = JSON.stringify(value);
202
+ if (json.length <= 4000) return value;
203
+ return { truncated: true, total_tokens: safeInt(value.total_tokens || value.totalTokens || value.totalTokenCount) };
204
+ } catch {
205
+ return undefined;
206
+ }
207
+ }
package/src/wtt-api.js CHANGED
@@ -117,4 +117,17 @@ export class WTTApi {
117
117
  });
118
118
  }
119
119
 
120
+ async reportAgentUsage(usage) {
121
+ if (!usage || !this.config.agentId || !this.config.token) return null;
122
+ try {
123
+ return await this.request('POST', `/agents/${encodeURIComponent(this.config.agentId)}/usage`, {
124
+ headers: this.agentHeaders(),
125
+ json: usage,
126
+ });
127
+ } catch (err) {
128
+ log('warn', 'report_agent_usage failed', { error: err.message });
129
+ return null;
130
+ }
131
+ }
132
+
120
133
  }