7 evolutionary tasks implemented: 1. PHP web development: php-developer agent + 6 skills (Laravel, Symfony, WordPress, security, testing, modular architecture) + 2 pipeline commands (/laravel, /wordpress) 2. Atomic task decomposition: 1 action = 1 task rule, task sizing guide, decomposition protocol for orchestrator, token budgets per complexity 3. Modular code rules: max 100 lines/file, max 30 lines/function, service/repository patterns, cross-module communication via events only 4. Gitea-centric workflow: mandatory issue creation before work, research with links, progress checkboxes, screenshots on test, git history as knowledge base 5. Fix: target project auto-detection — removed all hardcoded UniqueSoft/APAW from API calls, added get_target_repo() via git remote, GITEA_TARGET_REPO env override 6. Agent execution monitoring: agent-executions.jsonl logging, agent-stats.ts statistics script, required fields per invocation, Gitea comment includes duration/tokens 7. Token optimization: 1 action = 1 task principle, token budgets by task type, routing matrix, no scope creep, skip unnecessary pipeline steps
192 lines
5.0 KiB
TypeScript
192 lines
5.0 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* Agent Stats - Analyze agent execution logs
|
|
*
|
|
* Usage:
|
|
* bun run scripts/agent-stats.ts
|
|
* bun run scripts/agent-stats.ts --last 7
|
|
* bun run scripts/agent-stats.ts --project UniqueSoft/my-shop
|
|
*/
|
|
|
|
import { readFileSync, existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
interface AgentExecution {
|
|
ts: string;
|
|
agent: string;
|
|
issue: number;
|
|
project: string;
|
|
task: string;
|
|
subtask_type: string;
|
|
duration_ms: number;
|
|
tokens_used: number;
|
|
status: string;
|
|
files: string[];
|
|
score: number | null;
|
|
next_agent: string | null;
|
|
}
|
|
|
|
interface AgentStats {
|
|
calls: number;
|
|
avgDuration: number;
|
|
avgTokens: number;
|
|
avgScore: number;
|
|
successRate: number;
|
|
totalDuration: number;
|
|
totalTokens: number;
|
|
}
|
|
|
|
function parseArgs(): { lastDays: number; project: string | null } {
|
|
const args = process.argv.slice(2);
|
|
let lastDays = 30;
|
|
let project: string | null = null;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '--last' && args[i + 1]) {
|
|
lastDays = parseInt(args[i + 1], 10);
|
|
}
|
|
if (args[i] === '--project' && args[i + 1]) {
|
|
project = args[i + 1];
|
|
}
|
|
}
|
|
|
|
return { lastDays, project };
|
|
}
|
|
|
|
function loadExecutions(logPath: string): AgentExecution[] {
|
|
if (!existsSync(logPath)) {
|
|
console.log('No execution log found. Start using agents to build history.');
|
|
return [];
|
|
}
|
|
|
|
const content = readFileSync(logPath, 'utf-8');
|
|
return content
|
|
.split('\n')
|
|
.filter(line => line.trim())
|
|
.map(line => {
|
|
try { return JSON.parse(line); }
|
|
catch { return null; }
|
|
})
|
|
.filter((e): e is AgentExecution => e !== null);
|
|
}
|
|
|
|
function filterByDate(executions: AgentExecution[], days: number): AgentExecution[] {
|
|
const cutoff = new Date();
|
|
cutoff.setDate(cutoff.getDate() - days);
|
|
return executions.filter(e => new Date(e.ts) >= cutoff);
|
|
}
|
|
|
|
function filterByProject(executions: AgentExecution[], project: string): AgentExecution[] {
|
|
return executions.filter(e => e.project === project);
|
|
}
|
|
|
|
function computeStats(executions: AgentExecution[]): Map<string, AgentStats> {
|
|
const stats = new Map<string, AgentStats>();
|
|
|
|
for (const e of executions) {
|
|
const existing = stats.get(e.agent) || {
|
|
calls: 0,
|
|
avgDuration: 0,
|
|
avgTokens: 0,
|
|
avgScore: 0,
|
|
successRate: 0,
|
|
totalDuration: 0,
|
|
totalTokens: 0,
|
|
};
|
|
|
|
existing.calls++;
|
|
existing.totalDuration += e.duration_ms;
|
|
existing.totalTokens += e.tokens_used;
|
|
if (e.score) existing.avgScore = (existing.avgScore * (existing.calls - 1) + e.score) / existing.calls;
|
|
if (e.status === 'success' || e.status === 'pass') {
|
|
existing.successRate = (existing.successRate * (existing.calls - 1) + 1) / existing.calls;
|
|
}
|
|
|
|
stats.set(e.agent, existing);
|
|
}
|
|
|
|
// Compute averages
|
|
for (const [, s] of stats) {
|
|
s.avgDuration = s.calls > 0 ? s.totalDuration / s.calls : 0;
|
|
s.avgTokens = s.calls > 0 ? s.totalTokens / s.calls : 0;
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
function formatDuration(ms: number): string {
|
|
if (ms < 1000) return `${ms}ms`;
|
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
return `${(ms / 60000).toFixed(1)}m`;
|
|
}
|
|
|
|
function formatTokens(tokens: number): string {
|
|
if (tokens < 1000) return `${tokens}`;
|
|
return `${(tokens / 1000).toFixed(1)}k`;
|
|
}
|
|
|
|
const logPath = join(process.cwd(), '.kilo', 'logs', 'agent-executions.jsonl');
|
|
const { lastDays, project } = parseArgs();
|
|
|
|
let executions = loadExecutions(logPath);
|
|
|
|
if (executions.length === 0) {
|
|
console.log('\n📊 Agent Stats - No data yet\n');
|
|
console.log('Log file:', logPath);
|
|
console.log('Start using agents to build execution history.\n');
|
|
process.exit(0);
|
|
}
|
|
|
|
if (lastDays < 9999) {
|
|
executions = filterByDate(executions, lastDays);
|
|
}
|
|
|
|
if (project) {
|
|
executions = filterByProject(executions, project);
|
|
}
|
|
|
|
const stats = computeStats(executions);
|
|
|
|
console.log(`\n📊 Agent Stats (Last ${lastDays} days${project ? `, Project: ${project}` : ''})`);
|
|
console.log('═'.repeat(70));
|
|
|
|
const sortedStats = [...stats.entries()].sort((a, b) => b[1].calls - a[1].calls);
|
|
|
|
for (const [agent, s] of sortedStats) {
|
|
console.log(
|
|
`${agent.padEnd(20)} ${String(s.calls).padStart(3)} calls, ` +
|
|
`avg ${formatDuration(s.avgDuration).padStart(6)}, ` +
|
|
`score ${s.avgScore.toFixed(1)}/10, ` +
|
|
`${(s.successRate * 100).toFixed(0)}% success, ` +
|
|
`~${formatTokens(s.avgTokens)} tokens`
|
|
);
|
|
}
|
|
|
|
console.log('═'.repeat(70));
|
|
console.log(`Total: ${executions.length} executions\n`);
|
|
|
|
// Project breakdown
|
|
const projects = new Map<string, number>();
|
|
for (const e of executions) {
|
|
projects.set(e.project, (projects.get(e.project) || 0) + 1);
|
|
}
|
|
|
|
if (projects.size > 1) {
|
|
console.log('📁 By Project:');
|
|
for (const [proj, count] of projects) {
|
|
console.log(` ${proj}: ${count} executions`);
|
|
}
|
|
console.log('');
|
|
}
|
|
|
|
// Status breakdown
|
|
const statuses = new Map<string, number>();
|
|
for (const e of executions) {
|
|
statuses.set(e.status, (statuses.get(e.status) || 0) + 1);
|
|
}
|
|
|
|
console.log('📈 By Status:');
|
|
for (const [status, count] of statuses) {
|
|
console.log(` ${status}: ${count}`);
|
|
}
|
|
console.log(''); |