@sooneocean/agw 1.4.0 → 1.6.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.
- package/package.json +1 -1
- package/src/daemon/routes/export-import.ts +54 -0
- package/src/daemon/routes/replay.ts +22 -0
- package/src/daemon/routes/scheduler.ts +38 -0
- package/src/daemon/routes/templates.ts +60 -0
- package/src/daemon/routes/webhooks.ts +18 -0
- package/src/daemon/server.ts +23 -3
- package/src/daemon/services/condition-engine.ts +75 -0
- package/src/daemon/services/export-import.ts +62 -0
- package/src/daemon/services/replay.ts +53 -0
- package/src/daemon/services/scheduler.ts +115 -0
- package/src/daemon/services/template-engine.ts +145 -0
- package/src/daemon/services/webhook-manager.ts +98 -0
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { TemplateEngine } from '../services/template-engine.js';
|
|
3
|
+
import type { WebhookManager } from '../services/webhook-manager.js';
|
|
4
|
+
import type { Scheduler } from '../services/scheduler.js';
|
|
5
|
+
import type { MemoryRepo } from '../../store/memory-repo.js';
|
|
6
|
+
import { COMBO_PRESETS } from '../services/combo-executor.js';
|
|
7
|
+
import { createExport, validateImport } from '../services/export-import.js';
|
|
8
|
+
|
|
9
|
+
export function registerExportImportRoutes(
|
|
10
|
+
app: FastifyInstance,
|
|
11
|
+
templateEngine: TemplateEngine,
|
|
12
|
+
webhookManager: WebhookManager,
|
|
13
|
+
scheduler: Scheduler,
|
|
14
|
+
memoryRepo: MemoryRepo,
|
|
15
|
+
): void {
|
|
16
|
+
app.get('/export', async () => {
|
|
17
|
+
return createExport({
|
|
18
|
+
templates: templateEngine.list(),
|
|
19
|
+
comboPresets: COMBO_PRESETS,
|
|
20
|
+
webhooks: webhookManager.getWebhooks(),
|
|
21
|
+
memory: memoryRepo.list(1000),
|
|
22
|
+
scheduledJobs: scheduler.listJobs(),
|
|
23
|
+
version: '1.6.0',
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
app.post('/import', async (request, reply) => {
|
|
28
|
+
const data = request.body;
|
|
29
|
+
if (!validateImport(data)) {
|
|
30
|
+
return reply.status(400).send({ error: 'Invalid import format' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let imported = { templates: 0, memory: 0, jobs: 0 };
|
|
34
|
+
|
|
35
|
+
for (const t of data.templates) {
|
|
36
|
+
templateEngine.register(t);
|
|
37
|
+
imported.templates++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const m of data.memory) {
|
|
41
|
+
memoryRepo.set(m.key, m.value, m.scope);
|
|
42
|
+
imported.memory++;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const j of data.scheduledJobs) {
|
|
46
|
+
try {
|
|
47
|
+
scheduler.addJob(j);
|
|
48
|
+
imported.jobs++;
|
|
49
|
+
} catch { /* skip invalid */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { imported };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { ReplayManager } from '../services/replay.js';
|
|
3
|
+
|
|
4
|
+
export function registerReplayRoutes(app: FastifyInstance, replayManager: ReplayManager): void {
|
|
5
|
+
app.post<{ Params: { id: string } }>('/tasks/:id/replay', async (request, reply) => {
|
|
6
|
+
try {
|
|
7
|
+
const task = await replayManager.replayTask(request.params.id);
|
|
8
|
+
return reply.status(201).send(task);
|
|
9
|
+
} catch (err) {
|
|
10
|
+
return reply.status(404).send({ error: (err as Error).message });
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
app.post<{ Params: { id: string } }>('/combos/:id/replay', async (request, reply) => {
|
|
15
|
+
try {
|
|
16
|
+
const comboId = replayManager.replayCombo(request.params.id);
|
|
17
|
+
return reply.status(202).send({ comboId, status: 'replaying' });
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return reply.status(404).send({ error: (err as Error).message });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { Scheduler } from '../services/scheduler.js';
|
|
3
|
+
|
|
4
|
+
export function registerSchedulerRoutes(app: FastifyInstance, scheduler: Scheduler): void {
|
|
5
|
+
app.get('/scheduler/jobs', async () => scheduler.listJobs());
|
|
6
|
+
|
|
7
|
+
app.get<{ Params: { id: string } }>('/scheduler/jobs/:id', async (request, reply) => {
|
|
8
|
+
const job = scheduler.getJob(request.params.id);
|
|
9
|
+
if (!job) return reply.status(404).send({ error: 'Job not found' });
|
|
10
|
+
return job;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
app.post('/scheduler/jobs', async (request, reply) => {
|
|
14
|
+
const body = request.body as any;
|
|
15
|
+
try {
|
|
16
|
+
const job = scheduler.addJob(body);
|
|
17
|
+
return reply.status(201).send(job);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return reply.status(400).send({ error: (err as Error).message });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
app.delete<{ Params: { id: string } }>('/scheduler/jobs/:id', async (request, reply) => {
|
|
24
|
+
const removed = scheduler.removeJob(request.params.id);
|
|
25
|
+
if (!removed) return reply.status(404).send({ error: 'Job not found' });
|
|
26
|
+
return { removed: true };
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
app.post<{ Params: { id: string } }>('/scheduler/jobs/:id/enable', async (request, reply) => {
|
|
30
|
+
if (!scheduler.enableJob(request.params.id)) return reply.status(404).send({ error: 'Job not found' });
|
|
31
|
+
return { enabled: true };
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
app.post<{ Params: { id: string } }>('/scheduler/jobs/:id/disable', async (request, reply) => {
|
|
35
|
+
if (!scheduler.disableJob(request.params.id)) return reply.status(404).send({ error: 'Job not found' });
|
|
36
|
+
return { disabled: true };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { TemplateEngine, TaskTemplate, InstantiateRequest } from '../services/template-engine.js';
|
|
3
|
+
import type { TaskExecutor } from '../services/task-executor.js';
|
|
4
|
+
import type { LlmRouter } from '../../router/llm-router.js';
|
|
5
|
+
import type { AgentManager } from '../services/agent-manager.js';
|
|
6
|
+
|
|
7
|
+
export function registerTemplateRoutes(
|
|
8
|
+
app: FastifyInstance,
|
|
9
|
+
templateEngine: TemplateEngine,
|
|
10
|
+
executor: TaskExecutor,
|
|
11
|
+
router: LlmRouter,
|
|
12
|
+
agentManager: AgentManager,
|
|
13
|
+
): void {
|
|
14
|
+
// List templates
|
|
15
|
+
app.get<{ Querystring: { tag?: string } }>('/templates', async (request) => {
|
|
16
|
+
return templateEngine.list(request.query.tag);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Get template
|
|
20
|
+
app.get<{ Params: { id: string } }>('/templates/:id', async (request, reply) => {
|
|
21
|
+
const t = templateEngine.get(request.params.id);
|
|
22
|
+
if (!t) return reply.status(404).send({ error: 'Template not found' });
|
|
23
|
+
return t;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Register custom template
|
|
27
|
+
app.post<{ Body: TaskTemplate }>('/templates', async (request, reply) => {
|
|
28
|
+
templateEngine.register(request.body);
|
|
29
|
+
return reply.status(201).send(request.body);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Delete template
|
|
33
|
+
app.delete<{ Params: { id: string } }>('/templates/:id', async (request, reply) => {
|
|
34
|
+
const deleted = templateEngine.unregister(request.params.id);
|
|
35
|
+
if (!deleted) return reply.status(404).send({ error: 'Template not found' });
|
|
36
|
+
return { deleted: true };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Instantiate and execute a template
|
|
40
|
+
app.post<{ Body: InstantiateRequest }>('/templates/execute', async (request, reply) => {
|
|
41
|
+
try {
|
|
42
|
+
const { prompt, agent, priority } = templateEngine.instantiate(request.body);
|
|
43
|
+
const availableAgents = agentManager.getAvailableAgents();
|
|
44
|
+
|
|
45
|
+
const task = await executor.execute(
|
|
46
|
+
{
|
|
47
|
+
prompt,
|
|
48
|
+
preferredAgent: agent ?? request.body.overrides?.agent,
|
|
49
|
+
workingDirectory: request.body.overrides?.workingDirectory,
|
|
50
|
+
priority,
|
|
51
|
+
},
|
|
52
|
+
async (p) => router.route(p, availableAgents, agent),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return reply.status(201).send(task);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return reply.status(400).send({ error: (err as Error).message });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { WebhookManager, WebhookConfig } from '../services/webhook-manager.js';
|
|
3
|
+
|
|
4
|
+
export function registerWebhookRoutes(app: FastifyInstance, webhookManager: WebhookManager): void {
|
|
5
|
+
app.get('/webhooks', async () => {
|
|
6
|
+
return webhookManager.getWebhooks();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
app.post<{ Body: WebhookConfig }>('/webhooks', async (request, reply) => {
|
|
10
|
+
webhookManager.addWebhook(request.body);
|
|
11
|
+
return reply.status(201).send({ registered: true, url: request.body.url });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
app.delete<{ Body: { url: string } }>('/webhooks', async (request) => {
|
|
15
|
+
webhookManager.removeWebhook(request.body.url);
|
|
16
|
+
return { removed: true };
|
|
17
|
+
});
|
|
18
|
+
}
|
package/src/daemon/server.ts
CHANGED
|
@@ -10,22 +10,31 @@ import { AuditRepo } from '../store/audit-repo.js';
|
|
|
10
10
|
import { CostRepo } from '../store/cost-repo.js';
|
|
11
11
|
import { WorkflowRepo } from '../store/workflow-repo.js';
|
|
12
12
|
import { ComboRepo } from '../store/combo-repo.js';
|
|
13
|
+
import { MemoryRepo } from '../store/memory-repo.js';
|
|
13
14
|
import { AgentManager } from './services/agent-manager.js';
|
|
14
15
|
import { TaskExecutor } from './services/task-executor.js';
|
|
15
16
|
import { WorkflowExecutor } from './services/workflow-executor.js';
|
|
16
17
|
import { ComboExecutor } from './services/combo-executor.js';
|
|
17
18
|
import { LlmRouter } from '../router/llm-router.js';
|
|
19
|
+
import { MetricsCollector } from './services/metrics.js';
|
|
20
|
+
import { CircuitBreakerRegistry } from './services/circuit-breaker.js';
|
|
21
|
+
import { TemplateEngine } from './services/template-engine.js';
|
|
22
|
+
import { WebhookManager } from './services/webhook-manager.js';
|
|
18
23
|
import { registerAuthMiddleware } from './middleware/auth.js';
|
|
19
24
|
import { registerAgentRoutes } from './routes/agents.js';
|
|
20
25
|
import { registerTaskRoutes } from './routes/tasks.js';
|
|
21
26
|
import { registerWorkflowRoutes } from './routes/workflows.js';
|
|
22
27
|
import { registerCostRoutes } from './routes/costs.js';
|
|
23
28
|
import { registerComboRoutes } from './routes/combos.js';
|
|
24
|
-
import { MemoryRepo } from '../store/memory-repo.js';
|
|
25
29
|
import { registerMemoryRoutes } from './routes/memory.js';
|
|
26
30
|
import { registerHealthRoutes } from './routes/health.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
31
|
+
import { registerTemplateRoutes } from './routes/templates.js';
|
|
32
|
+
import { registerWebhookRoutes } from './routes/webhooks.js';
|
|
33
|
+
import { Scheduler } from './services/scheduler.js';
|
|
34
|
+
import { ReplayManager } from './services/replay.js';
|
|
35
|
+
import { registerSchedulerRoutes } from './routes/scheduler.js';
|
|
36
|
+
import { registerReplayRoutes } from './routes/replay.js';
|
|
37
|
+
import { registerExportImportRoutes } from './routes/export-import.js';
|
|
29
38
|
|
|
30
39
|
interface ServerOptions {
|
|
31
40
|
dbPath?: string;
|
|
@@ -58,6 +67,11 @@ export async function buildServer(options: ServerOptions = {}): Promise<FastifyI
|
|
|
58
67
|
const comboExecutor = new ComboExecutor(comboRepo, auditRepo, executor, agentManager);
|
|
59
68
|
const metrics = new MetricsCollector();
|
|
60
69
|
const cbRegistry = new CircuitBreakerRegistry();
|
|
70
|
+
const templateEngine = new TemplateEngine();
|
|
71
|
+
templateEngine.seedDefaults();
|
|
72
|
+
const webhookManager = new WebhookManager();
|
|
73
|
+
const scheduler = new Scheduler();
|
|
74
|
+
const replayManager = new ReplayManager(taskRepo, comboRepo, executor, comboExecutor, router, agentManager);
|
|
61
75
|
|
|
62
76
|
const app = Fastify({
|
|
63
77
|
logger: false,
|
|
@@ -73,6 +87,11 @@ export async function buildServer(options: ServerOptions = {}): Promise<FastifyI
|
|
|
73
87
|
registerComboRoutes(app, comboExecutor, config);
|
|
74
88
|
registerMemoryRoutes(app, memoryRepo);
|
|
75
89
|
registerHealthRoutes(app, metrics, agentManager, cbRegistry, taskRepo, costRepo, config);
|
|
90
|
+
registerTemplateRoutes(app, templateEngine, executor, router, agentManager);
|
|
91
|
+
registerWebhookRoutes(app, webhookManager);
|
|
92
|
+
registerSchedulerRoutes(app, scheduler);
|
|
93
|
+
registerReplayRoutes(app, replayManager);
|
|
94
|
+
registerExportImportRoutes(app, templateEngine, webhookManager, scheduler, memoryRepo);
|
|
76
95
|
|
|
77
96
|
app.register(import('./routes/ui.js'));
|
|
78
97
|
|
|
@@ -84,6 +103,7 @@ export async function buildServer(options: ServerOptions = {}): Promise<FastifyI
|
|
|
84
103
|
taskRepo.updateStatus(t.taskId, 'failed');
|
|
85
104
|
auditRepo.log(t.taskId, 'task.failed', { reason: 'daemon shutdown' });
|
|
86
105
|
}
|
|
106
|
+
scheduler.stopAll();
|
|
87
107
|
db.close();
|
|
88
108
|
});
|
|
89
109
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Condition Engine — evaluates conditions on agent output to control combo flow.
|
|
3
|
+
*
|
|
4
|
+
* Conditions:
|
|
5
|
+
* contains:"APPROVED" — output contains string
|
|
6
|
+
* !contains:"ERROR" — output does NOT contain string
|
|
7
|
+
* exitCode:0 — task exit code equals value
|
|
8
|
+
* length>100 — output length greater than threshold
|
|
9
|
+
* matches:/pattern/i — output matches regex
|
|
10
|
+
* always — always true (default)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface Condition {
|
|
14
|
+
type: 'contains' | 'not-contains' | 'exitCode' | 'length-gt' | 'length-lt' | 'matches' | 'always';
|
|
15
|
+
value?: string | number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ConditionalBranch {
|
|
19
|
+
condition: Condition;
|
|
20
|
+
thenStep: number; // step index to jump to
|
|
21
|
+
elseStep?: number; // step index if condition is false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseCondition(expr: string): Condition {
|
|
25
|
+
if (expr === 'always') return { type: 'always' };
|
|
26
|
+
|
|
27
|
+
const containsMatch = expr.match(/^contains:"(.+)"$/);
|
|
28
|
+
if (containsMatch) return { type: 'contains', value: containsMatch[1] };
|
|
29
|
+
|
|
30
|
+
const notContainsMatch = expr.match(/^!contains:"(.+)"$/);
|
|
31
|
+
if (notContainsMatch) return { type: 'not-contains', value: notContainsMatch[1] };
|
|
32
|
+
|
|
33
|
+
const exitCodeMatch = expr.match(/^exitCode:(\d+)$/);
|
|
34
|
+
if (exitCodeMatch) return { type: 'exitCode', value: parseInt(exitCodeMatch[1], 10) };
|
|
35
|
+
|
|
36
|
+
const lengthGtMatch = expr.match(/^length>(\d+)$/);
|
|
37
|
+
if (lengthGtMatch) return { type: 'length-gt', value: parseInt(lengthGtMatch[1], 10) };
|
|
38
|
+
|
|
39
|
+
const lengthLtMatch = expr.match(/^length<(\d+)$/);
|
|
40
|
+
if (lengthLtMatch) return { type: 'length-lt', value: parseInt(lengthLtMatch[1], 10) };
|
|
41
|
+
|
|
42
|
+
const matchesMatch = expr.match(/^matches:\/(.+)\/([gimsuy]*)$/);
|
|
43
|
+
if (matchesMatch) return { type: 'matches', value: matchesMatch[1] + (matchesMatch[2] ? `/${matchesMatch[2]}` : '') };
|
|
44
|
+
|
|
45
|
+
throw new Error(`Invalid condition: ${expr}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function evaluateCondition(condition: Condition, output: string, exitCode: number = 0): boolean {
|
|
49
|
+
switch (condition.type) {
|
|
50
|
+
case 'always':
|
|
51
|
+
return true;
|
|
52
|
+
|
|
53
|
+
case 'contains':
|
|
54
|
+
return output.includes(String(condition.value));
|
|
55
|
+
|
|
56
|
+
case 'not-contains':
|
|
57
|
+
return !output.includes(String(condition.value));
|
|
58
|
+
|
|
59
|
+
case 'exitCode':
|
|
60
|
+
return exitCode === condition.value;
|
|
61
|
+
|
|
62
|
+
case 'length-gt':
|
|
63
|
+
return output.length > (condition.value as number);
|
|
64
|
+
|
|
65
|
+
case 'length-lt':
|
|
66
|
+
return output.length < (condition.value as number);
|
|
67
|
+
|
|
68
|
+
case 'matches': {
|
|
69
|
+
const parts = String(condition.value).split('/');
|
|
70
|
+
const pattern = parts[0];
|
|
71
|
+
const flags = parts[1] ?? '';
|
|
72
|
+
return new RegExp(pattern, flags).test(output);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export/Import — serialize AGW configuration and data for sharing/backup.
|
|
3
|
+
*
|
|
4
|
+
* Exports: templates, combo presets, webhooks, memory entries, scheduled jobs.
|
|
5
|
+
* Does NOT export: tasks, audit logs, cost records (runtime data).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TaskTemplate } from './template-engine.js';
|
|
9
|
+
import type { WebhookConfig } from './webhook-manager.js';
|
|
10
|
+
import type { ScheduledJob } from './scheduler.js';
|
|
11
|
+
import type { MemoryEntry } from '../../store/memory-repo.js';
|
|
12
|
+
import type { ComboPreset } from '../../types.js';
|
|
13
|
+
|
|
14
|
+
export interface AgwExport {
|
|
15
|
+
version: string;
|
|
16
|
+
exportedAt: string;
|
|
17
|
+
templates: TaskTemplate[];
|
|
18
|
+
comboPresets: ComboPreset[];
|
|
19
|
+
webhooks: WebhookConfig[];
|
|
20
|
+
memory: MemoryEntry[];
|
|
21
|
+
scheduledJobs: Omit<ScheduledJob, 'id' | 'intervalMs' | 'nextRun' | 'runCount' | 'lastRun'>[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createExport(data: {
|
|
25
|
+
templates: TaskTemplate[];
|
|
26
|
+
comboPresets: ComboPreset[];
|
|
27
|
+
webhooks: WebhookConfig[];
|
|
28
|
+
memory: MemoryEntry[];
|
|
29
|
+
scheduledJobs: ScheduledJob[];
|
|
30
|
+
version: string;
|
|
31
|
+
}): AgwExport {
|
|
32
|
+
return {
|
|
33
|
+
version: data.version,
|
|
34
|
+
exportedAt: new Date().toISOString(),
|
|
35
|
+
templates: data.templates,
|
|
36
|
+
comboPresets: data.comboPresets,
|
|
37
|
+
webhooks: data.webhooks.map(w => ({ ...w, secret: undefined })), // Strip secrets
|
|
38
|
+
memory: data.memory,
|
|
39
|
+
scheduledJobs: data.scheduledJobs.map(j => ({
|
|
40
|
+
name: j.name,
|
|
41
|
+
type: j.type,
|
|
42
|
+
target: j.target,
|
|
43
|
+
params: j.params,
|
|
44
|
+
interval: j.interval,
|
|
45
|
+
agent: j.agent,
|
|
46
|
+
priority: j.priority,
|
|
47
|
+
workingDirectory: j.workingDirectory,
|
|
48
|
+
enabled: j.enabled,
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validateImport(data: unknown): data is AgwExport {
|
|
54
|
+
if (!data || typeof data !== 'object') return false;
|
|
55
|
+
const d = data as Record<string, unknown>;
|
|
56
|
+
return (
|
|
57
|
+
typeof d.version === 'string' &&
|
|
58
|
+
typeof d.exportedAt === 'string' &&
|
|
59
|
+
Array.isArray(d.templates) &&
|
|
60
|
+
Array.isArray(d.memory)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay — re-run a completed/failed task or combo with the same parameters.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TaskDescriptor, ComboDescriptor, CreateComboRequest } from '../../types.js';
|
|
6
|
+
import type { TaskExecutor } from './task-executor.js';
|
|
7
|
+
import type { ComboExecutor } from './combo-executor.js';
|
|
8
|
+
import type { TaskRepo } from '../../store/task-repo.js';
|
|
9
|
+
import type { ComboRepo } from '../../store/combo-repo.js';
|
|
10
|
+
import type { LlmRouter } from '../../router/llm-router.js';
|
|
11
|
+
import type { AgentManager } from './agent-manager.js';
|
|
12
|
+
|
|
13
|
+
export class ReplayManager {
|
|
14
|
+
constructor(
|
|
15
|
+
private taskRepo: TaskRepo,
|
|
16
|
+
private comboRepo: ComboRepo,
|
|
17
|
+
private taskExecutor: TaskExecutor,
|
|
18
|
+
private comboExecutor: ComboExecutor,
|
|
19
|
+
private router: LlmRouter,
|
|
20
|
+
private agentManager: AgentManager,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
async replayTask(taskId: string): Promise<TaskDescriptor> {
|
|
24
|
+
const original = this.taskRepo.getById(taskId);
|
|
25
|
+
if (!original) throw new Error(`Task ${taskId} not found`);
|
|
26
|
+
|
|
27
|
+
const availableAgents = this.agentManager.getAvailableAgents();
|
|
28
|
+
return this.taskExecutor.execute(
|
|
29
|
+
{
|
|
30
|
+
prompt: original.prompt,
|
|
31
|
+
preferredAgent: original.assignedAgent,
|
|
32
|
+
workingDirectory: original.workingDirectory,
|
|
33
|
+
priority: original.priority,
|
|
34
|
+
},
|
|
35
|
+
async (p) => this.router.route(p, availableAgents, original.assignedAgent),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
replayCombo(comboId: string): string {
|
|
40
|
+
const original = this.comboRepo.getById(comboId);
|
|
41
|
+
if (!original) throw new Error(`Combo ${comboId} not found`);
|
|
42
|
+
|
|
43
|
+
const request: CreateComboRequest = {
|
|
44
|
+
name: `${original.name} (replay)`,
|
|
45
|
+
pattern: original.pattern,
|
|
46
|
+
steps: original.steps,
|
|
47
|
+
input: original.input,
|
|
48
|
+
maxIterations: original.maxIterations,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return this.comboExecutor.start(request);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler — run tasks, combos, or templates on cron-like intervals.
|
|
3
|
+
*
|
|
4
|
+
* Supports: interval-based scheduling (every N minutes/hours).
|
|
5
|
+
* Cron expressions are parsed as simple patterns: "every 5m", "every 1h", "every 30s".
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { nanoid } from 'nanoid';
|
|
9
|
+
import { EventEmitter } from 'node:events';
|
|
10
|
+
|
|
11
|
+
export interface ScheduledJob {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
type: 'task' | 'combo-preset' | 'template';
|
|
15
|
+
target: string; // prompt for task, presetId for combo, templateId for template
|
|
16
|
+
params?: Record<string, string>; // for templates
|
|
17
|
+
interval: string; // "every 5m", "every 1h"
|
|
18
|
+
intervalMs: number; // parsed interval in ms
|
|
19
|
+
agent?: string;
|
|
20
|
+
priority?: number;
|
|
21
|
+
workingDirectory?: string;
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
lastRun?: string;
|
|
24
|
+
nextRun: string;
|
|
25
|
+
runCount: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseInterval(expr: string): number {
|
|
29
|
+
const match = expr.match(/^every\s+(\d+)\s*(s|m|h|d)$/i);
|
|
30
|
+
if (!match) throw new Error(`Invalid interval: "${expr}". Use "every Ns/m/h/d"`);
|
|
31
|
+
|
|
32
|
+
const value = parseInt(match[1], 10);
|
|
33
|
+
const unit = match[2].toLowerCase();
|
|
34
|
+
const multipliers: Record<string, number> = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
35
|
+
return value * multipliers[unit];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class Scheduler extends EventEmitter {
|
|
39
|
+
private jobs = new Map<string, ScheduledJob>();
|
|
40
|
+
private timers = new Map<string, NodeJS.Timeout>();
|
|
41
|
+
|
|
42
|
+
addJob(job: Omit<ScheduledJob, 'id' | 'intervalMs' | 'nextRun' | 'runCount'>): ScheduledJob {
|
|
43
|
+
const id = nanoid(8);
|
|
44
|
+
const intervalMs = parseInterval(job.interval);
|
|
45
|
+
const scheduled: ScheduledJob = {
|
|
46
|
+
...job,
|
|
47
|
+
id,
|
|
48
|
+
intervalMs,
|
|
49
|
+
nextRun: new Date(Date.now() + intervalMs).toISOString(),
|
|
50
|
+
runCount: 0,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
this.jobs.set(id, scheduled);
|
|
54
|
+
|
|
55
|
+
if (scheduled.enabled) {
|
|
56
|
+
this.startTimer(scheduled);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return scheduled;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
removeJob(id: string): boolean {
|
|
63
|
+
const timer = this.timers.get(id);
|
|
64
|
+
if (timer) { clearInterval(timer); this.timers.delete(id); }
|
|
65
|
+
return this.jobs.delete(id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
enableJob(id: string): boolean {
|
|
69
|
+
const job = this.jobs.get(id);
|
|
70
|
+
if (!job) return false;
|
|
71
|
+
job.enabled = true;
|
|
72
|
+
this.startTimer(job);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
disableJob(id: string): boolean {
|
|
77
|
+
const job = this.jobs.get(id);
|
|
78
|
+
if (!job) return false;
|
|
79
|
+
job.enabled = false;
|
|
80
|
+
const timer = this.timers.get(id);
|
|
81
|
+
if (timer) { clearInterval(timer); this.timers.delete(id); }
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getJob(id: string): ScheduledJob | undefined {
|
|
86
|
+
return this.jobs.get(id);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
listJobs(): ScheduledJob[] {
|
|
90
|
+
return Array.from(this.jobs.values());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private startTimer(job: ScheduledJob): void {
|
|
94
|
+
const existing = this.timers.get(job.id);
|
|
95
|
+
if (existing) clearInterval(existing);
|
|
96
|
+
|
|
97
|
+
const timer = setInterval(() => {
|
|
98
|
+
job.runCount++;
|
|
99
|
+
job.lastRun = new Date().toISOString();
|
|
100
|
+
job.nextRun = new Date(Date.now() + job.intervalMs).toISOString();
|
|
101
|
+
this.emit('job:trigger', job);
|
|
102
|
+
}, job.intervalMs);
|
|
103
|
+
|
|
104
|
+
// Don't keep process alive just for scheduling
|
|
105
|
+
timer.unref();
|
|
106
|
+
this.timers.set(job.id, timer);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
stopAll(): void {
|
|
110
|
+
for (const [id, timer] of this.timers) {
|
|
111
|
+
clearInterval(timer);
|
|
112
|
+
}
|
|
113
|
+
this.timers.clear();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Template Engine — reusable parameterized task definitions.
|
|
3
|
+
*
|
|
4
|
+
* Templates use {{param.name}} syntax for user-supplied values.
|
|
5
|
+
* Templates can be stored, listed, and instantiated.
|
|
6
|
+
*
|
|
7
|
+
* Example:
|
|
8
|
+
* { id: "code-review", prompt: "Review {{param.file}} for {{param.criteria}}", agent: "claude" }
|
|
9
|
+
* Instantiate with: { templateId: "code-review", params: { file: "auth.ts", criteria: "security" } }
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface TaskTemplate {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
prompt: string; // template with {{param.name}} placeholders
|
|
17
|
+
agent?: string; // default agent
|
|
18
|
+
priority?: number;
|
|
19
|
+
params: TemplateParam[]; // parameter definitions
|
|
20
|
+
tags?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TemplateParam {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
required: boolean;
|
|
27
|
+
default?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface InstantiateRequest {
|
|
31
|
+
templateId: string;
|
|
32
|
+
params: Record<string, string>;
|
|
33
|
+
overrides?: {
|
|
34
|
+
agent?: string;
|
|
35
|
+
priority?: number;
|
|
36
|
+
workingDirectory?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class TemplateEngine {
|
|
41
|
+
private templates = new Map<string, TaskTemplate>();
|
|
42
|
+
|
|
43
|
+
register(template: TaskTemplate): void {
|
|
44
|
+
this.templates.set(template.id, template);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
unregister(id: string): boolean {
|
|
48
|
+
return this.templates.delete(id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get(id: string): TaskTemplate | undefined {
|
|
52
|
+
return this.templates.get(id);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
list(tag?: string): TaskTemplate[] {
|
|
56
|
+
const all = Array.from(this.templates.values());
|
|
57
|
+
if (tag) return all.filter(t => t.tags?.includes(tag));
|
|
58
|
+
return all;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
instantiate(request: InstantiateRequest): { prompt: string; agent?: string; priority?: number } {
|
|
62
|
+
const template = this.templates.get(request.templateId);
|
|
63
|
+
if (!template) throw new Error(`Template not found: ${request.templateId}`);
|
|
64
|
+
|
|
65
|
+
// Validate required params
|
|
66
|
+
for (const param of template.params) {
|
|
67
|
+
if (param.required && !(request.params[param.name] ?? param.default)) {
|
|
68
|
+
throw new Error(`Missing required parameter: ${param.name}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Interpolate {{param.name}} with provided values
|
|
73
|
+
let prompt = template.prompt;
|
|
74
|
+
for (const param of template.params) {
|
|
75
|
+
const value = request.params[param.name] ?? param.default ?? '';
|
|
76
|
+
prompt = prompt.replace(new RegExp(`\\{\\{param\\.${param.name}\\}\\}`, 'g'), value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
prompt,
|
|
81
|
+
agent: request.overrides?.agent ?? template.agent,
|
|
82
|
+
priority: request.overrides?.priority ?? template.priority,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Seed built-in templates */
|
|
87
|
+
seedDefaults(): void {
|
|
88
|
+
this.register({
|
|
89
|
+
id: 'code-review',
|
|
90
|
+
name: 'Code Review',
|
|
91
|
+
description: 'Review a file for quality, security, and correctness',
|
|
92
|
+
prompt: 'Review {{param.file}} for {{param.criteria}}. Focus on actionable issues.',
|
|
93
|
+
agent: 'claude',
|
|
94
|
+
priority: 4,
|
|
95
|
+
params: [
|
|
96
|
+
{ name: 'file', description: 'File path to review', required: true },
|
|
97
|
+
{ name: 'criteria', description: 'Review criteria', required: false, default: 'quality, security, correctness' },
|
|
98
|
+
],
|
|
99
|
+
tags: ['review', 'quality'],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.register({
|
|
103
|
+
id: 'implement-feature',
|
|
104
|
+
name: 'Implement Feature',
|
|
105
|
+
description: 'Implement a new feature with tests',
|
|
106
|
+
prompt: 'Implement the following feature: {{param.description}}\n\nRequirements:\n{{param.requirements}}\n\nInclude unit tests.',
|
|
107
|
+
agent: 'codex',
|
|
108
|
+
priority: 3,
|
|
109
|
+
params: [
|
|
110
|
+
{ name: 'description', description: 'Feature description', required: true },
|
|
111
|
+
{ name: 'requirements', description: 'Detailed requirements', required: false, default: 'Follow existing patterns' },
|
|
112
|
+
],
|
|
113
|
+
tags: ['implementation', 'feature'],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
this.register({
|
|
117
|
+
id: 'explain-code',
|
|
118
|
+
name: 'Explain Code',
|
|
119
|
+
description: 'Explain what a piece of code does',
|
|
120
|
+
prompt: 'Explain this code in {{param.detail_level}} detail:\n\n{{param.code}}',
|
|
121
|
+
agent: 'claude',
|
|
122
|
+
priority: 2,
|
|
123
|
+
params: [
|
|
124
|
+
{ name: 'code', description: 'Code to explain', required: true },
|
|
125
|
+
{ name: 'detail_level', description: 'Detail level (brief/moderate/deep)', required: false, default: 'moderate' },
|
|
126
|
+
],
|
|
127
|
+
tags: ['explanation', 'documentation'],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
this.register({
|
|
131
|
+
id: 'debug-issue',
|
|
132
|
+
name: 'Debug Issue',
|
|
133
|
+
description: 'Diagnose and fix a bug',
|
|
134
|
+
prompt: 'Debug this issue: {{param.issue}}\n\nError message: {{param.error}}\n\nRelevant file: {{param.file}}',
|
|
135
|
+
agent: 'claude',
|
|
136
|
+
priority: 5,
|
|
137
|
+
params: [
|
|
138
|
+
{ name: 'issue', description: 'Description of the issue', required: true },
|
|
139
|
+
{ name: 'error', description: 'Error message or stack trace', required: false, default: 'N/A' },
|
|
140
|
+
{ name: 'file', description: 'Relevant file path', required: false, default: 'N/A' },
|
|
141
|
+
],
|
|
142
|
+
tags: ['debugging', 'fix'],
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Manager — sends HTTP POST notifications on AGW events.
|
|
3
|
+
*
|
|
4
|
+
* Config example:
|
|
5
|
+
* webhooks: [
|
|
6
|
+
* { url: "https://slack.com/...", events: ["task.completed", "combo.completed"], secret: "hmac-key" }
|
|
7
|
+
* ]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHmac } from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
export interface WebhookConfig {
|
|
13
|
+
url: string;
|
|
14
|
+
events: string[]; // event types to subscribe to, or ["*"] for all
|
|
15
|
+
secret?: string; // HMAC-SHA256 signing key
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
retries?: number;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WebhookPayload {
|
|
22
|
+
event: string;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
data: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class WebhookManager {
|
|
28
|
+
private hooks: WebhookConfig[] = [];
|
|
29
|
+
|
|
30
|
+
addWebhook(config: WebhookConfig): void {
|
|
31
|
+
this.hooks.push(config);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
removeWebhook(url: string): void {
|
|
35
|
+
this.hooks = this.hooks.filter(h => h.url !== url);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getWebhooks(): WebhookConfig[] {
|
|
39
|
+
return this.hooks.map(h => ({ ...h, secret: h.secret ? '***' : undefined }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async emit(event: string, data: Record<string, unknown>): Promise<void> {
|
|
43
|
+
const payload: WebhookPayload = {
|
|
44
|
+
event,
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
data,
|
|
47
|
+
};
|
|
48
|
+
const body = JSON.stringify(payload);
|
|
49
|
+
|
|
50
|
+
const matching = this.hooks.filter(h =>
|
|
51
|
+
h.events.includes('*') || h.events.includes(event)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const deliveries = matching.map(hook => this.deliver(hook, body));
|
|
55
|
+
await Promise.allSettled(deliveries);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async deliver(hook: WebhookConfig, body: string): Promise<void> {
|
|
59
|
+
const maxRetries = hook.retries ?? 2;
|
|
60
|
+
const timeout = hook.timeoutMs ?? 10_000;
|
|
61
|
+
|
|
62
|
+
const headers: Record<string, string> = {
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
'User-Agent': 'AGW-Webhook/1.5',
|
|
65
|
+
...hook.headers,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (hook.secret) {
|
|
69
|
+
const signature = createHmac('sha256', hook.secret).update(body).digest('hex');
|
|
70
|
+
headers['X-AGW-Signature'] = `sha256=${signature}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
74
|
+
try {
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
77
|
+
|
|
78
|
+
const res = await fetch(hook.url, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers,
|
|
81
|
+
body,
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
|
|
87
|
+
if (res.ok) return;
|
|
88
|
+
if (res.status >= 400 && res.status < 500) return; // Don't retry client errors
|
|
89
|
+
} catch {
|
|
90
|
+
// Retry on network errors
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (attempt < maxRetries) {
|
|
94
|
+
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|