@bopen-io/tortuga-plugin 0.0.3 → 0.0.5
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/dist/constants.js +48 -0
- package/dist/constants.js.map +7 -0
- package/dist/manifest.js +52 -9
- package/dist/manifest.js.map +2 -2
- package/dist/ui/index.js +893 -18
- package/dist/ui/index.js.map +2 -2
- package/dist/worker.js +505 -39
- package/dist/worker.js.map +3 -3
- package/package.json +4 -6
package/dist/worker.js
CHANGED
|
@@ -4,17 +4,17 @@ var __export = (target, all) => {
|
|
|
4
4
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
-
// node_modules
|
|
7
|
+
// node_modules/@paperclipai/plugin-sdk/dist/define-plugin.js
|
|
8
8
|
function definePlugin(definition) {
|
|
9
9
|
return Object.freeze({ definition });
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
// node_modules
|
|
12
|
+
// node_modules/@paperclipai/plugin-sdk/dist/worker-rpc-host.js
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { createInterface } from "node:readline";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
|
|
17
|
-
// node_modules
|
|
17
|
+
// node_modules/@paperclipai/plugin-sdk/dist/protocol.js
|
|
18
18
|
var JSONRPC_VERSION = "2.0";
|
|
19
19
|
var JSONRPC_ERROR_CODES = {
|
|
20
20
|
/** Invalid JSON was received by the server. */
|
|
@@ -144,7 +144,7 @@ var JsonRpcCallError = class extends Error {
|
|
|
144
144
|
}
|
|
145
145
|
};
|
|
146
146
|
|
|
147
|
-
// node_modules
|
|
147
|
+
// node_modules/@paperclipai/plugin-sdk/dist/worker-rpc-host.js
|
|
148
148
|
var DEFAULT_RPC_TIMEOUT_MS = 3e4;
|
|
149
149
|
function runWorker(plugin2, moduleUrl, options) {
|
|
150
150
|
if (options?.stdin != null && options?.stdout != null) {
|
|
@@ -860,7 +860,7 @@ function startWorkerRpcHost(options) {
|
|
|
860
860
|
};
|
|
861
861
|
}
|
|
862
862
|
|
|
863
|
-
// node_modules
|
|
863
|
+
// node_modules/zod/v3/external.js
|
|
864
864
|
var external_exports = {};
|
|
865
865
|
__export(external_exports, {
|
|
866
866
|
BRAND: () => BRAND,
|
|
@@ -972,7 +972,7 @@ __export(external_exports, {
|
|
|
972
972
|
void: () => voidType
|
|
973
973
|
});
|
|
974
974
|
|
|
975
|
-
// node_modules
|
|
975
|
+
// node_modules/zod/v3/helpers/util.js
|
|
976
976
|
var util;
|
|
977
977
|
(function(util2) {
|
|
978
978
|
util2.assertEqual = (_) => {
|
|
@@ -1106,7 +1106,7 @@ var getParsedType = (data) => {
|
|
|
1106
1106
|
}
|
|
1107
1107
|
};
|
|
1108
1108
|
|
|
1109
|
-
// node_modules
|
|
1109
|
+
// node_modules/zod/v3/ZodError.js
|
|
1110
1110
|
var ZodIssueCode = util.arrayToEnum([
|
|
1111
1111
|
"invalid_type",
|
|
1112
1112
|
"invalid_literal",
|
|
@@ -1224,7 +1224,7 @@ ZodError.create = (issues) => {
|
|
|
1224
1224
|
return error;
|
|
1225
1225
|
};
|
|
1226
1226
|
|
|
1227
|
-
// node_modules
|
|
1227
|
+
// node_modules/zod/v3/locales/en.js
|
|
1228
1228
|
var errorMap = (issue, _ctx) => {
|
|
1229
1229
|
let message;
|
|
1230
1230
|
switch (issue.code) {
|
|
@@ -1327,7 +1327,7 @@ var errorMap = (issue, _ctx) => {
|
|
|
1327
1327
|
};
|
|
1328
1328
|
var en_default = errorMap;
|
|
1329
1329
|
|
|
1330
|
-
// node_modules
|
|
1330
|
+
// node_modules/zod/v3/errors.js
|
|
1331
1331
|
var overrideErrorMap = en_default;
|
|
1332
1332
|
function setErrorMap(map) {
|
|
1333
1333
|
overrideErrorMap = map;
|
|
@@ -1336,7 +1336,7 @@ function getErrorMap() {
|
|
|
1336
1336
|
return overrideErrorMap;
|
|
1337
1337
|
}
|
|
1338
1338
|
|
|
1339
|
-
// node_modules
|
|
1339
|
+
// node_modules/zod/v3/helpers/parseUtil.js
|
|
1340
1340
|
var makeIssue = (params) => {
|
|
1341
1341
|
const { data, path: path2, errorMaps, issueData } = params;
|
|
1342
1342
|
const fullPath = [...path2, ...issueData.path || []];
|
|
@@ -1446,14 +1446,14 @@ var isDirty = (x) => x.status === "dirty";
|
|
|
1446
1446
|
var isValid = (x) => x.status === "valid";
|
|
1447
1447
|
var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
|
|
1448
1448
|
|
|
1449
|
-
// node_modules
|
|
1449
|
+
// node_modules/zod/v3/helpers/errorUtil.js
|
|
1450
1450
|
var errorUtil;
|
|
1451
1451
|
(function(errorUtil2) {
|
|
1452
1452
|
errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
|
|
1453
1453
|
errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
|
|
1454
1454
|
})(errorUtil || (errorUtil = {}));
|
|
1455
1455
|
|
|
1456
|
-
// node_modules
|
|
1456
|
+
// node_modules/zod/v3/types.js
|
|
1457
1457
|
var ParseInputLazyPath = class {
|
|
1458
1458
|
constructor(parent, value, path2, key) {
|
|
1459
1459
|
this._cachedPath = [];
|
|
@@ -4901,7 +4901,7 @@ var coerce = {
|
|
|
4901
4901
|
};
|
|
4902
4902
|
var NEVER = INVALID;
|
|
4903
4903
|
|
|
4904
|
-
// node_modules
|
|
4904
|
+
// node_modules/@paperclipai/shared/dist/constants.js
|
|
4905
4905
|
var COMPANY_STATUSES = ["active", "paused", "archived"];
|
|
4906
4906
|
var DEPLOYMENT_MODES = ["local_trusted", "authenticated"];
|
|
4907
4907
|
var DEPLOYMENT_EXPOSURES = ["private", "public"];
|
|
@@ -5168,7 +5168,7 @@ var PLUGIN_STATE_SCOPE_KINDS = [
|
|
|
5168
5168
|
"run"
|
|
5169
5169
|
];
|
|
5170
5170
|
|
|
5171
|
-
// node_modules
|
|
5171
|
+
// node_modules/@paperclipai/shared/dist/validators/company.js
|
|
5172
5172
|
var createCompanySchema = external_exports.object({
|
|
5173
5173
|
name: external_exports.string().min(1),
|
|
5174
5174
|
description: external_exports.string().optional().nullable(),
|
|
@@ -5181,7 +5181,7 @@ var updateCompanySchema = createCompanySchema.partial().extend({
|
|
|
5181
5181
|
brandColor: external_exports.string().regex(/^#[0-9a-fA-F]{6}$/).nullable().optional()
|
|
5182
5182
|
});
|
|
5183
5183
|
|
|
5184
|
-
// node_modules
|
|
5184
|
+
// node_modules/@paperclipai/shared/dist/validators/company-portability.js
|
|
5185
5185
|
var portabilityIncludeSchema = external_exports.object({
|
|
5186
5186
|
company: external_exports.boolean().optional(),
|
|
5187
5187
|
agents: external_exports.boolean().optional()
|
|
@@ -5271,7 +5271,7 @@ var companyPortabilityPreviewSchema = external_exports.object({
|
|
|
5271
5271
|
collisionStrategy: portabilityCollisionStrategySchema.optional()
|
|
5272
5272
|
});
|
|
5273
5273
|
|
|
5274
|
-
// node_modules
|
|
5274
|
+
// node_modules/@paperclipai/shared/dist/validators/secret.js
|
|
5275
5275
|
var envBindingPlainSchema = external_exports.object({
|
|
5276
5276
|
type: external_exports.literal("plain"),
|
|
5277
5277
|
value: external_exports.string()
|
|
@@ -5304,7 +5304,7 @@ var updateSecretSchema = external_exports.object({
|
|
|
5304
5304
|
externalRef: external_exports.string().optional().nullable()
|
|
5305
5305
|
});
|
|
5306
5306
|
|
|
5307
|
-
// node_modules
|
|
5307
|
+
// node_modules/@paperclipai/shared/dist/validators/agent.js
|
|
5308
5308
|
var agentPermissionsSchema = external_exports.object({
|
|
5309
5309
|
canCreateAgents: external_exports.boolean().optional().default(false)
|
|
5310
5310
|
});
|
|
@@ -5369,7 +5369,7 @@ var updateAgentPermissionsSchema = external_exports.object({
|
|
|
5369
5369
|
canCreateAgents: external_exports.boolean()
|
|
5370
5370
|
});
|
|
5371
5371
|
|
|
5372
|
-
// node_modules
|
|
5372
|
+
// node_modules/@paperclipai/shared/dist/validators/project.js
|
|
5373
5373
|
var executionWorkspaceStrategySchema = external_exports.object({
|
|
5374
5374
|
type: external_exports.enum(["project_primary", "git_worktree"]).optional(),
|
|
5375
5375
|
baseRef: external_exports.string().optional().nullable(),
|
|
@@ -5432,7 +5432,7 @@ var createProjectSchema = external_exports.object({
|
|
|
5432
5432
|
});
|
|
5433
5433
|
var updateProjectSchema = external_exports.object(projectFields).partial();
|
|
5434
5434
|
|
|
5435
|
-
// node_modules
|
|
5435
|
+
// node_modules/@paperclipai/shared/dist/validators/issue.js
|
|
5436
5436
|
var executionWorkspaceStrategySchema2 = external_exports.object({
|
|
5437
5437
|
type: external_exports.enum(["project_primary", "git_worktree"]).optional(),
|
|
5438
5438
|
baseRef: external_exports.string().optional().nullable(),
|
|
@@ -5500,7 +5500,7 @@ var upsertIssueDocumentSchema = external_exports.object({
|
|
|
5500
5500
|
baseRevisionId: external_exports.string().uuid().nullable().optional()
|
|
5501
5501
|
});
|
|
5502
5502
|
|
|
5503
|
-
// node_modules
|
|
5503
|
+
// node_modules/@paperclipai/shared/dist/validators/goal.js
|
|
5504
5504
|
var createGoalSchema = external_exports.object({
|
|
5505
5505
|
title: external_exports.string().min(1),
|
|
5506
5506
|
description: external_exports.string().optional().nullable(),
|
|
@@ -5511,7 +5511,7 @@ var createGoalSchema = external_exports.object({
|
|
|
5511
5511
|
});
|
|
5512
5512
|
var updateGoalSchema = createGoalSchema.partial();
|
|
5513
5513
|
|
|
5514
|
-
// node_modules
|
|
5514
|
+
// node_modules/@paperclipai/shared/dist/validators/approval.js
|
|
5515
5515
|
var createApprovalSchema = external_exports.object({
|
|
5516
5516
|
type: external_exports.enum(APPROVAL_TYPES),
|
|
5517
5517
|
requestedByAgentId: external_exports.string().uuid().optional().nullable(),
|
|
@@ -5533,7 +5533,7 @@ var addApprovalCommentSchema = external_exports.object({
|
|
|
5533
5533
|
body: external_exports.string().min(1)
|
|
5534
5534
|
});
|
|
5535
5535
|
|
|
5536
|
-
// node_modules
|
|
5536
|
+
// node_modules/@paperclipai/shared/dist/validators/cost.js
|
|
5537
5537
|
var createCostEventSchema = external_exports.object({
|
|
5538
5538
|
agentId: external_exports.string().uuid(),
|
|
5539
5539
|
issueId: external_exports.string().uuid().optional().nullable(),
|
|
@@ -5551,12 +5551,12 @@ var updateBudgetSchema = external_exports.object({
|
|
|
5551
5551
|
budgetMonthlyCents: external_exports.number().int().nonnegative()
|
|
5552
5552
|
});
|
|
5553
5553
|
|
|
5554
|
-
// node_modules
|
|
5554
|
+
// node_modules/@paperclipai/shared/dist/validators/asset.js
|
|
5555
5555
|
var createAssetImageMetadataSchema = external_exports.object({
|
|
5556
5556
|
namespace: external_exports.string().trim().min(1).max(120).regex(/^[a-zA-Z0-9/_-]+$/).optional()
|
|
5557
5557
|
});
|
|
5558
5558
|
|
|
5559
|
-
// node_modules
|
|
5559
|
+
// node_modules/@paperclipai/shared/dist/validators/access.js
|
|
5560
5560
|
var createCompanyInviteSchema = external_exports.object({
|
|
5561
5561
|
allowedJoinTypes: external_exports.enum(INVITE_JOIN_TYPES).default("both"),
|
|
5562
5562
|
defaultsPayload: external_exports.record(external_exports.string(), external_exports.unknown()).optional().nullable(),
|
|
@@ -5595,7 +5595,7 @@ var updateUserCompanyAccessSchema = external_exports.object({
|
|
|
5595
5595
|
companyIds: external_exports.array(external_exports.string().uuid()).default([])
|
|
5596
5596
|
});
|
|
5597
5597
|
|
|
5598
|
-
// node_modules
|
|
5598
|
+
// node_modules/@paperclipai/shared/dist/validators/plugin.js
|
|
5599
5599
|
var jsonSchemaSchema = external_exports.record(external_exports.unknown()).refine((val) => {
|
|
5600
5600
|
if (Object.keys(val).length === 0)
|
|
5601
5601
|
return true;
|
|
@@ -5964,7 +5964,7 @@ var listPluginStateSchema = external_exports.object({
|
|
|
5964
5964
|
namespace: external_exports.string().min(1).optional()
|
|
5965
5965
|
});
|
|
5966
5966
|
|
|
5967
|
-
// node_modules
|
|
5967
|
+
// node_modules/@paperclipai/shared/dist/api.js
|
|
5968
5968
|
var API_PREFIX = "/api";
|
|
5969
5969
|
var API = {
|
|
5970
5970
|
health: `${API_PREFIX}/health`,
|
|
@@ -5985,7 +5985,7 @@ var API = {
|
|
|
5985
5985
|
admin: `${API_PREFIX}/admin`
|
|
5986
5986
|
};
|
|
5987
5987
|
|
|
5988
|
-
// node_modules
|
|
5988
|
+
// node_modules/@paperclipai/shared/dist/config-schema.js
|
|
5989
5989
|
var configMetaSchema = external_exports.object({
|
|
5990
5990
|
version: external_exports.literal(1),
|
|
5991
5991
|
updatedAt: external_exports.string(),
|
|
@@ -6125,24 +6125,490 @@ var paperclipConfigSchema = external_exports.object({
|
|
|
6125
6125
|
}
|
|
6126
6126
|
});
|
|
6127
6127
|
|
|
6128
|
+
// src/constants.ts
|
|
6129
|
+
var JOB_KEYS = {
|
|
6130
|
+
fleetHealth: "fleet-health"
|
|
6131
|
+
};
|
|
6132
|
+
var STREAM_CHANNELS = {
|
|
6133
|
+
fleetStatus: "fleet-status"
|
|
6134
|
+
};
|
|
6135
|
+
|
|
6128
6136
|
// src/worker.ts
|
|
6129
|
-
var
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6137
|
+
var STATE_KEYS = {
|
|
6138
|
+
/** Per-agent health snapshot: status, lastHeartbeat, run counts */
|
|
6139
|
+
agentHealth: "health",
|
|
6140
|
+
/** Per-agent last-run timestamp */
|
|
6141
|
+
lastRun: "last-run",
|
|
6142
|
+
/** Per-agent run counters: started, completed, failed */
|
|
6143
|
+
runCounts: "run-counts",
|
|
6144
|
+
/** Instance-level: last fleet health check timestamp */
|
|
6145
|
+
lastHealthCheck: "last-health-check"
|
|
6146
|
+
};
|
|
6147
|
+
var DATA_KEYS = {
|
|
6148
|
+
fleetOverview: "fleet-overview",
|
|
6149
|
+
agentDetail: "agent-detail"
|
|
6150
|
+
};
|
|
6151
|
+
var ACTION_KEYS = {
|
|
6152
|
+
pauseAgent: "pause-agent",
|
|
6153
|
+
resumeAgent: "resume-agent",
|
|
6154
|
+
invokeAgent: "invoke-agent"
|
|
6155
|
+
};
|
|
6156
|
+
var currentContext = null;
|
|
6157
|
+
var openStreams = /* @__PURE__ */ new Set();
|
|
6158
|
+
function summarizeError(error) {
|
|
6159
|
+
if (error instanceof Error) return error.message;
|
|
6160
|
+
return String(error);
|
|
6161
|
+
}
|
|
6162
|
+
function requireParam(params, key) {
|
|
6163
|
+
const value = params[key];
|
|
6164
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
6165
|
+
throw new Error(`${key} is required`);
|
|
6166
|
+
}
|
|
6167
|
+
return value;
|
|
6168
|
+
}
|
|
6169
|
+
function classifyHealth(agent) {
|
|
6170
|
+
const terminalStatuses = ["terminated", "error"];
|
|
6171
|
+
if (terminalStatuses.includes(agent.status)) return "error";
|
|
6172
|
+
if (agent.status === "paused" || agent.status === "pending_approval") {
|
|
6173
|
+
return "degraded";
|
|
6174
|
+
}
|
|
6175
|
+
if (agent.lastHeartbeatAt) {
|
|
6176
|
+
const age = Date.now() - new Date(agent.lastHeartbeatAt).getTime();
|
|
6177
|
+
const fifteenMinutes = 15 * 60 * 1e3;
|
|
6178
|
+
if (age > fifteenMinutes) return "degraded";
|
|
6179
|
+
}
|
|
6180
|
+
return "healthy";
|
|
6181
|
+
}
|
|
6182
|
+
async function getRunCounts(ctx, agentId) {
|
|
6183
|
+
const counts = await ctx.state.get({
|
|
6184
|
+
scopeKind: "agent",
|
|
6185
|
+
scopeId: agentId,
|
|
6186
|
+
stateKey: STATE_KEYS.runCounts
|
|
6187
|
+
});
|
|
6188
|
+
return counts ?? { started: 0, completed: 0, failed: 0 };
|
|
6189
|
+
}
|
|
6190
|
+
async function setRunCounts(ctx, agentId, counts) {
|
|
6191
|
+
await ctx.state.set(
|
|
6192
|
+
{ scopeKind: "agent", scopeId: agentId, stateKey: STATE_KEYS.runCounts },
|
|
6193
|
+
counts
|
|
6194
|
+
);
|
|
6195
|
+
}
|
|
6196
|
+
async function getAgentHealthState(ctx, agentId) {
|
|
6197
|
+
const state = await ctx.state.get({
|
|
6198
|
+
scopeKind: "agent",
|
|
6199
|
+
scopeId: agentId,
|
|
6200
|
+
stateKey: STATE_KEYS.agentHealth
|
|
6201
|
+
});
|
|
6202
|
+
return state ?? null;
|
|
6203
|
+
}
|
|
6204
|
+
async function setAgentHealthState(ctx, agentId, state) {
|
|
6205
|
+
await ctx.state.set(
|
|
6206
|
+
{ scopeKind: "agent", scopeId: agentId, stateKey: STATE_KEYS.agentHealth },
|
|
6207
|
+
state
|
|
6208
|
+
);
|
|
6209
|
+
}
|
|
6210
|
+
async function performFleetHealthCheck(ctx) {
|
|
6211
|
+
const companies = await ctx.companies.list({ limit: 100 });
|
|
6212
|
+
const allAgents = [];
|
|
6213
|
+
for (const company of companies) {
|
|
6214
|
+
const agents = await ctx.agents.list({ companyId: company.id, limit: 200, offset: 0 });
|
|
6215
|
+
allAgents.push(...agents);
|
|
6216
|
+
}
|
|
6217
|
+
let healthy = 0;
|
|
6218
|
+
let degraded = 0;
|
|
6219
|
+
let errorCount = 0;
|
|
6220
|
+
const changedAgents = [];
|
|
6221
|
+
for (const agent of allAgents) {
|
|
6222
|
+
const health = classifyHealth(agent);
|
|
6223
|
+
const runCounts = await getRunCounts(ctx, agent.id);
|
|
6224
|
+
const previousState = await getAgentHealthState(ctx, agent.id);
|
|
6225
|
+
const newState = {
|
|
6226
|
+
status: agent.status,
|
|
6227
|
+
lastHeartbeatAt: agent.lastHeartbeatAt ? new Date(agent.lastHeartbeatAt).toISOString() : null,
|
|
6228
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6229
|
+
runCounts,
|
|
6230
|
+
lastRunAt: previousState?.lastRunAt ?? null
|
|
6231
|
+
};
|
|
6232
|
+
if (previousState && previousState.status !== agent.status) {
|
|
6233
|
+
changedAgents.push({
|
|
6234
|
+
agent,
|
|
6235
|
+
previousStatus: previousState.status,
|
|
6236
|
+
health
|
|
6237
|
+
});
|
|
6238
|
+
}
|
|
6239
|
+
await setAgentHealthState(ctx, agent.id, newState);
|
|
6240
|
+
switch (health) {
|
|
6241
|
+
case "healthy":
|
|
6242
|
+
healthy++;
|
|
6243
|
+
break;
|
|
6244
|
+
case "degraded":
|
|
6245
|
+
degraded++;
|
|
6246
|
+
break;
|
|
6247
|
+
case "error":
|
|
6248
|
+
errorCount++;
|
|
6249
|
+
break;
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
const result = {
|
|
6253
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6254
|
+
totalAgents: allAgents.length,
|
|
6255
|
+
healthy,
|
|
6256
|
+
degraded,
|
|
6257
|
+
error: errorCount
|
|
6258
|
+
};
|
|
6259
|
+
await ctx.state.set(
|
|
6260
|
+
{ scopeKind: "instance", stateKey: STATE_KEYS.lastHealthCheck },
|
|
6261
|
+
result
|
|
6262
|
+
);
|
|
6263
|
+
if (changedAgents.length > 0) {
|
|
6264
|
+
for (const { agent, previousStatus, health } of changedAgents) {
|
|
6265
|
+
ctx.streams.emit(STREAM_CHANNELS.fleetStatus, {
|
|
6266
|
+
type: "health-check-change",
|
|
6267
|
+
agentId: agent.id,
|
|
6268
|
+
agentName: agent.name,
|
|
6269
|
+
companyId: agent.companyId,
|
|
6270
|
+
previousStatus,
|
|
6271
|
+
currentStatus: agent.status,
|
|
6272
|
+
health,
|
|
6273
|
+
checkedAt: result.checkedAt
|
|
6274
|
+
});
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6277
|
+
ctx.streams.emit(STREAM_CHANNELS.fleetStatus, {
|
|
6278
|
+
type: "health-check-complete",
|
|
6279
|
+
...result
|
|
6280
|
+
});
|
|
6281
|
+
return result;
|
|
6282
|
+
}
|
|
6283
|
+
function registerJobHandlers(ctx) {
|
|
6284
|
+
ctx.jobs.register(JOB_KEYS.fleetHealth, async (job) => {
|
|
6285
|
+
ctx.logger.info("Starting fleet health check", {
|
|
6286
|
+
runId: job.runId,
|
|
6287
|
+
trigger: job.trigger
|
|
6288
|
+
});
|
|
6289
|
+
try {
|
|
6290
|
+
const result = await performFleetHealthCheck(ctx);
|
|
6291
|
+
ctx.logger.info("Fleet health check completed", {
|
|
6292
|
+
runId: job.runId,
|
|
6293
|
+
totalAgents: result.totalAgents,
|
|
6294
|
+
healthy: result.healthy,
|
|
6295
|
+
degraded: result.degraded,
|
|
6296
|
+
error: result.error
|
|
6297
|
+
});
|
|
6298
|
+
} catch (error) {
|
|
6299
|
+
ctx.logger.error("Fleet health check failed", {
|
|
6300
|
+
runId: job.runId,
|
|
6301
|
+
error: summarizeError(error)
|
|
6302
|
+
});
|
|
6303
|
+
throw error;
|
|
6304
|
+
}
|
|
6305
|
+
});
|
|
6306
|
+
}
|
|
6307
|
+
function registerEventHandlers(ctx) {
|
|
6308
|
+
ctx.events.on("agent.status_changed", async (event) => {
|
|
6309
|
+
const agentId = event.entityId;
|
|
6310
|
+
if (!agentId) return;
|
|
6311
|
+
const payload = event.payload;
|
|
6312
|
+
ctx.logger.info("Agent status changed", { agentId, payload });
|
|
6313
|
+
const runCounts = await getRunCounts(ctx, agentId);
|
|
6314
|
+
const previousState = await getAgentHealthState(ctx, agentId);
|
|
6315
|
+
const newStatus = payload.newStatus ?? payload.status ?? previousState?.status ?? "idle";
|
|
6316
|
+
await setAgentHealthState(ctx, agentId, {
|
|
6317
|
+
status: newStatus,
|
|
6318
|
+
lastHeartbeatAt: previousState?.lastHeartbeatAt ?? null,
|
|
6319
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6320
|
+
runCounts,
|
|
6321
|
+
lastRunAt: previousState?.lastRunAt ?? null
|
|
6322
|
+
});
|
|
6323
|
+
ctx.streams.emit(STREAM_CHANNELS.fleetStatus, {
|
|
6324
|
+
type: "agent-status-changed",
|
|
6325
|
+
agentId,
|
|
6326
|
+
companyId: event.companyId,
|
|
6327
|
+
previousStatus: previousState?.status ?? null,
|
|
6328
|
+
newStatus,
|
|
6329
|
+
occurredAt: event.occurredAt
|
|
6135
6330
|
});
|
|
6136
|
-
|
|
6137
|
-
|
|
6331
|
+
});
|
|
6332
|
+
ctx.events.on("agent.run.started", async (event) => {
|
|
6333
|
+
const agentId = event.entityId;
|
|
6334
|
+
if (!agentId) return;
|
|
6335
|
+
const payload = event.payload;
|
|
6336
|
+
ctx.logger.info("Agent run started", { agentId, runId: payload.runId });
|
|
6337
|
+
const counts = await getRunCounts(ctx, agentId);
|
|
6338
|
+
counts.started++;
|
|
6339
|
+
await setRunCounts(ctx, agentId, counts);
|
|
6340
|
+
const healthState = await getAgentHealthState(ctx, agentId);
|
|
6341
|
+
if (healthState) {
|
|
6342
|
+
healthState.lastRunAt = event.occurredAt;
|
|
6343
|
+
healthState.runCounts = counts;
|
|
6344
|
+
await setAgentHealthState(ctx, agentId, healthState);
|
|
6345
|
+
}
|
|
6346
|
+
ctx.streams.emit(STREAM_CHANNELS.fleetStatus, {
|
|
6347
|
+
type: "run-started",
|
|
6348
|
+
agentId,
|
|
6349
|
+
companyId: event.companyId,
|
|
6350
|
+
runId: payload.runId,
|
|
6351
|
+
occurredAt: event.occurredAt
|
|
6352
|
+
});
|
|
6353
|
+
});
|
|
6354
|
+
ctx.events.on("agent.run.finished", async (event) => {
|
|
6355
|
+
const agentId = event.entityId;
|
|
6356
|
+
if (!agentId) return;
|
|
6357
|
+
const payload = event.payload;
|
|
6358
|
+
ctx.logger.info("Agent run finished", { agentId, runId: payload.runId });
|
|
6359
|
+
const counts = await getRunCounts(ctx, agentId);
|
|
6360
|
+
counts.completed++;
|
|
6361
|
+
await setRunCounts(ctx, agentId, counts);
|
|
6362
|
+
const healthState = await getAgentHealthState(ctx, agentId);
|
|
6363
|
+
if (healthState) {
|
|
6364
|
+
healthState.lastRunAt = event.occurredAt;
|
|
6365
|
+
healthState.runCounts = counts;
|
|
6366
|
+
await setAgentHealthState(ctx, agentId, healthState);
|
|
6367
|
+
}
|
|
6368
|
+
ctx.streams.emit(STREAM_CHANNELS.fleetStatus, {
|
|
6369
|
+
type: "run-finished",
|
|
6370
|
+
agentId,
|
|
6371
|
+
companyId: event.companyId,
|
|
6372
|
+
runId: payload.runId,
|
|
6373
|
+
occurredAt: event.occurredAt
|
|
6374
|
+
});
|
|
6375
|
+
});
|
|
6376
|
+
ctx.events.on("agent.run.failed", async (event) => {
|
|
6377
|
+
const agentId = event.entityId;
|
|
6378
|
+
if (!agentId) return;
|
|
6379
|
+
const payload = event.payload;
|
|
6380
|
+
ctx.logger.warn("Agent run failed", {
|
|
6381
|
+
agentId,
|
|
6382
|
+
runId: payload.runId,
|
|
6383
|
+
error: payload.error
|
|
6384
|
+
});
|
|
6385
|
+
const counts = await getRunCounts(ctx, agentId);
|
|
6386
|
+
counts.failed++;
|
|
6387
|
+
await setRunCounts(ctx, agentId, counts);
|
|
6388
|
+
const healthState = await getAgentHealthState(ctx, agentId);
|
|
6389
|
+
if (healthState) {
|
|
6390
|
+
healthState.lastRunAt = event.occurredAt;
|
|
6391
|
+
healthState.runCounts = counts;
|
|
6392
|
+
await setAgentHealthState(ctx, agentId, healthState);
|
|
6393
|
+
}
|
|
6394
|
+
ctx.streams.emit(STREAM_CHANNELS.fleetStatus, {
|
|
6395
|
+
type: "run-failed",
|
|
6396
|
+
agentId,
|
|
6397
|
+
companyId: event.companyId,
|
|
6398
|
+
runId: payload.runId,
|
|
6399
|
+
error: payload.error ?? null,
|
|
6400
|
+
occurredAt: event.occurredAt
|
|
6138
6401
|
});
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6402
|
+
});
|
|
6403
|
+
}
|
|
6404
|
+
function registerDataHandlers(ctx) {
|
|
6405
|
+
ctx.data.register(DATA_KEYS.fleetOverview, async (params) => {
|
|
6406
|
+
const companyId = requireParam(params, "companyId");
|
|
6407
|
+
const agents = await ctx.agents.list({ companyId, limit: 200, offset: 0 });
|
|
6408
|
+
const fleet = await Promise.all(
|
|
6409
|
+
agents.map(async (agent) => {
|
|
6410
|
+
const healthState = await getAgentHealthState(ctx, agent.id);
|
|
6411
|
+
const health = classifyHealth(agent);
|
|
6412
|
+
return {
|
|
6413
|
+
id: agent.id,
|
|
6414
|
+
name: agent.name,
|
|
6415
|
+
role: agent.role,
|
|
6416
|
+
title: agent.title,
|
|
6417
|
+
icon: agent.icon,
|
|
6418
|
+
status: agent.status,
|
|
6419
|
+
health,
|
|
6420
|
+
lastHeartbeatAt: agent.lastHeartbeatAt ? new Date(agent.lastHeartbeatAt).toISOString() : null,
|
|
6421
|
+
budgetMonthlyCents: agent.budgetMonthlyCents,
|
|
6422
|
+
spentMonthlyCents: agent.spentMonthlyCents,
|
|
6423
|
+
runCounts: healthState?.runCounts ?? { started: 0, completed: 0, failed: 0 },
|
|
6424
|
+
lastRunAt: healthState?.lastRunAt ?? null
|
|
6425
|
+
};
|
|
6426
|
+
})
|
|
6427
|
+
);
|
|
6428
|
+
let healthy = 0;
|
|
6429
|
+
let degraded = 0;
|
|
6430
|
+
let errorCount = 0;
|
|
6431
|
+
for (const entry of fleet) {
|
|
6432
|
+
switch (entry.health) {
|
|
6433
|
+
case "healthy":
|
|
6434
|
+
healthy++;
|
|
6435
|
+
break;
|
|
6436
|
+
case "degraded":
|
|
6437
|
+
degraded++;
|
|
6438
|
+
break;
|
|
6439
|
+
case "error":
|
|
6440
|
+
errorCount++;
|
|
6441
|
+
break;
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6444
|
+
const lastCheck = await ctx.state.get({
|
|
6445
|
+
scopeKind: "instance",
|
|
6446
|
+
stateKey: STATE_KEYS.lastHealthCheck
|
|
6142
6447
|
});
|
|
6448
|
+
return {
|
|
6449
|
+
totalAgents: agents.length,
|
|
6450
|
+
healthy,
|
|
6451
|
+
degraded,
|
|
6452
|
+
error: errorCount,
|
|
6453
|
+
lastHealthCheckAt: lastCheck?.checkedAt ?? null,
|
|
6454
|
+
fleet
|
|
6455
|
+
};
|
|
6456
|
+
});
|
|
6457
|
+
ctx.data.register(DATA_KEYS.agentDetail, async (params) => {
|
|
6458
|
+
const agentId = requireParam(params, "agentId");
|
|
6459
|
+
const companyId = requireParam(params, "companyId");
|
|
6460
|
+
const agent = await ctx.agents.get(agentId, companyId);
|
|
6461
|
+
if (!agent) {
|
|
6462
|
+
throw new Error(`Agent ${agentId} not found`);
|
|
6463
|
+
}
|
|
6464
|
+
const healthState = await getAgentHealthState(ctx, agentId);
|
|
6465
|
+
const health = classifyHealth(agent);
|
|
6466
|
+
return {
|
|
6467
|
+
id: agent.id,
|
|
6468
|
+
name: agent.name,
|
|
6469
|
+
role: agent.role,
|
|
6470
|
+
title: agent.title,
|
|
6471
|
+
icon: agent.icon,
|
|
6472
|
+
status: agent.status,
|
|
6473
|
+
health,
|
|
6474
|
+
reportsTo: agent.reportsTo,
|
|
6475
|
+
capabilities: agent.capabilities,
|
|
6476
|
+
adapterType: agent.adapterType,
|
|
6477
|
+
budgetMonthlyCents: agent.budgetMonthlyCents,
|
|
6478
|
+
spentMonthlyCents: agent.spentMonthlyCents,
|
|
6479
|
+
lastHeartbeatAt: agent.lastHeartbeatAt ? new Date(agent.lastHeartbeatAt).toISOString() : null,
|
|
6480
|
+
runCounts: healthState?.runCounts ?? { started: 0, completed: 0, failed: 0 },
|
|
6481
|
+
lastRunAt: healthState?.lastRunAt ?? null,
|
|
6482
|
+
lastCheckedAt: healthState?.lastCheckedAt ?? null,
|
|
6483
|
+
createdAt: agent.createdAt ? new Date(agent.createdAt).toISOString() : null,
|
|
6484
|
+
updatedAt: agent.updatedAt ? new Date(agent.updatedAt).toISOString() : null
|
|
6485
|
+
};
|
|
6486
|
+
});
|
|
6487
|
+
}
|
|
6488
|
+
function registerActionHandlers(ctx) {
|
|
6489
|
+
ctx.actions.register(ACTION_KEYS.pauseAgent, async (params) => {
|
|
6490
|
+
const agentId = requireParam(params, "agentId");
|
|
6491
|
+
const companyId = requireParam(params, "companyId");
|
|
6492
|
+
ctx.logger.info("Pausing agent", { agentId, companyId });
|
|
6493
|
+
const agent = await ctx.agents.pause(agentId, companyId);
|
|
6494
|
+
return {
|
|
6495
|
+
ok: true,
|
|
6496
|
+
agentId: agent.id,
|
|
6497
|
+
name: agent.name,
|
|
6498
|
+
status: agent.status
|
|
6499
|
+
};
|
|
6500
|
+
});
|
|
6501
|
+
ctx.actions.register(ACTION_KEYS.resumeAgent, async (params) => {
|
|
6502
|
+
const agentId = requireParam(params, "agentId");
|
|
6503
|
+
const companyId = requireParam(params, "companyId");
|
|
6504
|
+
ctx.logger.info("Resuming agent", { agentId, companyId });
|
|
6505
|
+
const agent = await ctx.agents.resume(agentId, companyId);
|
|
6506
|
+
return {
|
|
6507
|
+
ok: true,
|
|
6508
|
+
agentId: agent.id,
|
|
6509
|
+
name: agent.name,
|
|
6510
|
+
status: agent.status
|
|
6511
|
+
};
|
|
6512
|
+
});
|
|
6513
|
+
ctx.actions.register(ACTION_KEYS.invokeAgent, async (params) => {
|
|
6514
|
+
const agentId = requireParam(params, "agentId");
|
|
6515
|
+
const companyId = requireParam(params, "companyId");
|
|
6516
|
+
const prompt = requireParam(params, "prompt");
|
|
6517
|
+
const reason = typeof params.reason === "string" ? params.reason : void 0;
|
|
6518
|
+
ctx.logger.info("Invoking agent", { agentId, companyId, reason });
|
|
6519
|
+
const { runId } = await ctx.agents.invoke(agentId, companyId, { prompt, reason });
|
|
6520
|
+
return {
|
|
6521
|
+
ok: true,
|
|
6522
|
+
agentId,
|
|
6523
|
+
runId
|
|
6524
|
+
};
|
|
6525
|
+
});
|
|
6526
|
+
}
|
|
6527
|
+
var plugin = definePlugin({
|
|
6528
|
+
async setup(ctx) {
|
|
6529
|
+
currentContext = ctx;
|
|
6530
|
+
ctx.logger.info("Tortuga plugin starting setup");
|
|
6531
|
+
registerJobHandlers(ctx);
|
|
6532
|
+
registerEventHandlers(ctx);
|
|
6533
|
+
registerDataHandlers(ctx);
|
|
6534
|
+
registerActionHandlers(ctx);
|
|
6535
|
+
ctx.logger.info("Tortuga plugin setup complete");
|
|
6143
6536
|
},
|
|
6144
6537
|
async onHealth() {
|
|
6145
|
-
|
|
6538
|
+
const ctx = currentContext;
|
|
6539
|
+
if (!ctx) {
|
|
6540
|
+
return { status: "error", message: "Plugin context not initialized" };
|
|
6541
|
+
}
|
|
6542
|
+
try {
|
|
6543
|
+
const lastCheck = await ctx.state.get({
|
|
6544
|
+
scopeKind: "instance",
|
|
6545
|
+
stateKey: STATE_KEYS.lastHealthCheck
|
|
6546
|
+
});
|
|
6547
|
+
if (!lastCheck) {
|
|
6548
|
+
return {
|
|
6549
|
+
status: "degraded",
|
|
6550
|
+
message: "No fleet health check has been performed yet",
|
|
6551
|
+
details: { lastCheck: null }
|
|
6552
|
+
};
|
|
6553
|
+
}
|
|
6554
|
+
const checkAge = Date.now() - new Date(lastCheck.checkedAt).getTime();
|
|
6555
|
+
const fifteenMinutes = 15 * 60 * 1e3;
|
|
6556
|
+
if (checkAge > fifteenMinutes) {
|
|
6557
|
+
return {
|
|
6558
|
+
status: "degraded",
|
|
6559
|
+
message: `Last fleet health check was ${Math.round(checkAge / 6e4)} minutes ago`,
|
|
6560
|
+
details: {
|
|
6561
|
+
lastCheckAt: lastCheck.checkedAt,
|
|
6562
|
+
totalAgents: lastCheck.totalAgents,
|
|
6563
|
+
healthy: lastCheck.healthy,
|
|
6564
|
+
degraded: lastCheck.degraded,
|
|
6565
|
+
error: lastCheck.error,
|
|
6566
|
+
checkAgeMs: checkAge
|
|
6567
|
+
}
|
|
6568
|
+
};
|
|
6569
|
+
}
|
|
6570
|
+
if (lastCheck.error > 0) {
|
|
6571
|
+
return {
|
|
6572
|
+
status: "degraded",
|
|
6573
|
+
message: `${lastCheck.error} agent(s) in error state. ${lastCheck.healthy}/${lastCheck.totalAgents} healthy.`,
|
|
6574
|
+
details: {
|
|
6575
|
+
lastCheckAt: lastCheck.checkedAt,
|
|
6576
|
+
totalAgents: lastCheck.totalAgents,
|
|
6577
|
+
healthy: lastCheck.healthy,
|
|
6578
|
+
degraded: lastCheck.degraded,
|
|
6579
|
+
error: lastCheck.error
|
|
6580
|
+
}
|
|
6581
|
+
};
|
|
6582
|
+
}
|
|
6583
|
+
return {
|
|
6584
|
+
status: "ok",
|
|
6585
|
+
message: `Fleet healthy. ${lastCheck.healthy}/${lastCheck.totalAgents} agents OK.`,
|
|
6586
|
+
details: {
|
|
6587
|
+
lastCheckAt: lastCheck.checkedAt,
|
|
6588
|
+
totalAgents: lastCheck.totalAgents,
|
|
6589
|
+
healthy: lastCheck.healthy,
|
|
6590
|
+
degraded: lastCheck.degraded,
|
|
6591
|
+
error: lastCheck.error
|
|
6592
|
+
}
|
|
6593
|
+
};
|
|
6594
|
+
} catch (error) {
|
|
6595
|
+
return {
|
|
6596
|
+
status: "error",
|
|
6597
|
+
message: `Health check failed: ${summarizeError(error)}`
|
|
6598
|
+
};
|
|
6599
|
+
}
|
|
6600
|
+
},
|
|
6601
|
+
async onShutdown() {
|
|
6602
|
+
if (currentContext) {
|
|
6603
|
+
for (const channel of openStreams) {
|
|
6604
|
+
try {
|
|
6605
|
+
currentContext.streams.close(channel);
|
|
6606
|
+
} catch {
|
|
6607
|
+
}
|
|
6608
|
+
}
|
|
6609
|
+
openStreams.clear();
|
|
6610
|
+
}
|
|
6611
|
+
currentContext = null;
|
|
6146
6612
|
}
|
|
6147
6613
|
});
|
|
6148
6614
|
var worker_default = plugin;
|