@sooneocean/agw 1.4.0

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.
Files changed (59) hide show
  1. package/README.md +116 -0
  2. package/bin/agw.ts +5 -0
  3. package/package.json +59 -0
  4. package/src/agents/base-adapter.ts +113 -0
  5. package/src/agents/claude-adapter.ts +29 -0
  6. package/src/agents/codex-adapter.ts +29 -0
  7. package/src/agents/gemini-adapter.ts +29 -0
  8. package/src/cli/commands/agents.ts +55 -0
  9. package/src/cli/commands/combo.ts +130 -0
  10. package/src/cli/commands/costs.ts +33 -0
  11. package/src/cli/commands/daemon.ts +110 -0
  12. package/src/cli/commands/history.ts +29 -0
  13. package/src/cli/commands/run.ts +59 -0
  14. package/src/cli/commands/status.ts +29 -0
  15. package/src/cli/commands/workflow.ts +73 -0
  16. package/src/cli/error-handler.ts +8 -0
  17. package/src/cli/http-client.ts +68 -0
  18. package/src/cli/index.ts +28 -0
  19. package/src/config.ts +68 -0
  20. package/src/daemon/middleware/auth.ts +35 -0
  21. package/src/daemon/middleware/rate-limiter.ts +63 -0
  22. package/src/daemon/middleware/tenant.ts +64 -0
  23. package/src/daemon/middleware/workspace.ts +40 -0
  24. package/src/daemon/routes/agents.ts +13 -0
  25. package/src/daemon/routes/combos.ts +103 -0
  26. package/src/daemon/routes/costs.ts +9 -0
  27. package/src/daemon/routes/health.ts +62 -0
  28. package/src/daemon/routes/memory.ts +32 -0
  29. package/src/daemon/routes/tasks.ts +157 -0
  30. package/src/daemon/routes/ui.ts +18 -0
  31. package/src/daemon/routes/workflows.ts +73 -0
  32. package/src/daemon/server.ts +91 -0
  33. package/src/daemon/services/agent-learning.ts +77 -0
  34. package/src/daemon/services/agent-manager.ts +71 -0
  35. package/src/daemon/services/auto-scaler.ts +77 -0
  36. package/src/daemon/services/circuit-breaker.ts +95 -0
  37. package/src/daemon/services/combo-executor.ts +300 -0
  38. package/src/daemon/services/dag-executor.ts +136 -0
  39. package/src/daemon/services/metrics.ts +35 -0
  40. package/src/daemon/services/stream-aggregator.ts +64 -0
  41. package/src/daemon/services/task-executor.ts +184 -0
  42. package/src/daemon/services/task-queue.ts +75 -0
  43. package/src/daemon/services/workflow-executor.ts +150 -0
  44. package/src/daemon/services/ws-manager.ts +90 -0
  45. package/src/dsl/parser.ts +124 -0
  46. package/src/plugins/plugin-loader.ts +72 -0
  47. package/src/router/keyword-router.ts +63 -0
  48. package/src/router/llm-router.ts +93 -0
  49. package/src/store/agent-repo.ts +57 -0
  50. package/src/store/audit-repo.ts +25 -0
  51. package/src/store/combo-repo.ts +99 -0
  52. package/src/store/cost-repo.ts +55 -0
  53. package/src/store/db.ts +137 -0
  54. package/src/store/memory-repo.ts +46 -0
  55. package/src/store/task-repo.ts +127 -0
  56. package/src/store/workflow-repo.ts +69 -0
  57. package/src/types.ts +208 -0
  58. package/tsconfig.json +17 -0
  59. package/ui/index.html +272 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * AGW DSL — a simple domain-specific language for agent orchestration.
