@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 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
- LLM_TIMEOUT_MS = 12e4;
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 MAX_JOBS_PER_CUSTOMER = 20;
2039
- var GLOBAL_MAX_JOBS_PER_WINDOW = 200;
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 (keyed on customer pubkey). */
2064
- customerLimiter = createSlidingWindowLimiter({
2079
+ /** Per-customer sliding-window rate limiter for free skills. */
2080
+ freeCustomerLimiter = createSlidingWindowLimiter({
2065
2081
  windowMs: RATE_LIMIT_WINDOW_MS,
2066
- maxPerWindow: MAX_JOBS_PER_CUSTOMER,
2082
+ maxPerWindow: FREE_MAX_JOBS_PER_CUSTOMER,
2067
2083
  maxKeys: MAX_TRACKED_CUSTOMERS
2068
2084
  });
2069
- /** Global sliding-window rate limiter (Sybil protection). */
2070
- globalLimiter = createSlidingWindowLimiter({
2085
+ /** Global sliding-window rate limiter for free skills (Sybil protection). */
2086
+ freeGlobalLimiter = createSlidingWindowLimiter({
2071
2087
  windowMs: RATE_LIMIT_WINDOW_MS,
2072
- maxPerWindow: GLOBAL_MAX_JOBS_PER_WINDOW,
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
- if (!this.customerLimiter.peek(job.customerId).allowed) {
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 (!this.globalLimiter.peek(GLOBAL_LIMITER_KEY).allowed) {
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
- this.customerLimiter.check(job.customerId);
2282
- this.globalLimiter.check(GLOBAL_LIMITER_KEY);
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.customerLimiter.prune();
2317
- this.globalLimiter.prune();
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
- /** Wrapper with total job timeout and error handling. */
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 Promise.race([
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 output = await skill.execute(
2769
- {
2770
- data: entry.input,
2771
- inputType: entry.input_type,
2772
- tags: entry.tags,
2773
- jobId: entry.job_id
2774
- },
2775
- { ...this.skillCtx, signal: recoveryAbort.signal }
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(