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 +1 -1
- package/src/runner.js +38 -7
- package/src/usage.js +207 -0
- package/src/wtt-api.js +13 -0
package/package.json
CHANGED
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
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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
|
}
|