@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,13 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { AgentManager } from '../services/agent-manager.js';
3
+
4
+ export function registerAgentRoutes(app: FastifyInstance, agentManager: AgentManager): void {
5
+ app.get('/agents', async () => {
6
+ return agentManager.listAgents();
7
+ });
8
+
9
+ app.post<{ Params: { id: string } }>('/agents/:id/health', async (request, reply) => {
10
+ const available = await agentManager.checkAgent(request.params.id);
11
+ return { id: request.params.id, available };
12
+ });
13
+ }
@@ -0,0 +1,103 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { ComboExecutor } from '../services/combo-executor.js';
3
+ import type { AppConfig, CreateComboRequest } from '../../types.js';
4
+ import { validateWorkspace } from '../middleware/workspace.js';
5
+
6
+ export function registerComboRoutes(
7
+ app: FastifyInstance,
8
+ comboExecutor: ComboExecutor,
9
+ config: AppConfig,
10
+ ): void {
11
+ const createComboSchema = {
12
+ body: {
13
+ type: 'object',
14
+ required: ['name', 'pattern', 'steps', 'input'],
15
+ properties: {
16
+ name: { type: 'string', minLength: 1, maxLength: 200 },
17
+ pattern: { type: 'string', enum: ['pipeline', 'map-reduce', 'review-loop', 'debate'] },
18
+ steps: {
19
+ type: 'array',
20
+ minItems: 2,
21
+ maxItems: config.maxWorkflowSteps,
22
+ items: {
23
+ type: 'object',
24
+ required: ['agent', 'prompt'],
25
+ properties: {
26
+ agent: { type: 'string' },
27
+ prompt: { type: 'string', minLength: 1, maxLength: config.maxPromptLength },
28
+ role: { type: 'string' },
29
+ },
30
+ additionalProperties: false,
31
+ },
32
+ },
33
+ input: { type: 'string', minLength: 1, maxLength: config.maxPromptLength },
34
+ workingDirectory: { type: 'string' },
35
+ priority: { type: 'integer', minimum: 1, maximum: 5, default: 3 },
36
+ maxIterations: { type: 'integer', minimum: 1, maximum: 10, default: 3 },
37
+ },
38
+ additionalProperties: false,
39
+ },
40
+ };
41
+
42
+ // Create and start a combo
43
+ app.post('/combos', { schema: createComboSchema }, async (request, reply) => {
44
+ const body = request.body as CreateComboRequest;
45
+ if (body.workingDirectory) {
46
+ try {
47
+ body.workingDirectory = validateWorkspace(body.workingDirectory, config.allowedWorkspaces);
48
+ } catch (err) {
49
+ return reply.status(400).send({ error: (err as Error).message });
50
+ }
51
+ }
52
+ const comboId = comboExecutor.start(body);
53
+ const combo = comboExecutor.getCombo(comboId);
54
+ return reply.status(202).send(combo);
55
+ });
56
+
57
+ // Start a combo from a preset
58
+ app.post<{ Params: { presetId: string }; Body: { input: string; workingDirectory?: string; priority?: number } }>(
59
+ '/combos/preset/:presetId',
60
+ async (request, reply) => {
61
+ const preset = comboExecutor.getPresets().find(p => p.id === request.params.presetId);
62
+ if (!preset) {
63
+ return reply.status(404).send({ error: `Preset not found: ${request.params.presetId}` });
64
+ }
65
+
66
+ const { input, workingDirectory, priority } = request.body;
67
+ if (!input || typeof input !== 'string') {
68
+ return reply.status(400).send({ error: 'input is required' });
69
+ }
70
+
71
+ const comboId = comboExecutor.start({
72
+ name: preset.name,
73
+ pattern: preset.pattern,
74
+ steps: preset.steps,
75
+ input,
76
+ workingDirectory,
77
+ priority,
78
+ maxIterations: preset.maxIterations,
79
+ });
80
+ const combo = comboExecutor.getCombo(comboId);
81
+ return reply.status(202).send(combo);
82
+ },
83
+ );
84
+
85
+ // List presets
86
+ app.get('/combos/presets', async () => {
87
+ return comboExecutor.getPresets();
88
+ });
89
+
90
+ // Get combo by ID
91
+ app.get<{ Params: { id: string } }>('/combos/:id', async (request, reply) => {
92
+ const combo = comboExecutor.getCombo(request.params.id);
93
+ if (!combo) return reply.status(404).send({ error: 'Combo not found' });
94
+ return combo;
95
+ });
96
+
97
+ // List combos
98
+ app.get<{ Querystring: { limit?: string; offset?: string } }>('/combos', async (request) => {
99
+ const limit = parseInt(request.query.limit ?? '20', 10);
100
+ const offset = parseInt(request.query.offset ?? '0', 10);
101
+ return comboExecutor.listCombos(limit, offset);
102
+ });
103
+ }
@@ -0,0 +1,9 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { CostRepo } from '../../store/cost-repo.js';
3
+ import type { AppConfig } from '../../types.js';
4
+
5
+ export function registerCostRoutes(app: FastifyInstance, costRepo: CostRepo, config: AppConfig): void {
6
+ app.get('/costs', async () => {
7
+ return costRepo.getSummary(config.dailyCostLimit, config.monthlyCostLimit);
8
+ });
9
+ }
@@ -0,0 +1,62 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { MetricsCollector } from '../services/metrics.js';
3
+ import type { AgentManager } from '../services/agent-manager.js';
4
+ import type { CircuitBreakerRegistry } from '../services/circuit-breaker.js';
5
+ import type { TaskRepo } from '../../store/task-repo.js';
6
+ import type { CostRepo } from '../../store/cost-repo.js';
7
+ import type { AppConfig } from '../../types.js';
8
+
9
+ export function registerHealthRoutes(
10
+ app: FastifyInstance,
11
+ metrics: MetricsCollector,
12
+ agentManager: AgentManager,
13
+ cbRegistry: CircuitBreakerRegistry,
14
+ taskRepo: TaskRepo,
15
+ costRepo: CostRepo | null,
16
+ config: AppConfig,
17
+ ): void {
18
+ app.get('/health', async () => {
19
+ return { status: 'ok', uptime: metrics.getUptime(), version: config.version ?? '1.4.0' };
20
+ });
21
+
22
+ app.get('/health/ready', async (_request, reply) => {
23
+ const agents = agentManager.getAvailableAgents();
24
+ if (agents.length === 0) {
25
+ return reply.status(503).send({ status: 'not_ready', reason: 'No agents available' });
26
+ }
27
+ return { status: 'ready', availableAgents: agents.length };
28
+ });
29
+
30
+ app.get('/metrics', async () => {
31
+ const agents = agentManager.listAgents();
32
+ const counts = taskRepo.countByStatus();
33
+ const total = Object.values(counts).reduce((a, b) => a + b, 0);
34
+ const perf = metrics.getPerformance();
35
+ const mem = metrics.getMemory();
36
+
37
+ return {
38
+ uptime: metrics.getUptime(),
39
+ tasks: {
40
+ total,
41
+ completed: counts.completed ?? 0,
42
+ failed: counts.failed ?? 0,
43
+ running: counts.running ?? 0,
44
+ pending: counts.pending ?? 0,
45
+ },
46
+ agents: {
47
+ total: agents.length,
48
+ available: agents.filter(a => a.available).length,
49
+ list: agents.map(a => ({ id: a.id, available: a.available })),
50
+ },
51
+ circuitBreakers: cbRegistry.getAll().map(cb => cb.toJSON()),
52
+ costs: costRepo ? { daily: costRepo.getDailyCost(), monthly: costRepo.getMonthlyCost() } : null,
53
+ performance: perf,
54
+ memory: { heapMB: Math.round(mem.heapUsed / 1048576), rssMB: Math.round(mem.rss / 1048576) },
55
+ limits: {
56
+ dailyCostLimit: config.dailyCostLimit,
57
+ monthlyCostLimit: config.monthlyCostLimit,
58
+ maxConcurrencyPerAgent: config.maxConcurrencyPerAgent,
59
+ },
60
+ };
61
+ });
62
+ }
@@ -0,0 +1,32 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { MemoryRepo } from '../../store/memory-repo.js';
3
+
4
+ export function registerMemoryRoutes(app: FastifyInstance, memoryRepo: MemoryRepo): void {
5
+ app.get('/memory', async (request) => {
6
+ const { scope, q } = request.query as { scope?: string; q?: string };
7
+ if (q) return memoryRepo.search(q);
8
+ if (scope) return memoryRepo.getByScope(scope);
9
+ return memoryRepo.list();
10
+ });
11
+
12
+ app.get<{ Params: { key: string } }>('/memory/:key', async (request, reply) => {
13
+ const value = memoryRepo.get(request.params.key);
14
+ if (value === undefined) return reply.status(404).send({ error: 'Key not found' });
15
+ return { key: request.params.key, value };
16
+ });
17
+
18
+ app.put<{ Params: { key: string }; Body: { value: string; scope?: string } }>(
19
+ '/memory/:key',
20
+ async (request) => {
21
+ const { value, scope } = request.body;
22
+ memoryRepo.set(request.params.key, value, scope);
23
+ return { key: request.params.key, value, scope: scope ?? 'global' };
24
+ },
25
+ );
26
+
27
+ app.delete<{ Params: { key: string } }>('/memory/:key', async (request, reply) => {
28
+ const deleted = memoryRepo.delete(request.params.key);
29
+ if (!deleted) return reply.status(404).send({ error: 'Key not found' });
30
+ return { deleted: true };
31
+ });
32
+ }
@@ -0,0 +1,157 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { TaskExecutor } from '../services/task-executor.js';
3
+ import type { LlmRouter } from '../../router/llm-router.js';
4
+ import type { AgentManager } from '../services/agent-manager.js';
5
+ import type { AppConfig } from '../../types.js';
6
+ import { validateWorkspace } from '../middleware/workspace.js';
7
+
8
+ export function registerTaskRoutes(
9
+ app: FastifyInstance,
10
+ executor: TaskExecutor,
11
+ router: LlmRouter,
12
+ agentManager: AgentManager,
13
+ config: AppConfig,
14
+ ): void {
15
+ const createTaskSchema = {
16
+ body: {
17
+ type: 'object',
18
+ required: ['prompt'],
19
+ properties: {
20
+ prompt: { type: 'string', minLength: 1, maxLength: config.maxPromptLength },
21
+ preferredAgent: { type: 'string' },
22
+ workingDirectory: { type: 'string' },
23
+ priority: { type: 'integer', minimum: 1, maximum: 5, default: 3 },
24
+ },
25
+ additionalProperties: false,
26
+ },
27
+ };
28
+
29
+ app.post<{ Body: { prompt: string; preferredAgent?: string; workingDirectory?: string; priority?: number } }>(
30
+ '/tasks',
31
+ { schema: createTaskSchema },
32
+ async (request, reply) => {
33
+ const { prompt, preferredAgent, priority } = request.body;
34
+
35
+ // Validate workspace (H2: path traversal protection)
36
+ let workingDirectory: string;
37
+ try {
38
+ workingDirectory = validateWorkspace(request.body.workingDirectory, config.allowedWorkspaces);
39
+ } catch (err) {
40
+ return reply.status(400).send({ error: (err as Error).message });
41
+ }
42
+
43
+ const availableAgents = agentManager.getAvailableAgents();
44
+ if (availableAgents.length === 0 && !preferredAgent) {
45
+ return reply.status(503).send({ error: 'No agents available. Check CLI installations.' });
46
+ }
47
+
48
+ let lowConfidence = false;
49
+ const task = await executor.execute(
50
+ { prompt, preferredAgent, workingDirectory, priority },
51
+ async (p) => {
52
+ const decision = await router.route(p, availableAgents, preferredAgent);
53
+ if (decision.confidence < 0.5) lowConfidence = true;
54
+ return decision;
55
+ },
56
+ );
57
+
58
+ return reply.status(201).send({ ...task, lowConfidence });
59
+ },
60
+ );
61
+
62
+ app.get<{ Params: { id: string } }>('/tasks/:id', async (request, reply) => {
63
+ const task = executor.getTask(request.params.id);
64
+ if (!task) return reply.status(404).send({ error: 'Task not found' });
65
+ return task;
66
+ });
67
+
68
+ // H4: SSE with task existence check, completed-task replay, idle timeout
69
+ app.get<{ Params: { id: string } }>('/tasks/:id/stream', async (request, reply) => {
70
+ const taskId = request.params.id;
71
+
72
+ // Check task exists
73
+ const existing = executor.getTask(taskId);
74
+ if (!existing) {
75
+ return reply.status(404).send({ error: 'Task not found' });
76
+ }
77
+
78
+ // If already completed/failed, replay result immediately
79
+ if (existing.status === 'completed' || existing.status === 'failed') {
80
+ reply.raw.writeHead(200, {
81
+ 'Content-Type': 'text/event-stream',
82
+ 'Cache-Control': 'no-cache',
83
+ Connection: 'keep-alive',
84
+ });
85
+ reply.raw.write(`event: status\ndata: ${JSON.stringify({ status: existing.status })}\n\n`);
86
+ if (existing.result) {
87
+ reply.raw.write(`event: done\ndata: ${JSON.stringify(existing.result)}\n\n`);
88
+ }
89
+ reply.raw.end();
90
+ return;
91
+ }
92
+
93
+ reply.raw.writeHead(200, {
94
+ 'Content-Type': 'text/event-stream',
95
+ 'Cache-Control': 'no-cache',
96
+ Connection: 'keep-alive',
97
+ });
98
+
99
+ const sendEvent = (event: string, data: unknown) => {
100
+ reply.raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
101
+ };
102
+
103
+ // Idle timeout — close stream if no events after 5 minutes
104
+ let idleTimer = setTimeout(() => {
105
+ sendEvent('timeout', { reason: 'idle timeout' });
106
+ cleanup();
107
+ reply.raw.end();
108
+ }, 300_000);
109
+
110
+ const resetIdle = () => {
111
+ clearTimeout(idleTimer);
112
+ idleTimer = setTimeout(() => {
113
+ sendEvent('timeout', { reason: 'idle timeout' });
114
+ cleanup();
115
+ reply.raw.end();
116
+ }, 300_000);
117
+ };
118
+
119
+ const onStatus = (id: string, info: unknown) => {
120
+ if (id === taskId) { sendEvent('status', info); resetIdle(); }
121
+ };
122
+ const onStdout = (id: string, chunk: string) => {
123
+ if (id === taskId) { sendEvent('stdout', { chunk }); resetIdle(); }
124
+ };
125
+ const onStderr = (id: string, chunk: string) => {
126
+ if (id === taskId) { sendEvent('stderr', { chunk }); resetIdle(); }
127
+ };
128
+ const onDone = (id: string, result: unknown) => {
129
+ if (id === taskId) {
130
+ sendEvent('done', result);
131
+ cleanup();
132
+ reply.raw.end();
133
+ }
134
+ };
135
+
136
+ const cleanup = () => {
137
+ clearTimeout(idleTimer);
138
+ executor.removeListener('task:status', onStatus);
139
+ executor.removeListener('task:stdout', onStdout);
140
+ executor.removeListener('task:stderr', onStderr);
141
+ executor.removeListener('task:done', onDone);
142
+ };
143
+
144
+ executor.on('task:status', onStatus);
145
+ executor.on('task:stdout', onStdout);
146
+ executor.on('task:stderr', onStderr);
147
+ executor.on('task:done', onDone);
148
+
149
+ request.raw.on('close', cleanup);
150
+ });
151
+
152
+ app.get<{ Querystring: { limit?: string; offset?: string } }>('/tasks', async (request) => {
153
+ const limit = parseInt(request.query.limit ?? '20', 10);
154
+ const offset = parseInt(request.query.offset ?? '0', 10);
155
+ return executor.listTasks(limit, offset);
156
+ });
157
+ }
@@ -0,0 +1,18 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const UI_DIR = path.join(__dirname, '..', '..', '..', 'ui');
8
+
9
+ export default async function uiPlugin(app: FastifyInstance): Promise<void> {
10
+ app.get('/ui', async (_request, reply) => {
11
+ const htmlPath = path.join(UI_DIR, 'index.html');
12
+ if (!fs.existsSync(htmlPath)) {
13
+ return reply.status(404).send({ error: 'Web UI not found' });
14
+ }
15
+ const html = fs.readFileSync(htmlPath, 'utf-8');
16
+ return reply.type('text/html').send(html);
17
+ });
18
+ }
@@ -0,0 +1,73 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { WorkflowExecutor } from '../services/workflow-executor.js';
3
+ import type { AppConfig } from '../../types.js';
4
+ import { validateWorkspace } from '../middleware/workspace.js';
5
+
6
+ export function registerWorkflowRoutes(
7
+ app: FastifyInstance,
8
+ workflowExecutor: WorkflowExecutor,
9
+ config: AppConfig,
10
+ ): void {
11
+ const createWorkflowSchema = {
12
+ body: {
13
+ type: 'object',
14
+ required: ['name', 'steps'],
15
+ properties: {
16
+ name: { type: 'string', minLength: 1, maxLength: 200 },
17
+ steps: {
18
+ type: 'array',
19
+ minItems: 1,
20
+ maxItems: config.maxWorkflowSteps,
21
+ items: {
22
+ type: 'object',
23
+ required: ['prompt'],
24
+ properties: {
25
+ prompt: { type: 'string', minLength: 1, maxLength: config.maxPromptLength },
26
+ preferredAgent: { type: 'string' },
27
+ },
28
+ additionalProperties: false,
29
+ },
30
+ },
31
+ mode: { type: 'string', enum: ['sequential', 'parallel'], default: 'sequential' },
32
+ workingDirectory: { type: 'string' },
33
+ priority: { type: 'integer', minimum: 1, maximum: 5, default: 3 },
34
+ },
35
+ additionalProperties: false,
36
+ },
37
+ };
38
+
39
+ app.post('/workflows', { schema: createWorkflowSchema }, async (request, reply) => {
40
+ const body = request.body as {
41
+ name: string;
42
+ steps: { prompt: string; preferredAgent?: string }[];
43
+ mode?: 'sequential' | 'parallel';
44
+ workingDirectory?: string;
45
+ priority?: number;
46
+ };
47
+
48
+ // Validate workspace
49
+ if (body.workingDirectory) {
50
+ try {
51
+ body.workingDirectory = validateWorkspace(body.workingDirectory, config.allowedWorkspaces);
52
+ } catch (err) {
53
+ return reply.status(400).send({ error: (err as Error).message });
54
+ }
55
+ }
56
+
57
+ const workflowId = workflowExecutor.start(body);
58
+ const wf = workflowExecutor.getWorkflow(workflowId);
59
+ return reply.status(202).send(wf);
60
+ });
61
+
62
+ app.get<{ Params: { id: string } }>('/workflows/:id', async (request, reply) => {
63
+ const wf = workflowExecutor.getWorkflow(request.params.id);
64
+ if (!wf) return reply.status(404).send({ error: 'Workflow not found' });
65
+ return wf;
66
+ });
67
+
68
+ app.get<{ Querystring: { limit?: string; offset?: string } }>('/workflows', async (request) => {
69
+ const limit = parseInt(request.query.limit ?? '20', 10);
70
+ const offset = parseInt(request.query.offset ?? '0', 10);
71
+ return workflowExecutor.listWorkflows(limit, offset);
72
+ });
73
+ }
@@ -0,0 +1,91 @@
1
+ import Fastify from 'fastify';
2
+ import type { FastifyInstance } from 'fastify';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { loadConfig } from '../config.js';
6
+ import { createDatabase } from '../store/db.js';
7
+ import { TaskRepo } from '../store/task-repo.js';
8
+ import { AgentRepo } from '../store/agent-repo.js';
9
+ import { AuditRepo } from '../store/audit-repo.js';
10
+ import { CostRepo } from '../store/cost-repo.js';
11
+ import { WorkflowRepo } from '../store/workflow-repo.js';
12
+ import { ComboRepo } from '../store/combo-repo.js';
13
+ import { AgentManager } from './services/agent-manager.js';
14
+ import { TaskExecutor } from './services/task-executor.js';
15
+ import { WorkflowExecutor } from './services/workflow-executor.js';
16
+ import { ComboExecutor } from './services/combo-executor.js';
17
+ import { LlmRouter } from '../router/llm-router.js';
18
+ import { registerAuthMiddleware } from './middleware/auth.js';
19
+ import { registerAgentRoutes } from './routes/agents.js';
20
+ import { registerTaskRoutes } from './routes/tasks.js';
21
+ import { registerWorkflowRoutes } from './routes/workflows.js';
22
+ import { registerCostRoutes } from './routes/costs.js';
23
+ import { registerComboRoutes } from './routes/combos.js';
24
+ import { MemoryRepo } from '../store/memory-repo.js';
25
+ import { registerMemoryRoutes } from './routes/memory.js';
26
+ import { registerHealthRoutes } from './routes/health.js';
27
+ import { MetricsCollector } from './services/metrics.js';
28
+ import { CircuitBreakerRegistry } from './services/circuit-breaker.js';
29
+
30
+ interface ServerOptions {
31
+ dbPath?: string;
32
+ configPath?: string;
33
+ }
34
+
35
+ export async function buildServer(options: ServerOptions = {}): Promise<FastifyInstance> {
36
+ const agwDir = path.join(os.homedir(), '.agw');
37
+ const configPath = options.configPath ?? path.join(agwDir, 'config.json');
38
+ const dbPath = options.dbPath ?? path.join(agwDir, 'agw.db');
39
+
40
+ const config = loadConfig(configPath);
41
+ const db = createDatabase(dbPath);
42
+ const taskRepo = new TaskRepo(db);
43
+ const agentRepo = new AgentRepo(db);
44
+ const auditRepo = new AuditRepo(db);
45
+ const costRepo = new CostRepo(db);
46
+ const workflowRepo = new WorkflowRepo(db);
47
+ const comboRepo = new ComboRepo(db);
48
+ const memoryRepo = new MemoryRepo(db);
49
+
50
+ const agentManager = new AgentManager(agentRepo, auditRepo, config);
51
+ const executor = new TaskExecutor(
52
+ taskRepo, auditRepo, agentManager, costRepo,
53
+ config.maxConcurrencyPerAgent,
54
+ config.dailyCostLimit, config.monthlyCostLimit, db,
55
+ );
56
+ const router = new LlmRouter(config.anthropicApiKey, config.routerModel);
57
+ const workflowExecutor = new WorkflowExecutor(workflowRepo, auditRepo, executor, router, agentManager);
58
+ const comboExecutor = new ComboExecutor(comboRepo, auditRepo, executor, agentManager);
59
+ const metrics = new MetricsCollector();
60
+ const cbRegistry = new CircuitBreakerRegistry();
61
+
62
+ const app = Fastify({
63
+ logger: false,
64
+ bodyLimit: 1_048_576,
65
+ });
66
+
67
+ registerAuthMiddleware(app, config.authToken);
68
+
69
+ registerAgentRoutes(app, agentManager);
70
+ registerTaskRoutes(app, executor, router, agentManager, config);
71
+ registerWorkflowRoutes(app, workflowExecutor, config);
72
+ registerCostRoutes(app, costRepo, config);
73
+ registerComboRoutes(app, comboExecutor, config);
74
+ registerMemoryRoutes(app, memoryRepo);
75
+ registerHealthRoutes(app, metrics, agentManager, cbRegistry, taskRepo, costRepo, config);
76
+
77
+ app.register(import('./routes/ui.js'));
78
+
79
+ agentManager.runHealthChecks().catch(() => {});
80
+
81
+ app.addHook('onClose', async () => {
82
+ const runningTasks = taskRepo.list(100, 0).filter(t => t.status === 'running');
83
+ for (const t of runningTasks) {
84
+ taskRepo.updateStatus(t.taskId, 'failed');
85
+ auditRepo.log(t.taskId, 'task.failed', { reason: 'daemon shutdown' });
86
+ }
87
+ db.close();
88
+ });
89
+
90
+ return app;
91
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Agent Learning — tracks which agent performs best for different task categories.
3
+ * Over time, routing decisions improve based on historical success rates.
4
+ */
5
+
6
+ export interface AgentScore {
7
+ agentId: string;
8
+ category: string;
9
+ successCount: number;
10
+ failCount: number;
11
+ avgDurationMs: number;
12
+ totalCost: number;
13
+ score: number; // calculated: success_rate * (1 / normalized_duration)
14
+ }
15
+
16
+ export class AgentLearning {
17
+ private scores = new Map<string, AgentScore>(); // key: `agentId:category`
18
+
19
+ record(agentId: string, category: string, success: boolean, durationMs: number, cost: number): void {
20
+ const key = `${agentId}:${category}`;
21
+ let score = this.scores.get(key);
22
+ if (!score) {
23
+ score = { agentId, category, successCount: 0, failCount: 0, avgDurationMs: 0, totalCost: 0, score: 0 };
24
+ this.scores.set(key, score);
25
+ }
26
+
27
+ if (success) score.successCount++;
28
+ else score.failCount++;
29
+
30
+ const total = score.successCount + score.failCount;
31
+ score.avgDurationMs = ((score.avgDurationMs * (total - 1)) + durationMs) / total;
32
+ score.totalCost += cost;
33
+ score.score = this.calculateScore(score);
34
+ }
35
+
36
+ private calculateScore(s: AgentScore): number {
37
+ const total = s.successCount + s.failCount;
38
+ if (total === 0) return 0;
39
+ const successRate = s.successCount / total;
40
+ const speedFactor = 1 / Math.max(1, s.avgDurationMs / 1000);
41
+ return Math.round(successRate * speedFactor * 1000) / 1000;
42
+ }
43
+
44
+ /** Get the best agent for a category based on historical performance */
45
+ getBestAgent(category: string): string | undefined {
46
+ const candidates = Array.from(this.scores.values())
47
+ .filter(s => s.category === category && (s.successCount + s.failCount) >= 3);
48
+
49
+ if (candidates.length === 0) return undefined;
50
+ candidates.sort((a, b) => b.score - a.score);
51
+ return candidates[0].agentId;
52
+ }
53
+
54
+ /** Get all scores for an agent */
55
+ getAgentScores(agentId: string): AgentScore[] {
56
+ return Array.from(this.scores.values()).filter(s => s.agentId === agentId);
57
+ }
58
+
59
+ /** Get all scores */
60
+ getAllScores(): AgentScore[] {
61
+ return Array.from(this.scores.values());
62
+ }
63
+
64
+ /** Categorize a prompt into a rough category */
65
+ static categorize(prompt: string): string {
66
+ const lower = prompt.toLowerCase();
67
+ if (/refactor|restructure|reorganize/.test(lower)) return 'refactoring';
68
+ if (/test|spec|coverage/.test(lower)) return 'testing';
69
+ if (/bug|fix|error|crash/.test(lower)) return 'debugging';
70
+ if (/review|audit|check/.test(lower)) return 'review';
71
+ if (/build|deploy|ci|cd/.test(lower)) return 'devops';
72
+ if (/explain|document|summarize/.test(lower)) return 'documentation';
73
+ if (/create|new|add|implement/.test(lower)) return 'implementation';
74
+ if (/research|compare|analyze/.test(lower)) return 'analysis';
75
+ return 'general';
76
+ }
77
+ }