3
+ *
4
+ * Syntax:
5
+ * claude: "analyze {{input}}"
6
+ * | codex: "implement {{prev}}"
7
+ * | claude: "review {{prev}}"
8
+ *
9
+ * [claude: "perspective A {{input}}", codex: "perspective B {{input}}"]
10
+ * | claude: "synthesize {{all}}"
11
+ *
12
+ * {codex: "implement {{input}}", claude: "review {{prev}}"} x3
13
+ *
14
+ * Operators:
15
+ * | = pipeline (sequential, output flows)
16
+ * [] = parallel (map phase)
17
+ * {} = review loop
18
+ * xN = max iterations for review loop
19
+ */
20
+
21
+ export interface DslStep {
22
+ agent: string;
23
+ prompt: string;
24
+ }
25
+
26
+ export interface DslProgram {
27
+ pattern: 'pipeline' | 'map-reduce' | 'review-loop';
28
+ steps: DslStep[];
29
+ maxIterations?: number;
30
+ }
31
+
32
+ export function parseDsl(source: string): DslProgram {
33
+ const trimmed = source.trim();
34
+
35
+ // Review loop: {agent1: "...", agent2: "..."} xN
36
+ const loopMatch = trimmed.match(/^\{(.+)\}\s*(?:x(\d+))?$/s);
37
+ if (loopMatch) {
38
+ const steps = parseStepList(loopMatch[1]);
39
+ if (steps.length < 2) throw new Error('Review loop requires at least 2 steps');
40
+ return {
41
+ pattern: 'review-loop',
42
+ steps,
43
+ maxIterations: loopMatch[2] ? parseInt(loopMatch[2], 10) : 3,
44
+ };
45
+ }
46
+
47
+ // Check for parallel blocks: [...] | agent: "..."
48
+ if (trimmed.includes('[') && trimmed.includes(']')) {
49
+ const parts = splitPipeline(trimmed);
50
+ const steps: DslStep[] = [];
51
+
52
+ for (const part of parts) {
53
+ const p = part.trim();
54
+ if (p.startsWith('[') && p.endsWith(']')) {
55
+ const inner = p.slice(1, -1);
56
+ steps.push(...parseStepList(inner));
57
+ } else {
58
+ steps.push(parseStep(p));
59
+ }
60
+ }
61
+
62
+ return { pattern: 'map-reduce', steps };
63
+ }
64
+
65
+ // Pipeline: agent1: "..." | agent2: "..."
66
+ if (trimmed.includes('|')) {
67
+ const parts = splitPipeline(trimmed);
68
+ const steps = parts.map(p => parseStep(p.trim()));
69
+ return { pattern: 'pipeline', steps };
70
+ }
71
+
72
+ // Single step
73
+ return { pattern: 'pipeline', steps: [parseStep(trimmed)] };
74
+ }
75
+
76
+ function parseStep(text: string): DslStep {
77
+ const match = text.match(/^(\w+)\s*:\s*"(.+)"$/s);
78
+ if (!match) throw new Error(`Invalid step syntax: ${text.slice(0, 50)}`);
79
+ return { agent: match[1], prompt: match[2] };
80
+ }
81
+
82
+ function parseStepList(text: string): DslStep[] {
83
+ // Split by comma, but respect quoted strings
84
+ const steps: DslStep[] = [];
85
+ let current = '';
86
+ let inQuote = false;
87
+
88
+ for (const char of text) {
89
+ if (char === '"') inQuote = !inQuote;
90
+ if (char === ',' && !inQuote) {
91
+ if (current.trim()) steps.push(parseStep(current.trim()));
92
+ current = '';
93
+ } else {
94
+ current += char;
95
+ }
96
+ }
97
+ if (current.trim()) steps.push(parseStep(current.trim()));
98
+
99
+ return steps;
100
+ }
101
+
102
+ function splitPipeline(text: string): string[] {
103
+ const parts: string[] = [];
104
+ let current = '';
105
+ let depth = 0;
106
+ let inQuote = false;
107
+
108
+ for (const char of text) {
109
+ if (char === '"') inQuote = !inQuote;
110
+ if (!inQuote) {
111
+ if (char === '[') depth++;
112
+ if (char === ']') depth--;
113
+ if (char === '|' && depth === 0) {
114
+ parts.push(current);
115
+ current = '';
116
+ continue;
117
+ }
118
+ }
119
+ current += char;
120
+ }
121
+ if (current.trim()) parts.push(current);
122
+
123
+ return parts;
124
+ }
@@ -0,0 +1,72 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ export interface AgentPlugin {
6
+ type: 'agent';
7
+ id: string;
8
+ name: string;
9
+ command: string;
10
+ args: string[];
11
+ healthCheckCommand: string;
12
+ useStdin?: boolean;
13
+ strengths?: string[]; // keywords for routing
14
+ }
15
+
16
+ export interface ComboPlugin {
17
+ type: 'combo';
18
+ id: string;
19
+ name: string;
20
+ description: string;
21
+ pattern: 'pipeline' | 'map-reduce' | 'review-loop' | 'debate';
22
+ steps: { agent: string; prompt: string; role?: string }[];
23
+ maxIterations?: number;
24
+ }
25
+
26
+ export interface RouterPlugin {
27
+ type: 'router';
28
+ id: string;
29
+ name: string;
30
+ keywords: Record<string, string[]>; // agentId → keywords
31
+ }
32
+
33
+ export type Plugin = AgentPlugin | ComboPlugin | RouterPlugin;
34
+
35
+ export interface PluginManifest {
36
+ plugins: Plugin[];
37
+ }
38
+
39
+ const PLUGIN_DIR = path.join(os.homedir(), '.agw', 'plugins');
40
+
41
+ export function loadPlugins(): Plugin[] {
42
+ const plugins: Plugin[] = [];
43
+
44
+ if (!fs.existsSync(PLUGIN_DIR)) return plugins;
45
+
46
+ const files = fs.readdirSync(PLUGIN_DIR).filter(f => f.endsWith('.json'));
47
+ for (const file of files) {
48
+ try {
49
+ const raw = fs.readFileSync(path.join(PLUGIN_DIR, file), 'utf-8');
50
+ const manifest = JSON.parse(raw) as PluginManifest;
51
+ if (Array.isArray(manifest.plugins)) {
52
+ plugins.push(...manifest.plugins);
53
+ }
54
+ } catch {
55
+ // Skip malformed plugin files
56
+ }
57
+ }
58
+
59
+ return plugins;
60
+ }
61
+
62
+ export function getAgentPlugins(plugins: Plugin[]): AgentPlugin[] {
63
+ return plugins.filter((p): p is AgentPlugin => p.type === 'agent');
64
+ }
65
+
66
+ export function getComboPlugins(plugins: Plugin[]): ComboPlugin[] {
67
+ return plugins.filter((p): p is ComboPlugin => p.type === 'combo');
68
+ }
69
+
70
+ export function getRouterPlugins(plugins: Plugin[]): RouterPlugin[] {
71
+ return plugins.filter((p): p is RouterPlugin => p.type === 'router');
72
+ }
@@ -0,0 +1,63 @@
1
+ import type { RouteDecision } from '../types.js';
2
+
3
+ interface KeywordRule {
4
+ patterns: RegExp[];
5
+ agentId: string;
6
+ }
7
+
8
+ const RULES: KeywordRule[] = [
9
+ {
10
+ agentId: 'claude',
11
+ patterns: [
12
+ /重構|refactor/i,
13
+ /架構|architect/i,
14
+ /review|審查|code review/i,
15
+ /分析|analy[sz]/i,
16
+ /explain|解釋/i,
17
+ /complex|複雜/i,
18
+ ],
19
+ },
20
+ {
21
+ agentId: 'codex',
22
+ patterns: [
23
+ /rename|重命名/i,
24
+ /file|檔案/i,
25
+ /script|腳本/i,
26
+ /run|執行|bash|shell/i,
27
+ /quick|快速/i,
28
+ /install|安裝/i,
29
+ ],
30
+ },
31
+ {
32
+ agentId: 'gemini',
33
+ patterns: [
34
+ /research|研究/i,
35
+ /search|搜尋/i,
36
+ /summarize|摘要/i,
37
+ /compare|比較/i,
38
+ /explore|探索/i,
39
+ ],
40
+ },
41
+ ];
42
+
43
+ export function keywordRoute(prompt: string, availableAgentIds: string[]): RouteDecision {
44
+ for (const rule of RULES) {
45
+ if (!availableAgentIds.includes(rule.agentId)) continue;
46
+ if (rule.patterns.some(p => p.test(prompt))) {
47
+ return {
48
+ agentId: rule.agentId,
49
+ reason: `Keyword match for ${rule.agentId}`,
50
+ confidence: 0.3,
51
+ };
52
+ }
53
+ }
54
+
55
+ // Default: pick first available, prefer claude
56
+ const preferred = ['claude', 'codex', 'gemini'];
57
+ const fallback = preferred.find(id => availableAgentIds.includes(id)) ?? availableAgentIds[0];
58
+ return {
59
+ agentId: fallback,
60
+ reason: 'No keyword match, using default agent',
61
+ confidence: 0.3,
62
+ };
63
+ }
@@ -0,0 +1,93 @@
1
+ import type { AgentDescriptor, RouteDecision } from '../types.js';
2
+ import { keywordRoute } from './keyword-router.js';
3
+
4
+ type CreateMessageFn = (params: {
5
+ model: string;
6
+ max_tokens: number;
7
+ system: string;
8
+ messages: Array<{ role: string; content: string }>;
9
+ }) => Promise<{ content: Array<{ type: string; text: string }> }>;
10
+
11
+ const AGENT_DESCRIPTIONS: Record<string, string> = {
12
+ claude: 'Large codebase understanding, structural refactoring, complex reasoning, long context',
13
+ codex: 'Local terminal-intensive development, fast iteration, file operations',
14
+ gemini: 'Open-ended research, multimodal understanding, broad tool integration',
15
+ };
16
+
17
+ export class LlmRouter {
18
+ constructor(
19
+ private apiKey: string,
20
+ private model: string,
21
+ private createMessage?: CreateMessageFn,
22
+ ) {}
23
+
24
+ async route(
25
+ prompt: string,
26
+ availableAgents: AgentDescriptor[],
27
+ preferredAgent?: string,
28
+ ): Promise<RouteDecision> {
29
+ // Override: skip LLM entirely
30
+ if (preferredAgent && availableAgents.some(a => a.id === preferredAgent)) {
31
+ return { agentId: preferredAgent, reason: 'User override', confidence: 1.0 };
32
+ }
33
+
34
+ const agentIds = availableAgents.map(a => a.id);
35
+
36
+ try {
37
+ return await this.callLlm(prompt, availableAgents, agentIds);
38
+ } catch {
39
+ return keywordRoute(prompt, agentIds);
40
+ }
41
+ }
42
+
43
+ private async callLlm(
44
+ prompt: string,
45
+ availableAgents: AgentDescriptor[],
46
+ agentIds: string[],
47
+ ): Promise<RouteDecision> {
48
+ const agentList = availableAgents
49
+ .map(a => `- ${a.id}: ${AGENT_DESCRIPTIONS[a.id] ?? 'General purpose agent'}`)
50
+ .join('\n');
51
+
52
+ const systemPrompt = `You are a task router. Given a task description, select the best agent from the available list.
53
+
54
+ Available agents:
55
+ ${agentList}
56
+
57
+ Return ONLY valid JSON: { "agentId": "...", "reason": "...", "confidence": 0.0-1.0 }`;
58
+
59
+ const createFn = this.createMessage ?? this.getDefaultCreateFn();
60
+
61
+ const response = await createFn({
62
+ model: this.model,
63
+ max_tokens: 200,
64
+ system: systemPrompt,
65
+ messages: [{ role: 'user', content: prompt }],
66
+ });
67
+
68
+ const text = response.content[0]?.text ?? '';
69
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
70
+ if (!jsonMatch) {
71
+ throw new Error('No JSON in LLM response');
72
+ }
73
+
74
+ const parsed = JSON.parse(jsonMatch[0]) as RouteDecision;
75
+
76
+ // Post-validation
77
+ if (!agentIds.includes(parsed.agentId)) {
78
+ return keywordRoute(prompt, agentIds);
79
+ }
80
+
81
+ return parsed;
82
+ }
83
+
84
+ private getDefaultCreateFn(): CreateMessageFn {
85
+ // Lazy import to avoid requiring the SDK at test time
86
+ return async (params) => {
87
+ const { default: Anthropic } = await import('@anthropic-ai/sdk');
88
+ const client = new Anthropic({ apiKey: this.apiKey });
89
+ const response = await client.messages.create(params as Parameters<typeof client.messages.create>[0]);
90
+ return response as unknown as { content: Array<{ type: string; text: string }> };
91
+ };
92
+ }
93
+ }
@@ -0,0 +1,57 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { AgentDescriptor } from '../types.js';
3
+
4
+ interface AgentRow {
5
+ id: string;
6
+ name: string;
7
+ command: string;
8
+ args: string;
9
+ health_check_command: string;
10
+ enabled: number;
11
+ available: number;
12
+ last_health_check: string | null;
13
+ }
14
+
15
+ function rowToAgent(row: AgentRow): AgentDescriptor {
16
+ return {
17
+ id: row.id,
18
+ name: row.name,
19
+ command: row.command,
20
+ args: JSON.parse(row.args),
21
+ healthCheckCommand: row.health_check_command,
22
+ enabled: row.enabled === 1,
23
+ available: row.available === 1,
24
+ lastHealthCheck: row.last_health_check ?? undefined,
25
+ };
26
+ }
27
+
28
+ export class AgentRepo {
29
+ constructor(private db: Database.Database) {}
30
+
31
+ listAll(): AgentDescriptor[] {
32
+ const rows = this.db.prepare('SELECT * FROM agents ORDER BY id').all() as AgentRow[];
33
+ return rows.map(rowToAgent);
34
+ }
35
+
36
+ listAvailable(): AgentDescriptor[] {
37
+ const rows = this.db.prepare(
38
+ 'SELECT * FROM agents WHERE enabled = 1 AND available = 1 ORDER BY id'
39
+ ).all() as AgentRow[];
40
+ return rows.map(rowToAgent);
41
+ }
42
+
43
+ getById(id: string): AgentDescriptor | undefined {
44
+ const row = this.db.prepare('SELECT * FROM agents WHERE id = ?').get(id) as AgentRow | undefined;
45
+ return row ? rowToAgent(row) : undefined;
46
+ }
47
+
48
+ setAvailability(id: string, available: boolean): void {
49
+ this.db.prepare(
50
+ 'UPDATE agents SET available = ?, last_health_check = ? WHERE id = ?'
51
+ ).run(available ? 1 : 0, new Date().toISOString(), id);
52
+ }
53
+
54
+ setEnabled(id: string, enabled: boolean): void {
55
+ this.db.prepare('UPDATE agents SET enabled = ? WHERE id = ?').run(enabled ? 1 : 0, id);
56
+ }
57
+ }
@@ -0,0 +1,25 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { AuditEventType, AuditEntry } from '../types.js';
3
+
4
+ export class AuditRepo {
5
+ constructor(private db: Database.Database) {}
6
+
7
+ log(taskId: string | null, eventType: AuditEventType, payload: Record<string, unknown>): void {
8
+ this.db.prepare(
9
+ 'INSERT INTO audit_log (task_id, event_type, payload, created_at) VALUES (?, ?, ?, ?)'
10
+ ).run(taskId, eventType, JSON.stringify(payload), new Date().toISOString());
11
+ }
12
+
13
+ getByTaskId(taskId: string): AuditEntry[] {
14
+ const rows = this.db.prepare(
15
+ 'SELECT * FROM audit_log WHERE task_id = ? ORDER BY id ASC'
16
+ ).all(taskId) as Array<{ id: number; task_id: string; event_type: string; payload: string; created_at: string }>;
17
+ return rows.map(r => ({
18
+ id: r.id,
19
+ taskId: r.task_id,
20
+ eventType: r.event_type as AuditEventType,
21
+ payload: JSON.parse(r.payload),
22
+ createdAt: r.created_at,
23
+ }));
24
+ }
25
+ }
@@ -0,0 +1,99 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { ComboDescriptor, ComboStatus, ComboStep, ComboPattern } from '../types.js';
3
+
4
+ interface ComboRow {
5
+ combo_id: string;
6
+ name: string;
7
+ pattern: string;
8
+ steps: string;
9
+ input: string;
10
+ status: string;
11
+ task_ids: string;
12
+ step_results: string;
13
+ final_output: string | null;
14
+ max_iterations: number;
15
+ iterations: number;
16
+ working_directory: string | null;
17
+ priority: number;
18
+ created_at: string;
19
+ completed_at: string | null;
20
+ }
21
+
22
+ function rowToCombo(row: ComboRow): ComboDescriptor {
23
+ return {
24
+ comboId: row.combo_id,
25
+ name: row.name,
26
+ pattern: row.pattern as ComboPattern,
27
+ steps: JSON.parse(row.steps) as ComboStep[],
28
+ input: row.input,
29
+ status: row.status as ComboStatus,
30
+ taskIds: JSON.parse(row.task_ids) as string[],
31
+ stepResults: JSON.parse(row.step_results) as Record<number, string>,
32
+ finalOutput: row.final_output ?? undefined,
33
+ maxIterations: row.max_iterations,
34
+ iterations: row.iterations,
35
+ createdAt: row.created_at,
36
+ completedAt: row.completed_at ?? undefined,
37
+ };
38
+ }
39
+
40
+ export class ComboRepo {
41
+ constructor(private db: Database.Database) {}
42
+
43
+ create(combo: {
44
+ comboId: string;
45
+ name: string;
46
+ pattern: ComboPattern;
47
+ steps: ComboStep[];
48
+ input: string;
49
+ status: ComboStatus;
50
+ maxIterations?: number;
51
+ workingDirectory?: string;
52
+ priority?: number;
53
+ createdAt: string;
54
+ }): void {
55
+ this.db.prepare(
56
+ `INSERT INTO combos (combo_id, name, pattern, steps, input, status, max_iterations, working_directory, priority, created_at)
57
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
58
+ ).run(combo.comboId, combo.name, combo.pattern, JSON.stringify(combo.steps),
59
+ combo.input, combo.status, combo.maxIterations ?? 3,
60
+ combo.workingDirectory ?? null, combo.priority ?? 3, combo.createdAt);
61
+ }
62
+
63
+ getById(comboId: string): ComboDescriptor | undefined {
64
+ const row = this.db.prepare('SELECT * FROM combos WHERE combo_id = ?').get(comboId) as ComboRow | undefined;
65
+ return row ? rowToCombo(row) : undefined;
66
+ }
67
+
68
+ updateStatus(comboId: string, status: ComboStatus): void {
69
+ const completedAt = status === 'completed' || status === 'failed' ? new Date().toISOString() : null;
70
+ this.db.prepare('UPDATE combos SET status = ?, completed_at = ? WHERE combo_id = ?').run(status, completedAt, comboId);
71
+ }
72
+
73
+ addTaskId(comboId: string, taskId: string): void {
74
+ this.db.prepare(
75
+ `UPDATE combos SET task_ids = json_insert(task_ids, '$[#]', ?) WHERE combo_id = ?`
76
+ ).run(taskId, comboId);
77
+ }
78
+
79
+ setStepResult(comboId: string, stepIndex: number, output: string): void {
80
+ this.db.prepare(
81
+ `UPDATE combos SET step_results = json_set(step_results, '$.' || ?, ?) WHERE combo_id = ?`
82
+ ).run(String(stepIndex), output, comboId);
83
+ }
84
+
85
+ setFinalOutput(comboId: string, output: string): void {
86
+ this.db.prepare('UPDATE combos SET final_output = ? WHERE combo_id = ?').run(output, comboId);
87
+ }
88
+
89
+ incrementIterations(comboId: string): void {
90
+ this.db.prepare('UPDATE combos SET iterations = iterations + 1 WHERE combo_id = ?').run(comboId);
91
+ }
92
+
93
+ list(limit: number = 20, offset: number = 0): ComboDescriptor[] {
94
+ const rows = this.db.prepare(
95
+ 'SELECT * FROM combos ORDER BY created_at DESC LIMIT ? OFFSET ?'
96
+ ).all(limit, offset) as ComboRow[];
97
+ return rows.map(rowToCombo);
98
+ }
99
+ }
@@ -0,0 +1,55 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { CostSummary } from '../types.js';
3
+
4
+ export class CostRepo {
5
+ constructor(private db: Database.Database) {}
6
+
7
+ record(taskId: string, agentId: string, cost: number, tokens: number): void {
8
+ this.db.prepare(
9
+ `INSERT INTO cost_records (task_id, agent_id, cost, tokens, recorded_at) VALUES (?, ?, ?, ?, ?)`
10
+ ).run(taskId, agentId, cost, tokens, new Date().toISOString());
11
+ }
12
+
13
+ getDailyCost(): number {
14
+ const today = new Date().toISOString().slice(0, 10);
15
+ const row = this.db.prepare(
16
+ `SELECT COALESCE(SUM(cost), 0) as total FROM cost_records WHERE recorded_at >= ?`
17
+ ).get(`${today}T00:00:00.000Z`) as { total: number };
18
+ return row.total;
19
+ }
20
+
21
+ getMonthlyCost(): number {
22
+ const monthStart = new Date().toISOString().slice(0, 7) + '-01';
23
+ const row = this.db.prepare(
24
+ `SELECT COALESCE(SUM(cost), 0) as total FROM cost_records WHERE recorded_at >= ?`
25
+ ).get(`${monthStart}T00:00:00.000Z`) as { total: number };
26
+ return row.total;
27
+ }
28
+
29
+ getAllTimeCost(): number {
30
+ const row = this.db.prepare(
31
+ `SELECT COALESCE(SUM(cost), 0) as total FROM cost_records`
32
+ ).get() as { total: number };
33
+ return row.total;
34
+ }
35
+
36
+ getCostByAgent(): Record<string, number> {
37
+ const rows = this.db.prepare(
38
+ `SELECT agent_id, COALESCE(SUM(cost), 0) as total FROM cost_records GROUP BY agent_id`
39
+ ).all() as { agent_id: string; total: number }[];
40
+ const result: Record<string, number> = {};
41
+ for (const row of rows) result[row.agent_id] = row.total;
42
+ return result;
43
+ }
44
+
45
+ getSummary(dailyLimit?: number, monthlyLimit?: number): CostSummary {
46
+ return {
47
+ daily: this.getDailyCost(),
48
+ monthly: this.getMonthlyCost(),
49
+ allTime: this.getAllTimeCost(),
50
+ byAgent: this.getCostByAgent(),
51
+ dailyLimit,
52
+ monthlyLimit,
53
+ };
54
+ }
55
+ }