@sooneocean/agw 1.6.0 → 1.7.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sooneocean/agw",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Agent Gateway — multi-agent task router for Claude, Codex, Gemini with combos, DAG, DSL",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,67 @@
1
+ import type { Command } from 'commander';
2
+ import { HttpClient } from '../http-client.js';
3
+
4
+ export function registerDashboardCommand(program: Command): void {
5
+ program
6
+ .command('dashboard')
7
+ .alias('dash')
8
+ .description('Live terminal dashboard — refresh every 3 seconds')
9
+ .option('--once', 'Print once and exit')
10
+ .action(async (options: { once?: boolean }) => {
11
+ const client = new HttpClient();
12
+
13
+ const render = async () => {
14
+ try {
15
+ const [metrics, agents, costs] = await Promise.all([
16
+ client.get<any>('/metrics').catch(() => null),
17
+ client.get<any[]>('/agents').catch(() => []),
18
+ client.get<any>('/costs').catch(() => null),
19
+ ]);
20
+
21
+ console.clear();
22
+ console.log('\x1b[36m╔══════════════════════════════════════════════╗\x1b[0m');
23
+ console.log('\x1b[36m║\x1b[0m \x1b[1mAGW Dashboard\x1b[0m \x1b[36m║\x1b[0m');
24
+ console.log('\x1b[36m╚══════════════════════════════════════════════╝\x1b[0m');
25
+
26
+ if (metrics) {
27
+ const up = Math.floor(metrics.uptime / 1000);
28
+ const h = Math.floor(up / 3600);
29
+ const m = Math.floor((up % 3600) / 60);
30
+ const s = up % 60;
31
+ console.log(`\n Uptime: ${h}h ${m}m ${s}s Memory: ${metrics.memory?.heapMB ?? '?'}MB`);
32
+ console.log(`\n \x1b[1mTasks\x1b[0m`);
33
+ console.log(` Total: ${metrics.tasks.total} ✓ ${metrics.tasks.completed} ✗ ${metrics.tasks.failed} ⟳ ${metrics.tasks.running} ⏳ ${metrics.tasks.pending ?? 0}`);
34
+ if (metrics.performance.avgDurationMs > 0) {
35
+ console.log(` Avg: ${(metrics.performance.avgDurationMs / 1000).toFixed(1)}s P95: ${(metrics.performance.p95DurationMs / 1000).toFixed(1)}s`);
36
+ }
37
+ } else {
38
+ console.log('\n \x1b[31mDaemon not running\x1b[0m');
39
+ }
40
+
41
+ console.log(`\n \x1b[1mAgents\x1b[0m`);
42
+ for (const a of agents) {
43
+ const icon = a.available ? '\x1b[32m●\x1b[0m' : '\x1b[31m●\x1b[0m';
44
+ console.log(` ${icon} ${a.name.padEnd(12)} ${a.available ? 'Ready' : 'Down'}`);
45
+ }
46
+
47
+ if (costs) {
48
+ console.log(`\n \x1b[1mCosts\x1b[0m`);
49
+ console.log(` Daily: $${costs.daily.toFixed(2)}${costs.dailyLimit ? ` / $${costs.dailyLimit.toFixed(2)}` : ''}`);
50
+ console.log(` Monthly: $${costs.monthly.toFixed(2)}${costs.monthlyLimit ? ` / $${costs.monthlyLimit.toFixed(2)}` : ''}`);
51
+ }
52
+
53
+ if (!options.once) {
54
+ console.log(`\n \x1b[2mRefreshing every 3s... (Ctrl+C to quit)\x1b[0m`);
55
+ }
56
+ } catch {
57
+ console.log('\x1b[31m Cannot connect to daemon\x1b[0m');
58
+ }
59
+ };
60
+
61
+ await render();
62
+ if (options.once) return;
63
+
64
+ const interval = setInterval(render, 3000);
65
+ process.on('SIGINT', () => { clearInterval(interval); process.exit(0); });
66
+ });
67
+ }
package/src/cli/index.ts CHANGED
@@ -7,13 +7,14 @@ import { registerDaemonCommand } from './commands/daemon.js';
7
7
  import { registerCostsCommand } from './commands/costs.js';
