wtt-connect 0.2.62 → 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.62",
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([
@@ -184,12 +185,14 @@ export class Runner {
184
185
  await this.wtt.typing(topicId, 'start', { statusText: 'Agent 已接收消息,正在准备执行', statusKind: 'queued', ttlMs: 30000 });
185
186
  let runtimeSelection = null;
186
187
  let chatStream = null;
188
+ let usageCollector = null;
187
189
  try {
188
190
  const transcripts = await this.transcribeAttachments(staged.files);
189
191
  const modelConfig = modelConfigFromMessage(m);
190
192
  const adapter = this.registry.select({ ...m, content, model_config: modelConfig });
193
+ usageCollector = createUsageCollector(adapter.name, modelConfig);
191
194
  runtimeSelection = { adapter: adapter.name, modelConfig };
192
- chatStream = this.createChatStream(topicId, adapter.name, modelConfig);
195
+ chatStream = this.createChatStream(topicId, adapter.name, modelConfig, () => usageCollector?.summary());
193
196
  await chatStream.start();
194
197
  this.recordRuntimeSelection(adapter.name, modelConfig, 'running');
195
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 });
@@ -211,12 +214,16 @@ export class Runner {
211
214
  modelConfig,
212
215
  files: staged.files,
213
216
  images: staged.images,
214
- 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
+ },
215
221
  });
216
222
  if (localSlash !== null) {
217
223
  const reply = stripHiddenContextLeak(localSlash || '(empty response)') || '(empty response)';
218
224
  await chatStream.finish(reply);
219
225
  await this.wtt.publish(topicId, reply, 'CHAT_REPLY', chatStream.finalMetadata());
226
+ await this.reportUsageBestEffort(usageCollector?.summary(), { runId: chatStream.id, topicId, messageId: m.id });
220
227
  log('info', 'slash command replied', { topicId, adapter: adapter.name, chars: reply.length });
221
228
  return;
222
229
  }
@@ -255,13 +262,17 @@ export class Runner {
255
262
  modelConfig,
256
263
  files: staged.files,
257
264
  images: staged.images,
258
- 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
+ },
259
269
  });
260
270
  const prepared = prepareGeneratedFileArtifacts(stripHiddenContextLeak(output || '(empty response)') || '(empty response)');
261
271
  const reply = prepared.text || '(empty response)';
262
272
  await chatStream.finish(reply);
263
273
  await this.maybeAttachSpeech(topicId, reply, `chat-${topicId}`);
264
274
  await this.wtt.publish(topicId, reply, 'CHAT_REPLY', chatStream.finalMetadata());
275
+ await this.reportUsageBestEffort(usageCollector?.summary(), { runId: chatStream.id, topicId, messageId: m.id });
265
276
  await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'chat', sourceId: topicId, adapter: adapter.name });
266
277
  log('info', 'chat replied', { topicId, chars: reply.length });
267
278
  } catch (err) {
@@ -284,6 +295,7 @@ export class Runner {
284
295
  const transcripts = await this.transcribeAttachments(staged.files);
285
296
  const modelConfig = modelConfigFromMessage(task);
286
297
  const adapter = this.registry.select({ ...task, model_config: modelConfig });
298
+ const usageCollector = createUsageCollector(adapter.name, modelConfig);
287
299
  this.recordRuntimeSelection(adapter.name, modelConfig, 'running');
288
300
  const agentProfile = await this.getAgentProfile();
289
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 });
@@ -314,14 +326,18 @@ export class Runner {
314
326
  modelConfig,
315
327
  files: staged.files,
316
328
  images: staged.images,
317
- 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
+ },
318
333
  });
319
334
  const prepared = prepareGeneratedFileArtifacts(output || '(empty response)');
320
335
  const summary = prepared.text || '(empty response)';
321
336
  await this.maybeRelayArenaOpenCLResult(task, summary);
322
337
  const artifact = await this.materializeTaskArtifact(taskId, summary);
323
338
  const commitIds = extractCommitIds(summary);
324
- 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) };
325
341
  if (commitIds.length) patch.commit_id = commitIds[0];
326
342
  await this.api.patchTask(taskId, patch);
327
343
  if (topicId) {
@@ -329,6 +345,7 @@ export class Runner {
329
345
  await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'task', sourceId: taskId, adapter: adapter.name });
330
346
  if (artifact?.asset?.url) await this.wtt.publish(topicId, `Artifact: ${artifact.asset.url}`, 'TASK_ARTIFACT');
331
347
  }
348
+ await this.reportUsageBestEffort(usage, { runId: `task-${taskId}-${Date.now().toString(36)}`, topicId, taskId });
332
349
  log('info', 'task completed', { taskId, chars: summary.length });
333
350
  } catch (err) {
334
351
  await this.api.patchTask(taskId, { status: 'blocked', output: String(err.message || err) });
@@ -626,7 +643,7 @@ export class Runner {
626
643
  }
627
644
  }
628
645
 
629
- createChatStream(topicId, adapterName = 'agent', modelConfig = {}) {
646
+ createChatStream(topicId, adapterName = 'agent', modelConfig = {}, usageProvider = null) {
630
647
  const enabled = this.config.messageStream !== false && Boolean(topicId);
631
648
  const streamId = `wtt-${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
632
649
  let seq = 0;
@@ -649,11 +666,13 @@ export class Runner {
649
666
  return {
650
667
  id: streamId,
651
668
  finalMetadata() {
669
+ const usage = typeof usageProvider === 'function' ? usageProvider() : null;
652
670
  return {
653
671
  stream_id: streamId,
654
672
  streamId,
655
673
  adapter: adapterName,
656
674
  model: modelConfig.model || modelConfig.model_id || modelConfig.modelId || '',
675
+ ...(usage ? { usage } : {}),
657
676
  };
658
677
  },
659
678
  async start() {
@@ -699,6 +718,17 @@ export class Runner {
699
718
  ]);
700
719
  }
701
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
+
702
732
  async maybePublishProgress(topicId, event, adapterName = 'agent') {
703
733
  if (!topicId) return;
704
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
  }