@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 +2 -0
- package/dist/index.js +80 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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(
|