8
8
  import { registerWorkflowCommand } from './commands/workflow.js';
9
9
  import { registerComboCommand } from './commands/combo.js';
10
+ import { registerDashboardCommand } from './commands/dashboard.js';
10
11
 
11
12
  export function createCli(): Command {
12
13
  const program = new Command();
13
14
  program
14
15
  .name('agw')
15
16
  .description('Agent Gateway — route tasks to the best AI agent')
16
- .version('0.3.0');
17
+ .version('1.7.0');
17
18
 
18
19
  registerRunCommand(program);
19
20
  registerStatusCommand(program);
@@ -23,6 +24,7 @@ export function createCli(): Command {
23
24
  registerCostsCommand(program);
24
25
  registerWorkflowCommand(program);
25
26
  registerComboCommand(program);
27
+ registerDashboardCommand(program);
26
28
 
27
29
  return program;
28
30
  }
@@ -0,0 +1,45 @@
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 { BatchExecutor } from '../services/batch.js';
6
+ import type { BatchItem } from '../services/batch.js';
7
+
8
+ export function registerBatchRoutes(
9
+ app: FastifyInstance,
10
+ executor: TaskExecutor,
11
+ router: LlmRouter,
12
+ agentManager: AgentManager,
13
+ ): void {
14
+ const batchExecutor = new BatchExecutor();
15
+
16
+ app.post<{ Body: { items: BatchItem[]; concurrency?: number } }>('/batch', async (request, reply) => {
17
+ const { items, concurrency } = request.body;
18
+ if (!items || !Array.isArray(items) || items.length === 0) {
19
+ return reply.status(400).send({ error: 'items array is required' });
20
+ }
21
+ if (items.length > 50) {
22
+ return reply.status(400).send({ error: 'Maximum 50 items per batch' });
23
+ }
24
+
25
+ const result = await batchExecutor.execute(
26
+ items,
27
+ async (prompt, agent, priority) => {
28
+ const availableAgents = agentManager.getAvailableAgents();
29
+ const task = await executor.execute(
30
+ { prompt, preferredAgent: agent, priority },
31
+ async (p) => router.route(p, availableAgents, agent),
32
+ );
33
+ return {
34
+ taskId: task.taskId,
35
+ status: task.status,
36
+ stdout: task.result?.stdout,
37
+ error: task.status === 'failed' ? task.result?.stderr : undefined,
38
+ };
39
+ },
40
+ concurrency ?? 5,
41
+ );
42
+
43
+ return reply.status(200).send(result);
44
+ });
45
+ }
@@ -0,0 +1,17 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { CapabilityDiscovery } from '../services/capability-discovery.js';
3
+
4
+ export function registerCapabilityRoutes(app: FastifyInstance, discovery: CapabilityDiscovery): void {
5
+ app.get('/capabilities', async () => discovery.getAll());
6
+
7
+ app.get<{ Params: { agentId: string } }>('/capabilities/:agentId', async (request, reply) => {
8
+ const cap = discovery.get(request.params.agentId);
9
+ if (!cap) return reply.status(404).send({ error: 'Agent not found' });
10
+ return cap;
11
+ });
12
+
13
+ app.post<{ Body: { prompt: string; availableAgents?: string[] } }>('/capabilities/match', async (request) => {
14
+ const agents = request.body.availableAgents ?? ['claude', 'codex', 'gemini'];
15
+ return discovery.findBestMatch(request.body.prompt, agents) ?? { agentId: agents[0], score: 0, reason: 'Fallback' };
16
+ });
17
+ }
@@ -0,0 +1,23 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { SnapshotManager } from '../services/snapshot.js';
3
+
4
+ export function registerSnapshotRoutes(app: FastifyInstance, snapshotManager: SnapshotManager): void {
5
+ app.get('/snapshots', async () => snapshotManager.list());
6
+
7
+ app.post<{ Body: { label?: string } }>('/snapshots', async (request, reply) => {
8
+ const info = snapshotManager.create(request.body?.label);
9
+ return reply.status(201).send(info);
10
+ });
11
+
12
+ app.post<{ Params: { id: string } }>('/snapshots/:id/restore', async (request, reply) => {
13
+ const ok = snapshotManager.restore(request.params.id);
14
+ if (!ok) return reply.status(404).send({ error: 'Snapshot not found' });
15
+ return { restored: true, note: 'Restart daemon to use restored data' };
16
+ });
17
+
18
+ app.delete<{ Params: { id: string } }>('/snapshots/:id', async (request, reply) => {
19
+ const ok = snapshotManager.delete(request.params.id);
20
+ if (!ok) return reply.status(404).send({ error: 'Snapshot not found' });
21
+ return { deleted: true };
22
+ });
23
+ }
@@ -35,6 +35,11 @@ import { ReplayManager } from './services/replay.js';
35
35
  import { registerSchedulerRoutes } from './routes/scheduler.js';
36
36
  import { registerReplayRoutes } from './routes/replay.js';
37
37
  import { registerExportImportRoutes } from './routes/export-import.js';
38
+ import { CapabilityDiscovery } from './services/capability-discovery.js';
39
+ import { SnapshotManager } from './services/snapshot.js';
40
+ import { registerCapabilityRoutes } from './routes/capabilities.js';
41
+ import { registerBatchRoutes } from './routes/batch.js';
42
+ import { registerSnapshotRoutes } from './routes/snapshots.js';
38
43
 
39
44
  interface ServerOptions {
40
45
  dbPath?: string;
@@ -72,6 +77,8 @@ export async function buildServer(options: ServerOptions = {}): Promise<FastifyI
72
77
  const webhookManager = new WebhookManager();
73
78
  const scheduler = new Scheduler();
74
79
  const replayManager = new ReplayManager(taskRepo, comboRepo, executor, comboExecutor, router, agentManager);
80
+ const capDiscovery = new CapabilityDiscovery();
81
+ const snapshotManager = new SnapshotManager(dbPath);
75
82
 
76
83
  const app = Fastify({
77
84
  logger: false,
@@ -92,6 +99,9 @@ export async function buildServer(options: ServerOptions = {}): Promise<FastifyI
92
99
  registerSchedulerRoutes(app, scheduler);
93
100
  registerReplayRoutes(app, replayManager);
94
101
  registerExportImportRoutes(app, templateEngine, webhookManager, scheduler, memoryRepo);
102
+ registerCapabilityRoutes(app, capDiscovery);
103
+ registerBatchRoutes(app, executor, router, agentManager);
104
+ registerSnapshotRoutes(app, snapshotManager);
95
105
 
96
106
  app.register(import('./routes/ui.js'));
97
107
 
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Batch Execution — submit multiple tasks at once, get unified results.
3
+ */
4
+
5
+ import { nanoid } from 'nanoid';
6
+
7
+ export interface BatchItem {
8
+ prompt: string;
9
+ agent?: string;
10
+ priority?: number;
11
+ }
12
+
13
+ export interface BatchResult {
14
+ batchId: string;
15
+ total: number;
16
+ completed: number;
17
+ failed: number;
18
+ results: { index: number; taskId: string; status: string; output?: string; error?: string }[];
19
+ totalDurationMs: number;
20
+ }
21
+
22
+ type ExecuteFn = (prompt: string, agent?: string, priority?: number) => Promise<{ taskId: string; status: string; stdout?: string; error?: string }>;
23
+
24
+ export class BatchExecutor {
25
+ async execute(items: BatchItem[], executeFn: ExecuteFn, concurrency: number = 5): Promise<BatchResult> {
26
+ const batchId = nanoid(8);
27
+ const start = Date.now();
28
+ const results: BatchResult['results'] = [];
29
+
30
+ // Process in chunks for concurrency control
31
+ for (let i = 0; i < items.length; i += concurrency) {
32
+ const chunk = items.slice(i, i + concurrency);
33
+ const chunkResults = await Promise.allSettled(
34
+ chunk.map(async (item, offset) => {
35
+ const result = await executeFn(item.prompt, item.agent, item.priority);
36
+ return {
37
+ index: i + offset,
38
+ taskId: result.taskId,
39
+ status: result.status,
40
+ output: result.stdout,
41
+ error: result.error,
42
+ };
43
+ })
44
+ );
45
+
46
+ for (const r of chunkResults) {
47
+ if (r.status === 'fulfilled') {
48
+ results.push(r.value);
49
+ } else {
50
+ results.push({
51
+ index: results.length,
52
+ taskId: '',
53
+ status: 'failed',
54
+ error: r.reason?.message ?? 'Unknown error',
55
+ });
56
+ }
57
+ }
58
+ }
59
+
60
+ return {
61
+ batchId,
62
+ total: items.length,
63
+ completed: results.filter(r => r.status === 'completed').length,
64
+ failed: results.filter(r => r.status === 'failed').length,
65
+ results,
66
+ totalDurationMs: Date.now() - start,
67
+ };
68
+ }
69
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Capability Discovery — probes agents to determine what they can do.
3
+ * Each agent is asked to self-describe its capabilities.
4
+ * Results are cached and used for smarter routing.
5
+ */
6
+
7
+ export interface AgentCapability {
8
+ agentId: string;
9
+ strengths: string[];
10
+ weaknesses: string[];
11
+ maxContextLength?: number;
12
+ supportsStreaming: boolean;
13
+ supportsImages: boolean;
14
+ languages: string[];
15
+ specializations: string[];
16
+ discoveredAt: string;
17
+ }
18
+
19
+ const KNOWN_CAPABILITIES: Record<string, AgentCapability> = {
20
+ claude: {
21
+ agentId: 'claude',
22
+ strengths: ['complex reasoning', 'code review', 'architecture', 'refactoring', 'explanation', 'security analysis', 'long context'],
23
+ weaknesses: ['real-time data', 'running arbitrary shell commands autonomously'],
24
+ maxContextLength: 1_000_000,
25
+ supportsStreaming: true,
26
+ supportsImages: true,
27
+ languages: ['typescript', 'python', 'rust', 'go', 'java', 'c++', 'sql', 'bash'],
28
+ specializations: ['code-review', 'architecture', 'debugging', 'documentation', 'security-audit'],
29
+ discoveredAt: new Date().toISOString(),
30
+ },
31
+ codex: {
32
+ agentId: 'codex',
33
+ strengths: ['code generation', 'file operations', 'shell commands', 'quick edits', 'scripting', 'automation'],
34
+ weaknesses: ['long context reasoning', 'multi-file architecture decisions'],
35
+ maxContextLength: 200_000,
36
+ supportsStreaming: true,
37
+ supportsImages: false,
38
+ languages: ['typescript', 'python', 'rust', 'go', 'bash', 'sql'],
39
+ specializations: ['implementation', 'scripting', 'file-ops', 'testing', 'automation'],
40
+ discoveredAt: new Date().toISOString(),
41
+ },
42
+ gemini: {
43
+ agentId: 'gemini',
44
+ strengths: ['research', 'web search', 'multimodal', 'summarization', 'comparison', 'broad knowledge'],
45
+ weaknesses: ['precise code editing', 'complex refactoring'],
46
+ maxContextLength: 1_000_000,
47
+ supportsStreaming: true,
48
+ supportsImages: true,
49
+ languages: ['typescript', 'python', 'java', 'go', 'kotlin'],
50
+ specializations: ['research', 'summarization', 'comparison', 'multimodal-analysis'],
51
+ discoveredAt: new Date().toISOString(),
52
+ },
53
+ };
54
+
55
+ export class CapabilityDiscovery {
56
+ private capabilities = new Map<string, AgentCapability>();
57
+
58
+ constructor() {
59
+ // Seed known capabilities
60
+ for (const [id, cap] of Object.entries(KNOWN_CAPABILITIES)) {
61
+ this.capabilities.set(id, cap);
62
+ }
63
+ }
64
+
65
+ get(agentId: string): AgentCapability | undefined {
66
+ return this.capabilities.get(agentId);
67
+ }
68
+
69
+ getAll(): AgentCapability[] {
70
+ return Array.from(this.capabilities.values());
71
+ }
72
+
73
+ /** Find the best agent for a given task description */
74
+ findBestMatch(taskDescription: string, availableAgentIds: string[]): { agentId: string; score: number; reason: string } | undefined {
75
+ const lower = taskDescription.toLowerCase();
76
+ let bestMatch: { agentId: string; score: number; reason: string } | undefined;
77
+
78
+ for (const agentId of availableAgentIds) {
79
+ const cap = this.capabilities.get(agentId);
80
+ if (!cap) continue;
81
+
82
+ let score = 0;
83
+ const matchedStrengths: string[] = [];
84
+
85
+ for (const strength of cap.strengths) {
86
+ const words = strength.toLowerCase().split(/\s+/);
87
+ for (const word of words) {
88
+ if (word.length > 3 && lower.includes(word)) {
89
+ score += 2;
90
+ matchedStrengths.push(strength);
91
+ break;
92
+ }
93
+ }
94
+ }
95
+
96
+ for (const spec of cap.specializations) {
97
+ if (lower.includes(spec.replace('-', ' ')) || lower.includes(spec)) {
98
+ score += 3;
99
+ matchedStrengths.push(`specialization: ${spec}`);
100
+ }
101
+ }
102
+
103
+ // Penalty for weaknesses
104
+ for (const weakness of cap.weaknesses) {
105
+ const words = weakness.toLowerCase().split(/\s+/);
106
+ for (const word of words) {
107
+ if (word.length > 4 && lower.includes(word)) {
108
+ score -= 1;
109
+ }
110
+ }
111
+ }
112
+
113
+ if (!bestMatch || score > bestMatch.score) {
114
+ bestMatch = {
115
+ agentId,
116
+ score,
117
+ reason: matchedStrengths.length > 0
118
+ ? `Matched: ${matchedStrengths.slice(0, 3).join(', ')}`
119
+ : 'Default selection',
120
+ };
121
+ }
122
+ }
123
+
124
+ return bestMatch;
125
+ }
126
+
127
+ register(cap: AgentCapability): void {
128
+ this.capabilities.set(cap.agentId, cap);
129
+ }
130
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Snapshot — full system state backup and restore.
3
+ * Copies the entire SQLite database file.
4
+ */
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+
10
+ const SNAPSHOT_DIR = path.join(os.homedir(), '.agw', 'snapshots');
11
+
12
+ export interface SnapshotInfo {
13
+ id: string;
14
+ filename: string;
15
+ createdAt: string;
16
+ sizeBytes: number;
17
+ }
18
+
19
+ export class SnapshotManager {
20
+ private dbPath: string;
21
+
22
+ constructor(dbPath: string) {
23
+ this.dbPath = dbPath;
24
+ if (!fs.existsSync(SNAPSHOT_DIR)) {
25
+ fs.mkdirSync(SNAPSHOT_DIR, { recursive: true });
26
+ }
27
+ }
28
+
29
+ create(label?: string): SnapshotInfo {
30
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
31
+ const id = label ? `${label}-${timestamp}` : timestamp;
32
+ const filename = `snapshot-${id}.db`;
33
+ const destPath = path.join(SNAPSHOT_DIR, filename);
34
+
35
+ fs.copyFileSync(this.dbPath, destPath);
36
+
37
+ const stat = fs.statSync(destPath);
38
+ return {
39
+ id,
40
+ filename,
41
+ createdAt: new Date().toISOString(),
42
+ sizeBytes: stat.size,
43
+ };
44
+ }
45
+
46
+ restore(id: string): boolean {
47
+ const filename = `snapshot-${id}.db`;
48
+ const srcPath = path.join(SNAPSHOT_DIR, filename);
49
+ if (!fs.existsSync(srcPath)) return false;
50
+
51
+ // Backup current before overwriting
52
+ const backupPath = `${this.dbPath}.pre-restore`;
53
+ fs.copyFileSync(this.dbPath, backupPath);
54
+ fs.copyFileSync(srcPath, this.dbPath);
55
+ return true;
56
+ }
57
+
58
+ list(): SnapshotInfo[] {
59
+ if (!fs.existsSync(SNAPSHOT_DIR)) return [];
60
+ return fs.readdirSync(SNAPSHOT_DIR)
61
+ .filter(f => f.startsWith('snapshot-') && f.endsWith('.db'))
62
+ .map(f => {
63
+ const stat = fs.statSync(path.join(SNAPSHOT_DIR, f));
64
+ const id = f.replace(/^snapshot-/, '').replace(/\.db$/, '');
65
+ return {
66
+ id,
67
+ filename: f,
68
+ createdAt: stat.mtime.toISOString(),
69
+ sizeBytes: stat.size,
70
+ };
71
+ })
72
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
73
+ }
74
+
75
+ delete(id: string): boolean {
76
+ const filename = `snapshot-${id}.db`;
77
+ const filePath = path.join(SNAPSHOT_DIR, filename);
78
+ if (!fs.existsSync(filePath)) return false;
79
+ fs.unlinkSync(filePath);
80
+ return true;
81
+ }
82
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Task Chain — linked tasks with automatic rollback on failure.
3
+ * Each step can define a rollback action that runs if a later step fails.
4
+ */
5
+
6
+ import { nanoid } from 'nanoid';
7
+ import { EventEmitter } from 'node:events';
8
+
9
+ export interface ChainStep {
10
+ prompt: string;
11
+ agent?: string;
12
+ rollbackPrompt?: string; // prompt to execute if this step needs rollback
13
+ }
14
+
15
+ export interface ChainResult {
16
+ chainId: string;
17
+ status: 'completed' | 'failed' | 'rolled-back';
18
+ completedSteps: number;
19
+ failedAtStep?: number;
20
+ rolledBackSteps: number;
21
+ stepResults: { stepIndex: number; output: string; exitCode: number }[];
22
+ }
23
+
24
+ type ExecuteFn = (prompt: string, agent?: string) => Promise<{ stdout: string; exitCode: number }>;
25
+
26
+ export class TaskChain extends EventEmitter {
27
+ async execute(steps: ChainStep[], executeFn: ExecuteFn): Promise<ChainResult> {
28
+ const chainId = nanoid(12);
29
+ const stepResults: ChainResult['stepResults'] = [];
30
+ let failedAtStep: number | undefined;
31
+
32
+ this.emit('chain:start', chainId, steps.length);
33
+
34
+ // Execute forward
35
+ for (let i = 0; i < steps.length; i++) {
36
+ this.emit('chain:step', chainId, i, 'executing');
37
+ try {
38
+ const result = await executeFn(steps[i].prompt, steps[i].agent);
39
+ stepResults.push({ stepIndex: i, output: result.stdout, exitCode: result.exitCode });
40
+
41
+ if (result.exitCode !== 0) {
42
+ failedAtStep = i;
43
+ this.emit('chain:step', chainId, i, 'failed');
44
+ break;
45
+ }
46
+ this.emit('chain:step', chainId, i, 'completed');
47
+ } catch (err) {
48
+ stepResults.push({ stepIndex: i, output: (err as Error).message, exitCode: 1 });
49
+ failedAtStep = i;
50
+ this.emit('chain:step', chainId, i, 'failed');
51
+ break;
52
+ }
53
+ }
54
+
55
+ // If all succeeded
56
+ if (failedAtStep === undefined) {
57
+ this.emit('chain:done', chainId, 'completed');
58
+ return {
59
+ chainId,
60
+ status: 'completed',
61
+ completedSteps: steps.length,
62
+ rolledBackSteps: 0,
63
+ stepResults,
64
+ };
65
+ }
66
+
67
+ // Rollback: execute rollback prompts in reverse order for completed steps
68
+ let rolledBack = 0;
69
+ for (let i = failedAtStep - 1; i >= 0; i--) {
70
+ if (!steps[i].rollbackPrompt) continue;
71
+ this.emit('chain:rollback', chainId, i);
72
+ try {
73
+ await executeFn(steps[i].rollbackPrompt!, steps[i].agent);
74
+ rolledBack++;
75
+ } catch {
76
+ // Rollback failure is logged but doesn't stop the rollback process
77
+ this.emit('chain:rollback-failed', chainId, i);
78
+ }
79
+ }
80
+
81
+ const status = rolledBack > 0 ? 'rolled-back' : 'failed';
82
+ this.emit('chain:done', chainId, status);
83
+
84
+ return {
85
+ chainId,
86
+ status,
87
+ completedSteps: failedAtStep,
88
+ failedAtStep,
89
+ rolledBackSteps: rolledBack,
90
+ stepResults,
91
+ };
92
+ }
93
+ }