@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.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: PLACEHOLDER_ADDRESS,
40
- serviceMarketplace: PLACEHOLDER_ADDRESS,
41
- jobEngine: PLACEHOLDER_ADDRESS,
42
- feeDistributor: PLACEHOLDER_ADDRESS,
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
- return write(client, {
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 isRecord(value) {
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 = isRecord(payload) ? payload : void 0;
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
- if (this.processedJobs.has(job.jobId)) continue;
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(job.jobId);
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 * 1e3;
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 * 1e4;
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
- if (this.processedJobs.has(job.jobId)) continue;
3245
- this.processedJobs.add(job.jobId);
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 * 1e3;
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
- if (this.processedJobs.has(job.jobId)) continue;
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.onError?.(e instanceof Error ? e : new Error(String(e)));
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 * 1e3;
3353
- if (this.processedJobs.has(deliverKey)) continue;
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
- this.onError?.(new Error(`Job ${job.jobId} is missing slaMinutes`));
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.onError?.(
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 result = await handler.executeJob(context);
3388
- const deliverableParams = {
3389
- type: result.type,
3390
- content: result.content,
3391
- url: result.url,
3392
- mimeType: result.mimeType,
3393
- jobId: BigInt(job.jobId)
3394
- };
3395
- await deployDeliverable(
3396
- this.client,
3397
- this.agentId,
3398
- BigInt(job.jobId),
3399
- deliverableParams,
3400
- this.indexerUrl
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.onError?.(e instanceof Error ? e : new Error(String(e)));
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,