@elisym/cli 0.12.0 → 0.14.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/README.md CHANGED
@@ -127,6 +127,8 @@ my-project/
127
127
 
128
128
  Skills are defined in `SKILL.md` files inside `./skills/<skill-name>/`. Each file has YAML frontmatter (between `---` delimiters) that describes the skill, followed by a markdown body that becomes the LLM system prompt.
129
129
 
130
+ > Canonical reference for every frontmatter field, execution mode, the LLM health-monitor contract, and the script exit-code convention lives in [`SKILLS.md`](./SKILLS.md). The summary below covers the common cases; consult `SKILLS.md` for the full schema.
131
+
130
132
  ```markdown
131
133
  ---
132
134
  name: youtube-summary
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools
8
8
  import YAML from 'yaml';
9
9
  import { Command } from 'commander';
10
10
  import { createHash } from 'node:crypto';
11
- import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, ScriptBillingExhaustedError, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey } from '@elisym/sdk/llm-health';
11
+ import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, ScriptBillingExhaustedError, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey, LlmHealthError } from '@elisym/sdk/llm-health';
12
12
  import { lookup } from 'node:dns/promises';
13
13
  import { Socket } from 'node:net';
14
14
  import pino from 'pino';
@@ -1959,6 +1959,7 @@ var payment = new SolanaPaymentStrategy();
1959
1959
  var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
1960
1960
  var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
1961
1961
  var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
1962
+ var MAX_PAID_AGE_MS = 24 * 60 * 60 * 1e3;
1962
1963
  var SIG_PATH_TIMEOUT_MS = 60 * 1e3;
1963
1964
  function resolveJobPrice(tags, skills) {
1964
1965
  const skill = skills.route(tags);
@@ -1969,6 +1970,13 @@ function resolveJobAsset(tags, skills) {
1969
1970
  return skill?.asset ?? NATIVE_SOL;
1970
1971
  }
1971
1972
  var BILLING_BODY_MARKERS3 = ["credit balance", "billing", "insufficient", "insufficient_quota"];
1973
+ var AGENT_UNAVAILABLE_MESSAGE = "Agent temporarily unavailable";
1974
+ var AgentUnavailableError = class extends Error {
1975
+ constructor() {
1976
+ super(AGENT_UNAVAILABLE_MESSAGE);
1977
+ this.name = "AgentUnavailableError";
1978
+ }
1979
+ };
1972
1980
  function bodyLooksLikeBilling3(body) {
1973
1981
  const lower = body.toLowerCase();
1974
1982
  return BILLING_BODY_MARKERS3.some((marker) => lower.includes(marker));
@@ -2064,7 +2072,7 @@ var AgentRuntime = class {
2064
2072
  */
2065
2073
  markHealthFromExecuteError(skill, err, log, jobId) {
2066
2074
  if (!this.healthMonitor) {
2067
- return;
2075
+ return false;
2068
2076
  }
2069
2077
  const tag = `[${jobId.slice(0, 8)}]`;
2070
2078
  if (err instanceof ScriptBillingExhaustedError) {
@@ -2074,19 +2082,19 @@ var AgentRuntime = class {
2074
2082
  log(
2075
2083
  `${tag} Script returned exit ${err.exitCode} (billing-exhausted) but skill "${skill.name}" did not declare provider/model in SKILL.md - cannot gate future jobs.`
2076
2084
  );
2077
- return;
2085
+ return false;
2078
2086
  }
2079
2087
  log(
2080
2088
  `${tag} Script signaled billing-exhausted (exit ${err.exitCode}). Marking ${provider}/${model} unhealthy; future jobs against this pair will be refused until recovery probe succeeds.`
2081
2089
  );
2082
2090
  this.healthMonitor.markUnhealthyFromJob(provider, model, "billing", err.message);
2083
- return;
2091
+ return true;
2084
2092
  }
2085
2093
  if (skill.mode === "llm" && skill.resolvedTriple) {
2086
2094
  const message = err instanceof Error ? err.message : String(err);
2087
2095
  const match = /API error:\s*(\d{3})\b\s*(.*)/i.exec(message);
2088
2096
  if (!match) {
2089
- return;
2097
+ return false;
2090
2098
  }
2091
2099
  const status = Number(match[1]);
2092
2100
  const body = (match[2] ?? "").slice(0, 200);
@@ -2095,7 +2103,7 @@ var AgentRuntime = class {
2095
2103
  const isBilling400 = status === 400 && bodyLooksLikeBilling3(body);
2096
2104
  const isBilling429 = status === 429 && bodyLooksLikeBilling3(body);
2097
2105
  if (!isBillingStatus && !isAuthStatus && !isBilling400 && !isBilling429) {
2098
- return;
2106
+ return false;
2099
2107
  }
2100
2108
  const reason = isAuthStatus ? "invalid" : "billing";
2101
2109
  const provider = skill.resolvedTriple.provider;
@@ -2104,7 +2112,9 @@ var AgentRuntime = class {
2104
2112
  `${tag} LLM provider returned HTTP ${status} (${reason}). Marking ${provider}/${model} unhealthy; future jobs against this pair will be refused until recovery probe succeeds.`
2105
2113
  );
2106
2114
  this.healthMonitor.markUnhealthyFromJob(provider, model, reason, body);
2115
+ return true;
2107
2116
  }
2117
+ return false;
2108
2118
  }
2109
2119
  /** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
2110
2120
  async fetchProtocolConfig() {
@@ -2267,11 +2277,24 @@ var AgentRuntime = class {
2267
2277
  const log = this.callbacks.onLog ?? console.log;
2268
2278
  log(`[${job.jobId.slice(0, 8)}] Error: ${e.message}`);
2269
2279
  const currentStatus = this.ledger.getStatus(job.jobId);
2270
- if (currentStatus !== "executed") {
2280
+ const keepPaidForRecovery = e instanceof AgentUnavailableError && currentStatus === "paid";
2281
+ if (currentStatus !== "executed" && !keepPaidForRecovery) {
2271
2282
  this.ledger.markFailed(job.jobId);
2272
2283
  }
2284
+ if (keepPaidForRecovery) {
2285
+ log(
2286
+ `[${job.jobId.slice(0, 8)}] Keeping status=paid; recovery will re-execute when LLM pair recovers (24h cutoff).`
2287
+ );
2288
+ }
2273
2289
  this.callbacks.onJobError?.(job.jobId, e.message);
2274
- const safeMessage = e.message?.includes("API") ? "Internal processing error" : e.message ?? "Unknown error";
2290
+ let safeMessage;
2291
+ if (e instanceof AgentUnavailableError) {
2292
+ safeMessage = e.message;
2293
+ } else if (e.message?.includes("API")) {
2294
+ safeMessage = "Internal processing error";
2295
+ } else {
2296
+ safeMessage = e.message ?? "Unknown error";
2297
+ }
2275
2298
  await this.transport.sendFeedback(job, { type: "error", message: safeMessage }).catch(() => {
2276
2299
  });
2277
2300
  } finally {
@@ -2295,7 +2318,7 @@ var AgentRuntime = class {
2295
2318
  log(`[${job.jobId.slice(0, 8)}] LLM health gate refused job: ${detail}`);
2296
2319
  await this.transport.sendFeedback(job, {
2297
2320
  type: "error",
2298
- message: "Service temporarily unavailable, try again later"
2321
+ message: AGENT_UNAVAILABLE_MESSAGE
2299
2322
  }).catch(() => {
2300
2323
  });
2301
2324
  return;
@@ -2348,7 +2371,10 @@ var AgentRuntime = class {
2348
2371
  { ...this.skillCtx, signal }
2349
2372
  );
2350
2373
  } catch (err) {
2351
- this.markHealthFromExecuteError(skill, err, log, job.jobId);
2374
+ const flippedToUnhealthy = this.markHealthFromExecuteError(skill, err, log, job.jobId);
2375
+ if (flippedToUnhealthy) {
2376
+ throw new AgentUnavailableError();
2377
+ }
2352
2378
  throw err;
2353
2379
  }
2354
2380
  this.ledger.markExecuted(job.jobId, output.data);
@@ -2524,9 +2550,35 @@ var AgentRuntime = class {
2524
2550
  }
2525
2551
  const log = this.callbacks.onLog ?? console.log;
2526
2552
  log(`Recovering ${pending.length} pending jobs...`);
2553
+ if (this.healthMonitor) {
2554
+ const snap = this.healthMonitor.snapshot();
2555
+ const unhealthyKeys = new Set(
2556
+ snap.filter(
2557
+ (entry) => entry.status === "invalid" || entry.status === "billing" || entry.status === "unavailable"
2558
+ ).map((entry) => `${entry.provider}::${entry.model}`)
2559
+ );
2560
+ if (unhealthyKeys.size > 0) {
2561
+ const hasPaidOnUnhealthy = pending.some((entry) => {
2562
+ if (entry.status !== "paid") {
2563
+ return false;
2564
+ }
2565
+ const skill = this.skills.route(entry.tags);
2566
+ const pair = resolveHealthPair(skill);
2567
+ return pair !== null && unhealthyKeys.has(`${pair.provider}::${pair.model}`);
2568
+ });
2569
+ if (hasPaidOnUnhealthy) {
2570
+ void this.healthMonitor.refreshUnhealthy().catch(() => {
2571
+ });
2572
+ }
2573
+ }
2574
+ }
2527
2575
  for (const entry of pending) {
2528
- if (entry.retry_count >= this.config.recoveryMaxRetries) {
2576
+ const ageMs = (Math.floor(Date.now() / 1e3) - entry.created_at) * 1e3;
2577
+ const expired = ageMs > MAX_PAID_AGE_MS;
2578
+ const exhaustedRetries = entry.retry_count >= this.config.recoveryMaxRetries;
2579
+ if (expired || exhaustedRetries) {
2529
2580
  this.ledger.markFailed(entry.job_id);
2581
+ const reason = expired ? "Job permanently failed: agent did not recover within 24 hours" : "Job permanently failed after maximum retries";
2530
2582
  if (entry.raw_event_json) {
2531
2583
  try {
2532
2584
  const rawEvent = JSON.parse(entry.raw_event_json);
@@ -2540,7 +2592,7 @@ var AgentRuntime = class {
2540
2592
  encrypted: false,
2541
2593
  rawEvent
2542
2594
  },
2543
- { type: "error", message: "Job permanently failed after maximum retries" }
2595
+ { type: "error", message: reason }
2544
2596
  ).catch(() => {
2545
2597
  });
2546
2598
  } catch {
@@ -2573,7 +2625,6 @@ var AgentRuntime = class {
2573
2625
  this.jobAbortControllers.add(recoveryAbort);
2574
2626
  const timeout = setTimeout(() => recoveryAbort.abort(), TOTAL_JOB_TIMEOUT_MS);
2575
2627
  try {
2576
- this.ledger.incrementRetry(entry.job_id);
2577
2628
  const rawEvent = JSON.parse(entry.raw_event_json);
2578
2629
  const fakeJob = {
2579
2630
  jobId: entry.job_id,
@@ -2585,6 +2636,7 @@ var AgentRuntime = class {
2585
2636
  rawEvent
2586
2637
  };
2587
2638
  if (entry.status === "executed" && entry.result !== void 0) {
2639
+ this.ledger.incrementRetry(entry.job_id);
2588
2640
  await this.transport.deliverResult(fakeJob, entry.result, entry.net_amount);
2589
2641
  this.ledger.markDelivered(entry.job_id);
2590
2642
  log(`[${entry.job_id.slice(0, 8)}] Recovery: re-delivered`);
@@ -2595,6 +2647,21 @@ var AgentRuntime = class {
2595
2647
  this.ledger.markFailed(entry.job_id);
2596
2648
  return;
2597
2649
  }
2650
+ const healthPair = resolveHealthPair(skill);
2651
+ if (this.healthMonitor && healthPair) {
2652
+ try {
2653
+ await this.healthMonitor.assertReady(healthPair.provider, healthPair.model);
2654
+ } catch (err) {
2655
+ if (err instanceof LlmHealthError) {
2656
+ log(
2657
+ `[${entry.job_id.slice(0, 8)}] Recovery: pair ${healthPair.provider}/${healthPair.model} still unhealthy (${err.reason}); waiting for recovery probe.`
2658
+ );
2659
+ return;
2660
+ }
2661
+ throw err;
2662
+ }
2663
+ }
2664
+ this.ledger.incrementRetry(entry.job_id);
2598
2665
  if (skill.priceSubunits > 0 && !entry.net_amount) {
2599
2666
  if (entry.payment_request) {
2600
2667
  const verified = await this.reVerifyPayment(