mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
251 lines
8.0 KiB
TypeScript
251 lines
8.0 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Qwen
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
export interface ToolUsageStats {
|
|
name: string;
|
|
count: number;
|
|
success: number;
|
|
failure: number;
|
|
lastError?: string;
|
|
totalDurationMs: number;
|
|
averageDurationMs: number;
|
|
}
|
|
|
|
export interface SubagentStatsSummary {
|
|
rounds: number;
|
|
totalDurationMs: number;
|
|
totalToolCalls: number;
|
|
successfulToolCalls: number;
|
|
failedToolCalls: number;
|
|
successRate: number;
|
|
inputTokens: number;
|
|
outputTokens: number;
|
|
totalTokens: number;
|
|
estimatedCost: number;
|
|
toolUsage: ToolUsageStats[];
|
|
}
|
|
|
|
export class SubagentStatistics {
|
|
private startTimeMs = 0;
|
|
private rounds = 0;
|
|
private totalToolCalls = 0;
|
|
private successfulToolCalls = 0;
|
|
private failedToolCalls = 0;
|
|
private inputTokens = 0;
|
|
private outputTokens = 0;
|
|
private toolUsage = new Map<string, ToolUsageStats>();
|
|
|
|
start(now = Date.now()) {
|
|
this.startTimeMs = now;
|
|
}
|
|
|
|
setRounds(rounds: number) {
|
|
this.rounds = rounds;
|
|
}
|
|
|
|
recordToolCall(
|
|
name: string,
|
|
success: boolean,
|
|
durationMs: number,
|
|
lastError?: string,
|
|
) {
|
|
this.totalToolCalls += 1;
|
|
if (success) this.successfulToolCalls += 1;
|
|
else this.failedToolCalls += 1;
|
|
|
|
const tu = this.toolUsage.get(name) || {
|
|
name,
|
|
count: 0,
|
|
success: 0,
|
|
failure: 0,
|
|
lastError: undefined,
|
|
totalDurationMs: 0,
|
|
averageDurationMs: 0,
|
|
};
|
|
tu.count += 1;
|
|
if (success) tu.success += 1;
|
|
else tu.failure += 1;
|
|
if (lastError) tu.lastError = lastError;
|
|
tu.totalDurationMs += Math.max(0, durationMs || 0);
|
|
tu.averageDurationMs = tu.count > 0 ? tu.totalDurationMs / tu.count : 0;
|
|
this.toolUsage.set(name, tu);
|
|
}
|
|
|
|
recordTokens(input: number, output: number) {
|
|
this.inputTokens += Math.max(0, input || 0);
|
|
this.outputTokens += Math.max(0, output || 0);
|
|
}
|
|
|
|
getSummary(now = Date.now()): SubagentStatsSummary {
|
|
const totalDurationMs = this.startTimeMs ? now - this.startTimeMs : 0;
|
|
const totalToolCalls = this.totalToolCalls;
|
|
const successRate =
|
|
totalToolCalls > 0
|
|
? (this.successfulToolCalls / totalToolCalls) * 100
|
|
: 0;
|
|
const totalTokens = this.inputTokens + this.outputTokens;
|
|
const estimatedCost = this.inputTokens * 3e-5 + this.outputTokens * 6e-5;
|
|
return {
|
|
rounds: this.rounds,
|
|
totalDurationMs,
|
|
totalToolCalls,
|
|
successfulToolCalls: this.successfulToolCalls,
|
|
failedToolCalls: this.failedToolCalls,
|
|
successRate,
|
|
inputTokens: this.inputTokens,
|
|
outputTokens: this.outputTokens,
|
|
totalTokens,
|
|
estimatedCost,
|
|
toolUsage: Array.from(this.toolUsage.values()),
|
|
};
|
|
}
|
|
|
|
formatCompact(taskDesc: string, now = Date.now()): string {
|
|
const stats = this.getSummary(now);
|
|
const sr =
|
|
stats.totalToolCalls > 0
|
|
? (stats.successRate ??
|
|
(stats.successfulToolCalls / stats.totalToolCalls) * 100)
|
|
: 0;
|
|
const lines = [
|
|
`📋 Task Completed: ${taskDesc}`,
|
|
`🔧 Tool Usage: ${stats.totalToolCalls} calls${stats.totalToolCalls ? `, ${sr.toFixed(1)}% success` : ''}`,
|
|
`⏱️ Duration: ${this.fmtDuration(stats.totalDurationMs)} | 🔁 Rounds: ${stats.rounds}`,
|
|
];
|
|
if (typeof stats.totalTokens === 'number') {
|
|
lines.push(
|
|
`🔢 Tokens: ${stats.totalTokens.toLocaleString()}${stats.inputTokens || stats.outputTokens ? ` (in ${stats.inputTokens ?? 0}, out ${stats.outputTokens ?? 0})` : ''}`,
|
|
);
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
formatDetailed(taskDesc: string, now = Date.now()): string {
|
|
const stats = this.getSummary(now);
|
|
const sr =
|
|
stats.totalToolCalls > 0
|
|
? (stats.successRate ??
|
|
(stats.successfulToolCalls / stats.totalToolCalls) * 100)
|
|
: 0;
|
|
const lines: string[] = [];
|
|
lines.push(`📋 Task Completed: ${taskDesc}`);
|
|
lines.push(
|
|
`⏱️ Duration: ${this.fmtDuration(stats.totalDurationMs)} | 🔁 Rounds: ${stats.rounds}`,
|
|
);
|
|
// Quality indicator
|
|
let quality = 'Poor execution';
|
|
if (sr >= 95) quality = 'Excellent execution';
|
|
else if (sr >= 85) quality = 'Good execution';
|
|
else if (sr >= 70) quality = 'Fair execution';
|
|
lines.push(`✅ Quality: ${quality} (${sr.toFixed(1)}% tool success)`);
|
|
// Speed category
|
|
const d = stats.totalDurationMs;
|
|
let speed = 'Long execution - consider breaking down tasks';
|
|
if (d < 10_000) speed = 'Fast completion - under 10 seconds';
|
|
else if (d < 60_000) speed = 'Good speed - under a minute';
|
|
else if (d < 300_000) speed = 'Moderate duration - a few minutes';
|
|
lines.push(`🚀 Speed: ${speed}`);
|
|
lines.push(
|
|
`🔧 Tools: ${stats.totalToolCalls} calls, ${sr.toFixed(1)}% success (${stats.successfulToolCalls} ok, ${stats.failedToolCalls} failed)`,
|
|
);
|
|
if (typeof stats.totalTokens === 'number') {
|
|
lines.push(
|
|
`🔢 Tokens: ${stats.totalTokens.toLocaleString()} (in ${stats.inputTokens ?? 0}, out ${stats.outputTokens ?? 0})`,
|
|
);
|
|
}
|
|
if (stats.toolUsage && stats.toolUsage.length) {
|
|
const sorted = [...stats.toolUsage]
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, 5);
|
|
lines.push('\nTop tools:');
|
|
for (const t of sorted) {
|
|
const avg =
|
|
typeof t.averageDurationMs === 'number'
|
|
? `, avg ${this.fmtDuration(Math.round(t.averageDurationMs))}`
|
|
: '';
|
|
lines.push(
|
|
` - ${t.name}: ${t.count} calls (${t.success} ok, ${t.failure} fail${avg}${t.lastError ? `, last error: ${t.lastError}` : ''})`,
|
|
);
|
|
}
|
|
}
|
|
const tips = this.generatePerformanceTips(stats);
|
|
if (tips.length) {
|
|
lines.push('\n💡 Performance Insights:');
|
|
for (const tip of tips.slice(0, 3)) lines.push(` - ${tip}`);
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
private fmtDuration(ms: number): string {
|
|
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
if (ms < 3600000) {
|
|
const m = Math.floor(ms / 60000);
|
|
const s = Math.floor((ms % 60000) / 1000);
|
|
return `${m}m ${s}s`;
|
|
}
|
|
const h = Math.floor(ms / 3600000);
|
|
const m = Math.floor((ms % 3600000) / 60000);
|
|
return `${h}h ${m}m`;
|
|
}
|
|
|
|
private generatePerformanceTips(stats: SubagentStatsSummary): string[] {
|
|
const tips: string[] = [];
|
|
const totalCalls = stats.totalToolCalls;
|
|
const sr =
|
|
stats.totalToolCalls > 0
|
|
? (stats.successRate ??
|
|
(stats.successfulToolCalls / stats.totalToolCalls) * 100)
|
|
: 0;
|
|
|
|
// High failure rate
|
|
if (sr < 80)
|
|
tips.push('Low tool success rate - review inputs and error messages');
|
|
|
|
// Long duration
|
|
if (stats.totalDurationMs > 60_000)
|
|
tips.push('Long execution time - consider breaking down complex tasks');
|
|
|
|
// Token usage
|
|
if (typeof stats.totalTokens === 'number' && stats.totalTokens > 100_000) {
|
|
tips.push(
|
|
'High token usage - consider optimizing prompts or narrowing scope',
|
|
);
|
|
}
|
|
if (typeof stats.totalTokens === 'number' && totalCalls > 0) {
|
|
const avgTokPerCall = stats.totalTokens / totalCalls;
|
|
if (avgTokPerCall > 5_000)
|
|
tips.push(
|
|
`High token usage per tool call (~${Math.round(avgTokPerCall)} tokens/call)`,
|
|
);
|
|
}
|
|
|
|
// Network failures
|
|
const isNetworkTool = (name: string) => /web|fetch|search/i.test(name);
|
|
const hadNetworkFailure = (stats.toolUsage || []).some(
|
|
(t) =>
|
|
isNetworkTool(t.name) &&
|
|
t.lastError &&
|
|
/timeout|network/i.test(t.lastError),
|
|
);
|
|
if (hadNetworkFailure)
|
|
tips.push(
|
|
'Network operations had failures - consider increasing timeout or checking connectivity',
|
|
);
|
|
|
|
// Slow tools
|
|
const slow = (stats.toolUsage || [])
|
|
.filter((t) => (t.averageDurationMs ?? 0) > 10_000)
|
|
.sort((a, b) => (b.averageDurationMs ?? 0) - (a.averageDurationMs ?? 0));
|
|
if (slow.length)
|
|
tips.push(
|
|
`Consider optimizing ${slow[0].name} operations (avg ${this.fmtDuration(Math.round(slow[0].averageDurationMs!))})`,
|
|
);
|
|
|
|
return tips;
|
|
}
|
|
}
|