@agrentingai/paperclip-adapter 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +306 -0
- package/dist/server/index.cjs +1406 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +895 -0
- package/dist/server/index.d.ts +895 -0
- package/dist/server/index.js +1336 -0
- package/dist/server/index.js.map +1 -0
- package/dist/ui/index.cjs +125 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +36 -0
- package/dist/ui/index.d.ts +36 -0
- package/dist/ui/index.js +98 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +65 -0
- package/server/src/adapter.test.ts +497 -0
- package/server/src/adapter.ts +1044 -0
- package/server/src/balance-monitor.test.ts +147 -0
- package/server/src/balance-monitor.ts +118 -0
- package/server/src/client.test.ts +949 -0
- package/server/src/client.ts +550 -0
- package/server/src/comment-sync.test.ts +25 -0
- package/server/src/comment-sync.ts +71 -0
- package/server/src/crypto.test.ts +62 -0
- package/server/src/crypto.ts +25 -0
- package/server/src/index.ts +67 -0
- package/server/src/polling.test.ts +208 -0
- package/server/src/polling.ts +183 -0
- package/server/src/types.ts +244 -0
- package/server/src/webhook-handler.test.ts +379 -0
- package/server/src/webhook-handler.ts +292 -0
- package/ui/src/adapter.ts +131 -0
- package/ui/src/index.ts +2 -0
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
import { AgrentingClient } from "./client.js";
|
|
2
|
+
import type {
|
|
3
|
+
AgrentingAdapterConfig,
|
|
4
|
+
AgrentingExecutionResult,
|
|
5
|
+
AgrentingTaskStatus,
|
|
6
|
+
AgentInfo,
|
|
7
|
+
AgentProfile,
|
|
8
|
+
BalanceInfo as BalanceInfoRaw,
|
|
9
|
+
HireAgentResult,
|
|
10
|
+
PaymentInfo,
|
|
11
|
+
ReassignTaskResult,
|
|
12
|
+
SendMessageResult,
|
|
13
|
+
TransactionInfo,
|
|
14
|
+
DiscoverAgentsOptions,
|
|
15
|
+
CreateTaskPaymentOptions,
|
|
16
|
+
Hiring,
|
|
17
|
+
TaskMessage,
|
|
18
|
+
HiringMessage,
|
|
19
|
+
Capability,
|
|
20
|
+
AutoSelectOptions,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
import { registerTaskMapping } from "./webhook-handler.js";
|
|
23
|
+
import { pollTaskUntilDone } from "./polling.js";
|
|
24
|
+
import { canSubmitTask } from "./balance-monitor.js";
|
|
25
|
+
import { verifyWebhookSignature } from "./crypto.js";
|
|
26
|
+
import { formatAgentResponse } from "./comment-sync.js";
|
|
27
|
+
|
|
28
|
+
const DEFAULT_WEBHOOK_PORT = 8765;
|
|
29
|
+
const MAX_WEBHOOK_BODY_SIZE = 1024 * 1024; // 1MB — prevent OOM from oversized bodies
|
|
30
|
+
const STALE_TASK_CLEANUP_INTERVAL_MS = 60_000; // 60s
|
|
31
|
+
const STALE_TASK_TTL_MS = 2 * 60 * 60 * 1000; // 2h — max age before cleanup sweeps it
|
|
32
|
+
|
|
33
|
+
/** In-memory store for webhook listeners keyed by task ID */
|
|
34
|
+
const pendingTasks = new Map<
|
|
35
|
+
string,
|
|
36
|
+
{
|
|
37
|
+
resolve: (result: AgrentingExecutionResult) => void;
|
|
38
|
+
status: AgrentingTaskStatus;
|
|
39
|
+
progressPercent: number;
|
|
40
|
+
progressMessage?: string;
|
|
41
|
+
startedAt: number;
|
|
42
|
+
createdAt: number;
|
|
43
|
+
settled: boolean;
|
|
44
|
+
}
|
|
45
|
+
>();
|
|
46
|
+
|
|
47
|
+
let webhookServer: ReturnType<typeof import("http").createServer> | null = null;
|
|
48
|
+
let staleCleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Sweep pendingTasks for entries that are settled or older than the TTL,
|
|
52
|
+
* removing them from the map to bound memory usage.
|
|
53
|
+
*/
|
|
54
|
+
function sweepStaleTasks(): void {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
for (const [id, entry] of pendingTasks) {
|
|
57
|
+
if (entry.settled || now - entry.createdAt > STALE_TASK_TTL_MS) {
|
|
58
|
+
pendingTasks.delete(id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* JSON Schema for Agrenting adapter configuration fields.
|
|
65
|
+
* Used by Paperclip server to validate adapter config at creation time.
|
|
66
|
+
*/
|
|
67
|
+
export function getConfigSchema(): Record<string, unknown> {
|
|
68
|
+
return {
|
|
69
|
+
type: "object",
|
|
70
|
+
required: ["agrentingUrl", "apiKey", "agentDid"],
|
|
71
|
+
properties: {
|
|
72
|
+
agrentingUrl: {
|
|
73
|
+
type: "string",
|
|
74
|
+
format: "uri",
|
|
75
|
+
description: "Agrenting platform URL (e.g. https://www.agrenting.com)",
|
|
76
|
+
default: "https://www.agrenting.com",
|
|
77
|
+
},
|
|
78
|
+
apiKey: {
|
|
79
|
+
type: "string",
|
|
80
|
+
description: "Agrenting API key for authentication",
|
|
81
|
+
sensitive: true,
|
|
82
|
+
},
|
|
83
|
+
agentDid: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description:
|
|
86
|
+
"Decentralized identifier of the target agent (did:agrenting:...)",
|
|
87
|
+
},
|
|
88
|
+
webhookSecret: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "Webhook signing secret for task completion callbacks",
|
|
91
|
+
sensitive: true,
|
|
92
|
+
},
|
|
93
|
+
webhookCallbackUrl: {
|
|
94
|
+
type: "string",
|
|
95
|
+
format: "uri",
|
|
96
|
+
description:
|
|
97
|
+
"URL where Agrenting should POST task events (e.g. https://your-host:8765/webhook)",
|
|
98
|
+
},
|
|
99
|
+
pricingModel: {
|
|
100
|
+
type: "string",
|
|
101
|
+
enum: ["fixed", "per-token", "subscription"],
|
|
102
|
+
description: "Pricing model for this agent",
|
|
103
|
+
default: "fixed",
|
|
104
|
+
},
|
|
105
|
+
timeoutSec: {
|
|
106
|
+
type: "integer",
|
|
107
|
+
minimum: 10,
|
|
108
|
+
maximum: 3600,
|
|
109
|
+
description: "Task timeout in seconds",
|
|
110
|
+
default: 600,
|
|
111
|
+
},
|
|
112
|
+
instructionsBundleMode: {
|
|
113
|
+
type: "string",
|
|
114
|
+
enum: ["managed", "inline"],
|
|
115
|
+
description: "How agent instructions are delivered",
|
|
116
|
+
default: "inline",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validate the adapter configuration and test connectivity.
|
|
124
|
+
*/
|
|
125
|
+
export async function testEnvironment(
|
|
126
|
+
config: AgrentingAdapterConfig
|
|
127
|
+
): Promise<{ ok: boolean; message: string }> {
|
|
128
|
+
const client = new AgrentingClient(config);
|
|
129
|
+
return client.testConnection();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Start an HTTP listener that receives webhook callbacks from Agrenting.
|
|
134
|
+
* Returns the base URL of the listener (for registering with Agrenting).
|
|
135
|
+
*
|
|
136
|
+
* The listener resolves pending `execute()` calls when task events arrive.
|
|
137
|
+
* Only call this once per process — subsequent calls return the existing server.
|
|
138
|
+
*/
|
|
139
|
+
export async function startWebhookListener(
|
|
140
|
+
config: AgrentingAdapterConfig
|
|
141
|
+
): Promise<string> {
|
|
142
|
+
if (webhookServer) {
|
|
143
|
+
const addr = webhookServer.address();
|
|
144
|
+
const port =
|
|
145
|
+
typeof addr === "object" && addr ? addr.port : DEFAULT_WEBHOOK_PORT;
|
|
146
|
+
return `http://localhost:${port}/webhook`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const http = await import("http");
|
|
150
|
+
const port = process.env.PAPERCLIP_WEBHOOK_PORT
|
|
151
|
+
? parseInt(process.env.PAPERCLIP_WEBHOOK_PORT, 10)
|
|
152
|
+
: DEFAULT_WEBHOOK_PORT;
|
|
153
|
+
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const server = http.createServer(async (req, res) => {
|
|
156
|
+
if (req.method !== "POST" || req.url !== "/webhook") {
|
|
157
|
+
res.writeHead(404);
|
|
158
|
+
res.end("Not found");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let bodyChunks: Buffer[] = [];
|
|
163
|
+
let bodyLength = 0;
|
|
164
|
+
let bodyTooLarge = false;
|
|
165
|
+
req.on("data", (chunk: Buffer) => {
|
|
166
|
+
bodyLength += chunk.length;
|
|
167
|
+
if (bodyLength > MAX_WEBHOOK_BODY_SIZE) {
|
|
168
|
+
bodyTooLarge = true;
|
|
169
|
+
res.writeHead(413);
|
|
170
|
+
res.end("Request body too large");
|
|
171
|
+
req.destroy();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
bodyChunks.push(chunk);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
req.on("end", async () => {
|
|
178
|
+
if (bodyTooLarge) return;
|
|
179
|
+
const rawBody = Buffer.concat(bodyChunks).toString("utf8");
|
|
180
|
+
bodyChunks = []; // free reference for GC
|
|
181
|
+
const taskId = req.headers["x-webhook-task-id"] as string | undefined;
|
|
182
|
+
const signature =
|
|
183
|
+
(req.headers["x-webhook-signature"] as string) || "";
|
|
184
|
+
|
|
185
|
+
let payload: Record<string, unknown>;
|
|
186
|
+
try {
|
|
187
|
+
payload = JSON.parse(rawBody);
|
|
188
|
+
} catch {
|
|
189
|
+
res.writeHead(400);
|
|
190
|
+
res.end("Invalid JSON");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Verify signature if secret is configured
|
|
195
|
+
if (config.webhookSecret) {
|
|
196
|
+
if (!signature) {
|
|
197
|
+
res.writeHead(401);
|
|
198
|
+
res.end("Missing signature");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const valid = await verifyWebhookSignature(
|
|
202
|
+
rawBody,
|
|
203
|
+
signature,
|
|
204
|
+
config.webhookSecret
|
|
205
|
+
);
|
|
206
|
+
if (!valid) {
|
|
207
|
+
res.writeHead(401);
|
|
208
|
+
res.end("Invalid signature");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Extract task ID from payload if not in header
|
|
214
|
+
const resolvedTaskId =
|
|
215
|
+
taskId ??
|
|
216
|
+
(payload.task_id as string) ??
|
|
217
|
+
(payload.taskId as string);
|
|
218
|
+
|
|
219
|
+
if (resolvedTaskId) {
|
|
220
|
+
const pending = pendingTasks.get(resolvedTaskId);
|
|
221
|
+
if (pending && !pending.settled) {
|
|
222
|
+
const status = (payload.status as AgrentingTaskStatus) ?? pending.status;
|
|
223
|
+
pending.status = status;
|
|
224
|
+
pending.progressPercent =
|
|
225
|
+
(payload.progress_percent as number) ?? pending.progressPercent;
|
|
226
|
+
pending.progressMessage =
|
|
227
|
+
(payload.progress_message as string) ?? pending.progressMessage;
|
|
228
|
+
|
|
229
|
+
if (status === "completed") {
|
|
230
|
+
pending.settled = true;
|
|
231
|
+
pending.resolve({
|
|
232
|
+
success: true,
|
|
233
|
+
output: (payload.output as string) ?? JSON.stringify(payload.output ?? {}),
|
|
234
|
+
taskId: resolvedTaskId,
|
|
235
|
+
durationMs: Date.now() - pending.startedAt,
|
|
236
|
+
});
|
|
237
|
+
} else if (status === "failed") {
|
|
238
|
+
pending.settled = true;
|
|
239
|
+
pending.resolve({
|
|
240
|
+
success: false,
|
|
241
|
+
error:
|
|
242
|
+
(payload.error_reason as string) ??
|
|
243
|
+
"Task failed with no reason provided",
|
|
244
|
+
taskId: resolvedTaskId,
|
|
245
|
+
durationMs: Date.now() - pending.startedAt,
|
|
246
|
+
});
|
|
247
|
+
} else if (status === "cancelled") {
|
|
248
|
+
pending.settled = true;
|
|
249
|
+
pending.resolve({
|
|
250
|
+
success: false,
|
|
251
|
+
error: "Task was cancelled",
|
|
252
|
+
taskId: resolvedTaskId,
|
|
253
|
+
durationMs: Date.now() - pending.startedAt,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
260
|
+
res.end("OK");
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
server.listen(port, () => {
|
|
265
|
+
webhookServer = server;
|
|
266
|
+
// Start periodic cleanup of stale entries
|
|
267
|
+
if (!staleCleanupTimer) {
|
|
268
|
+
staleCleanupTimer = setInterval(sweepStaleTasks, STALE_TASK_CLEANUP_INTERVAL_MS);
|
|
269
|
+
staleCleanupTimer.unref();
|
|
270
|
+
}
|
|
271
|
+
resolve(`http://localhost:${port}/webhook`);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
server.on("error", reject);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const WEBHOOK_STOP_TIMEOUT_MS = 5_000;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Stop the webhook listener if it was started.
|
|
282
|
+
* Closes all active connections and waits up to 5s for a clean shutdown.
|
|
283
|
+
*/
|
|
284
|
+
export async function stopWebhookListener(): Promise<void> {
|
|
285
|
+
if (!webhookServer) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (staleCleanupTimer) {
|
|
290
|
+
clearInterval(staleCleanupTimer);
|
|
291
|
+
staleCleanupTimer = null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const server = webhookServer;
|
|
295
|
+
webhookServer = null;
|
|
296
|
+
|
|
297
|
+
// Force-close all active connections (Node 18.2+) so server.close() doesn't hang
|
|
298
|
+
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
|
|
299
|
+
(server as import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>).closeAllConnections();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return new Promise((resolve) => {
|
|
303
|
+
const timeout = setTimeout(() => {
|
|
304
|
+
resolve();
|
|
305
|
+
}, WEBHOOK_STOP_TIMEOUT_MS);
|
|
306
|
+
timeout.unref();
|
|
307
|
+
|
|
308
|
+
server.close(() => {
|
|
309
|
+
clearTimeout(timeout);
|
|
310
|
+
resolve();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Register a webhook with Agrenting to receive task lifecycle events.
|
|
317
|
+
* Returns the webhook ID and secret key.
|
|
318
|
+
*/
|
|
319
|
+
export async function registerWebhook(
|
|
320
|
+
config: AgrentingAdapterConfig,
|
|
321
|
+
callbackUrl?: string
|
|
322
|
+
): Promise<{
|
|
323
|
+
id: string;
|
|
324
|
+
secretKey: string;
|
|
325
|
+
callbackUrl: string;
|
|
326
|
+
}> {
|
|
327
|
+
const client = new AgrentingClient(config);
|
|
328
|
+
const url = callbackUrl ?? (await startWebhookListener(config));
|
|
329
|
+
|
|
330
|
+
const result = await client.registerWebhook({
|
|
331
|
+
callbackUrl: url,
|
|
332
|
+
eventTypes: [
|
|
333
|
+
"task.created",
|
|
334
|
+
"task.claimed",
|
|
335
|
+
"task.in_progress",
|
|
336
|
+
"task.completed",
|
|
337
|
+
"task.failed",
|
|
338
|
+
"task.cancelled",
|
|
339
|
+
],
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
id: result.id,
|
|
344
|
+
secretKey: result.secret_key,
|
|
345
|
+
callbackUrl: result.callback_url,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Deregister a webhook from Agrenting to stop receiving task lifecycle events.
|
|
351
|
+
* Use this to clean up orphaned webhooks when they are no longer needed.
|
|
352
|
+
*/
|
|
353
|
+
export async function deregisterWebhook(
|
|
354
|
+
config: AgrentingAdapterConfig,
|
|
355
|
+
webhookId: string
|
|
356
|
+
): Promise<void> {
|
|
357
|
+
const client = new AgrentingClient(config);
|
|
358
|
+
await client.deleteWebhook(webhookId);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Execute a task by submitting it to the Agrenting platform.
|
|
363
|
+
*
|
|
364
|
+
* Uses webhook callbacks when `webhookCallbackUrl` is configured in the adapter config
|
|
365
|
+
* (or when `startWebhookListener()` has been called). Falls back to polling
|
|
366
|
+
* when webhooks are not available.
|
|
367
|
+
*
|
|
368
|
+
* When `maxPrice` is provided, the task is created with a budget and escrow funds
|
|
369
|
+
* are locked via `createTaskPayment()` after submission.
|
|
370
|
+
*/
|
|
371
|
+
export async function execute(
|
|
372
|
+
config: AgrentingAdapterConfig,
|
|
373
|
+
params: {
|
|
374
|
+
input: string;
|
|
375
|
+
capability: string;
|
|
376
|
+
instructions?: string;
|
|
377
|
+
/** Maximum price in USD to budget for this task. Triggers escrow payment. */
|
|
378
|
+
maxPrice?: string;
|
|
379
|
+
/** Payment type: "crypto" | "escrow" | "nowpayments". Defaults to "crypto". */
|
|
380
|
+
paymentType?: string;
|
|
381
|
+
}
|
|
382
|
+
): Promise<AgrentingExecutionResult> {
|
|
383
|
+
const client = new AgrentingClient(config);
|
|
384
|
+
const startTime = Date.now();
|
|
385
|
+
|
|
386
|
+
// Upload instructions if managed mode is configured
|
|
387
|
+
if (
|
|
388
|
+
config.instructionsBundleMode === "managed" &&
|
|
389
|
+
params.instructions
|
|
390
|
+
) {
|
|
391
|
+
await client.uploadDocument({
|
|
392
|
+
name: "instructions",
|
|
393
|
+
content: params.instructions,
|
|
394
|
+
documentType: "instructions",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Pre-submission balance check (non-blocking — logs warning but doesn't prevent)
|
|
399
|
+
const balanceCheck = await canSubmitTask({ config });
|
|
400
|
+
if (!balanceCheck.ok) {
|
|
401
|
+
// Log but don't block — let the task fail naturally
|
|
402
|
+
console.warn(`[adapter-agrenting] ${balanceCheck.reason}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Submit the task to Agrenting
|
|
406
|
+
const task = await client.createTask({
|
|
407
|
+
providerAgentId: config.agentDid,
|
|
408
|
+
capability: params.capability,
|
|
409
|
+
input: params.input,
|
|
410
|
+
maxPrice: params.maxPrice,
|
|
411
|
+
paymentType: params.paymentType,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const taskId = task.id;
|
|
415
|
+
|
|
416
|
+
// Lock escrow funds if a max price was specified.
|
|
417
|
+
// If payment fails, cancel the orphaned task to avoid leaving it stuck on the server.
|
|
418
|
+
let payment: PaymentInfo | undefined;
|
|
419
|
+
if (params.maxPrice) {
|
|
420
|
+
try {
|
|
421
|
+
const paymentOptions: CreateTaskPaymentOptions = {};
|
|
422
|
+
if (params.paymentType) paymentOptions.paymentType = params.paymentType;
|
|
423
|
+
payment = await client.createTaskPayment(taskId, paymentOptions);
|
|
424
|
+
console.log(`[adapter-agrenting] Escrow locked for task ${taskId}: ${payment.amount} ${payment.currency} (${payment.status})`);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
console.error(`[adapter-agrenting] Failed to lock escrow for task ${taskId}, cancelling orphaned task:`, err);
|
|
427
|
+
try {
|
|
428
|
+
await client.cancelTask(taskId);
|
|
429
|
+
} catch {
|
|
430
|
+
// Best-effort cleanup — log but don't throw, the payment error is the real problem
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
success: false,
|
|
434
|
+
error: `Escrow payment failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
435
|
+
taskId,
|
|
436
|
+
durationMs: Date.now() - startTime,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Register for webhook callbacks only when webhook mode is actually configured.
|
|
442
|
+
// The taskRegistry in webhook-handler.ts is for the Paperclip-side webhook handler
|
|
443
|
+
// (issue status updates), while pendingTasks below is for the in-process listener
|
|
444
|
+
// (resolving execute() promises). They serve different purposes.
|
|
445
|
+
if (config.webhookCallbackUrl || config.webhookSecret) {
|
|
446
|
+
registerTaskMapping(taskId, taskId, config.agrentingUrl, config);
|
|
447
|
+
return executeWithWebhook(client, config, taskId, startTime);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Fall back to polling — delegate to pollTaskUntilDone
|
|
451
|
+
return executeWithPolling(config, taskId, startTime);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Execute with webhook: register a listener, submit task, wait for callback.
|
|
456
|
+
* Falls back to polling if no webhook received within the grace period.
|
|
457
|
+
*/
|
|
458
|
+
async function executeWithWebhook(
|
|
459
|
+
_client: AgrentingClient,
|
|
460
|
+
config: AgrentingAdapterConfig,
|
|
461
|
+
taskId: string,
|
|
462
|
+
startTime: number
|
|
463
|
+
): Promise<AgrentingExecutionResult> {
|
|
464
|
+
// Ensure the listener is running
|
|
465
|
+
await startWebhookListener(config);
|
|
466
|
+
|
|
467
|
+
const deadline = startTime + (config.timeoutSec ?? 600) * 1000;
|
|
468
|
+
|
|
469
|
+
// AbortController for clean cancellation when webhook resolves first
|
|
470
|
+
const abortController = new AbortController();
|
|
471
|
+
|
|
472
|
+
// Register the pending task so the webhook handler can resolve it
|
|
473
|
+
const pending = new Promise<AgrentingExecutionResult>((resolve) => {
|
|
474
|
+
pendingTasks.set(taskId, {
|
|
475
|
+
resolve,
|
|
476
|
+
status: "pending",
|
|
477
|
+
progressPercent: 0,
|
|
478
|
+
startedAt: startTime,
|
|
479
|
+
createdAt: Date.now(),
|
|
480
|
+
settled: false,
|
|
481
|
+
});
|
|
482
|
+
}).then((result) => {
|
|
483
|
+
// Webhook resolved — abort any in-flight polling
|
|
484
|
+
abortController.abort();
|
|
485
|
+
return result;
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Race between webhook callback and timeout
|
|
489
|
+
const timeout = new Promise<AgrentingExecutionResult>((resolve) => {
|
|
490
|
+
const ms = deadline - Date.now();
|
|
491
|
+
setTimeout(() => {
|
|
492
|
+
const entry = pendingTasks.get(taskId);
|
|
493
|
+
if (entry && !entry.settled) {
|
|
494
|
+
entry.settled = true;
|
|
495
|
+
}
|
|
496
|
+
resolve({
|
|
497
|
+
success: false,
|
|
498
|
+
error: `Task timed out after ${config.timeoutSec ?? 600}s`,
|
|
499
|
+
taskId,
|
|
500
|
+
durationMs: Date.now() - startTime,
|
|
501
|
+
});
|
|
502
|
+
}, Math.max(ms, 0));
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// If webhook doesn't resolve within grace period, fall back to polling.
|
|
506
|
+
// Polling is deferred so it only starts when the timeout actually fires,
|
|
507
|
+
// avoiding wasted HTTP calls when the webhook wins.
|
|
508
|
+
return Promise.race([pending, timeout.then(async (result) => {
|
|
509
|
+
if (!result.success && result.error?.includes("timed out")) {
|
|
510
|
+
const pollingFallback = await pollTaskUntilDone({
|
|
511
|
+
config,
|
|
512
|
+
taskId,
|
|
513
|
+
deadline,
|
|
514
|
+
signal: abortController.signal,
|
|
515
|
+
});
|
|
516
|
+
return pollingFallback.result;
|
|
517
|
+
}
|
|
518
|
+
return result;
|
|
519
|
+
})]);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Execute with polling by delegating to pollTaskUntilDone.
|
|
524
|
+
* Avoids reimplementing the backoff loop that polling.ts already provides.
|
|
525
|
+
*/
|
|
526
|
+
async function executeWithPolling(
|
|
527
|
+
config: AgrentingAdapterConfig,
|
|
528
|
+
taskId: string,
|
|
529
|
+
startTime: number
|
|
530
|
+
): Promise<AgrentingExecutionResult> {
|
|
531
|
+
const deadline = startTime + (config.timeoutSec ?? 600) * 1000;
|
|
532
|
+
const { result } = await pollTaskUntilDone({ config, taskId, deadline });
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Get the current progress of a task including percentage and message.
|
|
538
|
+
* Useful for progress monitoring in UI dashboards.
|
|
539
|
+
*/
|
|
540
|
+
export async function getTaskProgress(
|
|
541
|
+
config: AgrentingAdapterConfig,
|
|
542
|
+
taskId: string
|
|
543
|
+
): Promise<{
|
|
544
|
+
status: AgrentingTaskStatus;
|
|
545
|
+
progressPercent: number;
|
|
546
|
+
progressMessage?: string;
|
|
547
|
+
timeline: Array<{
|
|
548
|
+
event_type: string;
|
|
549
|
+
timestamp: string;
|
|
550
|
+
progress_percent?: number;
|
|
551
|
+
progress_message?: string;
|
|
552
|
+
}>;
|
|
553
|
+
}> {
|
|
554
|
+
const client = new AgrentingClient(config);
|
|
555
|
+
const [progress, timeline] = await Promise.all([
|
|
556
|
+
client.getTaskProgress(taskId),
|
|
557
|
+
client.getTaskTimeline(taskId),
|
|
558
|
+
]);
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
status: progress.status as AgrentingTaskStatus,
|
|
562
|
+
progressPercent: progress.progress_percent,
|
|
563
|
+
progressMessage: progress.progress_message,
|
|
564
|
+
timeline: timeline.events,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Discover marketplace agents available for hire.
|
|
570
|
+
* Filters by capability, price range, reputation, and availability.
|
|
571
|
+
*/
|
|
572
|
+
export async function discoverAgents(
|
|
573
|
+
config: AgrentingAdapterConfig,
|
|
574
|
+
options: DiscoverAgentsOptions = {}
|
|
575
|
+
): Promise<AgentInfo[]> {
|
|
576
|
+
const client = new AgrentingClient(config);
|
|
577
|
+
return client.discoverAgents(options);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Get the current platform balance including available, escrowed, and total amounts.
|
|
582
|
+
*/
|
|
583
|
+
export async function getBalance(
|
|
584
|
+
config: AgrentingAdapterConfig
|
|
585
|
+
): Promise<BalanceInfoRaw> {
|
|
586
|
+
const client = new AgrentingClient(config);
|
|
587
|
+
return client.getBalance();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* List recent ledger transactions.
|
|
592
|
+
*/
|
|
593
|
+
export async function getTransactions(
|
|
594
|
+
config: AgrentingAdapterConfig,
|
|
595
|
+
options: { limit?: number; offset?: number; type?: string } = {}
|
|
596
|
+
): Promise<TransactionInfo[]> {
|
|
597
|
+
const client = new AgrentingClient(config);
|
|
598
|
+
return client.getTransactions(options);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Deposit funds into the Agrenting ledger.
|
|
603
|
+
*/
|
|
604
|
+
export async function deposit(
|
|
605
|
+
config: AgrentingAdapterConfig,
|
|
606
|
+
params: { amount: string; currency?: string; paymentMethod?: string }
|
|
607
|
+
): Promise<{ transaction_id: string; status: string; deposit_address?: string; payment_url?: string }> {
|
|
608
|
+
const client = new AgrentingClient(config);
|
|
609
|
+
return client.deposit(params);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Withdraw funds from the Agrenting ledger to an external wallet.
|
|
614
|
+
*/
|
|
615
|
+
export async function withdraw(
|
|
616
|
+
config: AgrentingAdapterConfig,
|
|
617
|
+
params: { amount: string; currency?: string; withdrawalAddressId?: string }
|
|
618
|
+
): Promise<{ transaction_id: string; status: string }> {
|
|
619
|
+
const client = new AgrentingClient(config);
|
|
620
|
+
return client.withdraw(params);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Get the payment status and escrow details for a task.
|
|
625
|
+
*/
|
|
626
|
+
export async function getTaskPayment(
|
|
627
|
+
config: AgrentingAdapterConfig,
|
|
628
|
+
taskId: string
|
|
629
|
+
): Promise<PaymentInfo | undefined> {
|
|
630
|
+
const client = new AgrentingClient(config);
|
|
631
|
+
try {
|
|
632
|
+
return await client.getTaskPayment(taskId);
|
|
633
|
+
} catch {
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Cancel a running task.
|
|
640
|
+
*/
|
|
641
|
+
export async function cancelTask(
|
|
642
|
+
config: AgrentingAdapterConfig,
|
|
643
|
+
taskId: string
|
|
644
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
645
|
+
const client = new AgrentingClient(config);
|
|
646
|
+
try {
|
|
647
|
+
await client.cancelTask(taskId);
|
|
648
|
+
pendingTasks.delete(taskId);
|
|
649
|
+
return { success: true };
|
|
650
|
+
} catch (err) {
|
|
651
|
+
return {
|
|
652
|
+
success: false,
|
|
653
|
+
error: err instanceof Error ? err.message : "Failed to cancel task",
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// -------------------------------------------------------------------------
|
|
659
|
+
// New adapter functions for hire, messaging, auto-select, and retry
|
|
660
|
+
// -------------------------------------------------------------------------
|
|
661
|
+
|
|
662
|
+
/** Task retry configuration */
|
|
663
|
+
const TASK_MAX_RETRIES = 2;
|
|
664
|
+
const TASK_RETRY_BASE_DELAY_MS = 1000; // 1s initial delay
|
|
665
|
+
const TASK_RETRY_MAX_DELAY_MS = 30_000; // 30s max delay
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Hire an agent by DID. Returns hiring record and adapter config for auto-provisioning.
|
|
669
|
+
* This is the primary entry point for the "browse marketplace, click Hire" flow.
|
|
670
|
+
*/
|
|
671
|
+
export async function hireAgent(
|
|
672
|
+
config: AgrentingAdapterConfig,
|
|
673
|
+
agentDid: string
|
|
674
|
+
): Promise<HireAgentResult> {
|
|
675
|
+
const client = new AgrentingClient(config);
|
|
676
|
+
return client.hireAgent(agentDid);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Get full agent profile by DID.
|
|
681
|
+
* Returns description, capabilities, pricing, reputation, and availability.
|
|
682
|
+
*/
|
|
683
|
+
export async function getAgentProfile(
|
|
684
|
+
config: AgrentingAdapterConfig,
|
|
685
|
+
agentDid: string
|
|
686
|
+
): Promise<AgentProfile> {
|
|
687
|
+
const client = new AgrentingClient(config);
|
|
688
|
+
return client.getAgentProfile(agentDid);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Send a message to an active task for bidirectional communication.
|
|
693
|
+
* Used for sending follow-up instructions to the remote agent mid-task.
|
|
694
|
+
*/
|
|
695
|
+
export async function sendMessageToTask(
|
|
696
|
+
config: AgrentingAdapterConfig,
|
|
697
|
+
taskId: string,
|
|
698
|
+
message: string
|
|
699
|
+
): Promise<SendMessageResult> {
|
|
700
|
+
const client = new AgrentingClient(config);
|
|
701
|
+
return client.sendMessageToTask(taskId, { message });
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Get messages for a task (bidirectional comment history).
|
|
706
|
+
*/
|
|
707
|
+
export async function getTaskMessages(
|
|
708
|
+
config: AgrentingAdapterConfig,
|
|
709
|
+
taskId: string
|
|
710
|
+
): Promise<TaskMessage[]> {
|
|
711
|
+
const client = new AgrentingClient(config);
|
|
712
|
+
return client.getTaskMessages(taskId);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Reassign a failed/cancelled task to a different agent.
|
|
717
|
+
* If newAgentDid is not provided, the system will auto-select a replacement.
|
|
718
|
+
*/
|
|
719
|
+
export async function reassignTask(
|
|
720
|
+
config: AgrentingAdapterConfig,
|
|
721
|
+
taskId: string,
|
|
722
|
+
newAgentDid?: string
|
|
723
|
+
): Promise<ReassignTaskResult> {
|
|
724
|
+
const client = new AgrentingClient(config);
|
|
725
|
+
return client.reassignTask(taskId, newAgentDid);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* List all available capabilities with descriptions and usage stats.
|
|
730
|
+
* Helps with agent discovery and validation.
|
|
731
|
+
*/
|
|
732
|
+
export async function listCapabilities(
|
|
733
|
+
config: AgrentingAdapterConfig
|
|
734
|
+
): Promise<Capability[]> {
|
|
735
|
+
const client = new AgrentingClient(config);
|
|
736
|
+
return client.listCapabilities();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Send a message to a hiring for communication with the hired agent.
|
|
741
|
+
*/
|
|
742
|
+
export async function sendMessageToHiring(
|
|
743
|
+
config: AgrentingAdapterConfig,
|
|
744
|
+
hiringId: string,
|
|
745
|
+
message: string
|
|
746
|
+
): Promise<HiringMessage> {
|
|
747
|
+
const client = new AgrentingClient(config);
|
|
748
|
+
return client.sendMessageToHiring(hiringId, message);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Get messages for a hiring.
|
|
753
|
+
*/
|
|
754
|
+
export async function getHiringMessages(
|
|
755
|
+
config: AgrentingAdapterConfig,
|
|
756
|
+
hiringId: string
|
|
757
|
+
): Promise<HiringMessage[]> {
|
|
758
|
+
const client = new AgrentingClient(config);
|
|
759
|
+
return client.getHiringMessages(hiringId);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Retry a failed hiring.
|
|
764
|
+
*/
|
|
765
|
+
export async function retryHiring(
|
|
766
|
+
config: AgrentingAdapterConfig,
|
|
767
|
+
hiringId: string,
|
|
768
|
+
options?: { reason?: string }
|
|
769
|
+
): Promise<Hiring> {
|
|
770
|
+
const client = new AgrentingClient(config);
|
|
771
|
+
return client.retryHiring(hiringId, options);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Get a hiring by ID.
|
|
776
|
+
*/
|
|
777
|
+
export async function getHiring(
|
|
778
|
+
config: AgrentingAdapterConfig,
|
|
779
|
+
hiringId: string
|
|
780
|
+
): Promise<Hiring> {
|
|
781
|
+
const client = new AgrentingClient(config);
|
|
782
|
+
return client.getHiring(hiringId);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* List hirings for the authenticated agent.
|
|
787
|
+
*/
|
|
788
|
+
export async function listHirings(
|
|
789
|
+
config: AgrentingAdapterConfig,
|
|
790
|
+
options?: { status?: string; limit?: number; offset?: number }
|
|
791
|
+
): Promise<Hiring[]> {
|
|
792
|
+
const client = new AgrentingClient(config);
|
|
793
|
+
return client.listHirings(options);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Auto-select mode: given a capability requirement, discover the best agent,
|
|
798
|
+
* hire them, and return the adapter config for immediate use.
|
|
799
|
+
*
|
|
800
|
+
* Selection algorithm:
|
|
801
|
+
* 1. Call listCapabilities() to validate capability exists
|
|
802
|
+
* 2. Call GET /api/v1/agents filtered by capability
|
|
803
|
+
* 3. Sort by: availability first, then reputation_score desc, then base_price asc
|
|
804
|
+
* 4. Call hireAgent() to auto-provision
|
|
805
|
+
* 5. Return adapter config
|
|
806
|
+
*/
|
|
807
|
+
export async function autoSelectAgent(
|
|
808
|
+
config: AgrentingAdapterConfig,
|
|
809
|
+
options: AutoSelectOptions
|
|
810
|
+
): Promise<HireAgentResult & { selectedAgent: AgentProfile }> {
|
|
811
|
+
const client = new AgrentingClient(config);
|
|
812
|
+
|
|
813
|
+
// 1. Validate capability exists
|
|
814
|
+
const capabilities = await client.listCapabilities();
|
|
815
|
+
const capabilityExists = capabilities.some(
|
|
816
|
+
(c) => c.name === options.capability || c.name.toLowerCase() === options.capability.toLowerCase()
|
|
817
|
+
);
|
|
818
|
+
if (!capabilityExists) {
|
|
819
|
+
throw new Error(`Capability "${options.capability}" not found. Available: ${capabilities.map(c => c.name).join(", ")}`);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// 2. Get agents filtered by capability
|
|
823
|
+
const agents = await client.listAgentsByCapability(options.capability);
|
|
824
|
+
if (agents.length === 0) {
|
|
825
|
+
throw new Error(`No agents available for capability "${options.capability}"`);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// 3. Filter by options and sort
|
|
829
|
+
let filtered = agents;
|
|
830
|
+
|
|
831
|
+
// Filter by max price
|
|
832
|
+
if (options.maxPrice) {
|
|
833
|
+
const maxPriceNum = parseFloat(options.maxPrice);
|
|
834
|
+
filtered = filtered.filter((a) => {
|
|
835
|
+
if (!a.base_price) return true; // No price info = assume fits budget
|
|
836
|
+
return parseFloat(a.base_price) <= maxPriceNum;
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Filter by min reputation
|
|
841
|
+
if (options.minReputation) {
|
|
842
|
+
const minRep = options.minReputation;
|
|
843
|
+
filtered = filtered.filter((a) => {
|
|
844
|
+
if (!a.reputation_score) return false; // No reputation = excluded
|
|
845
|
+
return a.reputation_score >= minRep;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (filtered.length === 0) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
`No agents match criteria for capability "${options.capability}" (maxPrice=${options.maxPrice ?? "any"}, minReputation=${options.minReputation ?? "any"})`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Sort: availability first, then by specified sort criteria
|
|
856
|
+
const sortBy = options.sortBy ?? "reputation_score";
|
|
857
|
+
|
|
858
|
+
// Prefer available agents if requested
|
|
859
|
+
if (options.preferAvailable ?? true) {
|
|
860
|
+
filtered.sort((a, b) => {
|
|
861
|
+
const aAvail = a.availability_status === "available" ? 0 : 1;
|
|
862
|
+
const bAvail = b.availability_status === "available" ? 0 : 1;
|
|
863
|
+
if (aAvail !== bAvail) return aAvail - bAvail;
|
|
864
|
+
return 0;
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Secondary sort
|
|
869
|
+
filtered.sort((a, b) => {
|
|
870
|
+
if (sortBy === "reputation_score") {
|
|
871
|
+
return (b.reputation_score ?? 0) - (a.reputation_score ?? 0);
|
|
872
|
+
}
|
|
873
|
+
if (sortBy === "base_price") {
|
|
874
|
+
const aPrice = parseFloat(a.base_price ?? "999999");
|
|
875
|
+
const bPrice = parseFloat(b.base_price ?? "999999");
|
|
876
|
+
return aPrice - bPrice;
|
|
877
|
+
}
|
|
878
|
+
if (sortBy === "availability") {
|
|
879
|
+
const aAvail = a.availability_status === "available" ? 0 : 1;
|
|
880
|
+
const bAvail = b.availability_status === "available" ? 0 : 1;
|
|
881
|
+
return aAvail - bAvail;
|
|
882
|
+
}
|
|
883
|
+
return 0;
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
// 4. Hire the best agent
|
|
887
|
+
const selectedAgent = filtered[0];
|
|
888
|
+
const hireResult = await client.hireAgent(selectedAgent.did);
|
|
889
|
+
|
|
890
|
+
// 5. Return combined result
|
|
891
|
+
return {
|
|
892
|
+
...hireResult,
|
|
893
|
+
selectedAgent,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Execute a task with retry logic.
|
|
899
|
+
* If the task fails, it will be retried up to TASK_MAX_RETRIES times
|
|
900
|
+
* with exponential backoff.
|
|
901
|
+
*/
|
|
902
|
+
export async function executeWithRetry(
|
|
903
|
+
config: AgrentingAdapterConfig,
|
|
904
|
+
params: {
|
|
905
|
+
input: string;
|
|
906
|
+
capability: string;
|
|
907
|
+
instructions?: string;
|
|
908
|
+
maxPrice?: string;
|
|
909
|
+
paymentType?: string;
|
|
910
|
+
maxRetries?: number;
|
|
911
|
+
}
|
|
912
|
+
): Promise<AgrentingExecutionResult> {
|
|
913
|
+
const maxRetries = params.maxRetries ?? TASK_MAX_RETRIES;
|
|
914
|
+
let lastResult: AgrentingExecutionResult | undefined;
|
|
915
|
+
let lastError: Error | null = null;
|
|
916
|
+
|
|
917
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
918
|
+
try {
|
|
919
|
+
lastResult = await execute(config, params);
|
|
920
|
+
|
|
921
|
+
if (lastResult.success) {
|
|
922
|
+
return lastResult;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// If task failed but we have retries remaining, wait and retry
|
|
926
|
+
if (attempt < maxRetries && lastResult.error) {
|
|
927
|
+
const delayMs = Math.min(
|
|
928
|
+
TASK_RETRY_BASE_DELAY_MS * Math.pow(2, attempt),
|
|
929
|
+
TASK_RETRY_MAX_DELAY_MS
|
|
930
|
+
);
|
|
931
|
+
console.warn(
|
|
932
|
+
`[adapter-agrenting] Task failed on attempt ${attempt + 1}, retrying in ${delayMs}ms: ${lastResult.error}`
|
|
933
|
+
);
|
|
934
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return lastResult;
|
|
939
|
+
} catch (err) {
|
|
940
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
941
|
+
|
|
942
|
+
if (attempt < maxRetries) {
|
|
943
|
+
const delayMs = Math.min(
|
|
944
|
+
TASK_RETRY_BASE_DELAY_MS * Math.pow(2, attempt),
|
|
945
|
+
TASK_RETRY_MAX_DELAY_MS
|
|
946
|
+
);
|
|
947
|
+
console.warn(
|
|
948
|
+
`[adapter-agrenting] Execution error on attempt ${attempt + 1}, retrying in ${delayMs}ms: ${lastError.message}`
|
|
949
|
+
);
|
|
950
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// All retries exhausted
|
|
957
|
+
return {
|
|
958
|
+
success: false,
|
|
959
|
+
error: lastError?.message ?? lastResult?.error ?? "Task failed after all retries",
|
|
960
|
+
taskId: lastResult?.taskId,
|
|
961
|
+
durationMs: lastResult?.durationMs ?? 0,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Forward a comment from Paperclip to the Agrenting task.
|
|
967
|
+
* Used for bidirectional comment sync when the user adds a comment
|
|
968
|
+
* to a Paperclip issue that has an active Agrenting task.
|
|
969
|
+
*/
|
|
970
|
+
export async function forwardCommentToAgrenting(
|
|
971
|
+
config: AgrentingAdapterConfig,
|
|
972
|
+
taskId: string,
|
|
973
|
+
comment: string,
|
|
974
|
+
authorName?: string
|
|
975
|
+
): Promise<SendMessageResult | null> {
|
|
976
|
+
const client = new AgrentingClient(config);
|
|
977
|
+
|
|
978
|
+
// Format the comment for Agrenting
|
|
979
|
+
const formattedComment = authorName
|
|
980
|
+
? `[${authorName}]: ${comment}`
|
|
981
|
+
: comment;
|
|
982
|
+
|
|
983
|
+
try {
|
|
984
|
+
return await client.sendMessageToTask(taskId, { message: formattedComment });
|
|
985
|
+
} catch (err) {
|
|
986
|
+
// Log but don't throw — comment sync is non-critical
|
|
987
|
+
console.error(
|
|
988
|
+
`[adapter-agrenting] Failed to forward comment to task ${taskId}:`,
|
|
989
|
+
err instanceof Error ? err.message : String(err)
|
|
990
|
+
);
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Process incoming Agrenting messages and format them for Paperclip.
|
|
997
|
+
* Called by the webhook handler when it receives task messages.
|
|
998
|
+
*/
|
|
999
|
+
export function processIncomingMessage(
|
|
1000
|
+
message: TaskMessage
|
|
1001
|
+
): string {
|
|
1002
|
+
const senderName = message.sender_name ?? "Agent";
|
|
1003
|
+
return formatAgentResponse(senderName, message.content);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Create the server-side adapter module.
|
|
1008
|
+
* This is the entry point for Paperclip's server adapter registry.
|
|
1009
|
+
*/
|
|
1010
|
+
export function createServerAdapter() {
|
|
1011
|
+
return {
|
|
1012
|
+
name: "agrenting" as const,
|
|
1013
|
+
execute,
|
|
1014
|
+
testEnvironment,
|
|
1015
|
+
getConfigSchema,
|
|
1016
|
+
startWebhookListener,
|
|
1017
|
+
stopWebhookListener,
|
|
1018
|
+
registerWebhook,
|
|
1019
|
+
deregisterWebhook,
|
|
1020
|
+
getTaskProgress,
|
|
1021
|
+
getTaskPayment,
|
|
1022
|
+
cancelTask,
|
|
1023
|
+
discoverAgents,
|
|
1024
|
+
getAgentProfile,
|
|
1025
|
+
hireAgent,
|
|
1026
|
+
sendMessageToTask,
|
|
1027
|
+
getTaskMessages,
|
|
1028
|
+
reassignTask,
|
|
1029
|
+
listCapabilities,
|
|
1030
|
+
sendMessageToHiring,
|
|
1031
|
+
getHiringMessages,
|
|
1032
|
+
retryHiring,
|
|
1033
|
+
getHiring,
|
|
1034
|
+
listHirings,
|
|
1035
|
+
autoSelectAgent,
|
|
1036
|
+
executeWithRetry,
|
|
1037
|
+
forwardCommentToAgrenting,
|
|
1038
|
+
processIncomingMessage,
|
|
1039
|
+
getBalance,
|
|
1040
|
+
getTransactions,
|
|
1041
|
+
deposit,
|
|
1042
|
+
withdraw,
|
|
1043
|
+
};
|
|
1044
|
+
}
|