Files
APAW/scripts/agent-stats.ts
¨NW¨ b46a1a20a8 feat: add PHP development stack, atomic tasks, modular code rules, agent monitoring, fix target project detection
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
2026-04-18 23:43:04 +01:00

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('');