@sooneocean/agw 1.5.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/server.ts +11 -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/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
|
+
}
|
package/src/daemon/server.ts
CHANGED
|
@@ -30,6 +30,11 @@ import { registerMemoryRoutes } from './routes/memory.js';
|
|
|
30
30
|
import { registerHealthRoutes } from './routes/health.js';
|
|
31
31
|
import { registerTemplateRoutes } from './routes/templates.js';
|
|
32
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';
|
|
33
38
|
|
|
34
39
|
interface ServerOptions {
|
|
35
40
|
dbPath?: string;
|
|
@@ -65,6 +70,8 @@ export async function buildServer(options: ServerOptions = {}): Promise<FastifyI
|
|
|
65
70
|
const templateEngine = new TemplateEngine();
|
|
66
71
|
templateEngine.seedDefaults();
|
|
67
72
|
const webhookManager = new WebhookManager();
|
|
73
|
+
const scheduler = new Scheduler();
|
|
74
|
+
const replayManager = new ReplayManager(taskRepo, comboRepo, executor, comboExecutor, router, agentManager);
|
|
68
75
|
|
|
69
76
|
const app = Fastify({
|
|
70
77
|
logger: false,
|
|
@@ -82,6 +89,9 @@ export async function buildServer(options: ServerOptions = {}): Promise<FastifyI
|
|
|
82
89
|
registerHealthRoutes(app, metrics, agentManager, cbRegistry, taskRepo, costRepo, config);
|
|
83
90
|
registerTemplateRoutes(app, templateEngine, executor, router, agentManager);
|
|
84
91
|
registerWebhookRoutes(app, webhookManager);
|
|
92
|
+
registerSchedulerRoutes(app, scheduler);
|
|
93
|
+
registerReplayRoutes(app, replayManager);
|
|
94
|
+
registerExportImportRoutes(app, templateEngine, webhookManager, scheduler, memoryRepo);
|
|
85
95
|
|
|
86
96
|
app.register(import('./routes/ui.js'));
|
|
87
97
|
|
|
@@ -93,6 +103,7 @@ export async function buildServer(options: ServerOptions = {}): Promise<FastifyI
|
|
|
93
103
|
taskRepo.updateStatus(t.taskId, 'failed');
|
|
94
104
|
auditRepo.log(t.taskId, 'task.failed', { reason: 'daemon shutdown' });
|
|
95
105
|
}
|
|
106
|
+
scheduler.stopAll();
|
|
96
107
|
db.close();
|
|
97
108
|
});
|
|
98
109
|
|
|
@@ -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
|
+
}
|