@pulseai/sdk 0.1.0 → 0.1.1
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.d.ts +69 -4
- package/dist/index.js +594 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -34,12 +34,11 @@ var TESTNET_ADDRESSES = {
|
|
|
34
34
|
reputationRegistry: "0x8004B663056A597Dffe9eCcC1965A193B7388713",
|
|
35
35
|
usdm: "0x939Ff43f7c4A2E94069af7DBbc4497377DdcCE3f"
|
|
36
36
|
};
|
|
37
|
-
var PLACEHOLDER_ADDRESS = "0x0";
|
|
38
37
|
var MAINNET_ADDRESSES = {
|
|
39
|
-
pulseExtension:
|
|
40
|
-
serviceMarketplace:
|
|
41
|
-
jobEngine:
|
|
42
|
-
feeDistributor:
|
|
38
|
+
pulseExtension: "0xf1616D2008c4Ff5Ed7BDBd448DAE68615b7A71f0",
|
|
39
|
+
serviceMarketplace: "0xfC180058FCB69531818B832C12473302811dfFF6",
|
|
40
|
+
jobEngine: "0xb5E56262b55aE453E8B16470228F0a5Ef617FF67",
|
|
41
|
+
feeDistributor: "0x51EdD8E4C4B423b952821fc9e2a7dad15a858B56",
|
|
43
42
|
identityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
|
|
44
43
|
reputationRegistry: "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63",
|
|
45
44
|
usdm: "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7"
|
|
@@ -2387,13 +2386,29 @@ async function acceptJob(client, jobId, warrenTermsHash) {
|
|
|
2387
2386
|
args: [jobId, signature]
|
|
2388
2387
|
});
|
|
2389
2388
|
}
|
|
2390
|
-
async function submitDeliverable(client, jobId, deliverableHash) {
|
|
2391
|
-
|
|
2389
|
+
async function submitDeliverable(client, jobId, deliverableHash, options) {
|
|
2390
|
+
const txHash = await write(client, {
|
|
2392
2391
|
address: client.addresses.jobEngine,
|
|
2393
2392
|
abi: jobEngineAbi,
|
|
2394
2393
|
functionName: "submitDeliverable",
|
|
2395
2394
|
args: [jobId, deliverableHash]
|
|
2396
2395
|
});
|
|
2396
|
+
if (options?.content && options?.indexerUrl) {
|
|
2397
|
+
try {
|
|
2398
|
+
await fetch(`${options.indexerUrl.replace(/\/$/, "")}/deliverables`, {
|
|
2399
|
+
method: "POST",
|
|
2400
|
+
headers: { "Content-Type": "application/json" },
|
|
2401
|
+
body: JSON.stringify({
|
|
2402
|
+
jobId: Number(jobId),
|
|
2403
|
+
content: options.content,
|
|
2404
|
+
contentHash: deliverableHash,
|
|
2405
|
+
storageType: "indexer"
|
|
2406
|
+
})
|
|
2407
|
+
});
|
|
2408
|
+
} catch {
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
return txHash;
|
|
2397
2412
|
}
|
|
2398
2413
|
async function evaluate(client, jobId, approved, feedback) {
|
|
2399
2414
|
return write(client, {
|
|
@@ -2435,6 +2450,17 @@ async function getJobCount(client) {
|
|
|
2435
2450
|
functionName: "getJobCount"
|
|
2436
2451
|
});
|
|
2437
2452
|
}
|
|
2453
|
+
async function rejectJob(client, jobId, feedback) {
|
|
2454
|
+
return evaluate(client, jobId, false, feedback);
|
|
2455
|
+
}
|
|
2456
|
+
async function resolveDispute(client, jobId, favorBuyer) {
|
|
2457
|
+
return write(client, {
|
|
2458
|
+
address: client.addresses.jobEngine,
|
|
2459
|
+
abi: jobEngineAbi,
|
|
2460
|
+
functionName: "resolveDispute",
|
|
2461
|
+
args: [jobId, favorBuyer]
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2438
2464
|
|
|
2439
2465
|
// src/warren/index.ts
|
|
2440
2466
|
import { keccak256, toHex as toHex2 } from "viem";
|
|
@@ -2664,6 +2690,21 @@ async function deployDeliverable(client, agentId, jobId, params, indexerUrl) {
|
|
|
2664
2690
|
} catch {
|
|
2665
2691
|
}
|
|
2666
2692
|
}
|
|
2693
|
+
if (indexerUrl) {
|
|
2694
|
+
try {
|
|
2695
|
+
await fetch(`${indexerUrl.replace(/\/$/, "")}/deliverables`, {
|
|
2696
|
+
method: "POST",
|
|
2697
|
+
headers: { "Content-Type": "application/json" },
|
|
2698
|
+
body: JSON.stringify({
|
|
2699
|
+
jobId: Number(jobId),
|
|
2700
|
+
content: json,
|
|
2701
|
+
contentHash: hash,
|
|
2702
|
+
storageType: "warren"
|
|
2703
|
+
})
|
|
2704
|
+
});
|
|
2705
|
+
} catch {
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2667
2708
|
return { masterAddress, pageAddress, hash, txHash };
|
|
2668
2709
|
}
|
|
2669
2710
|
async function readDeliverable(client, jobId, indexerUrl) {
|
|
@@ -2787,6 +2828,406 @@ function createRequirements(params) {
|
|
|
2787
2828
|
return { json, hash };
|
|
2788
2829
|
}
|
|
2789
2830
|
|
|
2831
|
+
// src/ai/provider.ts
|
|
2832
|
+
function parseErrorMessage(payload) {
|
|
2833
|
+
if (typeof payload !== "object" || payload === null) return null;
|
|
2834
|
+
const record = payload;
|
|
2835
|
+
const error = record.error;
|
|
2836
|
+
if (typeof error === "string") return error;
|
|
2837
|
+
if (typeof error === "object" && error !== null) {
|
|
2838
|
+
const message2 = error.message;
|
|
2839
|
+
if (typeof message2 === "string" && message2.length > 0) return message2;
|
|
2840
|
+
}
|
|
2841
|
+
const message = record.message;
|
|
2842
|
+
if (typeof message === "string" && message.length > 0) return message;
|
|
2843
|
+
return null;
|
|
2844
|
+
}
|
|
2845
|
+
function extractTextContent(content) {
|
|
2846
|
+
if (typeof content === "string") return content.trim();
|
|
2847
|
+
if (!Array.isArray(content)) return "";
|
|
2848
|
+
return content.map((entry) => {
|
|
2849
|
+
if (typeof entry === "string") return entry;
|
|
2850
|
+
if (typeof entry !== "object" || entry === null) return "";
|
|
2851
|
+
const text = entry.text;
|
|
2852
|
+
return typeof text === "string" ? text : "";
|
|
2853
|
+
}).join("\n").trim();
|
|
2854
|
+
}
|
|
2855
|
+
async function parseJsonResponse(response) {
|
|
2856
|
+
try {
|
|
2857
|
+
return await response.json();
|
|
2858
|
+
} catch {
|
|
2859
|
+
return null;
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
async function callOpenAI(params) {
|
|
2863
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
2864
|
+
method: "POST",
|
|
2865
|
+
headers: {
|
|
2866
|
+
"Content-Type": "application/json",
|
|
2867
|
+
Authorization: `Bearer ${params.apiKey}`
|
|
2868
|
+
},
|
|
2869
|
+
body: JSON.stringify({
|
|
2870
|
+
model: params.model,
|
|
2871
|
+
messages: params.messages.map((message) => ({
|
|
2872
|
+
role: message.role,
|
|
2873
|
+
content: message.content
|
|
2874
|
+
})),
|
|
2875
|
+
max_tokens: params.maxTokens
|
|
2876
|
+
}),
|
|
2877
|
+
signal: params.signal
|
|
2878
|
+
});
|
|
2879
|
+
const payload = await parseJsonResponse(response);
|
|
2880
|
+
if (!response.ok) {
|
|
2881
|
+
throw new Error(
|
|
2882
|
+
`OpenAI request failed (${response.status}): ${parseErrorMessage(payload) ?? response.statusText}`
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
const content = extractTextContent(payload?.choices?.[0]?.message?.content);
|
|
2886
|
+
if (!content) {
|
|
2887
|
+
throw new Error("OpenAI returned an empty response");
|
|
2888
|
+
}
|
|
2889
|
+
return { content, raw: payload };
|
|
2890
|
+
}
|
|
2891
|
+
async function callAnthropic(params) {
|
|
2892
|
+
const systemPrompt = params.messages.filter((message) => message.role === "system").map((message) => message.content).join("\n\n").trim();
|
|
2893
|
+
const messages = params.messages.filter((message) => message.role !== "system").map((message) => ({
|
|
2894
|
+
role: message.role === "assistant" ? "assistant" : "user",
|
|
2895
|
+
content: message.content
|
|
2896
|
+
}));
|
|
2897
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
2898
|
+
method: "POST",
|
|
2899
|
+
headers: {
|
|
2900
|
+
"Content-Type": "application/json",
|
|
2901
|
+
"x-api-key": params.apiKey,
|
|
2902
|
+
"anthropic-version": "2023-06-01"
|
|
2903
|
+
},
|
|
2904
|
+
body: JSON.stringify({
|
|
2905
|
+
model: params.model,
|
|
2906
|
+
max_tokens: params.maxTokens ?? 1024,
|
|
2907
|
+
system: systemPrompt.length > 0 ? systemPrompt : void 0,
|
|
2908
|
+
messages
|
|
2909
|
+
}),
|
|
2910
|
+
signal: params.signal
|
|
2911
|
+
});
|
|
2912
|
+
const payload = await parseJsonResponse(response);
|
|
2913
|
+
if (!response.ok) {
|
|
2914
|
+
throw new Error(
|
|
2915
|
+
`Anthropic request failed (${response.status}): ${parseErrorMessage(payload) ?? response.statusText}`
|
|
2916
|
+
);
|
|
2917
|
+
}
|
|
2918
|
+
const content = extractTextContent(payload?.content);
|
|
2919
|
+
if (!content) {
|
|
2920
|
+
throw new Error("Anthropic returned an empty response");
|
|
2921
|
+
}
|
|
2922
|
+
return { content, raw: payload };
|
|
2923
|
+
}
|
|
2924
|
+
async function callGoogle(params) {
|
|
2925
|
+
const systemPrompt = params.messages.filter((message) => message.role === "system").map((message) => message.content).join("\n\n").trim();
|
|
2926
|
+
const contents = params.messages.filter((message) => message.role !== "system").map((message) => ({
|
|
2927
|
+
role: message.role === "assistant" ? "model" : "user",
|
|
2928
|
+
parts: [{ text: message.content }]
|
|
2929
|
+
}));
|
|
2930
|
+
const url = new URL(
|
|
2931
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(params.model)}:generateContent`
|
|
2932
|
+
);
|
|
2933
|
+
url.searchParams.set("key", params.apiKey);
|
|
2934
|
+
const response = await fetch(url, {
|
|
2935
|
+
method: "POST",
|
|
2936
|
+
headers: {
|
|
2937
|
+
"Content-Type": "application/json"
|
|
2938
|
+
},
|
|
2939
|
+
body: JSON.stringify({
|
|
2940
|
+
contents,
|
|
2941
|
+
systemInstruction: systemPrompt.length > 0 ? { parts: [{ text: systemPrompt }] } : void 0,
|
|
2942
|
+
generationConfig: params.maxTokens !== void 0 ? { maxOutputTokens: params.maxTokens } : void 0
|
|
2943
|
+
}),
|
|
2944
|
+
signal: params.signal
|
|
2945
|
+
});
|
|
2946
|
+
const payload = await parseJsonResponse(response);
|
|
2947
|
+
if (!response.ok) {
|
|
2948
|
+
throw new Error(
|
|
2949
|
+
`Google request failed (${response.status}): ${parseErrorMessage(payload) ?? response.statusText}`
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
const content = extractTextContent(payload?.candidates?.[0]?.content?.parts);
|
|
2953
|
+
if (!content) {
|
|
2954
|
+
throw new Error("Google returned an empty response");
|
|
2955
|
+
}
|
|
2956
|
+
return { content, raw: payload };
|
|
2957
|
+
}
|
|
2958
|
+
async function callAI(params) {
|
|
2959
|
+
if (params.provider === "openai") {
|
|
2960
|
+
return callOpenAI(params);
|
|
2961
|
+
}
|
|
2962
|
+
if (params.provider === "anthropic") {
|
|
2963
|
+
return callAnthropic(params);
|
|
2964
|
+
}
|
|
2965
|
+
if (params.provider === "google") {
|
|
2966
|
+
return callGoogle(params);
|
|
2967
|
+
}
|
|
2968
|
+
throw new Error(`Unsupported AI provider: ${String(params.provider)}`);
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// src/handler/site-modifier.ts
|
|
2972
|
+
var FETCH_TIMEOUT_MS = 3e4;
|
|
2973
|
+
var MAX_HTML_BYTES = 500 * 1024;
|
|
2974
|
+
var DEFAULT_MAX_TOKENS = 4096;
|
|
2975
|
+
var SUPPORTED_PROVIDERS = ["anthropic", "google", "openai"];
|
|
2976
|
+
var DEFAULT_MODELS = {
|
|
2977
|
+
openai: "gpt-4o-mini",
|
|
2978
|
+
anthropic: "claude-3-5-sonnet-latest",
|
|
2979
|
+
google: "gemini-1.5-pro"
|
|
2980
|
+
};
|
|
2981
|
+
var DEFAULT_SYSTEM_PROMPT = [
|
|
2982
|
+
"You are an expert frontend engineer modifying an existing HTML document.",
|
|
2983
|
+
"Return a complete, self-contained HTML file.",
|
|
2984
|
+
"Inline all CSS in <style> tags and do not use external stylesheets.",
|
|
2985
|
+
"Do not add any external dependencies (no CDNs, external scripts, fonts, or assets).",
|
|
2986
|
+
"Preserve all existing functionality unless explicitly requested to change it.",
|
|
2987
|
+
"Keep the output valid HTML and include all required tags.",
|
|
2988
|
+
"Wrap the final HTML output in a ```html code block."
|
|
2989
|
+
].join("\n");
|
|
2990
|
+
function isRecord(value) {
|
|
2991
|
+
return typeof value === "object" && value !== null;
|
|
2992
|
+
}
|
|
2993
|
+
function isNonEmptyString(value) {
|
|
2994
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
2995
|
+
}
|
|
2996
|
+
function isSupportedProvider(value) {
|
|
2997
|
+
return typeof value === "string" && SUPPORTED_PROVIDERS.includes(value);
|
|
2998
|
+
}
|
|
2999
|
+
function normalizeDomain(domain) {
|
|
3000
|
+
return domain.trim().toLowerCase().replace(/^\*\./, "").replace(/\.$/, "");
|
|
3001
|
+
}
|
|
3002
|
+
function isAllowedDomain(hostname, allowedDomains) {
|
|
3003
|
+
const host = normalizeDomain(hostname);
|
|
3004
|
+
return allowedDomains.some((domain) => {
|
|
3005
|
+
const normalized = normalizeDomain(domain);
|
|
3006
|
+
if (!normalized) return false;
|
|
3007
|
+
return host === normalized || host.endsWith(`.${normalized}`);
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
function parseAndValidateUrl(urlString) {
|
|
3011
|
+
let url;
|
|
3012
|
+
try {
|
|
3013
|
+
url = new URL(urlString);
|
|
3014
|
+
} catch {
|
|
3015
|
+
throw new Error("siteUrl must be a valid URL");
|
|
3016
|
+
}
|
|
3017
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
3018
|
+
throw new Error("siteUrl must use http or https");
|
|
3019
|
+
}
|
|
3020
|
+
return url;
|
|
3021
|
+
}
|
|
3022
|
+
function ensureAllowedUrl(url, allowedDomains) {
|
|
3023
|
+
if (!allowedDomains || allowedDomains.length === 0) return;
|
|
3024
|
+
if (!isAllowedDomain(url.hostname, allowedDomains)) {
|
|
3025
|
+
throw new Error(`siteUrl domain "${url.hostname}" is not allowed`);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
function isHtmlContentType(contentType) {
|
|
3029
|
+
const lower = contentType.toLowerCase();
|
|
3030
|
+
return lower.includes("text/html") || lower.includes("application/xhtml+xml");
|
|
3031
|
+
}
|
|
3032
|
+
function extractHtmlFromText(text) {
|
|
3033
|
+
const htmlFence = text.match(/```html\s*([\s\S]*?)```/i);
|
|
3034
|
+
if (htmlFence?.[1]) {
|
|
3035
|
+
const fencedHtml = htmlFence[1].trim();
|
|
3036
|
+
if (fencedHtml) return fencedHtml;
|
|
3037
|
+
}
|
|
3038
|
+
const anyFence = text.match(/```\s*([\s\S]*?)```/);
|
|
3039
|
+
if (anyFence?.[1]) {
|
|
3040
|
+
const fencedContent = anyFence[1].trim();
|
|
3041
|
+
if (/<html[\s>]|<!doctype html/i.test(fencedContent)) {
|
|
3042
|
+
return fencedContent;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
const trimmed = text.trim();
|
|
3046
|
+
if (/<html[\s>]|<!doctype html/i.test(trimmed)) {
|
|
3047
|
+
return trimmed;
|
|
3048
|
+
}
|
|
3049
|
+
return null;
|
|
3050
|
+
}
|
|
3051
|
+
function getByteLength(content) {
|
|
3052
|
+
return new TextEncoder().encode(content).byteLength;
|
|
3053
|
+
}
|
|
3054
|
+
var SiteModifierHandler = class {
|
|
3055
|
+
offeringId;
|
|
3056
|
+
autoAccept;
|
|
3057
|
+
config;
|
|
3058
|
+
constructor(offeringId, config, options) {
|
|
3059
|
+
this.offeringId = offeringId;
|
|
3060
|
+
this.config = config;
|
|
3061
|
+
this.autoAccept = options?.autoAccept;
|
|
3062
|
+
}
|
|
3063
|
+
async validateRequirements(context) {
|
|
3064
|
+
try {
|
|
3065
|
+
this.parseRequirements(context.requirements);
|
|
3066
|
+
return { valid: true };
|
|
3067
|
+
} catch (error) {
|
|
3068
|
+
return {
|
|
3069
|
+
valid: false,
|
|
3070
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
async executeJob(context) {
|
|
3075
|
+
const requirements = this.parseRequirements(context.requirements);
|
|
3076
|
+
const targetUrl = parseAndValidateUrl(requirements.siteUrl);
|
|
3077
|
+
ensureAllowedUrl(targetUrl, this.config.allowedDomains);
|
|
3078
|
+
const provider = requirements.provider ?? this.config.defaultProvider ?? "openai";
|
|
3079
|
+
const model = requirements.model?.trim() || this.config.defaultModel?.trim() || DEFAULT_MODELS[provider];
|
|
3080
|
+
const maxTokens = this.config.maxTokens && this.config.maxTokens > 0 ? this.config.maxTokens : DEFAULT_MAX_TOKENS;
|
|
3081
|
+
if (!model) {
|
|
3082
|
+
throw new Error(
|
|
3083
|
+
`No model configured for provider "${provider}". Set defaultModel or provide requirements.model`
|
|
3084
|
+
);
|
|
3085
|
+
}
|
|
3086
|
+
const originalHtml = await this.fetchHtmlWithTimeout(
|
|
3087
|
+
targetUrl.toString(),
|
|
3088
|
+
context.abortSignal
|
|
3089
|
+
);
|
|
3090
|
+
const apiKey = await this.config.getApiKey(provider);
|
|
3091
|
+
if (!isNonEmptyString(apiKey)) {
|
|
3092
|
+
throw new Error(
|
|
3093
|
+
`Missing API key for provider "${provider}". Check BYOK setup for indexer ${this.config.indexerUrl}`
|
|
3094
|
+
);
|
|
3095
|
+
}
|
|
3096
|
+
const systemPrompt = this.config.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
3097
|
+
const userPrompt = [
|
|
3098
|
+
`Modification request: ${requirements.modificationRequest}`,
|
|
3099
|
+
"",
|
|
3100
|
+
"Current HTML:",
|
|
3101
|
+
"```html",
|
|
3102
|
+
originalHtml,
|
|
3103
|
+
"```"
|
|
3104
|
+
].join("\n");
|
|
3105
|
+
const aiResponse = await callAI({
|
|
3106
|
+
provider,
|
|
3107
|
+
model,
|
|
3108
|
+
maxTokens,
|
|
3109
|
+
apiKey,
|
|
3110
|
+
signal: context.abortSignal,
|
|
3111
|
+
messages: [
|
|
3112
|
+
{ role: "system", content: systemPrompt },
|
|
3113
|
+
{ role: "user", content: userPrompt }
|
|
3114
|
+
]
|
|
3115
|
+
});
|
|
3116
|
+
const modifiedHtml = extractHtmlFromText(aiResponse.content);
|
|
3117
|
+
if (!modifiedHtml) {
|
|
3118
|
+
throw new Error("AI response did not contain valid HTML output");
|
|
3119
|
+
}
|
|
3120
|
+
if (getByteLength(modifiedHtml) > MAX_HTML_BYTES) {
|
|
3121
|
+
throw new Error(
|
|
3122
|
+
`Modified HTML exceeds size limit (${MAX_HTML_BYTES} bytes)`
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
return {
|
|
3126
|
+
type: "inline",
|
|
3127
|
+
content: modifiedHtml,
|
|
3128
|
+
mimeType: "text/html"
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
parseRequirements(requirements) {
|
|
3132
|
+
if (!isRecord(requirements)) {
|
|
3133
|
+
throw new Error("Missing requirements object");
|
|
3134
|
+
}
|
|
3135
|
+
const siteUrl = requirements.siteUrl;
|
|
3136
|
+
if (!isNonEmptyString(siteUrl)) {
|
|
3137
|
+
throw new Error("requirements.siteUrl must be a non-empty string");
|
|
3138
|
+
}
|
|
3139
|
+
const parsedUrl = parseAndValidateUrl(siteUrl);
|
|
3140
|
+
ensureAllowedUrl(parsedUrl, this.config.allowedDomains);
|
|
3141
|
+
const modificationRequest = requirements.modificationRequest;
|
|
3142
|
+
if (!isNonEmptyString(modificationRequest)) {
|
|
3143
|
+
throw new Error(
|
|
3144
|
+
"requirements.modificationRequest must be a non-empty string"
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
const provider = requirements.provider;
|
|
3148
|
+
if (provider !== void 0 && !isSupportedProvider(provider)) {
|
|
3149
|
+
throw new Error(
|
|
3150
|
+
`requirements.provider must be one of: ${SUPPORTED_PROVIDERS.join(", ")}`
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
const model = requirements.model;
|
|
3154
|
+
if (model !== void 0 && !isNonEmptyString(model)) {
|
|
3155
|
+
throw new Error("requirements.model must be a non-empty string");
|
|
3156
|
+
}
|
|
3157
|
+
return {
|
|
3158
|
+
siteUrl: siteUrl.trim(),
|
|
3159
|
+
modificationRequest: modificationRequest.trim(),
|
|
3160
|
+
provider,
|
|
3161
|
+
model: typeof model === "string" ? model.trim() : void 0
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
async fetchHtmlWithTimeout(siteUrl, parentSignal) {
|
|
3165
|
+
const controller = new AbortController();
|
|
3166
|
+
const timeoutId = setTimeout(() => {
|
|
3167
|
+
controller.abort(new Error(`Site fetch timed out after ${FETCH_TIMEOUT_MS}ms`));
|
|
3168
|
+
}, FETCH_TIMEOUT_MS);
|
|
3169
|
+
const parentAbort = () => controller.abort(parentSignal?.reason);
|
|
3170
|
+
if (parentSignal) {
|
|
3171
|
+
if (parentSignal.aborted) {
|
|
3172
|
+
controller.abort(parentSignal.reason);
|
|
3173
|
+
} else {
|
|
3174
|
+
parentSignal.addEventListener("abort", parentAbort, { once: true });
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
try {
|
|
3178
|
+
const response = await fetch(siteUrl, {
|
|
3179
|
+
method: "GET",
|
|
3180
|
+
headers: { Accept: "text/html,application/xhtml+xml" },
|
|
3181
|
+
signal: controller.signal
|
|
3182
|
+
});
|
|
3183
|
+
if (!response.ok) {
|
|
3184
|
+
throw new Error(
|
|
3185
|
+
`Failed to fetch site HTML: ${response.status} ${response.statusText}`
|
|
3186
|
+
);
|
|
3187
|
+
}
|
|
3188
|
+
const contentType = response.headers.get("content-type");
|
|
3189
|
+
if (contentType && !isHtmlContentType(contentType)) {
|
|
3190
|
+
throw new Error(
|
|
3191
|
+
`Fetched content is not HTML (content-type: ${contentType})`
|
|
3192
|
+
);
|
|
3193
|
+
}
|
|
3194
|
+
const contentLengthHeader = response.headers.get("content-length");
|
|
3195
|
+
if (contentLengthHeader) {
|
|
3196
|
+
const contentLength = Number.parseInt(contentLengthHeader, 10);
|
|
3197
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_HTML_BYTES) {
|
|
3198
|
+
throw new Error(
|
|
3199
|
+
`HTML too large (${contentLength} bytes). Limit is ${MAX_HTML_BYTES} bytes`
|
|
3200
|
+
);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
const htmlBuffer = await response.arrayBuffer();
|
|
3204
|
+
if (htmlBuffer.byteLength > MAX_HTML_BYTES) {
|
|
3205
|
+
throw new Error(
|
|
3206
|
+
`HTML too large (${htmlBuffer.byteLength} bytes). Limit is ${MAX_HTML_BYTES} bytes`
|
|
3207
|
+
);
|
|
3208
|
+
}
|
|
3209
|
+
const html = new TextDecoder().decode(htmlBuffer).trim();
|
|
3210
|
+
if (!html) {
|
|
3211
|
+
throw new Error("Fetched HTML is empty");
|
|
3212
|
+
}
|
|
3213
|
+
return html;
|
|
3214
|
+
} catch (error) {
|
|
3215
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
3216
|
+
if (parentSignal?.aborted) {
|
|
3217
|
+
throw new Error("Site fetch aborted");
|
|
3218
|
+
}
|
|
3219
|
+
throw new Error(`Site fetch timed out after ${FETCH_TIMEOUT_MS}ms`);
|
|
3220
|
+
}
|
|
3221
|
+
throw error;
|
|
3222
|
+
} finally {
|
|
3223
|
+
clearTimeout(timeoutId);
|
|
3224
|
+
if (parentSignal) {
|
|
3225
|
+
parentSignal.removeEventListener("abort", parentAbort);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
};
|
|
3230
|
+
|
|
2790
3231
|
// src/utils.ts
|
|
2791
3232
|
var USDM_DECIMALS = 18;
|
|
2792
3233
|
var USDM_SCALE = 10n ** BigInt(USDM_DECIMALS);
|
|
@@ -2858,7 +3299,7 @@ var IndexerClientError = class extends Error {
|
|
|
2858
3299
|
this.body = options.body;
|
|
2859
3300
|
}
|
|
2860
3301
|
};
|
|
2861
|
-
function
|
|
3302
|
+
function isRecord2(value) {
|
|
2862
3303
|
return typeof value === "object" && value !== null;
|
|
2863
3304
|
}
|
|
2864
3305
|
function toIndexerAgent(raw) {
|
|
@@ -2986,6 +3427,31 @@ var IndexerClient = class {
|
|
|
2986
3427
|
);
|
|
2987
3428
|
return toIndexerWarrenLinks(payload.data);
|
|
2988
3429
|
}
|
|
3430
|
+
async postDeliverable(jobId, content, contentHash, storageType) {
|
|
3431
|
+
await this.requestJson("/deliverables", {
|
|
3432
|
+
method: "POST",
|
|
3433
|
+
headers: { "Content-Type": "application/json" },
|
|
3434
|
+
body: JSON.stringify({
|
|
3435
|
+
jobId,
|
|
3436
|
+
content,
|
|
3437
|
+
contentHash,
|
|
3438
|
+
storageType: storageType ?? "indexer"
|
|
3439
|
+
})
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
async getDeliverable(jobId) {
|
|
3443
|
+
try {
|
|
3444
|
+
const response = await this.request(`/deliverables/${jobId}`);
|
|
3445
|
+
if (response.status === 404) return null;
|
|
3446
|
+
const payload = await this.parseResponse(
|
|
3447
|
+
response,
|
|
3448
|
+
`/deliverables/${jobId}`
|
|
3449
|
+
);
|
|
3450
|
+
return { content: payload.data.content, storageType: payload.data.storage_type };
|
|
3451
|
+
} catch {
|
|
3452
|
+
return null;
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
2989
3455
|
async request(path, init) {
|
|
2990
3456
|
const url = `${this.baseUrl}${path}`;
|
|
2991
3457
|
const controller = new AbortController();
|
|
@@ -3021,7 +3487,7 @@ var IndexerClient = class {
|
|
|
3021
3487
|
});
|
|
3022
3488
|
}
|
|
3023
3489
|
if (!response.ok) {
|
|
3024
|
-
const apiError =
|
|
3490
|
+
const apiError = isRecord2(payload) ? payload : void 0;
|
|
3025
3491
|
const message = response.status === 404 && notFoundMessage ? notFoundMessage : apiError?.error ?? apiError?.message ?? `Indexer request failed (${response.status})`;
|
|
3026
3492
|
throw new IndexerClientError(message, {
|
|
3027
3493
|
url,
|
|
@@ -3075,10 +3541,11 @@ var ProviderRuntime = class {
|
|
|
3075
3541
|
});
|
|
3076
3542
|
const jobs = await this.indexer.getJobs({ status: 0 });
|
|
3077
3543
|
for (const job of jobs) {
|
|
3078
|
-
|
|
3544
|
+
const newKey = `new:${job.jobId}`;
|
|
3545
|
+
if (this.processedJobs.has(newKey)) continue;
|
|
3079
3546
|
const matchesOffering = offerings.some((o) => o.offeringId === job.offeringId);
|
|
3080
3547
|
if (!matchesOffering) continue;
|
|
3081
|
-
this.processedJobs.add(
|
|
3548
|
+
this.processedJobs.add(newKey);
|
|
3082
3549
|
if (this.callbacks.onJobReceived) {
|
|
3083
3550
|
try {
|
|
3084
3551
|
const accept = await this.callbacks.onJobReceived(job);
|
|
@@ -3103,7 +3570,7 @@ var ProviderRuntime = class {
|
|
|
3103
3570
|
agentId: Number(this.agentId)
|
|
3104
3571
|
});
|
|
3105
3572
|
for (const job of inProgressJobs) {
|
|
3106
|
-
const deliverKey = job.jobId
|
|
3573
|
+
const deliverKey = `deliver:${job.jobId}`;
|
|
3107
3574
|
if (this.processedJobs.has(deliverKey)) continue;
|
|
3108
3575
|
if (this.callbacks.onDeliverableRequested) {
|
|
3109
3576
|
try {
|
|
@@ -3130,7 +3597,7 @@ var ProviderRuntime = class {
|
|
|
3130
3597
|
agentId: Number(this.agentId)
|
|
3131
3598
|
});
|
|
3132
3599
|
for (const job of completedJobs) {
|
|
3133
|
-
const completeKey = job.jobId
|
|
3600
|
+
const completeKey = `complete:${job.jobId}`;
|
|
3134
3601
|
if (this.processedJobs.has(completeKey)) continue;
|
|
3135
3602
|
this.processedJobs.add(completeKey);
|
|
3136
3603
|
this.callbacks.onJobCompleted?.(job);
|
|
@@ -3241,8 +3708,9 @@ var BuyerRuntime = class {
|
|
|
3241
3708
|
agentId: Number(this.agentId)
|
|
3242
3709
|
});
|
|
3243
3710
|
for (const job of deliveredJobs) {
|
|
3244
|
-
|
|
3245
|
-
this.processedJobs.
|
|
3711
|
+
const deliveredKey = `delivered:${job.jobId}`;
|
|
3712
|
+
if (this.processedJobs.has(deliveredKey)) continue;
|
|
3713
|
+
this.processedJobs.add(deliveredKey);
|
|
3246
3714
|
try {
|
|
3247
3715
|
const deliverable = await readDeliverable(
|
|
3248
3716
|
this.client,
|
|
@@ -3267,7 +3735,7 @@ var BuyerRuntime = class {
|
|
|
3267
3735
|
agentId: Number(this.agentId)
|
|
3268
3736
|
});
|
|
3269
3737
|
for (const job of completedJobs) {
|
|
3270
|
-
const completeKey = job.jobId
|
|
3738
|
+
const completeKey = `complete:${job.jobId}`;
|
|
3271
3739
|
if (this.processedJobs.has(completeKey)) continue;
|
|
3272
3740
|
this.processedJobs.add(completeKey);
|
|
3273
3741
|
this.callbacks.onJobCompleted?.(job);
|
|
@@ -3283,8 +3751,11 @@ var HandlerProviderRuntime = class {
|
|
|
3283
3751
|
indexer;
|
|
3284
3752
|
indexerUrl;
|
|
3285
3753
|
pollInterval;
|
|
3754
|
+
executionTimeoutMs;
|
|
3286
3755
|
running = false;
|
|
3287
3756
|
processedJobs = /* @__PURE__ */ new Set();
|
|
3757
|
+
failedJobs = /* @__PURE__ */ new Map();
|
|
3758
|
+
maxRetries = 3;
|
|
3288
3759
|
onError;
|
|
3289
3760
|
constructor(client, agentId, options) {
|
|
3290
3761
|
this.client = client;
|
|
@@ -3292,6 +3763,7 @@ var HandlerProviderRuntime = class {
|
|
|
3292
3763
|
this.indexer = new IndexerClient({ baseUrl: options.indexerUrl });
|
|
3293
3764
|
this.indexerUrl = options.indexerUrl.replace(/\/$/, "");
|
|
3294
3765
|
this.pollInterval = options.pollInterval ?? 5e3;
|
|
3766
|
+
this.executionTimeoutMs = options.executionTimeoutMs ?? 5 * 60 * 1e3;
|
|
3295
3767
|
}
|
|
3296
3768
|
registerHandler(handler) {
|
|
3297
3769
|
this.handlers.set(handler.offeringId, handler);
|
|
@@ -3324,13 +3796,38 @@ var HandlerProviderRuntime = class {
|
|
|
3324
3796
|
});
|
|
3325
3797
|
const jobs = await this.indexer.getJobs({ status: 0 });
|
|
3326
3798
|
for (const job of jobs) {
|
|
3327
|
-
|
|
3799
|
+
const newKey = `new:${job.jobId}`;
|
|
3800
|
+
if (!this.canProcess(newKey)) continue;
|
|
3328
3801
|
const matchesOffering = offerings.some((o) => o.offeringId === job.offeringId);
|
|
3329
3802
|
if (!matchesOffering) continue;
|
|
3330
3803
|
const handler = this.handlers.get(job.offeringId);
|
|
3331
3804
|
if (!handler) continue;
|
|
3332
|
-
this.processedJobs.add(job.jobId);
|
|
3333
3805
|
try {
|
|
3806
|
+
if (job.slaMinutes === null) {
|
|
3807
|
+
throw new Error(`Job ${job.jobId} is missing slaMinutes`);
|
|
3808
|
+
}
|
|
3809
|
+
const context = {
|
|
3810
|
+
jobId: BigInt(job.jobId),
|
|
3811
|
+
offeringId: BigInt(job.offeringId),
|
|
3812
|
+
buyerAgentId: BigInt(job.buyerAgentId),
|
|
3813
|
+
providerAgentId: BigInt(job.providerAgentId),
|
|
3814
|
+
priceUsdm: job.priceUsdm,
|
|
3815
|
+
slaMinutes: job.slaMinutes
|
|
3816
|
+
};
|
|
3817
|
+
const reqData = await readRequirements(
|
|
3818
|
+
this.client,
|
|
3819
|
+
BigInt(job.jobId),
|
|
3820
|
+
this.indexerUrl
|
|
3821
|
+
);
|
|
3822
|
+
if (reqData) {
|
|
3823
|
+
context.requirements = reqData.requirements;
|
|
3824
|
+
}
|
|
3825
|
+
if (handler.validateRequirements) {
|
|
3826
|
+
const validation = await handler.validateRequirements(context);
|
|
3827
|
+
if (!validation.valid) {
|
|
3828
|
+
throw new Error(`Validation failed for job ${job.jobId}: ${validation.reason}`);
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3334
3831
|
if (handler.autoAccept !== false) {
|
|
3335
3832
|
await acceptJob(
|
|
3336
3833
|
this.client,
|
|
@@ -3338,8 +3835,9 @@ var HandlerProviderRuntime = class {
|
|
|
3338
3835
|
job.warrenTermsHash
|
|
3339
3836
|
);
|
|
3340
3837
|
}
|
|
3838
|
+
this.markProcessed(newKey);
|
|
3341
3839
|
} catch (e) {
|
|
3342
|
-
this.
|
|
3840
|
+
this.markFailed(newKey, e);
|
|
3343
3841
|
}
|
|
3344
3842
|
}
|
|
3345
3843
|
}
|
|
@@ -3349,15 +3847,13 @@ var HandlerProviderRuntime = class {
|
|
|
3349
3847
|
agentId: Number(this.agentId)
|
|
3350
3848
|
});
|
|
3351
3849
|
for (const job of inProgressJobs) {
|
|
3352
|
-
const deliverKey = job.jobId
|
|
3353
|
-
if (this.
|
|
3850
|
+
const deliverKey = `deliver:${job.jobId}`;
|
|
3851
|
+
if (!this.canProcess(deliverKey)) continue;
|
|
3354
3852
|
const handler = this.handlers.get(job.offeringId);
|
|
3355
3853
|
if (!handler) continue;
|
|
3356
|
-
this.processedJobs.add(deliverKey);
|
|
3357
3854
|
try {
|
|
3358
3855
|
if (job.slaMinutes === null) {
|
|
3359
|
-
|
|
3360
|
-
continue;
|
|
3856
|
+
throw new Error(`Job ${job.jobId} is missing slaMinutes`);
|
|
3361
3857
|
}
|
|
3362
3858
|
const context = {
|
|
3363
3859
|
jobId: BigInt(job.jobId),
|
|
@@ -3378,32 +3874,85 @@ var HandlerProviderRuntime = class {
|
|
|
3378
3874
|
if (handler.validateRequirements) {
|
|
3379
3875
|
const validation = await handler.validateRequirements(context);
|
|
3380
3876
|
if (!validation.valid) {
|
|
3381
|
-
this.
|
|
3877
|
+
this.markFailed(
|
|
3878
|
+
deliverKey,
|
|
3382
3879
|
new Error(`Validation failed for job ${job.jobId}: ${validation.reason}`)
|
|
3383
3880
|
);
|
|
3384
3881
|
continue;
|
|
3385
3882
|
}
|
|
3386
3883
|
}
|
|
3387
|
-
const
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
}
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3884
|
+
const remainingSlaMs = this.getRemainingSlaMs(job.createdAt, job.acceptedAt, job.slaMinutes);
|
|
3885
|
+
if (remainingSlaMs !== null && remainingSlaMs < 10 * 60 * 1e3) {
|
|
3886
|
+
console.warn(
|
|
3887
|
+
`Skipping job ${job.jobId}: SLA remaining ${Math.max(0, Math.floor(remainingSlaMs / 1e3))}s is below 10 minutes`
|
|
3888
|
+
);
|
|
3889
|
+
this.markProcessed(deliverKey);
|
|
3890
|
+
continue;
|
|
3891
|
+
}
|
|
3892
|
+
const controller = new AbortController();
|
|
3893
|
+
const executionContext = { ...context, abortSignal: controller.signal };
|
|
3894
|
+
let timeoutId;
|
|
3895
|
+
try {
|
|
3896
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
3897
|
+
timeoutId = setTimeout(() => {
|
|
3898
|
+
controller.abort();
|
|
3899
|
+
reject(
|
|
3900
|
+
new Error(
|
|
3901
|
+
`Execution timed out for job ${job.jobId} after ${this.executionTimeoutMs}ms`
|
|
3902
|
+
)
|
|
3903
|
+
);
|
|
3904
|
+
}, this.executionTimeoutMs);
|
|
3905
|
+
});
|
|
3906
|
+
const result = await Promise.race([
|
|
3907
|
+
handler.executeJob(executionContext),
|
|
3908
|
+
timeoutPromise
|
|
3909
|
+
]);
|
|
3910
|
+
const deliverableParams = {
|
|
3911
|
+
type: result.type,
|
|
3912
|
+
content: result.content,
|
|
3913
|
+
url: result.url,
|
|
3914
|
+
mimeType: result.mimeType,
|
|
3915
|
+
jobId: BigInt(job.jobId)
|
|
3916
|
+
};
|
|
3917
|
+
await deployDeliverable(
|
|
3918
|
+
this.client,
|
|
3919
|
+
this.agentId,
|
|
3920
|
+
BigInt(job.jobId),
|
|
3921
|
+
deliverableParams,
|
|
3922
|
+
this.indexerUrl
|
|
3923
|
+
);
|
|
3924
|
+
} finally {
|
|
3925
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
3926
|
+
}
|
|
3927
|
+
this.markProcessed(deliverKey);
|
|
3402
3928
|
} catch (e) {
|
|
3403
|
-
this.
|
|
3929
|
+
this.markFailed(deliverKey, e);
|
|
3404
3930
|
}
|
|
3405
3931
|
}
|
|
3406
3932
|
}
|
|
3933
|
+
canProcess(key) {
|
|
3934
|
+
if (this.processedJobs.has(key)) return false;
|
|
3935
|
+
return (this.failedJobs.get(key) ?? 0) < this.maxRetries;
|
|
3936
|
+
}
|
|
3937
|
+
markProcessed(key) {
|
|
3938
|
+
this.failedJobs.delete(key);
|
|
3939
|
+
this.processedJobs.add(key);
|
|
3940
|
+
}
|
|
3941
|
+
markFailed(key, error) {
|
|
3942
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3943
|
+
this.onError?.(err);
|
|
3944
|
+
const retries = (this.failedJobs.get(key) ?? 0) + 1;
|
|
3945
|
+
this.failedJobs.set(key, retries);
|
|
3946
|
+
if (retries >= this.maxRetries) {
|
|
3947
|
+
this.onError?.(new Error(`Giving up on ${key} after ${retries} failed attempts`));
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
getRemainingSlaMs(createdAt, acceptedAt, slaMinutes) {
|
|
3951
|
+
const start = acceptedAt ?? createdAt;
|
|
3952
|
+
if (start === null) return null;
|
|
3953
|
+
const startMs = start > 1e12 ? start : start * 1e3;
|
|
3954
|
+
return startMs + slaMinutes * 60 * 1e3 - Date.now();
|
|
3955
|
+
}
|
|
3407
3956
|
};
|
|
3408
3957
|
|
|
3409
3958
|
// src/abis/FeeDistributor.ts
|
|
@@ -4270,11 +4819,13 @@ export {
|
|
|
4270
4819
|
ProviderRuntime,
|
|
4271
4820
|
RISK_POOL_BPS,
|
|
4272
4821
|
ServiceType,
|
|
4822
|
+
SiteModifierHandler,
|
|
4273
4823
|
TESTNET_ADDRESSES,
|
|
4274
4824
|
TREASURY_BPS,
|
|
4275
4825
|
USDM_MAINNET,
|
|
4276
4826
|
acceptJob,
|
|
4277
4827
|
activateOffering,
|
|
4828
|
+
callAI,
|
|
4278
4829
|
cancelJob,
|
|
4279
4830
|
createAgentCard,
|
|
4280
4831
|
createDeliverable,
|
|
@@ -4317,6 +4868,8 @@ export {
|
|
|
4317
4868
|
readWarrenMaster,
|
|
4318
4869
|
readWarrenPage,
|
|
4319
4870
|
registerAgent,
|
|
4871
|
+
rejectJob,
|
|
4872
|
+
resolveDispute,
|
|
4320
4873
|
serviceMarketplaceAbi,
|
|
4321
4874
|
setOperator,
|
|
4322
4875
|
setWarrenContract,
|