@elisym/cli 0.10.0 → 0.11.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 +239 -34
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -8,6 +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, startLlmHeartbeat, createFreeLlmLimiterSet, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey } from '@elisym/sdk/llm-health';
|
|
11
12
|
import { lookup } from 'node:dns/promises';
|
|
12
13
|
import { Socket } from 'node:net';
|
|
13
14
|
import pino from 'pino';
|
|
@@ -149,7 +150,54 @@ async function verifyKey(apiKey, signal) {
|
|
|
149
150
|
return { ok: false, reason: "unavailable", error: message };
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
+
function bodyLooksLikeBilling(body) {
|
|
154
|
+
const lower = body.toLowerCase();
|
|
155
|
+
return BILLING_BODY_MARKERS.some((marker) => lower.includes(marker));
|
|
156
|
+
}
|
|
157
|
+
async function verifyKeyDeep(apiKey, model, signal) {
|
|
158
|
+
try {
|
|
159
|
+
const response = await fetchWithTimeout(
|
|
160
|
+
"https://api.anthropic.com/v1/messages",
|
|
161
|
+
{
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
"x-api-key": apiKey,
|
|
166
|
+
"anthropic-version": "2023-06-01"
|
|
167
|
+
},
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
model,
|
|
170
|
+
max_tokens: 1,
|
|
171
|
+
messages: [{ role: "user", content: "." }]
|
|
172
|
+
})
|
|
173
|
+
},
|
|
174
|
+
signal
|
|
175
|
+
);
|
|
176
|
+
if (response.ok) {
|
|
177
|
+
await response.body?.cancel().catch(() => void 0);
|
|
178
|
+
return { ok: true };
|
|
179
|
+
}
|
|
180
|
+
const body = (await response.text().catch(() => "")).slice(0, 500);
|
|
181
|
+
if (response.status === 401 || response.status === 403) {
|
|
182
|
+
return { ok: false, reason: "invalid", status: response.status, body };
|
|
183
|
+
}
|
|
184
|
+
if (response.status === 402) {
|
|
185
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
186
|
+
}
|
|
187
|
+
if (response.status === 400 && bodyLooksLikeBilling(body)) {
|
|
188
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
reason: "unavailable",
|
|
193
|
+
error: `HTTP ${response.status}: ${body.slice(0, 200)}`
|
|
194
|
+
};
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
return { ok: false, reason: "unavailable", error: message };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
var DEFAULT_MODEL, DEFAULT_MAX_TOKENS, FALLBACK_MODELS, AnthropicClient, BILLING_BODY_MARKERS, ANTHROPIC_PROVIDER;
|
|
153
201
|
var init_anthropic = __esm({
|
|
154
202
|
"src/llm/providers/anthropic.ts"() {
|
|
155
203
|
init_http();
|
|
@@ -270,6 +318,7 @@ var init_anthropic = __esm({
|
|
|
270
318
|
];
|
|
271
319
|
}
|
|
272
320
|
};
|
|
321
|
+
BILLING_BODY_MARKERS = ["credit balance", "billing", "insufficient"];
|
|
273
322
|
ANTHROPIC_PROVIDER = {
|
|
274
323
|
id: "anthropic",
|
|
275
324
|
displayName: "Anthropic (Claude)",
|
|
@@ -278,6 +327,7 @@ var init_anthropic = __esm({
|
|
|
278
327
|
fallbackModels: FALLBACK_MODELS,
|
|
279
328
|
fetchModels,
|
|
280
329
|
verifyKey,
|
|
330
|
+
verifyKeyDeep,
|
|
281
331
|
createClient: (config) => new AnthropicClient({
|
|
282
332
|
apiKey: config.apiKey,
|
|
283
333
|
model: config.model ?? DEFAULT_MODEL,
|
|
@@ -342,7 +392,57 @@ async function verifyKey2(apiKey, signal) {
|
|
|
342
392
|
return { ok: false, reason: "unavailable", error: message };
|
|
343
393
|
}
|
|
344
394
|
}
|
|
345
|
-
|
|
395
|
+
function bodyLooksLikeBilling2(body) {
|
|
396
|
+
const lower = body.toLowerCase();
|
|
397
|
+
return BILLING_BODY_MARKERS2.some((marker) => lower.includes(marker));
|
|
398
|
+
}
|
|
399
|
+
async function verifyKeyDeep2(apiKey, model, signal) {
|
|
400
|
+
try {
|
|
401
|
+
const reasoning = isOpenAIReasoningModel(model);
|
|
402
|
+
const response = await fetchWithTimeout(
|
|
403
|
+
"https://api.openai.com/v1/chat/completions",
|
|
404
|
+
{
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: {
|
|
407
|
+
"Content-Type": "application/json",
|
|
408
|
+
Authorization: `Bearer ${apiKey}`
|
|
409
|
+
},
|
|
410
|
+
body: JSON.stringify({
|
|
411
|
+
model,
|
|
412
|
+
...reasoning ? { max_completion_tokens: 1 } : { max_tokens: 1 },
|
|
413
|
+
messages: [{ role: "user", content: "." }]
|
|
414
|
+
})
|
|
415
|
+
},
|
|
416
|
+
signal
|
|
417
|
+
);
|
|
418
|
+
if (response.ok) {
|
|
419
|
+
await response.body?.cancel().catch(() => void 0);
|
|
420
|
+
return { ok: true };
|
|
421
|
+
}
|
|
422
|
+
const body = (await response.text().catch(() => "")).slice(0, 500);
|
|
423
|
+
if (response.status === 401 || response.status === 403) {
|
|
424
|
+
return { ok: false, reason: "invalid", status: response.status, body };
|
|
425
|
+
}
|
|
426
|
+
if (response.status === 402) {
|
|
427
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
428
|
+
}
|
|
429
|
+
if (response.status === 429 && bodyLooksLikeBilling2(body)) {
|
|
430
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
431
|
+
}
|
|
432
|
+
if (response.status === 400 && bodyLooksLikeBilling2(body)) {
|
|
433
|
+
return { ok: false, reason: "billing", status: response.status, body };
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
reason: "unavailable",
|
|
438
|
+
error: `HTTP ${response.status}: ${body.slice(0, 200)}`
|
|
439
|
+
};
|
|
440
|
+
} catch (error) {
|
|
441
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
442
|
+
return { ok: false, reason: "unavailable", error: message };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
var DEFAULT_MODEL2, DEFAULT_MAX_TOKENS2, FALLBACK_MODELS2, OpenAIClient, BILLING_BODY_MARKERS2, OPENAI_PROVIDER;
|
|
346
446
|
var init_openai = __esm({
|
|
347
447
|
"src/llm/providers/openai.ts"() {
|
|
348
448
|
init_http();
|
|
@@ -466,6 +566,7 @@ var init_openai = __esm({
|
|
|
466
566
|
}));
|
|
467
567
|
}
|
|
468
568
|
};
|
|
569
|
+
BILLING_BODY_MARKERS2 = ["credit balance", "billing", "insufficient_quota", "insufficient"];
|
|
469
570
|
OPENAI_PROVIDER = {
|
|
470
571
|
id: "openai",
|
|
471
572
|
displayName: "OpenAI (GPT)",
|
|
@@ -474,6 +575,7 @@ var init_openai = __esm({
|
|
|
474
575
|
fallbackModels: FALLBACK_MODELS2,
|
|
475
576
|
fetchModels: fetchModels2,
|
|
476
577
|
verifyKey: verifyKey2,
|
|
578
|
+
verifyKeyDeep: verifyKeyDeep2,
|
|
477
579
|
createClient: (config) => new OpenAIClient({
|
|
478
580
|
apiKey: config.apiKey,
|
|
479
581
|
model: config.model ?? DEFAULT_MODEL2,
|
|
@@ -526,7 +628,7 @@ function createLlmClient(config) {
|
|
|
526
628
|
logUsage: config.logUsage
|
|
527
629
|
});
|
|
528
630
|
}
|
|
529
|
-
async function
|
|
631
|
+
async function verifyLlmApiKeyDeep(provider, apiKey, model, signal) {
|
|
530
632
|
const descriptor = getLlmProvider(provider);
|
|
531
633
|
if (!descriptor) {
|
|
532
634
|
return {
|
|
@@ -535,7 +637,7 @@ async function verifyLlmApiKey(provider, apiKey, signal) {
|
|
|
535
637
|
error: `Unknown LLM provider "${provider}"`
|
|
536
638
|
};
|
|
537
639
|
}
|
|
538
|
-
return descriptor.
|
|
640
|
+
return descriptor.verifyKeyDeep(apiKey, model, signal);
|
|
539
641
|
}
|
|
540
642
|
var init_llm = __esm({
|
|
541
643
|
"src/llm/index.ts"() {
|
|
@@ -1546,13 +1648,14 @@ var GLOBAL_MAX_JOBS_PER_WINDOW = 200;
|
|
|
1546
1648
|
var MAX_TRACKED_CUSTOMERS = 1e3;
|
|
1547
1649
|
var GLOBAL_LIMITER_KEY = "__global__";
|
|
1548
1650
|
var AgentRuntime = class {
|
|
1549
|
-
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}) {
|
|
1651
|
+
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor) {
|
|
1550
1652
|
this.transport = transport;
|
|
1551
1653
|
this.skills = skills;
|
|
1552
1654
|
this.skillCtx = skillCtx;
|
|
1553
1655
|
this.config = config;
|
|
1554
1656
|
this.ledger = ledger;
|
|
1555
1657
|
this.callbacks = callbacks;
|
|
1658
|
+
this.healthMonitor = healthMonitor;
|
|
1556
1659
|
this.limit = pLimit(config.maxConcurrentJobs);
|
|
1557
1660
|
this.maxQueueSize = config.maxQueueSize ?? config.maxConcurrentJobs * 10;
|
|
1558
1661
|
}
|
|
@@ -1577,6 +1680,13 @@ var AgentRuntime = class {
|
|
|
1577
1680
|
maxPerWindow: GLOBAL_MAX_JOBS_PER_WINDOW,
|
|
1578
1681
|
maxKeys: 1
|
|
1579
1682
|
});
|
|
1683
|
+
/**
|
|
1684
|
+
* Two-tier limiter applied only to free LLM skills (mode='llm', price=0).
|
|
1685
|
+
* Existing per-customer + global limiters above remain in effect for all
|
|
1686
|
+
* jobs; this set adds tighter caps to prevent unpaid spam from draining
|
|
1687
|
+
* the operator's API key.
|
|
1688
|
+
*/
|
|
1689
|
+
freeLlmLimiters = createFreeLlmLimiterSet();
|
|
1580
1690
|
/** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
|
|
1581
1691
|
async fetchProtocolConfig() {
|
|
1582
1692
|
if (this.config.network !== "devnet") {
|
|
@@ -1633,8 +1743,33 @@ var AgentRuntime = class {
|
|
|
1633
1743
|
});
|
|
1634
1744
|
return;
|
|
1635
1745
|
}
|
|
1746
|
+
const matched = this.skills.route(job.tags);
|
|
1747
|
+
const isFreeLlm = matched?.mode === "llm" && matched.priceSubunits === 0;
|
|
1748
|
+
let perCustomerLimiter;
|
|
1749
|
+
let perSkillKey;
|
|
1750
|
+
if (isFreeLlm && matched) {
|
|
1751
|
+
if (!this.freeLlmLimiters.globalLimiter.peek(FREE_LLM_GLOBAL_KEY).allowed) {
|
|
1752
|
+
this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
|
|
1753
|
+
});
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
perCustomerLimiter = this.freeLlmLimiters.getPerCustomerLimiter(
|
|
1757
|
+
matched.name,
|
|
1758
|
+
matched.rateLimit
|
|
1759
|
+
);
|
|
1760
|
+
perSkillKey = freeLlmCustomerKey(job.customerId, matched.name);
|
|
1761
|
+
if (!perCustomerLimiter.peek(perSkillKey).allowed) {
|
|
1762
|
+
this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
|
|
1763
|
+
});
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1636
1767
|
this.customerLimiter.check(job.customerId);
|
|
1637
1768
|
this.globalLimiter.check(GLOBAL_LIMITER_KEY);
|
|
1769
|
+
if (isFreeLlm && perCustomerLimiter && perSkillKey) {
|
|
1770
|
+
this.freeLlmLimiters.globalLimiter.check(FREE_LLM_GLOBAL_KEY);
|
|
1771
|
+
perCustomerLimiter.check(perSkillKey);
|
|
1772
|
+
}
|
|
1638
1773
|
this.callbacks.onJobReceived?.(job);
|
|
1639
1774
|
this.inFlight.add(job.jobId);
|
|
1640
1775
|
this.pending++;
|
|
@@ -1662,10 +1797,12 @@ var AgentRuntime = class {
|
|
|
1662
1797
|
process.on("SIGTERM", onSignal);
|
|
1663
1798
|
});
|
|
1664
1799
|
}
|
|
1665
|
-
/** Drop expired hits from
|
|
1800
|
+
/** Drop expired hits from every sliding-window limiter. */
|
|
1666
1801
|
cleanupRateLimits() {
|
|
1667
1802
|
this.customerLimiter.prune();
|
|
1668
1803
|
this.globalLimiter.prune();
|
|
1804
|
+
this.freeLlmLimiters.globalLimiter.prune();
|
|
1805
|
+
this.freeLlmLimiters.prunePerCustomer();
|
|
1669
1806
|
}
|
|
1670
1807
|
stop() {
|
|
1671
1808
|
if (this.stopped) {
|
|
@@ -1729,6 +1866,24 @@ var AgentRuntime = class {
|
|
|
1729
1866
|
if (job.input.length > LIMITS.MAX_INPUT_LENGTH) {
|
|
1730
1867
|
throw new Error(`Input too long: ${job.input.length} chars (max ${LIMITS.MAX_INPUT_LENGTH})`);
|
|
1731
1868
|
}
|
|
1869
|
+
const matched = this.skills.route(job.tags);
|
|
1870
|
+
if (this.healthMonitor && matched && matched.mode === "llm" && matched.resolvedTriple) {
|
|
1871
|
+
try {
|
|
1872
|
+
await this.healthMonitor.assertReady(
|
|
1873
|
+
matched.resolvedTriple.provider,
|
|
1874
|
+
matched.resolvedTriple.model
|
|
1875
|
+
);
|
|
1876
|
+
} catch (err) {
|
|
1877
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1878
|
+
log(`[${job.jobId.slice(0, 8)}] LLM health gate refused job: ${detail}`);
|
|
1879
|
+
await this.transport.sendFeedback(job, {
|
|
1880
|
+
type: "error",
|
|
1881
|
+
message: "Service temporarily unavailable, try again later"
|
|
1882
|
+
}).catch(() => {
|
|
1883
|
+
});
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1732
1887
|
const jobPrice = resolveJobPrice(job.tags, this.skills);
|
|
1733
1888
|
const jobAsset = resolveJobAsset(job.tags, this.skills);
|
|
1734
1889
|
let netAmount;
|
|
@@ -2325,9 +2480,10 @@ var ScriptSkill = class {
|
|
|
2325
2480
|
|
|
2326
2481
|
// src/skill/loader.ts
|
|
2327
2482
|
function buildCliSkill(parsed, entryPath) {
|
|
2483
|
+
let skill;
|
|
2328
2484
|
switch (parsed.mode) {
|
|
2329
2485
|
case "llm":
|
|
2330
|
-
|
|
2486
|
+
skill = new ScriptSkill(
|
|
2331
2487
|
parsed.name,
|
|
2332
2488
|
parsed.description,
|
|
2333
2489
|
parsed.capabilities,
|
|
@@ -2341,6 +2497,7 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2341
2497
|
parsed.maxToolRounds,
|
|
2342
2498
|
parsed.llmOverride
|
|
2343
2499
|
);
|
|
2500
|
+
break;
|
|
2344
2501
|
case "static-file": {
|
|
2345
2502
|
if (parsed.outputFile === void 0) {
|
|
2346
2503
|
throw new Error(
|
|
@@ -2353,7 +2510,7 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2353
2510
|
`SKILL.md "${parsed.name}": "output_file" must stay inside the skill directory`
|
|
2354
2511
|
);
|
|
2355
2512
|
}
|
|
2356
|
-
|
|
2513
|
+
skill = new StaticFileSkill({
|
|
2357
2514
|
name: parsed.name,
|
|
2358
2515
|
description: parsed.description,
|
|
2359
2516
|
capabilities: parsed.capabilities,
|
|
@@ -2364,6 +2521,7 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2364
2521
|
imageFile: parsed.imageFile,
|
|
2365
2522
|
dir: entryPath
|
|
2366
2523
|
});
|
|
2524
|
+
break;
|
|
2367
2525
|
}
|
|
2368
2526
|
case "static-script":
|
|
2369
2527
|
case "dynamic-script": {
|
|
@@ -2377,7 +2535,7 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2377
2535
|
throw new Error(`SKILL.md "${parsed.name}": "script" must stay inside the skill directory`);
|
|
2378
2536
|
}
|
|
2379
2537
|
const Ctor = parsed.mode === "static-script" ? StaticScriptSkill : DynamicScriptSkill;
|
|
2380
|
-
|
|
2538
|
+
skill = new Ctor({
|
|
2381
2539
|
name: parsed.name,
|
|
2382
2540
|
description: parsed.description,
|
|
2383
2541
|
capabilities: parsed.capabilities,
|
|
@@ -2390,8 +2548,13 @@ function buildCliSkill(parsed, entryPath) {
|
|
|
2390
2548
|
imageFile: parsed.imageFile,
|
|
2391
2549
|
dir: entryPath
|
|
2392
2550
|
});
|
|
2551
|
+
break;
|
|
2393
2552
|
}
|
|
2394
2553
|
}
|
|
2554
|
+
if (parsed.rateLimit) {
|
|
2555
|
+
skill.rateLimit = parsed.rateLimit;
|
|
2556
|
+
}
|
|
2557
|
+
return skill;
|
|
2395
2558
|
}
|
|
2396
2559
|
function loadSkillsFromDir(skillsDir) {
|
|
2397
2560
|
const skills = [];
|
|
@@ -2797,6 +2960,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2797
2960
|
const dependentSkillsByProvider = /* @__PURE__ */ new Map();
|
|
2798
2961
|
let agentDefaultCacheKey;
|
|
2799
2962
|
const llmClientCache = /* @__PURE__ */ new Map();
|
|
2963
|
+
const healthMonitor = new LlmHealthMonitor();
|
|
2800
2964
|
if (llmSkills.length > 0) {
|
|
2801
2965
|
const resolutionErrors = [];
|
|
2802
2966
|
for (const skill of llmSkills) {
|
|
@@ -2811,6 +2975,11 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2811
2975
|
}
|
|
2812
2976
|
const cacheKey = cacheKeyFor(result);
|
|
2813
2977
|
triplesByKey.set(cacheKey, result);
|
|
2978
|
+
skill.resolvedTriple = {
|
|
2979
|
+
provider: result.provider,
|
|
2980
|
+
model: result.model,
|
|
2981
|
+
maxTokens: result.maxTokens
|
|
2982
|
+
};
|
|
2814
2983
|
const list = dependentSkillsByProvider.get(result.provider) ?? [];
|
|
2815
2984
|
list.push(skill.name);
|
|
2816
2985
|
dependentSkillsByProvider.set(result.provider, list);
|
|
@@ -2854,28 +3023,47 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2854
3023
|
console.error("");
|
|
2855
3024
|
process.exit(1);
|
|
2856
3025
|
}
|
|
2857
|
-
for (const
|
|
2858
|
-
const apiKey = keyByProvider.get(provider);
|
|
3026
|
+
for (const [, triple] of triplesByKey) {
|
|
3027
|
+
const apiKey = keyByProvider.get(triple.provider);
|
|
2859
3028
|
if (!apiKey) {
|
|
2860
3029
|
continue;
|
|
2861
3030
|
}
|
|
2862
|
-
const envVar = getLlmProvider(provider)?.envVar ?? `${provider.toUpperCase()}_API_KEY`;
|
|
2863
|
-
process.stdout.write(` Verifying ${provider}
|
|
2864
|
-
const verification = await
|
|
3031
|
+
const envVar = getLlmProvider(triple.provider)?.envVar ?? `${triple.provider.toUpperCase()}_API_KEY`;
|
|
3032
|
+
process.stdout.write(` Verifying ${triple.provider} ${triple.model}... `);
|
|
3033
|
+
const verification = await verifyLlmApiKeyDeep(triple.provider, apiKey, triple.model);
|
|
3034
|
+
const descriptor = getLlmProvider(triple.provider);
|
|
3035
|
+
const verifyFn = async (signal) => descriptor ? descriptor.verifyKeyDeep(apiKey, triple.model, signal) : { ok: false, reason: "unavailable", error: "no descriptor" };
|
|
3036
|
+
healthMonitor.register({
|
|
3037
|
+
provider: triple.provider,
|
|
3038
|
+
model: triple.model,
|
|
3039
|
+
verifyFn
|
|
3040
|
+
});
|
|
3041
|
+
healthMonitor.seed(triple.provider, triple.model, verification);
|
|
2865
3042
|
if (verification.ok) {
|
|
2866
3043
|
console.log("ok");
|
|
2867
3044
|
} else if (verification.reason === "invalid") {
|
|
2868
3045
|
console.log("INVALID");
|
|
2869
|
-
console.error(` ! ${provider} rejected the API key (HTTP ${verification.status}).`);
|
|
3046
|
+
console.error(` ! ${triple.provider} rejected the API key (HTTP ${verification.status}).`);
|
|
2870
3047
|
console.error(
|
|
2871
3048
|
` Update it via \`npx @elisym/cli profile ${agentName}\` or set ${envVar} to a valid key.
|
|
3049
|
+
`
|
|
3050
|
+
);
|
|
3051
|
+
process.exit(1);
|
|
3052
|
+
} else if (verification.reason === "billing") {
|
|
3053
|
+
console.log("BILLING");
|
|
3054
|
+
const detail = verification.body ? ` ${verification.body.slice(0, 200)}` : "";
|
|
3055
|
+
console.error(
|
|
3056
|
+
` ! ${triple.provider} reports a billing/quota issue for ${triple.model}.${detail}`
|
|
3057
|
+
);
|
|
3058
|
+
console.error(
|
|
3059
|
+
` Top up the account at the provider console, or set ${envVar} to a key on a funded org.
|
|
2872
3060
|
`
|
|
2873
3061
|
);
|
|
2874
3062
|
process.exit(1);
|
|
2875
3063
|
} else {
|
|
2876
3064
|
console.log("unavailable");
|
|
2877
3065
|
console.warn(
|
|
2878
|
-
` ! Could not verify ${provider}
|
|
3066
|
+
` ! Could not verify ${triple.provider} ${triple.model} (${verification.error}). Continuing - jobs will fail if the key is invalid.
|
|
2879
3067
|
`
|
|
2880
3068
|
);
|
|
2881
3069
|
}
|
|
@@ -3093,28 +3281,45 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
3093
3281
|
log: diagLog,
|
|
3094
3282
|
logger
|
|
3095
3283
|
});
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3284
|
+
let llmHeartbeat;
|
|
3285
|
+
if (llmSkills.length > 0) {
|
|
3286
|
+
llmHeartbeat = startLlmHeartbeat({
|
|
3287
|
+
monitor: healthMonitor,
|
|
3288
|
+
log: diagLog
|
|
3289
|
+
});
|
|
3290
|
+
diagLog("LLM health monitor armed (10min TTL, 10min heartbeat).");
|
|
3291
|
+
}
|
|
3292
|
+
const runtime = new AgentRuntime(
|
|
3293
|
+
transport,
|
|
3294
|
+
registry,
|
|
3295
|
+
skillCtx,
|
|
3296
|
+
runtimeConfig,
|
|
3297
|
+
ledger,
|
|
3298
|
+
{
|
|
3299
|
+
onJobReceived: (job) => {
|
|
3300
|
+
const cap = job.tags.find((t) => t !== "elisym") ?? "unknown";
|
|
3301
|
+
process.stdout.write(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}
|
|
3100
3302
|
`);
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3303
|
+
logger.info({ event: "job_received", jobId: job.jobId, capability: cap });
|
|
3304
|
+
},
|
|
3305
|
+
onJobCompleted: (jobId) => {
|
|
3306
|
+
process.stdout.write(` [job] ${jobId.slice(0, 16)} | delivered
|
|
3105
3307
|
`);
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3308
|
+
logger.info({ event: "job_delivered", jobId });
|
|
3309
|
+
},
|
|
3310
|
+
onJobError: (jobId, error) => {
|
|
3311
|
+
process.stderr.write(` [job] ${jobId.slice(0, 16)} | error: ${error}
|
|
3110
3312
|
`);
|
|
3111
|
-
|
|
3313
|
+
logger.error({ event: "job_error", jobId, error });
|
|
3314
|
+
},
|
|
3315
|
+
onLog: diagLog,
|
|
3316
|
+
onStop: () => {
|
|
3317
|
+
watchdog.stop();
|
|
3318
|
+
llmHeartbeat?.stop();
|
|
3319
|
+
}
|
|
3112
3320
|
},
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
watchdog.stop();
|
|
3116
|
-
}
|
|
3117
|
-
});
|
|
3321
|
+
healthMonitor
|
|
3322
|
+
);
|
|
3118
3323
|
console.log(" * Running. Press Ctrl+C to stop.\n");
|
|
3119
3324
|
await runtime.run();
|
|
3120
3325
|
}
|