web-agent-bridge 2.6.0 → 2.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.
@@ -0,0 +1,126 @@
1
+ -- Migration 005: Marketplace & Usage Metering tables
2
+
3
+ -- Marketplace listings
4
+ CREATE TABLE IF NOT EXISTS marketplace_listings (
5
+ id TEXT PRIMARY KEY,
6
+ name TEXT NOT NULL,
7
+ description TEXT DEFAULT '',
8
+ type TEXT NOT NULL,
9
+ category TEXT DEFAULT 'automation',
10
+ seller_id TEXT NOT NULL,
11
+ seller_name TEXT DEFAULT 'Anonymous',
12
+ price REAL DEFAULT 0,
13
+ currency TEXT DEFAULT 'usd',
14
+ version TEXT DEFAULT '1.0.0',
15
+ tags TEXT DEFAULT '[]',
16
+ icon TEXT,
17
+ readme TEXT DEFAULT '',
18
+ install_command TEXT,
19
+ config_schema TEXT DEFAULT '{}',
20
+ entry_point TEXT,
21
+ installs INTEGER DEFAULT 0,
22
+ revenue REAL DEFAULT 0,
23
+ rating REAL DEFAULT 0,
24
+ review_count INTEGER DEFAULT 0,
25
+ status TEXT DEFAULT 'pending_review',
26
+ rejection_reason TEXT,
27
+ published_at INTEGER,
28
+ created_at INTEGER NOT NULL,
29
+ updated_at INTEGER NOT NULL
30
+ );
31
+
32
+ CREATE INDEX IF NOT EXISTS idx_mkt_listings_status ON marketplace_listings(status);
33
+ CREATE INDEX IF NOT EXISTS idx_mkt_listings_category ON marketplace_listings(category);
34
+ CREATE INDEX IF NOT EXISTS idx_mkt_listings_seller ON marketplace_listings(seller_id);
35
+
36
+ -- Marketplace purchases
37
+ CREATE TABLE IF NOT EXISTS marketplace_purchases (
38
+ id TEXT PRIMARY KEY,
39
+ listing_id TEXT NOT NULL,
40
+ listing_name TEXT,
41
+ buyer_id TEXT NOT NULL,
42
+ seller_id TEXT NOT NULL,
43
+ price REAL DEFAULT 0,
44
+ commission REAL DEFAULT 0,
45
+ seller_earning REAL DEFAULT 0,
46
+ currency TEXT DEFAULT 'usd',
47
+ status TEXT DEFAULT 'pending_payment',
48
+ created_at INTEGER NOT NULL,
49
+ completed_at INTEGER,
50
+ FOREIGN KEY (listing_id) REFERENCES marketplace_listings(id)
51
+ );
52
+
53
+ CREATE INDEX IF NOT EXISTS idx_mkt_purchases_buyer ON marketplace_purchases(buyer_id);
54
+ CREATE INDEX IF NOT EXISTS idx_mkt_purchases_seller ON marketplace_purchases(seller_id);
55
+
56
+ -- Marketplace reviews
57
+ CREATE TABLE IF NOT EXISTS marketplace_reviews (
58
+ id TEXT PRIMARY KEY,
59
+ listing_id TEXT NOT NULL,
60
+ user_id TEXT NOT NULL,
61
+ rating INTEGER NOT NULL,
62
+ comment TEXT DEFAULT '',
63
+ created_at INTEGER NOT NULL,
64
+ FOREIGN KEY (listing_id) REFERENCES marketplace_listings(id)
65
+ );
66
+
67
+ -- Seller earnings
68
+ CREATE TABLE IF NOT EXISTS marketplace_earnings (
69
+ seller_id TEXT PRIMARY KEY,
70
+ total REAL DEFAULT 0,
71
+ pending REAL DEFAULT 0,
72
+ paid REAL DEFAULT 0,
73
+ last_payout INTEGER
74
+ );
75
+
76
+ -- Usage metering daily records
77
+ CREATE TABLE IF NOT EXISTS usage_metering (
78
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
79
+ entity_id TEXT NOT NULL,
80
+ metric TEXT NOT NULL,
81
+ date TEXT NOT NULL,
82
+ count INTEGER DEFAULT 0,
83
+ overage INTEGER DEFAULT 0,
84
+ overage_cost REAL DEFAULT 0,
85
+ UNIQUE(entity_id, metric, date)
86
+ );
87
+
88
+ CREATE INDEX IF NOT EXISTS idx_usage_entity ON usage_metering(entity_id);
89
+ CREATE INDEX IF NOT EXISTS idx_usage_date ON usage_metering(date);
90
+
91
+ -- Hosted runtime instances
92
+ CREATE TABLE IF NOT EXISTS hosted_instances (
93
+ id TEXT PRIMARY KEY,
94
+ agent_id TEXT NOT NULL,
95
+ tier TEXT DEFAULT 'starter',
96
+ region TEXT DEFAULT 'auto',
97
+ cpu TEXT DEFAULT '0.5',
98
+ memory TEXT DEFAULT '512',
99
+ status TEXT DEFAULT 'starting',
100
+ execution_count INTEGER DEFAULT 0,
101
+ compute_minutes REAL DEFAULT 0,
102
+ errors INTEGER DEFAULT 0,
103
+ started_at INTEGER NOT NULL,
104
+ stopped_at INTEGER,
105
+ last_activity INTEGER
106
+ );
107
+
108
+ CREATE INDEX IF NOT EXISTS idx_hosted_agent ON hosted_instances(agent_id);
109
+ CREATE INDEX IF NOT EXISTS idx_hosted_status ON hosted_instances(status);
110
+
111
+ -- Hosted executions
112
+ CREATE TABLE IF NOT EXISTS hosted_executions (
113
+ id TEXT PRIMARY KEY,
114
+ instance_id TEXT NOT NULL,
115
+ agent_id TEXT NOT NULL,
116
+ task_type TEXT,
117
+ task_action TEXT,
118
+ status TEXT DEFAULT 'running',
119
+ started_at INTEGER NOT NULL,
120
+ completed_at INTEGER,
121
+ compute_ms INTEGER DEFAULT 0,
122
+ error TEXT,
123
+ FOREIGN KEY (instance_id) REFERENCES hosted_instances(id)
124
+ );
125
+
126
+ CREATE INDEX IF NOT EXISTS idx_hexe_instance ON hosted_executions(instance_id);
@@ -28,6 +28,11 @@ const { commandRegistry, siteRegistry, templateRegistry } = require('../registry
28
28
  const { certificationEngine } = require('../registry/certification');
29
29
  const { adapterManager, mcpAdapter, restAdapter, browserAdapter } = require('../adapters');
30
30
  const { replayEngine } = require('../runtime/replay');
31
+ const { featureGate, usageLimit } = require('../middleware/featureGate');
32
+ const { listPlans, getPlan, USAGE_PRICING, MARKETPLACE } = require('../config/plans');
33
+ const metering = require('../services/metering');
34
+ const { marketplace } = require('../services/marketplace');
35
+ const { hostedRuntime } = require('../services/hosted-runtime');
31
36
  const { sessionEngine } = require('../runtime/session-engine');
32
37
 
33
38
  // ═══════════════════════════════════════════════════════════════════════════
@@ -48,6 +53,8 @@ const PUBLIC_PATHS = [
48
53
  '/registry/commands',
49
54
  '/registry/sites',
50
55
  '/registry/templates',
56
+ '/plans',
57
+ '/marketplace',
51
58
  ];
52
59
 
53
60
  function authMiddleware(req, res, next) {
@@ -99,6 +106,7 @@ function authMiddleware(req, res, next) {
99
106
  }
100
107
 
101
108
  router.use(authMiddleware);
109
+ router.use(featureGate);
102
110
 
103
111
  // ═══════════════════════════════════════════════════════════════════════════
104
112
  // PROTOCOL ENDPOINTS
@@ -241,7 +249,7 @@ router.delete('/agents/:agentId', (req, res) => {
241
249
  /**
242
250
  * Submit a task
243
251
  */
244
- router.post('/tasks', (req, res) => {
252
+ router.post('/tasks', usageLimit('tasksPerDay'), (req, res) => {
245
253
  try {
246
254
  const result = runtime.submitTask(req.body);
247
255
  metrics.increment('tasks.submitted', 1, { type: req.body.type });
@@ -299,7 +307,7 @@ router.post('/tasks/:taskId/resume', (req, res) => {
299
307
  /**
300
308
  * Execute a semantic action
301
309
  */
302
- router.post('/execute', async (req, res) => {
310
+ router.post('/execute', usageLimit('executionsPerDay'), async (req, res) => {
303
311
  try {
304
312
  const result = await executor.execute(req.body);
305
313
  res.json(result);
@@ -523,6 +531,9 @@ router.get('/observability/health', (req, res) => {
523
531
  health.sessions = sessionEngine.getStats();
524
532
  health.failures = failureAnalyzer.getStats();
525
533
  health.certification = certificationEngine.getStats();
534
+ health.marketplace = marketplace.getStats();
535
+ health.hostedRuntime = hostedRuntime.getStats();
536
+ health.metering = metering.getStats();
526
537
  res.json(health);
527
538
  });
528
539
 
@@ -634,7 +645,7 @@ router.get('/registry/templates/:templateId', (req, res) => {
634
645
  /**
635
646
  * LLM completion
636
647
  */
637
- router.post('/llm/complete', async (req, res) => {
648
+ router.post('/llm/complete', usageLimit('executionsPerDay'), async (req, res) => {
638
649
  try {
639
650
  const result = await llm.complete(req.body.prompt, req.body.options || req.body);
640
651
  metrics.increment('llm.api.requests');
@@ -1133,4 +1144,303 @@ router.delete('/certification/:domain', (req, res) => {
1133
1144
  res.json({ success: true });
1134
1145
  });
1135
1146
 
1147
+ // ═══════════════════════════════════════════════════════════════════════════
1148
+ // PLANS & PRICING
1149
+ // ═══════════════════════════════════════════════════════════════════════════
1150
+
1151
+ /**
1152
+ * List available plans
1153
+ */
1154
+ router.get('/plans', (req, res) => {
1155
+ const plans = listPlans().map(p => ({
1156
+ id: p.id,
1157
+ name: p.name,
1158
+ price: p.price,
1159
+ interval: p.interval,
1160
+ description: p.description,
1161
+ limits: p.limits,
1162
+ features: Object.entries(p.features)
1163
+ .filter(([, v]) => v === true)
1164
+ .map(([k]) => k),
1165
+ }));
1166
+ res.json({ plans, usagePricing: USAGE_PRICING });
1167
+ });
1168
+
1169
+ /**
1170
+ * Get specific plan details
1171
+ */
1172
+ router.get('/plans/:planId', (req, res) => {
1173
+ const plan = getPlan(req.params.planId);
1174
+ if (!plan || plan.id === 'free' && req.params.planId !== 'free') {
1175
+ return res.status(404).json({ error: 'Plan not found' });
1176
+ }
1177
+ res.json(plan);
1178
+ });
1179
+
1180
+ // ═══════════════════════════════════════════════════════════════════════════
1181
+ // USAGE METERING
1182
+ // ═══════════════════════════════════════════════════════════════════════════
1183
+
1184
+ /**
1185
+ * Get usage for current agent
1186
+ */
1187
+ router.get('/usage', (req, res) => {
1188
+ const entityId = req.agentId || req.ip;
1189
+ const tier = req.agentTier || req.session?.tier || 'free';
1190
+ res.json(metering.getUsage(entityId, tier));
1191
+ });
1192
+
1193
+ /**
1194
+ * Get billing summary (overages)
1195
+ */
1196
+ router.get('/usage/billing', (req, res) => {
1197
+ const entityId = req.agentId || req.ip;
1198
+ res.json(metering.getBillingSummary(entityId));
1199
+ });
1200
+
1201
+ /**
1202
+ * Get metering stats (admin)
1203
+ */
1204
+ router.get('/usage/stats', (req, res) => {
1205
+ res.json(metering.getStats());
1206
+ });
1207
+
1208
+ // ═══════════════════════════════════════════════════════════════════════════
1209
+ // MARKETPLACE
1210
+ // ═══════════════════════════════════════════════════════════════════════════
1211
+
1212
+ /**
1213
+ * Search marketplace
1214
+ */
1215
+ router.get('/marketplace', (req, res) => {
1216
+ const listings = marketplace.search({
1217
+ type: req.query.type,
1218
+ category: req.query.category,
1219
+ query: req.query.q,
1220
+ tag: req.query.tag,
1221
+ free: req.query.free === 'true',
1222
+ paid: req.query.paid === 'true',
1223
+ minRating: req.query.minRating ? parseFloat(req.query.minRating) : undefined,
1224
+ sortBy: req.query.sortBy,
1225
+ }, parseInt(req.query.limit) || 50);
1226
+ res.json({ listings, total: listings.length });
1227
+ });
1228
+
1229
+ /**
1230
+ * Get listing
1231
+ */
1232
+ router.get('/marketplace/:listingId', (req, res) => {
1233
+ const listing = marketplace.getListing(req.params.listingId);
1234
+ if (!listing) return res.status(404).json({ error: 'Listing not found' });
1235
+ res.json(listing);
1236
+ });
1237
+
1238
+ /**
1239
+ * Get reviews
1240
+ */
1241
+ router.get('/marketplace/:listingId/reviews', (req, res) => {
1242
+ res.json({ reviews: marketplace.getReviews(req.params.listingId) });
1243
+ });
1244
+
1245
+ /**
1246
+ * Publish listing
1247
+ */
1248
+ router.post('/marketplace/publish', (req, res) => {
1249
+ try {
1250
+ const listing = marketplace.publish({
1251
+ ...req.body,
1252
+ sellerId: req.agentId || req.body.sellerId,
1253
+ });
1254
+ res.json(listing);
1255
+ } catch (err) {
1256
+ res.status(400).json({ error: err.message });
1257
+ }
1258
+ });
1259
+
1260
+ /**
1261
+ * Purchase/install listing
1262
+ */
1263
+ router.post('/marketplace/:listingId/purchase', (req, res) => {
1264
+ try {
1265
+ const buyerId = req.agentId || req.body.buyerId;
1266
+ if (!buyerId) return res.status(400).json({ error: 'buyerId required' });
1267
+ const purchase = marketplace.purchase(req.params.listingId, buyerId);
1268
+ res.json(purchase);
1269
+ } catch (err) {
1270
+ res.status(400).json({ error: err.message });
1271
+ }
1272
+ });
1273
+
1274
+ /**
1275
+ * Add review
1276
+ */
1277
+ router.post('/marketplace/:listingId/review', (req, res) => {
1278
+ try {
1279
+ const review = marketplace.addReview(req.params.listingId, {
1280
+ userId: req.agentId || req.body.userId,
1281
+ rating: req.body.rating,
1282
+ comment: req.body.comment,
1283
+ });
1284
+ res.json(review);
1285
+ } catch (err) {
1286
+ res.status(400).json({ error: err.message });
1287
+ }
1288
+ });
1289
+
1290
+ /**
1291
+ * Get my purchases
1292
+ */
1293
+ router.get('/marketplace/my/purchases', (req, res) => {
1294
+ const buyerId = req.agentId || req.query.buyerId;
1295
+ res.json({ purchases: marketplace.getPurchases(buyerId) });
1296
+ });
1297
+
1298
+ /**
1299
+ * Get seller earnings
1300
+ */
1301
+ router.get('/marketplace/my/earnings', (req, res) => {
1302
+ const sellerId = req.agentId || req.query.sellerId;
1303
+ res.json(marketplace.getEarnings(sellerId));
1304
+ });
1305
+
1306
+ /**
1307
+ * Admin: pending listings
1308
+ */
1309
+ router.get('/marketplace/admin/pending', (req, res) => {
1310
+ res.json({ listings: marketplace.getPendingListings() });
1311
+ });
1312
+
1313
+ /**
1314
+ * Admin: approve listing
1315
+ */
1316
+ router.post('/marketplace/admin/:listingId/approve', (req, res) => {
1317
+ try {
1318
+ const listing = marketplace.approve(req.params.listingId);
1319
+ res.json(listing);
1320
+ } catch (err) {
1321
+ res.status(400).json({ error: err.message });
1322
+ }
1323
+ });
1324
+
1325
+ /**
1326
+ * Admin: reject listing
1327
+ */
1328
+ router.post('/marketplace/admin/:listingId/reject', (req, res) => {
1329
+ try {
1330
+ const listing = marketplace.reject(req.params.listingId, req.body.reason);
1331
+ res.json(listing);
1332
+ } catch (err) {
1333
+ res.status(400).json({ error: err.message });
1334
+ }
1335
+ });
1336
+
1337
+ /**
1338
+ * Marketplace stats
1339
+ */
1340
+ router.get('/marketplace/stats', (req, res) => {
1341
+ res.json(marketplace.getStats());
1342
+ });
1343
+
1344
+ // ═══════════════════════════════════════════════════════════════════════════
1345
+ // HOSTED RUNTIME
1346
+ // ═══════════════════════════════════════════════════════════════════════════
1347
+
1348
+ /**
1349
+ * Launch hosted instance
1350
+ */
1351
+ router.post('/hosted/launch', (req, res) => {
1352
+ try {
1353
+ const instance = hostedRuntime.launch({
1354
+ agentId: req.agentId || req.body.agentId,
1355
+ tier: req.agentTier || req.session?.tier || 'starter',
1356
+ region: req.body.region,
1357
+ cpu: req.body.cpu,
1358
+ memory: req.body.memory,
1359
+ timeout: req.body.timeout,
1360
+ });
1361
+ res.json(instance);
1362
+ } catch (err) {
1363
+ res.status(400).json({ error: err.message });
1364
+ }
1365
+ });
1366
+
1367
+ /**
1368
+ * Execute on hosted instance
1369
+ */
1370
+ router.post('/hosted/:instanceId/execute', async (req, res) => {
1371
+ try {
1372
+ const execution = await hostedRuntime.execute(req.params.instanceId, req.body);
1373
+ res.json(execution);
1374
+ } catch (err) {
1375
+ res.status(400).json({ error: err.message });
1376
+ }
1377
+ });
1378
+
1379
+ /**
1380
+ * Complete execution
1381
+ */
1382
+ router.post('/hosted/executions/:executionId/complete', (req, res) => {
1383
+ const execution = hostedRuntime.completeExecution(
1384
+ req.params.executionId,
1385
+ req.body.result,
1386
+ req.body.error ? new Error(req.body.error) : null
1387
+ );
1388
+ if (!execution) return res.status(404).json({ error: 'Execution not found' });
1389
+ res.json(execution);
1390
+ });
1391
+
1392
+ /**
1393
+ * Stop hosted instance
1394
+ */
1395
+ router.post('/hosted/:instanceId/stop', (req, res) => {
1396
+ const success = hostedRuntime.stop(req.params.instanceId);
1397
+ res.json({ success });
1398
+ });
1399
+
1400
+ /**
1401
+ * Get hosted instance
1402
+ */
1403
+ router.get('/hosted/:instanceId', (req, res) => {
1404
+ const instance = hostedRuntime.getInstance(req.params.instanceId);
1405
+ if (!instance) return res.status(404).json({ error: 'Instance not found' });
1406
+ res.json(instance);
1407
+ });
1408
+
1409
+ /**
1410
+ * List instances
1411
+ */
1412
+ router.get('/hosted', (req, res) => {
1413
+ const instances = hostedRuntime.listInstances({
1414
+ agentId: req.query.agentId,
1415
+ status: req.query.status,
1416
+ region: req.query.region,
1417
+ }, parseInt(req.query.limit) || 50);
1418
+ res.json({ instances, total: instances.length });
1419
+ });
1420
+
1421
+ /**
1422
+ * List executions for instance
1423
+ */
1424
+ router.get('/hosted/:instanceId/executions', (req, res) => {
1425
+ const executions = hostedRuntime.listExecutions(
1426
+ req.params.instanceId,
1427
+ parseInt(req.query.limit) || 50
1428
+ );
1429
+ res.json({ executions, total: executions.length });
1430
+ });
1431
+
1432
+ /**
1433
+ * Get compute usage
1434
+ */
1435
+ router.get('/hosted/usage/:agentId', (req, res) => {
1436
+ res.json(hostedRuntime.getComputeUsage(req.params.agentId));
1437
+ });
1438
+
1439
+ /**
1440
+ * Hosted runtime stats
1441
+ */
1442
+ router.get('/hosted/stats', (req, res) => {
1443
+ res.json(hostedRuntime.getStats());
1444
+ });
1445
+
1136
1446
  module.exports = router;
@@ -0,0 +1,205 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Hosted Runtime Service
5
+ *
6
+ * Cloud execution abstraction for running agents without local infrastructure.
7
+ * Pay-as-you-go model with auto-scaling, resource tracking, and multi-region support.
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+ const { bus } = require('../runtime/event-bus');
12
+ const metering = require('./metering');
13
+
14
+ class HostedRuntime {
15
+ constructor() {
16
+ this._instances = new Map(); // instanceId → RuntimeInstance
17
+ this._executions = new Map(); // executionId → Execution
18
+ this._maxInstances = 1000;
19
+ }
20
+
21
+ /**
22
+ * Launch a hosted runtime instance for an agent
23
+ */
24
+ launch(config) {
25
+ if (!config.agentId) throw new Error('agentId required');
26
+
27
+ const instanceId = `hrt_${crypto.randomBytes(8).toString('hex')}`;
28
+ const instance = {
29
+ id: instanceId,
30
+ agentId: config.agentId,
31
+ tier: config.tier || 'starter',
32
+ region: config.region || 'auto',
33
+ resources: {
34
+ cpu: config.cpu || '0.5', // vCPU
35
+ memory: config.memory || '512', // MB
36
+ timeout: config.timeout || 300000, // 5 min default
37
+ },
38
+ status: 'starting',
39
+ startedAt: Date.now(),
40
+ lastActivity: Date.now(),
41
+ executionCount: 0,
42
+ computeMinutes: 0,
43
+ errors: 0,
44
+ };
45
+
46
+ this._instances.set(instanceId, instance);
47
+
48
+ // Simulate startup (in real deployment, this would provision container/lambda)
49
+ instance.status = 'running';
50
+ bus.emit('hosted.launched', { instanceId, agentId: config.agentId, region: instance.region });
51
+
52
+ return instance;
53
+ }
54
+
55
+ /**
56
+ * Execute a task on a hosted instance
57
+ */
58
+ async execute(instanceId, task) {
59
+ const instance = this._instances.get(instanceId);
60
+ if (!instance) throw new Error('Instance not found');
61
+ if (instance.status !== 'running') throw new Error(`Instance not running (status: ${instance.status})`);
62
+
63
+ const executionId = `hexe_${crypto.randomBytes(8).toString('hex')}`;
64
+ const execution = {
65
+ id: executionId,
66
+ instanceId,
67
+ agentId: instance.agentId,
68
+ task: {
69
+ type: task.type,
70
+ action: task.action,
71
+ params: task.params || {},
72
+ },
73
+ status: 'running',
74
+ startedAt: Date.now(),
75
+ completedAt: null,
76
+ result: null,
77
+ error: null,
78
+ computeMs: 0,
79
+ resources: {
80
+ cpuUsage: 0,
81
+ memoryUsage: 0,
82
+ networkCalls: 0,
83
+ },
84
+ };
85
+
86
+ this._executions.set(executionId, execution);
87
+ instance.executionCount++;
88
+ instance.lastActivity = Date.now();
89
+
90
+ // Record metering
91
+ metering.record(instance.agentId, 'executionsPerDay', instance.tier, 1);
92
+
93
+ bus.emit('hosted.execution.started', { executionId, instanceId, type: task.type });
94
+
95
+ // Return execution handle (actual execution is async in real deployment)
96
+ return execution;
97
+ }
98
+
99
+ /**
100
+ * Complete an execution (called by worker after task finishes)
101
+ */
102
+ completeExecution(executionId, result, error = null) {
103
+ const execution = this._executions.get(executionId);
104
+ if (!execution) return null;
105
+
106
+ execution.status = error ? 'failed' : 'completed';
107
+ execution.completedAt = Date.now();
108
+ execution.result = result;
109
+ execution.error = error ? { message: error.message || String(error) } : null;
110
+ execution.computeMs = execution.completedAt - execution.startedAt;
111
+
112
+ // Update instance stats
113
+ const instance = this._instances.get(execution.instanceId);
114
+ if (instance) {
115
+ const minutes = execution.computeMs / 60000;
116
+ instance.computeMinutes += minutes;
117
+ metering.record(instance.agentId, 'computeMinutesPerDay', instance.tier, minutes);
118
+ if (error) instance.errors++;
119
+ }
120
+
121
+ bus.emit('hosted.execution.completed', {
122
+ executionId, instanceId: execution.instanceId,
123
+ status: execution.status, computeMs: execution.computeMs,
124
+ });
125
+
126
+ return execution;
127
+ }
128
+
129
+ /**
130
+ * Stop a hosted instance
131
+ */
132
+ stop(instanceId) {
133
+ const instance = this._instances.get(instanceId);
134
+ if (!instance) return false;
135
+ instance.status = 'stopped';
136
+ instance.stoppedAt = Date.now();
137
+ bus.emit('hosted.stopped', { instanceId, agentId: instance.agentId });
138
+ return true;
139
+ }
140
+
141
+ /**
142
+ * Get instance
143
+ */
144
+ getInstance(instanceId) {
145
+ return this._instances.get(instanceId) || null;
146
+ }
147
+
148
+ /**
149
+ * List instances
150
+ */
151
+ listInstances(filters = {}, limit = 50) {
152
+ let instances = Array.from(this._instances.values());
153
+ if (filters.agentId) instances = instances.filter(i => i.agentId === filters.agentId);
154
+ if (filters.status) instances = instances.filter(i => i.status === filters.status);
155
+ if (filters.region) instances = instances.filter(i => i.region === filters.region);
156
+ return instances.slice(0, limit);
157
+ }
158
+
159
+ /**
160
+ * Get execution
161
+ */
162
+ getExecution(executionId) {
163
+ return this._executions.get(executionId) || null;
164
+ }
165
+
166
+ /**
167
+ * List executions for an instance
168
+ */
169
+ listExecutions(instanceId, limit = 50) {
170
+ return Array.from(this._executions.values())
171
+ .filter(e => e.instanceId === instanceId)
172
+ .sort((a, b) => b.startedAt - a.startedAt)
173
+ .slice(0, limit);
174
+ }
175
+
176
+ /**
177
+ * Get compute usage for an agent
178
+ */
179
+ getComputeUsage(agentId) {
180
+ const instances = Array.from(this._instances.values())
181
+ .filter(i => i.agentId === agentId);
182
+
183
+ return {
184
+ activeInstances: instances.filter(i => i.status === 'running').length,
185
+ totalExecutions: instances.reduce((sum, i) => sum + i.executionCount, 0),
186
+ totalComputeMinutes: Math.round(instances.reduce((sum, i) => sum + i.computeMinutes, 0) * 100) / 100,
187
+ totalErrors: instances.reduce((sum, i) => sum + i.errors, 0),
188
+ };
189
+ }
190
+
191
+ getStats() {
192
+ const instances = Array.from(this._instances.values());
193
+ return {
194
+ totalInstances: instances.length,
195
+ running: instances.filter(i => i.status === 'running').length,
196
+ stopped: instances.filter(i => i.status === 'stopped').length,
197
+ totalExecutions: this._executions.size,
198
+ totalComputeMinutes: Math.round(instances.reduce((sum, i) => sum + i.computeMinutes, 0) * 100) / 100,
199
+ };
200
+ }
201
+ }
202
+
203
+ const hostedRuntime = new HostedRuntime();
204
+
205
+ module.exports = { HostedRuntime, hostedRuntime };