Files
qwen-code/packages/core/src/subagents/subagent-statistics.ts
2025-09-09 20:53:53 +08:00

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;
}
}