@elisym/cli 0.22.0 → 0.22.3
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 +262 -60
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --no-deprecation
|
|
2
2
|
import { ReadableStream } from 'node:stream/web';
|
|
3
3
|
import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { dirname, join, resolve, basename, relative, sep
|
|
5
|
-
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, makeCensor, DEFAULT_REDACT_PATHS, POLICY_T_TAG, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, utf8ByteLength, LIMITS, calculateProtocolFee, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
|
|
4
|
+
import { dirname, join, resolve, basename, extname, relative, sep } from 'node:path';
|
|
5
|
+
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, createBlossomTransport, makeCensor, DEFAULT_REDACT_PATHS, POLICY_T_TAG, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, utf8ByteLength, LIMITS, readAcceptedTransports, calculateProtocolFee, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
|
|
6
6
|
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
|
|
7
7
|
import { isAddress, createSolanaRpc, address } from '@solana/kit';
|
|
8
8
|
import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
|
|
@@ -14,7 +14,7 @@ import { createIrohTransport } from '@elisym/sdk/node';
|
|
|
14
14
|
import { lookup } from 'node:dns/promises';
|
|
15
15
|
import { Socket } from 'node:net';
|
|
16
16
|
import pino from 'pino';
|
|
17
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
17
|
+
import { readFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
18
18
|
import { tmpdir } from 'node:os';
|
|
19
19
|
import pLimit from 'p-limit';
|
|
20
20
|
import { parseSkillMd, validateSkillFrontmatter, resolveInsidePath, DEFAULT_SCRIPT_TIMEOUT_MS, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
|
|
@@ -1865,6 +1865,9 @@ var JobLedger = class {
|
|
|
1865
1865
|
if (fields.resultAttachment !== void 0) {
|
|
1866
1866
|
entry.result_attachment = fields.resultAttachment;
|
|
1867
1867
|
}
|
|
1868
|
+
if (fields.resultAttachments !== void 0) {
|
|
1869
|
+
entry.result_attachments = fields.resultAttachments;
|
|
1870
|
+
}
|
|
1868
1871
|
this.flush();
|
|
1869
1872
|
}
|
|
1870
1873
|
markDelivered(jobId) {
|
|
@@ -2106,9 +2109,35 @@ var ExecutionBudgetExceededError = class extends Error {
|
|
|
2106
2109
|
this.name = "ExecutionBudgetExceededError";
|
|
2107
2110
|
}
|
|
2108
2111
|
};
|
|
2112
|
+
var SeedFailedError = class extends Error {
|
|
2113
|
+
constructor(cause) {
|
|
2114
|
+
super("Result is ready - delivery is retrying and will arrive shortly.", { cause });
|
|
2115
|
+
this.name = "SeedFailedError";
|
|
2116
|
+
}
|
|
2117
|
+
};
|
|
2118
|
+
var MIME_EXTENSIONS = {
|
|
2119
|
+
"image/png": ".png",
|
|
2120
|
+
"image/jpeg": ".jpg",
|
|
2121
|
+
"image/webp": ".webp",
|
|
2122
|
+
"image/gif": ".gif",
|
|
2123
|
+
"image/svg+xml": ".svg",
|
|
2124
|
+
"application/pdf": ".pdf",
|
|
2125
|
+
"text/plain": ".txt",
|
|
2126
|
+
"text/markdown": ".md",
|
|
2127
|
+
"application/json": ".json",
|
|
2128
|
+
"audio/mpeg": ".mp3",
|
|
2129
|
+
"audio/wav": ".wav",
|
|
2130
|
+
"video/mp4": ".mp4",
|
|
2131
|
+
"video/webm": ".webm",
|
|
2132
|
+
"application/zip": ".zip"
|
|
2133
|
+
};
|
|
2134
|
+
function extensionForMime(mime) {
|
|
2135
|
+
return mime ? MIME_EXTENSIONS[mime] ?? "" : "";
|
|
2136
|
+
}
|
|
2137
|
+
var SLOW_SEED_LOG_MS = 5e3;
|
|
2109
2138
|
var CUSTOMER_SAFE_MESSAGE_PREFIXES = ["Input too long", "No skill matched", "Payment timeout"];
|
|
2110
2139
|
function customerSafeMessage(error) {
|
|
2111
|
-
if (error instanceof AgentUnavailableError || error instanceof ExecutionBudgetExceededError) {
|
|
2140
|
+
if (error instanceof AgentUnavailableError || error instanceof ExecutionBudgetExceededError || error instanceof SeedFailedError) {
|
|
2112
2141
|
return error.message;
|
|
2113
2142
|
}
|
|
2114
2143
|
if (error instanceof ScriptExecutionError) {
|
|
@@ -2152,7 +2181,7 @@ var PAID_GLOBAL_MAX_JOBS_PER_WINDOW = 2e3;
|
|
|
2152
2181
|
var MAX_TRACKED_CUSTOMERS = 1e3;
|
|
2153
2182
|
var GLOBAL_LIMITER_KEY = "__global__";
|
|
2154
2183
|
var AgentRuntime = class {
|
|
2155
|
-
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport) {
|
|
2184
|
+
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport, identity, blossomTransport) {
|
|
2156
2185
|
this.transport = transport;
|
|
2157
2186
|
this.skills = skills;
|
|
2158
2187
|
this.skillCtx = skillCtx;
|
|
@@ -2161,6 +2190,8 @@ var AgentRuntime = class {
|
|
|
2161
2190
|
this.callbacks = callbacks;
|
|
2162
2191
|
this.healthMonitor = healthMonitor;
|
|
2163
2192
|
this.irohTransport = irohTransport;
|
|
2193
|
+
this.identity = identity;
|
|
2194
|
+
this.blossomTransport = blossomTransport;
|
|
2164
2195
|
this.limit = pLimit(config.maxConcurrentJobs);
|
|
2165
2196
|
this.maxQueueSize = config.maxQueueSize ?? config.maxConcurrentJobs * 10;
|
|
2166
2197
|
}
|
|
@@ -2513,14 +2544,12 @@ var AgentRuntime = class {
|
|
|
2513
2544
|
const log = this.callbacks.onLog ?? console.log;
|
|
2514
2545
|
log(`[${job.jobId.slice(0, 8)}] Error: ${e.message}`);
|
|
2515
2546
|
const currentStatus = this.ledger.getStatus(job.jobId);
|
|
2516
|
-
const keepPaidForRecovery = e instanceof AgentUnavailableError && currentStatus === "paid";
|
|
2547
|
+
const keepPaidForRecovery = (e instanceof AgentUnavailableError || e instanceof SeedFailedError) && currentStatus === "paid";
|
|
2517
2548
|
if (currentStatus !== "executed" && !keepPaidForRecovery) {
|
|
2518
2549
|
this.ledger.markFailed(job.jobId);
|
|
2519
2550
|
}
|
|
2520
2551
|
if (keepPaidForRecovery) {
|
|
2521
|
-
log(
|
|
2522
|
-
`[${job.jobId.slice(0, 8)}] Keeping status=paid; recovery will re-execute when LLM pair recovers (24h cutoff).`
|
|
2523
|
-
);
|
|
2552
|
+
log(`[${job.jobId.slice(0, 8)}] Keeping status=paid; recovery will retry (24h cutoff).`);
|
|
2524
2553
|
}
|
|
2525
2554
|
const operatorMessage = e instanceof ScriptExecutionError ? `${e.message}: ${e.detail}` : e.message ?? "Unknown error";
|
|
2526
2555
|
this.callbacks.onJobError?.(job.jobId, operatorMessage);
|
|
@@ -2588,7 +2617,7 @@ var AgentRuntime = class {
|
|
|
2588
2617
|
);
|
|
2589
2618
|
this.callbacks.onPaymentReceived?.(job.jobId, netAmount);
|
|
2590
2619
|
}
|
|
2591
|
-
const inputFile = await this.resolveInputFile(job.attachment);
|
|
2620
|
+
const inputFile = await this.resolveInputFile(job.attachment, job.customerId);
|
|
2592
2621
|
await this.transport.sendFeedback(job, { type: "processing" }).catch(() => {
|
|
2593
2622
|
});
|
|
2594
2623
|
const skill = this.skills.route(job.tags);
|
|
@@ -2658,19 +2687,48 @@ var AgentRuntime = class {
|
|
|
2658
2687
|
});
|
|
2659
2688
|
}
|
|
2660
2689
|
}
|
|
2661
|
-
const {
|
|
2690
|
+
const { attachments, deliveredContent } = await this.buildResultAttachment(
|
|
2691
|
+
job.jobId,
|
|
2692
|
+
output,
|
|
2693
|
+
job.customerId,
|
|
2694
|
+
readAcceptedTransports(job.rawEvent.tags)
|
|
2695
|
+
);
|
|
2662
2696
|
this.ledger.markExecuted(job.jobId, deliveredContent);
|
|
2663
2697
|
log(`[${job.jobId.slice(0, 8)}] Skill completed, delivering result`);
|
|
2664
2698
|
const eventId = await this.transport.deliverResult(
|
|
2665
2699
|
job,
|
|
2666
2700
|
deliveredContent,
|
|
2667
2701
|
netAmount,
|
|
2668
|
-
|
|
2702
|
+
attachments.length > 0 ? attachments : void 0
|
|
2669
2703
|
);
|
|
2670
2704
|
this.ledger.markDelivered(job.jobId);
|
|
2671
2705
|
log(`[${job.jobId.slice(0, 8)}] Delivered: ${eventId.slice(0, 16)}...`);
|
|
2672
2706
|
this.callbacks.onJobCompleted?.(job.jobId, output.data);
|
|
2673
2707
|
}
|
|
2708
|
+
/**
|
|
2709
|
+
* Seed a result blob through iroh, timing it and converting any failure (incl. the
|
|
2710
|
+
* transport's seed timeout, which also resets the wedged node) into a
|
|
2711
|
+
* `SeedFailedError` so the post-execute catch keeps the job `paid` for recovery
|
|
2712
|
+
* rather than losing the customer's paid job.
|
|
2713
|
+
*/
|
|
2714
|
+
async seedGuarded(jobId, kind, run) {
|
|
2715
|
+
const log = this.callbacks.onLog ?? console.log;
|
|
2716
|
+
const started = Date.now();
|
|
2717
|
+
try {
|
|
2718
|
+
const seeded = await run();
|
|
2719
|
+
const elapsed = Date.now() - started;
|
|
2720
|
+
if (elapsed > SLOW_SEED_LOG_MS) {
|
|
2721
|
+
log(`[${jobId.slice(0, 8)}] iroh seed (${kind}) slow: ${elapsed}ms`);
|
|
2722
|
+
}
|
|
2723
|
+
return seeded;
|
|
2724
|
+
} catch (error) {
|
|
2725
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2726
|
+
log(
|
|
2727
|
+
`[${jobId.slice(0, 8)}] iroh seed (${kind}) failed after ${Date.now() - started}ms: ${detail}`
|
|
2728
|
+
);
|
|
2729
|
+
throw new SeedFailedError(error);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2674
2732
|
/**
|
|
2675
2733
|
* Decide how a skill's result travels: inline text, a seeded file, or seeded
|
|
2676
2734
|
* large text. Returns the attachment descriptor (if any) PLUS the content to
|
|
@@ -2685,21 +2743,27 @@ var AgentRuntime = class {
|
|
|
2685
2743
|
* recovery re-executes (rather than re-delivering a dead ticket). Runs after the
|
|
2686
2744
|
* `skill.execute` budget window, so a slow seed never trips `max_execution_secs`.
|
|
2687
2745
|
*/
|
|
2688
|
-
async buildResultAttachment(jobId, output) {
|
|
2689
|
-
|
|
2746
|
+
async buildResultAttachment(jobId, output, customerPubkey, acceptedTransports) {
|
|
2747
|
+
const wantBlossom = acceptedTransports === void 0 || acceptedTransports.includes("blossom");
|
|
2748
|
+
const filePaths = output.filePaths ?? (output.filePath !== void 0 ? [output.filePath] : []);
|
|
2749
|
+
if (filePaths.length > 0) {
|
|
2690
2750
|
try {
|
|
2691
|
-
|
|
2692
|
-
|
|
2751
|
+
const attachments = [];
|
|
2752
|
+
for (const filePath of filePaths) {
|
|
2753
|
+
attachments.push(
|
|
2754
|
+
await this.seedFileToAttachment(
|
|
2755
|
+
jobId,
|
|
2756
|
+
filePath,
|
|
2757
|
+
output.outputMime,
|
|
2758
|
+
customerPubkey,
|
|
2759
|
+
wantBlossom
|
|
2760
|
+
)
|
|
2761
|
+
);
|
|
2693
2762
|
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
mime: output.outputMime ?? "application/octet-stream",
|
|
2699
|
-
transports: [{ kind: "iroh", ticket: seeded.ticket }]
|
|
2700
|
-
};
|
|
2701
|
-
this.ledger.recordAttachment(jobId, { resultAttachment: JSON.stringify(attachment) });
|
|
2702
|
-
return { attachment, deliveredContent: output.data };
|
|
2763
|
+
this.ledger.recordAttachment(jobId, {
|
|
2764
|
+
resultAttachments: attachments.map((a) => JSON.stringify(a))
|
|
2765
|
+
});
|
|
2766
|
+
return { attachments, deliveredContent: output.data };
|
|
2703
2767
|
} finally {
|
|
2704
2768
|
await output.cleanup?.().catch(() => {
|
|
2705
2769
|
});
|
|
@@ -2709,17 +2773,82 @@ var AgentRuntime = class {
|
|
|
2709
2773
|
if (!this.irohTransport) {
|
|
2710
2774
|
throw new Error("Result is too large to deliver inline and iroh transport is unavailable.");
|
|
2711
2775
|
}
|
|
2712
|
-
const
|
|
2776
|
+
const iroh = this.irohTransport;
|
|
2777
|
+
const dataBytes = Buffer.from(output.data, "utf8");
|
|
2778
|
+
const seeded = await this.seedGuarded(jobId, "large-text", () => iroh.seedBytes(dataBytes));
|
|
2779
|
+
const transports = [{ kind: "iroh", ticket: seeded.ticket }];
|
|
2780
|
+
if (wantBlossom) {
|
|
2781
|
+
const blossomMember = await this.seedBlossomMember(dataBytes, customerPubkey);
|
|
2782
|
+
if (blossomMember !== void 0) {
|
|
2783
|
+
transports.unshift(blossomMember);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2713
2786
|
const attachment = {
|
|
2714
2787
|
name: "result.txt",
|
|
2715
2788
|
size: seeded.size,
|
|
2716
2789
|
mime: "text/plain",
|
|
2717
|
-
transports
|
|
2790
|
+
transports
|
|
2718
2791
|
};
|
|
2719
|
-
this.ledger.recordAttachment(jobId, {
|
|
2720
|
-
return { attachment, deliveredContent: "" };
|
|
2792
|
+
this.ledger.recordAttachment(jobId, { resultAttachments: [JSON.stringify(attachment)] });
|
|
2793
|
+
return { attachments: [attachment], deliveredContent: "" };
|
|
2794
|
+
}
|
|
2795
|
+
return { attachments: [], deliveredContent: output.data };
|
|
2796
|
+
}
|
|
2797
|
+
/**
|
|
2798
|
+
* Seed ONE result file to iroh (+ encrypted Blossom when wanted) and build its
|
|
2799
|
+
* FileAttachment. A seed timeout throws SeedFailedError (job stays `paid` for
|
|
2800
|
+
* recovery). Shared by the single- and multi-file result paths.
|
|
2801
|
+
*/
|
|
2802
|
+
async seedFileToAttachment(jobId, filePath, outputMime, customerPubkey, wantBlossom) {
|
|
2803
|
+
if (!this.irohTransport) {
|
|
2804
|
+
throw new Error("Skill produced a file result but iroh transport is unavailable.");
|
|
2805
|
+
}
|
|
2806
|
+
const iroh = this.irohTransport;
|
|
2807
|
+
const seeded = await this.seedGuarded(jobId, "file", () => iroh.seedPath(filePath));
|
|
2808
|
+
const transports = [{ kind: "iroh", ticket: seeded.ticket }];
|
|
2809
|
+
if (wantBlossom) {
|
|
2810
|
+
const blossomMember = await this.seedBlossomFromPath(filePath, seeded.size, customerPubkey);
|
|
2811
|
+
if (blossomMember !== void 0) {
|
|
2812
|
+
transports.unshift(blossomMember);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
const producedName = basename(filePath);
|
|
2816
|
+
const name = extname(producedName) ? producedName : `${producedName}${extensionForMime(outputMime)}`;
|
|
2817
|
+
return { name, size: seeded.size, mime: outputMime ?? "application/octet-stream", transports };
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* Encrypt `bytes` to `recipientPubkey` and seed them to Blossom, returning a `blossom` transport
|
|
2821
|
+
* member - or `undefined` when blossom isn't configured, the bytes exceed the encrypted cap, or the
|
|
2822
|
+
* upload fails. Returning undefined (never throwing) keeps iroh as the guaranteed transport: an
|
|
2823
|
+
* optional second path must never fail the job.
|
|
2824
|
+
*/
|
|
2825
|
+
async seedBlossomMember(bytes, recipientPubkey) {
|
|
2826
|
+
if (this.blossomTransport === void 0 || this.identity === void 0) {
|
|
2827
|
+
return void 0;
|
|
2828
|
+
}
|
|
2829
|
+
if (bytes.byteLength > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
|
|
2830
|
+
return void 0;
|
|
2831
|
+
}
|
|
2832
|
+
try {
|
|
2833
|
+
return await this.blossomTransport.seedBytes({ bytes, recipientPubkey });
|
|
2834
|
+
} catch {
|
|
2835
|
+
return void 0;
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
/** Read a file (only when within the encrypted cap, to bound memory) and seed it to Blossom. */
|
|
2839
|
+
async seedBlossomFromPath(filePath, size, recipientPubkey) {
|
|
2840
|
+
if (this.blossomTransport === void 0 || this.identity === void 0) {
|
|
2841
|
+
return void 0;
|
|
2842
|
+
}
|
|
2843
|
+
if (size > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
|
|
2844
|
+
return void 0;
|
|
2845
|
+
}
|
|
2846
|
+
try {
|
|
2847
|
+
const bytes = await readFile(filePath);
|
|
2848
|
+
return await this.seedBlossomMember(bytes, recipientPubkey);
|
|
2849
|
+
} catch {
|
|
2850
|
+
return void 0;
|
|
2721
2851
|
}
|
|
2722
|
-
return { attachment: void 0, deliveredContent: output.data };
|
|
2723
2852
|
}
|
|
2724
2853
|
/**
|
|
2725
2854
|
* Rebuild a file-result attachment for crash-recovery re-delivery: re-share the
|
|
@@ -2735,12 +2864,34 @@ var AgentRuntime = class {
|
|
|
2735
2864
|
throw new Error("Cannot recover a file result: iroh transport is unavailable.");
|
|
2736
2865
|
}
|
|
2737
2866
|
const stored = JSON.parse(resultAttachmentJson);
|
|
2738
|
-
const irohTransport = stored.transports.find(
|
|
2867
|
+
const irohTransport = stored.transports.find(
|
|
2868
|
+
(transport) => transport.kind === "iroh"
|
|
2869
|
+
);
|
|
2739
2870
|
if (!irohTransport) {
|
|
2740
2871
|
throw new Error("Stored result attachment has no iroh transport.");
|
|
2741
2872
|
}
|
|
2742
2873
|
const freshTicket = await this.irohTransport.reShare(irohTransport.ticket);
|
|
2743
|
-
|
|
2874
|
+
const transports = stored.transports.map(
|
|
2875
|
+
(transport) => transport.kind === "iroh" ? { kind: "iroh", ticket: freshTicket } : transport
|
|
2876
|
+
);
|
|
2877
|
+
return { ...stored, transports };
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Re-share ALL of a recovered job's result attachments (multi-file aware). Prefers
|
|
2881
|
+
* the `result_attachments` array; falls back to the legacy single `result_attachment`.
|
|
2882
|
+
* A failure on ANY attachment throws (the caller marks the whole job failed - a
|
|
2883
|
+
* partial multi-file delivery is not a valid paid result).
|
|
2884
|
+
*/
|
|
2885
|
+
async reShareResultAttachments(entry) {
|
|
2886
|
+
const stored = entry.result_attachments ?? (entry.result_attachment !== void 0 ? [entry.result_attachment] : []);
|
|
2887
|
+
const out = [];
|
|
2888
|
+
for (const serialized of stored) {
|
|
2889
|
+
const attachment = await this.reShareResultAttachment(serialized);
|
|
2890
|
+
if (attachment !== void 0) {
|
|
2891
|
+
out.push(attachment);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
return out;
|
|
2744
2895
|
}
|
|
2745
2896
|
/**
|
|
2746
2897
|
* Resolve a job's file/large-text input (when it carries an attachment) via iroh.
|
|
@@ -2755,31 +2906,67 @@ var AgentRuntime = class {
|
|
|
2755
2906
|
* `fetchToBytes`/`fetchToPath` enforce the real cap on the BLAKE3-verified size, so
|
|
2756
2907
|
* an incorrect declared `size` cannot exceed the in-memory ceiling.
|
|
2757
2908
|
*/
|
|
2758
|
-
async resolveInputFile(attachment) {
|
|
2909
|
+
async resolveInputFile(attachment, senderPubkey) {
|
|
2759
2910
|
if (attachment === void 0) {
|
|
2760
2911
|
return void 0;
|
|
2761
2912
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
const
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2913
|
+
const irohMember = attachment.transports.find(
|
|
2914
|
+
(t) => t.kind === "iroh"
|
|
2915
|
+
);
|
|
2916
|
+
const blossomMember = attachment.transports.find(
|
|
2917
|
+
(t) => t.kind === "blossom"
|
|
2918
|
+
);
|
|
2919
|
+
if (irohMember !== void 0 && this.irohTransport !== void 0) {
|
|
2920
|
+
if (attachment.mime.startsWith("text/") && attachment.size <= LIMITS.MAX_REINLINE_TEXT_BYTES) {
|
|
2921
|
+
const bytes = await this.irohTransport.fetchToBytes(irohMember.ticket, {
|
|
2922
|
+
maxBytes: LIMITS.MAX_REINLINE_TEXT_BYTES
|
|
2923
|
+
});
|
|
2924
|
+
return this.materializeBytesInput(bytes, attachment.mime);
|
|
2925
|
+
}
|
|
2926
|
+
const dir = await mkdtemp(join(tmpdir(), "elisym-job-"));
|
|
2927
|
+
const filePath = join(dir, "input");
|
|
2928
|
+
try {
|
|
2929
|
+
await this.irohTransport.fetchToPath(irohMember.ticket, filePath);
|
|
2930
|
+
} catch (error) {
|
|
2931
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
2932
|
+
});
|
|
2933
|
+
throw error;
|
|
2934
|
+
}
|
|
2773
2935
|
return {
|
|
2774
|
-
|
|
2936
|
+
filePath,
|
|
2775
2937
|
cleanup: async () => {
|
|
2938
|
+
await rm(dir, { recursive: true, force: true });
|
|
2776
2939
|
}
|
|
2777
2940
|
};
|
|
2778
2941
|
}
|
|
2942
|
+
if (blossomMember !== void 0 && this.blossomTransport !== void 0 && this.identity !== void 0) {
|
|
2943
|
+
if (attachment.size > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
|
|
2944
|
+
throw new Error("Blossom file input exceeds the encrypted size cap.");
|
|
2945
|
+
}
|
|
2946
|
+
const bytes = await this.blossomTransport.fetchToBytes({
|
|
2947
|
+
transport: blossomMember,
|
|
2948
|
+
senderPubkey,
|
|
2949
|
+
maxBytes: LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES
|
|
2950
|
+
});
|
|
2951
|
+
return this.materializeBytesInput(bytes, attachment.mime);
|
|
2952
|
+
}
|
|
2953
|
+
throw new Error("Job carries a file input but no supported transport is available.");
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Turn fetched input bytes into a SkillInput: text within the re-inline ceiling becomes an
|
|
2957
|
+
* in-memory string; anything else is written to a fixed `input` name inside a unique mkdtemp dir
|
|
2958
|
+
* (the untrusted attachment `name` never touches the path). Shared by the iroh-text and blossom
|
|
2959
|
+
* paths (both have the bytes in hand); the iroh-binary path streams to disk separately.
|
|
2960
|
+
*/
|
|
2961
|
+
async materializeBytesInput(bytes, mime) {
|
|
2962
|
+
if (mime.startsWith("text/") && bytes.byteLength <= LIMITS.MAX_REINLINE_TEXT_BYTES) {
|
|
2963
|
+
return { inlineText: Buffer.from(bytes).toString("utf8"), cleanup: async () => {
|
|
2964
|
+
} };
|
|
2965
|
+
}
|
|
2779
2966
|
const dir = await mkdtemp(join(tmpdir(), "elisym-job-"));
|
|
2780
2967
|
const filePath = join(dir, "input");
|
|
2781
2968
|
try {
|
|
2782
|
-
await
|
|
2969
|
+
await writeFile(filePath, bytes);
|
|
2783
2970
|
} catch (error) {
|
|
2784
2971
|
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
2785
2972
|
});
|
|
@@ -3058,15 +3245,20 @@ var AgentRuntime = class {
|
|
|
3058
3245
|
};
|
|
3059
3246
|
if (entry.status === "executed" && entry.result !== void 0) {
|
|
3060
3247
|
this.ledger.incrementRetry(entry.job_id);
|
|
3061
|
-
let
|
|
3248
|
+
let attachments;
|
|
3062
3249
|
try {
|
|
3063
|
-
|
|
3250
|
+
attachments = await this.reShareResultAttachments(entry);
|
|
3064
3251
|
} catch {
|
|
3065
3252
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: result blob unavailable, marking failed`);
|
|
3066
3253
|
this.ledger.markFailed(entry.job_id);
|
|
3067
3254
|
return;
|
|
3068
3255
|
}
|
|
3069
|
-
await this.transport.deliverResult(
|
|
3256
|
+
await this.transport.deliverResult(
|
|
3257
|
+
fakeJob,
|
|
3258
|
+
entry.result,
|
|
3259
|
+
entry.net_amount,
|
|
3260
|
+
attachments.length > 0 ? attachments : void 0
|
|
3261
|
+
);
|
|
3070
3262
|
this.ledger.markDelivered(entry.job_id);
|
|
3071
3263
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: re-delivered`);
|
|
3072
3264
|
} else if (entry.status === "paid") {
|
|
@@ -3112,7 +3304,8 @@ var AgentRuntime = class {
|
|
|
3112
3304
|
let recoveryInputFile;
|
|
3113
3305
|
try {
|
|
3114
3306
|
recoveryInputFile = await this.resolveInputFile(
|
|
3115
|
-
decodeJobPayload(rawEvent.content).attachment
|
|
3307
|
+
decodeJobPayload(rawEvent.content).attachment,
|
|
3308
|
+
entry.customer_id
|
|
3116
3309
|
);
|
|
3117
3310
|
} catch {
|
|
3118
3311
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: input file unavailable, marking failed`);
|
|
@@ -3157,16 +3350,18 @@ var AgentRuntime = class {
|
|
|
3157
3350
|
});
|
|
3158
3351
|
}
|
|
3159
3352
|
}
|
|
3160
|
-
const {
|
|
3353
|
+
const { attachments: resultAttachments, deliveredContent } = await this.buildResultAttachment(
|
|
3161
3354
|
entry.job_id,
|
|
3162
|
-
output
|
|
3355
|
+
output,
|
|
3356
|
+
entry.customer_id,
|
|
3357
|
+
readAcceptedTransports(rawEvent.tags)
|
|
3163
3358
|
);
|
|
3164
3359
|
this.ledger.markExecuted(entry.job_id, deliveredContent);
|
|
3165
3360
|
await this.transport.deliverResult(
|
|
3166
3361
|
fakeJob,
|
|
3167
3362
|
deliveredContent,
|
|
3168
3363
|
entry.net_amount,
|
|
3169
|
-
|
|
3364
|
+
resultAttachments.length > 0 ? resultAttachments : void 0
|
|
3170
3365
|
);
|
|
3171
3366
|
this.ledger.markDelivered(entry.job_id);
|
|
3172
3367
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: re-executed and delivered`);
|
|
@@ -3359,8 +3554,9 @@ var DynamicScriptSkill = class {
|
|
|
3359
3554
|
llmOverride;
|
|
3360
3555
|
// Discovery hints surfaced in the published capability card (buildCard).
|
|
3361
3556
|
// `outputMime` is also forwarded to the inner SDK runner (it labels the file
|
|
3362
|
-
// result); `inputMime`
|
|
3557
|
+
// result); `inputMime`/`inputText` are publish-time metadata only.
|
|
3363
3558
|
inputMime;
|
|
3559
|
+
inputText;
|
|
3364
3560
|
outputMime;
|
|
3365
3561
|
inner;
|
|
3366
3562
|
constructor(params) {
|
|
@@ -3374,6 +3570,7 @@ var DynamicScriptSkill = class {
|
|
|
3374
3570
|
this.dir = params.dir;
|
|
3375
3571
|
this.llmOverride = params.llmOverride;
|
|
3376
3572
|
this.inputMime = params.inputMime;
|
|
3573
|
+
this.inputText = params.inputText;
|
|
3377
3574
|
this.outputMime = params.outputMime;
|
|
3378
3575
|
this.inner = new DynamicScriptSkill$1({
|
|
3379
3576
|
name: params.name,
|
|
@@ -3542,7 +3739,8 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3542
3739
|
skill = parsed.mode === "dynamic-script" ? new DynamicScriptSkill({
|
|
3543
3740
|
...scriptParams,
|
|
3544
3741
|
outputMime: parsed.outputMime,
|
|
3545
|
-
inputMime: parsed.inputMime
|
|
3742
|
+
inputMime: parsed.inputMime,
|
|
3743
|
+
inputText: parsed.inputText
|
|
3546
3744
|
}) : new StaticScriptSkill(scriptParams);
|
|
3547
3745
|
break;
|
|
3548
3746
|
}
|
|
@@ -3751,7 +3949,7 @@ var NostrTransport = class {
|
|
|
3751
3949
|
}
|
|
3752
3950
|
}
|
|
3753
3951
|
/** Deliver result to customer. Retries with exponential backoff via SDK. */
|
|
3754
|
-
async deliverResult(job, content, amount,
|
|
3952
|
+
async deliverResult(job, content, amount, attachments, retries = 3) {
|
|
3755
3953
|
return this.client.marketplace.submitJobResultWithRetry(
|
|
3756
3954
|
this.identity,
|
|
3757
3955
|
job.rawEvent,
|
|
@@ -3759,7 +3957,7 @@ var NostrTransport = class {
|
|
|
3759
3957
|
amount,
|
|
3760
3958
|
retries,
|
|
3761
3959
|
void 0,
|
|
3762
|
-
|
|
3960
|
+
attachments
|
|
3763
3961
|
);
|
|
3764
3962
|
}
|
|
3765
3963
|
/** Returns true if an event was received within the given idle window. */
|
|
@@ -4312,9 +4510,10 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4312
4510
|
capabilities: skill.capabilities,
|
|
4313
4511
|
image: skill.image,
|
|
4314
4512
|
...isStatic ? { static: true } : {},
|
|
4315
|
-
// File-exchange hints (dynamic-script only). `inputMime`
|
|
4316
|
-
//
|
|
4513
|
+
// File-exchange hints (dynamic-script only). `inputMime` flags a file input;
|
|
4514
|
+
// `inputText` tells the web whether to also show its text box for that file job.
|
|
4317
4515
|
...skill.inputMime ? { inputMime: skill.inputMime } : {},
|
|
4516
|
+
...skill.inputText ? { inputText: skill.inputText } : {},
|
|
4318
4517
|
...skill.outputMime ? { outputMime: skill.outputMime } : {},
|
|
4319
4518
|
payment: solanaAddress ? {
|
|
4320
4519
|
chain: "solana",
|
|
@@ -4445,6 +4644,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4445
4644
|
});
|
|
4446
4645
|
diagLog("LLM health monitor armed (lazy recovery, 5min interval).");
|
|
4447
4646
|
}
|
|
4647
|
+
const blossomTransport = createBlossomTransport({ blossom: client.blossom, identity });
|
|
4448
4648
|
const runtime = new AgentRuntime(
|
|
4449
4649
|
transport,
|
|
4450
4650
|
registry,
|
|
@@ -4476,7 +4676,9 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4476
4676
|
}
|
|
4477
4677
|
},
|
|
4478
4678
|
healthMonitor,
|
|
4479
|
-
irohTransport
|
|
4679
|
+
irohTransport,
|
|
4680
|
+
identity,
|
|
4681
|
+
blossomTransport
|
|
4480
4682
|
);
|
|
4481
4683
|
console.log(" * Running. Press Ctrl+C to stop.\n");
|
|
4482
4684
|
await runtime.run();
|