@elisym/cli 0.17.1 → 0.18.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/dist/index.js +123 -43
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -27,6 +27,14 @@ var __export = (target, all) => {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
// src/llm/providers/http.ts
|
|
30
|
+
function resolveLlmTimeoutMs() {
|
|
31
|
+
const raw = process.env.ELISYM_LLM_TIMEOUT_MS;
|
|
32
|
+
if (raw === void 0) {
|
|
33
|
+
return DEFAULT_LLM_TIMEOUT_MS;
|
|
34
|
+
}
|
|
35
|
+
const parsed = Number(raw);
|
|
36
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_LLM_TIMEOUT_MS;
|
|
37
|
+
}
|
|
30
38
|
function createAbortError() {
|
|
31
39
|
const err = new Error("The operation was aborted");
|
|
32
40
|
err.name = "AbortError";
|
|
@@ -92,10 +100,11 @@ async function fetchWithRetry(url, init, signal) {
|
|
|
92
100
|
await sleepWithSignal(delay, signal);
|
|
93
101
|
}
|
|
94
102
|
}
|
|
95
|
-
var LLM_TIMEOUT_MS, MAX_RETRIES, RETRYABLE_STATUSES;
|
|
103
|
+
var DEFAULT_LLM_TIMEOUT_MS, LLM_TIMEOUT_MS, MAX_RETRIES, RETRYABLE_STATUSES;
|
|
96
104
|
var init_http = __esm({
|
|
97
105
|
"src/llm/providers/http.ts"() {
|
|
98
|
-
|
|
106
|
+
DEFAULT_LLM_TIMEOUT_MS = 6e5;
|
|
107
|
+
LLM_TIMEOUT_MS = resolveLlmTimeoutMs();
|
|
99
108
|
MAX_RETRIES = 2;
|
|
100
109
|
RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
101
110
|
}
|
|
@@ -1977,7 +1986,6 @@ function createLogger(options = {}) {
|
|
|
1977
1986
|
var payment = new SolanaPaymentStrategy();
|
|
1978
1987
|
var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
|
|
1979
1988
|
var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1980
|
-
var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1981
1989
|
var MAX_PAID_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1982
1990
|
var SIG_PATH_TIMEOUT_MS = 60 * 1e3;
|
|
1983
1991
|
function resolveJobPrice(tags, skills) {
|
|
@@ -2012,6 +2020,12 @@ var AgentUnavailableError = class extends Error {
|
|
|
2012
2020
|
this.name = "AgentUnavailableError";
|
|
2013
2021
|
}
|
|
2014
2022
|
};
|
|
2023
|
+
var ExecutionBudgetExceededError = class extends Error {
|
|
2024
|
+
constructor(budgetMs) {
|
|
2025
|
+
super(`Execution exceeded budget (${Math.round(budgetMs / 1e3)}s)`);
|
|
2026
|
+
this.name = "ExecutionBudgetExceededError";
|
|
2027
|
+
}
|
|
2028
|
+
};
|
|
2015
2029
|
function bodyLooksLikeBilling3(body) {
|
|
2016
2030
|
const lower = body.toLowerCase();
|
|
2017
2031
|
return BILLING_BODY_MARKERS3.some((marker) => lower.includes(marker));
|
|
@@ -2035,8 +2049,10 @@ function resolveHealthPair(skill) {
|
|
|
2035
2049
|
return null;
|
|
2036
2050
|
}
|
|
2037
2051
|
var RATE_LIMIT_WINDOW_MS = 10 * 60 * 1e3;
|
|
2038
|
-
var
|
|
2039
|
-
var
|
|
2052
|
+
var FREE_MAX_JOBS_PER_CUSTOMER = 20;
|
|
2053
|
+
var FREE_GLOBAL_MAX_JOBS_PER_WINDOW = 200;
|
|
2054
|
+
var PAID_MAX_JOBS_PER_CUSTOMER = 200;
|
|
2055
|
+
var PAID_GLOBAL_MAX_JOBS_PER_WINDOW = 2e3;
|
|
2040
2056
|
var MAX_TRACKED_CUSTOMERS = 1e3;
|
|
2041
2057
|
var GLOBAL_LIMITER_KEY = "__global__";
|
|
2042
2058
|
var AgentRuntime = class {
|
|
@@ -2060,16 +2076,32 @@ var AgentRuntime = class {
|
|
|
2060
2076
|
recoveryInterval = null;
|
|
2061
2077
|
gcInterval = null;
|
|
2062
2078
|
stopped = false;
|
|
2063
|
-
/** Per-customer sliding-window rate limiter
|
|
2064
|
-
|
|
2079
|
+
/** Per-customer sliding-window rate limiter for free skills. */
|
|
2080
|
+
freeCustomerLimiter = createSlidingWindowLimiter({
|
|
2065
2081
|
windowMs: RATE_LIMIT_WINDOW_MS,
|
|
2066
|
-
maxPerWindow:
|
|
2082
|
+
maxPerWindow: FREE_MAX_JOBS_PER_CUSTOMER,
|
|
2067
2083
|
maxKeys: MAX_TRACKED_CUSTOMERS
|
|
2068
2084
|
});
|
|
2069
|
-
/** Global sliding-window rate limiter (Sybil protection). */
|
|
2070
|
-
|
|
2085
|
+
/** Global sliding-window rate limiter for free skills (Sybil protection). */
|
|
2086
|
+
freeGlobalLimiter = createSlidingWindowLimiter({
|
|
2071
2087
|
windowMs: RATE_LIMIT_WINDOW_MS,
|
|
2072
|
-
maxPerWindow:
|
|
2088
|
+
maxPerWindow: FREE_GLOBAL_MAX_JOBS_PER_WINDOW,
|
|
2089
|
+
maxKeys: 1
|
|
2090
|
+
});
|
|
2091
|
+
/**
|
|
2092
|
+
* Per-customer sliding-window limiter for paid skills (10x looser than free).
|
|
2093
|
+
* Payment is the primary economic deterrent; this cap exists to bound the
|
|
2094
|
+
* "claim paid skill but never pay" queue-spam vector.
|
|
2095
|
+
*/
|
|
2096
|
+
paidCustomerLimiter = createSlidingWindowLimiter({
|
|
2097
|
+
windowMs: RATE_LIMIT_WINDOW_MS,
|
|
2098
|
+
maxPerWindow: PAID_MAX_JOBS_PER_CUSTOMER,
|
|
2099
|
+
maxKeys: MAX_TRACKED_CUSTOMERS
|
|
2100
|
+
});
|
|
2101
|
+
/** Global sliding-window limiter for paid skills (Sybil protection, 10x free). */
|
|
2102
|
+
paidGlobalLimiter = createSlidingWindowLimiter({
|
|
2103
|
+
windowMs: RATE_LIMIT_WINDOW_MS,
|
|
2104
|
+
maxPerWindow: PAID_GLOBAL_MAX_JOBS_PER_WINDOW,
|
|
2073
2105
|
maxKeys: 1
|
|
2074
2106
|
});
|
|
2075
2107
|
/**
|
|
@@ -2247,17 +2279,20 @@ var AgentRuntime = class {
|
|
|
2247
2279
|
});
|
|
2248
2280
|
return;
|
|
2249
2281
|
}
|
|
2250
|
-
|
|
2282
|
+
const matched = this.skills.route(job.tags);
|
|
2283
|
+
const isPaid = matched ? matched.priceSubunits > 0 : false;
|
|
2284
|
+
const customerLimiter = isPaid ? this.paidCustomerLimiter : this.freeCustomerLimiter;
|
|
2285
|
+
const globalLimiter = isPaid ? this.paidGlobalLimiter : this.freeGlobalLimiter;
|
|
2286
|
+
if (!customerLimiter.peek(job.customerId).allowed) {
|
|
2251
2287
|
this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
|
|
2252
2288
|
});
|
|
2253
2289
|
return;
|
|
2254
2290
|
}
|
|
2255
|
-
if (!
|
|
2291
|
+
if (!globalLimiter.peek(GLOBAL_LIMITER_KEY).allowed) {
|
|
2256
2292
|
this.transport.sendFeedback(job, { type: "error", message: "Server busy, try again later" }).catch(() => {
|
|
2257
2293
|
});
|
|
2258
2294
|
return;
|
|
2259
2295
|
}
|
|
2260
|
-
const matched = this.skills.route(job.tags);
|
|
2261
2296
|
const isFreeLlm = matched?.mode === "llm" && matched.priceSubunits === 0;
|
|
2262
2297
|
let perCustomerLimiter;
|
|
2263
2298
|
let perSkillKey;
|
|
@@ -2278,8 +2313,8 @@ var AgentRuntime = class {
|
|
|
2278
2313
|
return;
|
|
2279
2314
|
}
|
|
2280
2315
|
}
|
|
2281
|
-
|
|
2282
|
-
|
|
2316
|
+
customerLimiter.check(job.customerId);
|
|
2317
|
+
globalLimiter.check(GLOBAL_LIMITER_KEY);
|
|
2283
2318
|
if (isFreeLlm && perCustomerLimiter && perSkillKey) {
|
|
2284
2319
|
this.freeLlmLimiters.globalLimiter.check(FREE_LLM_GLOBAL_KEY);
|
|
2285
2320
|
perCustomerLimiter.check(perSkillKey);
|
|
@@ -2313,8 +2348,10 @@ var AgentRuntime = class {
|
|
|
2313
2348
|
}
|
|
2314
2349
|
/** Drop expired hits from every sliding-window limiter. */
|
|
2315
2350
|
cleanupRateLimits() {
|
|
2316
|
-
this.
|
|
2317
|
-
this.
|
|
2351
|
+
this.freeCustomerLimiter.prune();
|
|
2352
|
+
this.freeGlobalLimiter.prune();
|
|
2353
|
+
this.paidCustomerLimiter.prune();
|
|
2354
|
+
this.paidGlobalLimiter.prune();
|
|
2318
2355
|
this.freeLlmLimiters.globalLimiter.prune();
|
|
2319
2356
|
this.freeLlmLimiters.prunePerCustomer();
|
|
2320
2357
|
}
|
|
@@ -2343,21 +2380,26 @@ var AgentRuntime = class {
|
|
|
2343
2380
|
}
|
|
2344
2381
|
this.transport.stop();
|
|
2345
2382
|
}
|
|
2346
|
-
/**
|
|
2383
|
+
/**
|
|
2384
|
+
* Resolve a job's execution budget in milliseconds. Per-skill
|
|
2385
|
+
* `executionTimeoutSecs` wins over the agent-level `config.executionTimeoutSecs`;
|
|
2386
|
+
* `0` (explicit unlimited) and `undefined` both collapse to `0` => no timer.
|
|
2387
|
+
*/
|
|
2388
|
+
resolveExecutionBudgetMs(skill) {
|
|
2389
|
+
const secs = skill.executionTimeoutSecs ?? this.config.executionTimeoutSecs ?? 0;
|
|
2390
|
+
return secs > 0 ? secs * 1e3 : 0;
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Wrapper with error handling. The execution budget (if any) is enforced
|
|
2394
|
+
* around `skill.execute` inside `executeJob`, not here - payment collection
|
|
2395
|
+
* and result delivery run on their own bounds. `jobAbort` is retained so
|
|
2396
|
+
* `stop()` can still abort an in-flight job.
|
|
2397
|
+
*/
|
|
2347
2398
|
async processJob(job) {
|
|
2348
|
-
let timeoutId;
|
|
2349
2399
|
const jobAbort = new AbortController();
|
|
2350
2400
|
this.jobAbortControllers.add(jobAbort);
|
|
2351
2401
|
try {
|
|
2352
|
-
await
|
|
2353
|
-
this.executeJob(job, jobAbort.signal),
|
|
2354
|
-
new Promise((_, reject) => {
|
|
2355
|
-
timeoutId = setTimeout(() => {
|
|
2356
|
-
jobAbort.abort();
|
|
2357
|
-
reject(new Error("Job processing timeout"));
|
|
2358
|
-
}, TOTAL_JOB_TIMEOUT_MS);
|
|
2359
|
-
})
|
|
2360
|
-
]);
|
|
2402
|
+
await this.executeJob(job, jobAbort.signal);
|
|
2361
2403
|
} catch (e) {
|
|
2362
2404
|
const log = this.callbacks.onLog ?? console.log;
|
|
2363
2405
|
log(`[${job.jobId.slice(0, 8)}] Error: ${e.message}`);
|
|
@@ -2383,7 +2425,6 @@ var AgentRuntime = class {
|
|
|
2383
2425
|
await this.transport.sendFeedback(job, { type: "error", message: safeMessage }).catch(() => {
|
|
2384
2426
|
});
|
|
2385
2427
|
} finally {
|
|
2386
|
-
clearTimeout(timeoutId);
|
|
2387
2428
|
this.jobAbortControllers.delete(jobAbort);
|
|
2388
2429
|
}
|
|
2389
2430
|
}
|
|
@@ -2445,6 +2486,21 @@ var AgentRuntime = class {
|
|
|
2445
2486
|
}
|
|
2446
2487
|
log(`[${job.jobId.slice(0, 8)}] Executing skill: ${skill.name}`);
|
|
2447
2488
|
let output;
|
|
2489
|
+
const budgetMs = this.resolveExecutionBudgetMs(skill);
|
|
2490
|
+
let budgetExceeded = false;
|
|
2491
|
+
const execAbort = new AbortController();
|
|
2492
|
+
const onOuterAbort = () => execAbort.abort();
|
|
2493
|
+
if (signal) {
|
|
2494
|
+
if (signal.aborted) {
|
|
2495
|
+
execAbort.abort();
|
|
2496
|
+
} else {
|
|
2497
|
+
signal.addEventListener("abort", onOuterAbort);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
const budgetTimer = budgetMs > 0 ? setTimeout(() => {
|
|
2501
|
+
budgetExceeded = true;
|
|
2502
|
+
execAbort.abort();
|
|
2503
|
+
}, budgetMs) : void 0;
|
|
2448
2504
|
try {
|
|
2449
2505
|
output = await skill.execute(
|
|
2450
2506
|
{
|
|
@@ -2453,14 +2509,25 @@ var AgentRuntime = class {
|
|
|
2453
2509
|
tags: job.tags,
|
|
2454
2510
|
jobId: job.jobId
|
|
2455
2511
|
},
|
|
2456
|
-
{ ...this.skillCtx, signal }
|
|
2512
|
+
{ ...this.skillCtx, signal: execAbort.signal }
|
|
2457
2513
|
);
|
|
2458
2514
|
} catch (err) {
|
|
2515
|
+
if (budgetExceeded) {
|
|
2516
|
+
log(`[${job.jobId.slice(0, 8)}] Execution exceeded budget (${budgetMs / 1e3}s)`);
|
|
2517
|
+
throw new ExecutionBudgetExceededError(budgetMs);
|
|
2518
|
+
}
|
|
2459
2519
|
const flippedToUnhealthy = this.markHealthFromExecuteError(skill, err, log, job.jobId);
|
|
2460
2520
|
if (flippedToUnhealthy) {
|
|
2461
2521
|
throw new AgentUnavailableError();
|
|
2462
2522
|
}
|
|
2463
2523
|
throw err;
|
|
2524
|
+
} finally {
|
|
2525
|
+
if (budgetTimer) {
|
|
2526
|
+
clearTimeout(budgetTimer);
|
|
2527
|
+
}
|
|
2528
|
+
if (signal) {
|
|
2529
|
+
signal.removeEventListener("abort", onOuterAbort);
|
|
2530
|
+
}
|
|
2464
2531
|
}
|
|
2465
2532
|
this.ledger.markExecuted(job.jobId, output.data);
|
|
2466
2533
|
log(`[${job.jobId.slice(0, 8)}] Skill completed, delivering result`);
|
|
@@ -2708,7 +2775,6 @@ var AgentRuntime = class {
|
|
|
2708
2775
|
async recoverSingleJob(entry, log) {
|
|
2709
2776
|
const recoveryAbort = new AbortController();
|
|
2710
2777
|
this.jobAbortControllers.add(recoveryAbort);
|
|
2711
|
-
const timeout = setTimeout(() => recoveryAbort.abort(), TOTAL_JOB_TIMEOUT_MS);
|
|
2712
2778
|
try {
|
|
2713
2779
|
const rawEvent = JSON.parse(entry.raw_event_json);
|
|
2714
2780
|
const fakeJob = {
|
|
@@ -2765,22 +2831,30 @@ var AgentRuntime = class {
|
|
|
2765
2831
|
return;
|
|
2766
2832
|
}
|
|
2767
2833
|
}
|
|
2768
|
-
const
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2834
|
+
const recoveryBudgetMs = this.resolveExecutionBudgetMs(skill);
|
|
2835
|
+
const budgetTimer = recoveryBudgetMs > 0 ? setTimeout(() => recoveryAbort.abort(), recoveryBudgetMs) : void 0;
|
|
2836
|
+
let output;
|
|
2837
|
+
try {
|
|
2838
|
+
output = await skill.execute(
|
|
2839
|
+
{
|
|
2840
|
+
data: entry.input,
|
|
2841
|
+
inputType: entry.input_type,
|
|
2842
|
+
tags: entry.tags,
|
|
2843
|
+
jobId: entry.job_id
|
|
2844
|
+
},
|
|
2845
|
+
{ ...this.skillCtx, signal: recoveryAbort.signal }
|
|
2846
|
+
);
|
|
2847
|
+
} finally {
|
|
2848
|
+
if (budgetTimer) {
|
|
2849
|
+
clearTimeout(budgetTimer);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2777
2852
|
this.ledger.markExecuted(entry.job_id, output.data);
|
|
2778
2853
|
await this.transport.deliverResult(fakeJob, output.data, entry.net_amount);
|
|
2779
2854
|
this.ledger.markDelivered(entry.job_id);
|
|
2780
2855
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: re-executed and delivered`);
|
|
2781
2856
|
}
|
|
2782
2857
|
} finally {
|
|
2783
|
-
clearTimeout(timeout);
|
|
2784
2858
|
this.jobAbortControllers.delete(recoveryAbort);
|
|
2785
2859
|
}
|
|
2786
2860
|
}
|
|
@@ -3140,6 +3214,9 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3140
3214
|
if (parsed.rateLimit) {
|
|
3141
3215
|
skill.rateLimit = parsed.rateLimit;
|
|
3142
3216
|
}
|
|
3217
|
+
if (parsed.executionTimeoutSecs !== void 0) {
|
|
3218
|
+
skill.executionTimeoutSecs = parsed.executionTimeoutSecs;
|
|
3219
|
+
}
|
|
3143
3220
|
return skill;
|
|
3144
3221
|
}
|
|
3145
3222
|
function loadSkillsFromDir(skillsDir, options = {}) {
|
|
@@ -3917,7 +3994,10 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
3917
3994
|
recoveryMaxRetries: RECOVERY_MAX_RETRIES,
|
|
3918
3995
|
recoveryIntervalSecs: RECOVERY_INTERVAL_SECS,
|
|
3919
3996
|
network: walletNetwork,
|
|
3920
|
-
solanaAddress
|
|
3997
|
+
solanaAddress,
|
|
3998
|
+
// Agent-level default execution budget; per-skill `max_execution_secs`
|
|
3999
|
+
// overrides it. Undefined => unlimited (operator-owned, no protocol default).
|
|
4000
|
+
executionTimeoutSecs: loaded.yaml.execution_timeout_secs
|
|
3921
4001
|
};
|
|
3922
4002
|
const rpcUrlForLog = stripRpcSecrets(process.env.SOLANA_RPC_URL ?? getRpcUrl());
|
|
3923
4003
|
logger.debug(
|