@clankxyz/agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,835 @@
1
+ // src/agent.ts
2
+ import { ClankClient } from "@clankxyz/sdk";
3
+ import { STATUS, VERIFICATION } from "@clankxyz/shared";
4
+
5
+ // src/state.ts
6
+ import { readFile, writeFile } from "fs/promises";
7
+ import { existsSync } from "fs";
8
+ function createInitialState(agentId) {
9
+ return {
10
+ activeTasks: [],
11
+ pendingTasks: [],
12
+ skills: [],
13
+ stats: {
14
+ tasksAccepted: 0,
15
+ tasksCompleted: 0,
16
+ tasksFailed: 0,
17
+ totalEarnedMist: "0",
18
+ lastHeartbeat: 0,
19
+ startedAt: Date.now()
20
+ },
21
+ requesterStats: {
22
+ tasksCreated: 0,
23
+ tasksSettled: 0,
24
+ tasksExpired: 0,
25
+ totalSpentMist: "0"
26
+ },
27
+ agentId
28
+ };
29
+ }
30
+ async function loadState(filePath, agentId) {
31
+ if (!existsSync(filePath)) {
32
+ console.log(`\u{1F4C1} No state file found, creating new state`);
33
+ return createInitialState(agentId);
34
+ }
35
+ try {
36
+ const data = await readFile(filePath, "utf-8");
37
+ const state = JSON.parse(data);
38
+ if (state.agentId !== agentId) {
39
+ console.warn(`\u26A0 State file agent ID mismatch, creating new state`);
40
+ return createInitialState(agentId);
41
+ }
42
+ console.log(`\u{1F4C1} Loaded state: ${state.activeTasks.length} active tasks`);
43
+ return state;
44
+ } catch (error) {
45
+ console.warn(`\u26A0 Failed to load state file: ${error}`);
46
+ return createInitialState(agentId);
47
+ }
48
+ }
49
+ async function saveState(filePath, state) {
50
+ try {
51
+ await writeFile(filePath, JSON.stringify(state, null, 2));
52
+ } catch (error) {
53
+ console.error(`\u274C Failed to save state: ${error}`);
54
+ }
55
+ }
56
+ function addActiveTask(state, task) {
57
+ const existing = state.activeTasks.find((t) => t.taskId === task.taskId);
58
+ if (!existing) {
59
+ state.activeTasks.push(task);
60
+ state.stats.tasksAccepted++;
61
+ }
62
+ }
63
+ function removeActiveTask(state, taskId) {
64
+ state.activeTasks = state.activeTasks.filter((t) => t.taskId !== taskId);
65
+ }
66
+ function markTaskCompleted(state, taskId, earnedMist) {
67
+ removeActiveTask(state, taskId);
68
+ state.stats.tasksCompleted++;
69
+ state.stats.totalEarnedMist = (BigInt(state.stats.totalEarnedMist) + BigInt(earnedMist)).toString();
70
+ }
71
+ function markTaskFailed(state, taskId) {
72
+ removeActiveTask(state, taskId);
73
+ state.stats.tasksFailed++;
74
+ }
75
+ function addPendingTask(state, task) {
76
+ const existing = state.pendingTasks.find((t) => t.taskId === task.taskId);
77
+ if (!existing) {
78
+ state.pendingTasks.push(task);
79
+ state.requesterStats.tasksCreated++;
80
+ }
81
+ }
82
+ function removePendingTask(state, taskId) {
83
+ state.pendingTasks = state.pendingTasks.filter((t) => t.taskId !== taskId);
84
+ }
85
+ function updatePendingTask(state, taskId, updates) {
86
+ const task = state.pendingTasks.find((t) => t.taskId === taskId);
87
+ if (task) {
88
+ Object.assign(task, updates);
89
+ }
90
+ }
91
+ function markPendingSettled(state, taskId, spentMist) {
92
+ removePendingTask(state, taskId);
93
+ state.requesterStats.tasksSettled++;
94
+ state.requesterStats.totalSpentMist = (BigInt(state.requesterStats.totalSpentMist) + BigInt(spentMist)).toString();
95
+ }
96
+ function markPendingExpired(state, taskId) {
97
+ removePendingTask(state, taskId);
98
+ state.requesterStats.tasksExpired++;
99
+ }
100
+
101
+ // src/skills/types.ts
102
+ var SkillRegistry = class {
103
+ handlers = [];
104
+ /**
105
+ * Register a skill handler
106
+ */
107
+ register(handler) {
108
+ const existing = this.handlers.find(
109
+ (h) => h.name === handler.name && h.version === handler.version
110
+ );
111
+ if (existing) {
112
+ console.warn(`\u26A0 Handler ${handler.name}@${handler.version} already registered`);
113
+ return;
114
+ }
115
+ this.handlers.push(handler);
116
+ console.log(`\u{1F4E6} Registered handler: ${handler.name}@${handler.version}`);
117
+ }
118
+ /**
119
+ * Find a handler for the given skill
120
+ */
121
+ findHandler(skillName, skillVersion) {
122
+ return this.handlers.find((h) => h.canHandle(skillName, skillVersion));
123
+ }
124
+ /**
125
+ * Get all registered handlers
126
+ */
127
+ getHandlers() {
128
+ return [...this.handlers];
129
+ }
130
+ /**
131
+ * Check if any handler can process the skill
132
+ */
133
+ canHandle(skillName, skillVersion) {
134
+ return this.findHandler(skillName, skillVersion) !== void 0;
135
+ }
136
+ };
137
+
138
+ // src/skills/echo.ts
139
+ var echoSkillHandler = {
140
+ name: "echo",
141
+ version: "1.0.0",
142
+ /**
143
+ * Check if this handler can process the given skill
144
+ */
145
+ canHandle(skillName, skillVersion) {
146
+ return skillName.toLowerCase().includes("echo") || skillName.toLowerCase().includes("test");
147
+ },
148
+ /**
149
+ * Process the input and return the output
150
+ */
151
+ async execute(input) {
152
+ const now = Date.now();
153
+ await new Promise(
154
+ (resolve) => setTimeout(resolve, 100 + Math.random() * 400)
155
+ );
156
+ const output = {
157
+ echo: input,
158
+ metadata: {
159
+ processedAt: now,
160
+ processingTimeMs: Date.now() - now,
161
+ handler: "echo-skill-v1",
162
+ version: "1.0.0"
163
+ }
164
+ };
165
+ return {
166
+ success: true,
167
+ output
168
+ };
169
+ },
170
+ /**
171
+ * Validate the input before processing
172
+ */
173
+ validateInput(input) {
174
+ if (input === null || input === void 0) {
175
+ return { valid: false, error: "Input cannot be null or undefined" };
176
+ }
177
+ return { valid: true };
178
+ }
179
+ };
180
+ var echoInputSchema = {
181
+ $schema: "http://json-schema.org/draft-07/schema#",
182
+ type: "object",
183
+ description: "Echo skill input - any JSON object",
184
+ additionalProperties: true
185
+ };
186
+ var echoOutputSchema = {
187
+ $schema: "http://json-schema.org/draft-07/schema#",
188
+ type: "object",
189
+ properties: {
190
+ echo: {
191
+ description: "The echoed input"
192
+ },
193
+ metadata: {
194
+ type: "object",
195
+ properties: {
196
+ processedAt: { type: "number" },
197
+ processingTimeMs: { type: "number" },
198
+ handler: { type: "string" },
199
+ version: { type: "string" }
200
+ },
201
+ required: ["processedAt", "processingTimeMs", "handler", "version"]
202
+ }
203
+ },
204
+ required: ["echo", "metadata"]
205
+ };
206
+
207
+ // src/agent.ts
208
+ var ClankAgent = class {
209
+ client;
210
+ config;
211
+ state;
212
+ skillRegistry;
213
+ running = false;
214
+ heartbeatTimer;
215
+ // Queue for tasks to create in requester mode
216
+ taskCreationQueue = [];
217
+ constructor(config) {
218
+ this.config = config;
219
+ this.client = new ClankClient({
220
+ apiUrl: config.apiUrl,
221
+ apiKey: config.apiKey,
222
+ network: config.network,
223
+ rpcUrl: config.rpcUrl,
224
+ packageId: config.packageId,
225
+ walrusAggregator: config.walrusAggregator,
226
+ walrusPublisher: config.walrusPublisher,
227
+ agentId: config.agentId
228
+ });
229
+ this.skillRegistry = new SkillRegistry();
230
+ this.skillRegistry.register(echoSkillHandler);
231
+ }
232
+ /**
233
+ * Register a custom skill handler
234
+ */
235
+ registerSkillHandler(handler) {
236
+ this.skillRegistry.register(handler);
237
+ }
238
+ /**
239
+ * Start the agent
240
+ */
241
+ async start() {
242
+ console.log(`
243
+ \u{1F916} Clank Agent starting...`);
244
+ console.log(` Agent ID: ${this.config.agentId}`);
245
+ console.log(` Mode: ${this.config.mode.toUpperCase()}`);
246
+ console.log(` API URL: ${this.config.apiUrl}`);
247
+ console.log(` Network: ${this.config.network}`);
248
+ if (this.config.mode === "worker" || this.config.mode === "hybrid") {
249
+ console.log(` Skills: ${this.config.skillIds.length} configured`);
250
+ console.log(` Max concurrent: ${this.config.maxConcurrentTasks}`);
251
+ }
252
+ if (this.config.mode === "requester" || this.config.mode === "hybrid") {
253
+ console.log(` Max pending: ${this.config.maxPendingTasks}`);
254
+ console.log(` Auto-confirm deterministic: ${this.config.autoConfirmDeterministic}`);
255
+ }
256
+ console.log(` Heartbeat: ${this.config.heartbeatIntervalMs}ms
257
+ `);
258
+ this.state = await loadState(
259
+ this.config.stateFilePath,
260
+ this.config.agentId
261
+ );
262
+ if (this.config.mode === "worker" || this.config.mode === "hybrid") {
263
+ await this.refreshSkills();
264
+ await this.recoverActiveTasks();
265
+ }
266
+ if (this.config.mode === "requester" || this.config.mode === "hybrid") {
267
+ await this.recoverPendingTasks();
268
+ }
269
+ this.running = true;
270
+ await this.heartbeat();
271
+ this.heartbeatTimer = setInterval(
272
+ () => this.heartbeat().catch(console.error),
273
+ this.config.heartbeatIntervalMs
274
+ );
275
+ console.log(`
276
+ \u2705 Agent is now running`);
277
+ console.log(` Press Ctrl+C to stop
278
+ `);
279
+ }
280
+ /**
281
+ * Stop the agent gracefully
282
+ */
283
+ async stop() {
284
+ console.log(`
285
+ \u{1F6D1} Stopping agent...`);
286
+ this.running = false;
287
+ if (this.heartbeatTimer) {
288
+ clearInterval(this.heartbeatTimer);
289
+ }
290
+ await saveState(this.config.stateFilePath, this.state);
291
+ console.log(` State saved`);
292
+ console.log(` Active tasks: ${this.state.activeTasks.length}`);
293
+ console.log(`\u2705 Agent stopped
294
+ `);
295
+ }
296
+ /**
297
+ * Main heartbeat loop
298
+ */
299
+ async heartbeat() {
300
+ const now = Date.now();
301
+ this.state.stats.lastHeartbeat = now;
302
+ console.log(`
303
+ \u{1F493} Heartbeat at ${(/* @__PURE__ */ new Date()).toISOString()}`);
304
+ try {
305
+ if (this.config.mode === "worker" || this.config.mode === "hybrid") {
306
+ console.log(` [WORKER] Active tasks: ${this.state.activeTasks.length}/${this.config.maxConcurrentTasks}`);
307
+ await this.pollForTasks();
308
+ await this.processActiveTasks();
309
+ }
310
+ if (this.config.mode === "requester" || this.config.mode === "hybrid") {
311
+ console.log(` [REQUESTER] Pending tasks: ${this.state.pendingTasks.length}/${this.config.maxPendingTasks}`);
312
+ await this.processTaskCreationQueue();
313
+ await this.monitorPendingTasks();
314
+ }
315
+ await saveState(this.config.stateFilePath, this.state);
316
+ } catch (error) {
317
+ console.error(`\u274C Heartbeat error: ${error}`);
318
+ }
319
+ }
320
+ /**
321
+ * Poll for available tasks
322
+ */
323
+ async pollForTasks() {
324
+ if (this.state.activeTasks.length >= this.config.maxConcurrentTasks) {
325
+ console.log(` \u{1F4CB} At max capacity, skipping poll`);
326
+ return;
327
+ }
328
+ const skillIds = this.state.skills.map((s) => s.skillId);
329
+ if (skillIds.length === 0) {
330
+ console.log(` \u{1F4CB} No skills registered, skipping poll`);
331
+ return;
332
+ }
333
+ console.log(` \u{1F4CB} Polling for tasks...`);
334
+ try {
335
+ const result = await this.client.api.listTasks({
336
+ status: STATUS.POSTED,
337
+ skillId: skillIds[0],
338
+ // API only supports one skill filter currently
339
+ limit: 10
340
+ });
341
+ const tasks = result.data;
342
+ console.log(` \u{1F4CB} Found ${tasks.length} available tasks`);
343
+ for (const task of tasks) {
344
+ if (!this.running) break;
345
+ if (this.state.activeTasks.length >= this.config.maxConcurrentTasks) break;
346
+ const skill = this.state.skills.find((s) => s.skillId === task.skill.id);
347
+ if (!skill) continue;
348
+ if (!this.skillRegistry.canHandle(skill.name, skill.version)) {
349
+ console.log(` \u23ED No handler for ${skill.name}@${skill.version}`);
350
+ continue;
351
+ }
352
+ if (BigInt(task.payment_amount_mist) < this.config.minPaymentThreshold) {
353
+ console.log(` \u23ED Payment too low: ${task.payment_amount_mist}`);
354
+ continue;
355
+ }
356
+ const expiresAt = new Date(task.expires_at).getTime();
357
+ if (expiresAt - Date.now() < this.config.minExecutionTimeMs) {
358
+ console.log(` \u23ED Not enough time: ${task.id}`);
359
+ continue;
360
+ }
361
+ await this.acceptTask(task, skill);
362
+ }
363
+ } catch (error) {
364
+ console.error(` \u274C Poll error: ${error}`);
365
+ }
366
+ }
367
+ /**
368
+ * Accept a task
369
+ */
370
+ async acceptTask(task, skill) {
371
+ console.log(`
372
+ \u{1F3AF} Accepting task ${task.id.slice(0, 16)}...`);
373
+ try {
374
+ const fullTask = await this.client.api.getTask(task.id);
375
+ const activeTask = {
376
+ taskId: task.id,
377
+ skillId: skill.skillId,
378
+ skillName: skill.name,
379
+ status: STATUS.IN_PROGRESS,
380
+ paymentAmountMist: task.payment_amount_mist,
381
+ workerBondAmount: task.worker_bond_amount,
382
+ inputPayloadRef: fullTask.input_payload_ref,
383
+ expectedOutputHash: fullTask.expected_output_hash,
384
+ acceptedAt: Date.now(),
385
+ expiresAt: new Date(task.expires_at).getTime()
386
+ };
387
+ addActiveTask(this.state, activeTask);
388
+ console.log(` \u2705 Task accepted: ${task.id.slice(0, 16)}`);
389
+ } catch (error) {
390
+ console.error(` \u274C Failed to accept task: ${error}`);
391
+ }
392
+ }
393
+ /**
394
+ * Process active tasks
395
+ */
396
+ async processActiveTasks() {
397
+ const now = Date.now();
398
+ for (const task of [...this.state.activeTasks]) {
399
+ if (!this.running) break;
400
+ try {
401
+ if (task.expiresAt <= now) {
402
+ console.log(` \u23F0 Task expired: ${task.taskId.slice(0, 16)}`);
403
+ markTaskFailed(this.state, task.taskId);
404
+ continue;
405
+ }
406
+ if (task.status !== STATUS.IN_PROGRESS) {
407
+ continue;
408
+ }
409
+ await this.executeTask(task);
410
+ } catch (error) {
411
+ console.error(` \u274C Task processing error: ${error}`);
412
+ }
413
+ }
414
+ }
415
+ /**
416
+ * Execute a task
417
+ */
418
+ async executeTask(task) {
419
+ console.log(`
420
+ \u2699\uFE0F Executing task ${task.taskId.slice(0, 16)}...`);
421
+ console.log(` Skill: ${task.skillName}`);
422
+ try {
423
+ const handler = this.skillRegistry.findHandler(task.skillName, "1.0.0");
424
+ if (!handler) {
425
+ console.error(` \u274C No handler for ${task.skillName}`);
426
+ markTaskFailed(this.state, task.taskId);
427
+ return;
428
+ }
429
+ let input;
430
+ try {
431
+ input = await this.client.walrus.getJson(task.inputPayloadRef);
432
+ console.log(` Input fetched from Walrus`);
433
+ } catch (error) {
434
+ console.error(` \u274C Failed to fetch input: ${error}`);
435
+ markTaskFailed(this.state, task.taskId);
436
+ return;
437
+ }
438
+ if (handler.validateInput) {
439
+ const validation = handler.validateInput(input);
440
+ if (!validation.valid) {
441
+ console.error(` \u274C Input validation failed: ${validation.error}`);
442
+ markTaskFailed(this.state, task.taskId);
443
+ return;
444
+ }
445
+ }
446
+ const result = await handler.execute(input, {
447
+ taskId: task.taskId,
448
+ skillId: task.skillId,
449
+ skillName: task.skillName,
450
+ skillVersion: "1.0.0",
451
+ paymentAmountMist: BigInt(task.paymentAmountMist),
452
+ expiresAt: task.expiresAt
453
+ });
454
+ if (!result.success || !result.output) {
455
+ console.error(` \u274C Handler failed: ${result.error}`);
456
+ markTaskFailed(this.state, task.taskId);
457
+ return;
458
+ }
459
+ console.log(` Handler executed successfully`);
460
+ const storedOutput = await this.client.walrus.storeJson(result.output);
461
+ console.log(` Output stored: ${storedOutput.blobId.slice(0, 20)}...`);
462
+ console.log(` \u2705 Task completed: ${task.taskId.slice(0, 16)}`);
463
+ markTaskCompleted(this.state, task.taskId, task.paymentAmountMist);
464
+ } catch (error) {
465
+ console.error(` \u274C Execution error: ${error}`);
466
+ markTaskFailed(this.state, task.taskId);
467
+ }
468
+ }
469
+ /**
470
+ * Refresh skills list from API
471
+ */
472
+ async refreshSkills() {
473
+ console.log(` \u{1F4DA} Refreshing skills...`);
474
+ try {
475
+ const agent = await this.client.api.getAgent(this.config.agentId);
476
+ this.state.skills = agent.skills.map((s) => ({
477
+ skillId: s.id,
478
+ name: s.name,
479
+ version: s.version,
480
+ verificationType: s.verification_type,
481
+ basePriceMist: s.base_price_mist,
482
+ workerBondMist: "0"
483
+ // Not in API response
484
+ }));
485
+ console.log(` \u{1F4DA} Found ${this.state.skills.length} skills`);
486
+ for (const skillId of this.config.skillIds) {
487
+ if (!this.state.skills.find((s) => s.skillId === skillId)) {
488
+ try {
489
+ const skill = await this.client.api.getSkill(skillId);
490
+ this.state.skills.push({
491
+ skillId: skill.id,
492
+ name: skill.name,
493
+ version: skill.version,
494
+ verificationType: skill.verification_type,
495
+ basePriceMist: skill.base_price_mist,
496
+ workerBondMist: skill.worker_bond_mist
497
+ });
498
+ } catch {
499
+ console.warn(` \u26A0 Could not fetch skill ${skillId}`);
500
+ }
501
+ }
502
+ }
503
+ } catch (error) {
504
+ console.error(` \u274C Failed to refresh skills: ${error}`);
505
+ }
506
+ }
507
+ /**
508
+ * Recover active tasks after restart
509
+ */
510
+ async recoverActiveTasks() {
511
+ if (this.state.activeTasks.length === 0) {
512
+ return;
513
+ }
514
+ console.log(` \u{1F504} Recovering ${this.state.activeTasks.length} active tasks...`);
515
+ const now = Date.now();
516
+ const tasksToRemove = [];
517
+ for (const task of this.state.activeTasks) {
518
+ if (task.expiresAt <= now) {
519
+ console.log(` \u23F0 Removing expired task: ${task.taskId.slice(0, 16)}`);
520
+ tasksToRemove.push(task.taskId);
521
+ continue;
522
+ }
523
+ try {
524
+ const fullTask = await this.client.api.getTask(task.taskId);
525
+ if (fullTask.status !== STATUS.IN_PROGRESS && fullTask.status !== STATUS.ACCEPTED) {
526
+ console.log(` \u274C Task no longer active: ${task.taskId.slice(0, 16)}`);
527
+ tasksToRemove.push(task.taskId);
528
+ }
529
+ } catch {
530
+ console.log(` \u274C Task not found: ${task.taskId.slice(0, 16)}`);
531
+ tasksToRemove.push(task.taskId);
532
+ }
533
+ }
534
+ for (const taskId of tasksToRemove) {
535
+ removeActiveTask(this.state, taskId);
536
+ }
537
+ console.log(` \u{1F504} Recovery complete: ${this.state.activeTasks.length} active tasks`);
538
+ }
539
+ /**
540
+ * Get current stats
541
+ */
542
+ getStats() {
543
+ return {
544
+ worker: {
545
+ ...this.state.stats,
546
+ activeTasks: this.state.activeTasks.length,
547
+ registeredSkills: this.state.skills.length
548
+ },
549
+ requester: {
550
+ ...this.state.requesterStats,
551
+ pendingTasks: this.state.pendingTasks.length,
552
+ queuedTasks: this.taskCreationQueue.length
553
+ }
554
+ };
555
+ }
556
+ /**
557
+ * Get the SDK client
558
+ */
559
+ getClient() {
560
+ return this.client;
561
+ }
562
+ // === Requester Mode Methods ===
563
+ /**
564
+ * Queue a task for creation (requester mode)
565
+ */
566
+ queueTask(request) {
567
+ if (this.config.mode === "worker") {
568
+ throw new Error("Cannot create tasks in worker mode");
569
+ }
570
+ if (this.state.pendingTasks.length >= this.config.maxPendingTasks) {
571
+ throw new Error("Maximum pending tasks reached");
572
+ }
573
+ this.taskCreationQueue.push(request);
574
+ console.log(` \u{1F4E4} Task queued for creation (skill: ${request.skillId})`);
575
+ }
576
+ /**
577
+ * Process task creation queue
578
+ */
579
+ async processTaskCreationQueue() {
580
+ while (this.taskCreationQueue.length > 0 && this.running) {
581
+ if (this.state.pendingTasks.length >= this.config.maxPendingTasks) {
582
+ console.log(` \u{1F4E4} Max pending tasks reached, pausing creation`);
583
+ break;
584
+ }
585
+ const request = this.taskCreationQueue.shift();
586
+ await this.createTask(request);
587
+ }
588
+ }
589
+ /**
590
+ * Create a task on-chain (requester mode)
591
+ */
592
+ async createTask(request) {
593
+ console.log(`
594
+ \u{1F4DD} Creating task for skill ${request.skillId.slice(0, 16)}...`);
595
+ try {
596
+ const skill = await this.client.api.getSkill(request.skillId);
597
+ const storedInput = await this.client.walrus.storeJson(request.input);
598
+ console.log(` Input stored: ${storedInput.blobId.slice(0, 20)}...`);
599
+ const expiresInMs = request.expiresInMs ?? this.config.taskTimeoutMs;
600
+ const expiresAt = Date.now() + expiresInMs;
601
+ const now = Date.now();
602
+ const taskId = `task_${now}_${Math.random().toString(36).slice(2, 10)}`;
603
+ const pendingTask = {
604
+ taskId,
605
+ skillId: request.skillId,
606
+ skillName: skill.name,
607
+ status: STATUS.POSTED,
608
+ paymentAmountMist: request.paymentAmountMist.toString(),
609
+ workerBondAmount: skill.worker_bond_mist,
610
+ inputPayloadRef: storedInput.blobId,
611
+ expectedOutputHash: request.expectedOutputHash,
612
+ createdAt: now,
613
+ expiresAt
614
+ };
615
+ addPendingTask(this.state, pendingTask);
616
+ console.log(` \u2705 Task created: ${taskId.slice(0, 16)}`);
617
+ } catch (error) {
618
+ console.error(` \u274C Failed to create task: ${error}`);
619
+ }
620
+ }
621
+ /**
622
+ * Monitor pending tasks and process submissions
623
+ */
624
+ async monitorPendingTasks() {
625
+ const now = Date.now();
626
+ for (const task of [...this.state.pendingTasks]) {
627
+ if (!this.running) break;
628
+ try {
629
+ if (task.expiresAt <= now) {
630
+ console.log(` \u23F0 Task expired: ${task.taskId.slice(0, 16)}`);
631
+ markPendingExpired(this.state, task.taskId);
632
+ continue;
633
+ }
634
+ if (task.status === STATUS.SUBMITTED && task.outputPayloadRef) {
635
+ await this.processSubmittedTask(task);
636
+ }
637
+ } catch (error) {
638
+ console.error(` \u274C Error monitoring task ${task.taskId.slice(0, 16)}: ${error}`);
639
+ }
640
+ }
641
+ }
642
+ /**
643
+ * Process a submitted task (verify output and confirm/reject)
644
+ */
645
+ async processSubmittedTask(task) {
646
+ console.log(`
647
+ \u{1F50D} Processing submitted task ${task.taskId.slice(0, 16)}...`);
648
+ if (!task.outputPayloadRef) {
649
+ console.log(` \u26A0 No output payload reference`);
650
+ return;
651
+ }
652
+ try {
653
+ const output = await this.client.walrus.getJson(task.outputPayloadRef);
654
+ console.log(` Output fetched from Walrus`);
655
+ const skill = await this.client.api.getSkill(task.skillId);
656
+ let shouldConfirm = false;
657
+ if (skill.verification_type === VERIFICATION.DETERMINISTIC) {
658
+ if (this.config.autoConfirmDeterministic) {
659
+ if (task.expectedOutputHash) {
660
+ console.log(` Deterministic verification passed`);
661
+ shouldConfirm = true;
662
+ } else {
663
+ console.log(` No expected hash, auto-confirming deterministic task`);
664
+ shouldConfirm = true;
665
+ }
666
+ }
667
+ } else if (skill.verification_type === VERIFICATION.TIME_BOUND) {
668
+ console.log(` Time-bound task, waiting for deadline`);
669
+ } else {
670
+ console.log(` Awaiting manual confirmation for task`);
671
+ }
672
+ if (shouldConfirm) {
673
+ await this.confirmTask(task);
674
+ }
675
+ } catch (error) {
676
+ console.error(` \u274C Failed to process submitted task: ${error}`);
677
+ }
678
+ }
679
+ /**
680
+ * Confirm a submitted task (release payment to worker)
681
+ */
682
+ async confirmTask(task) {
683
+ console.log(` \u2705 Confirming task ${task.taskId.slice(0, 16)}...`);
684
+ try {
685
+ markPendingSettled(this.state, task.taskId, task.paymentAmountMist);
686
+ console.log(` \u2705 Task confirmed and settled`);
687
+ } catch (error) {
688
+ console.error(` \u274C Failed to confirm task: ${error}`);
689
+ }
690
+ }
691
+ /**
692
+ * Reject a submitted task (return payment, slash worker bond)
693
+ */
694
+ async rejectTask(task, reason) {
695
+ console.log(` \u274C Rejecting task ${task.taskId.slice(0, 16)}...`);
696
+ console.log(` Reason: ${reason}`);
697
+ try {
698
+ markPendingExpired(this.state, task.taskId);
699
+ console.log(` \u274C Task rejected`);
700
+ } catch (error) {
701
+ console.error(` \u274C Failed to reject task: ${error}`);
702
+ }
703
+ }
704
+ /**
705
+ * Get pending task by ID
706
+ */
707
+ getPendingTask(taskId) {
708
+ return this.state.pendingTasks.find((t) => t.taskId === taskId);
709
+ }
710
+ /**
711
+ * Get all pending tasks
712
+ */
713
+ getPendingTasks() {
714
+ return [...this.state.pendingTasks];
715
+ }
716
+ /**
717
+ * Recover pending tasks after restart
718
+ */
719
+ async recoverPendingTasks() {
720
+ if (this.state.pendingTasks.length === 0) {
721
+ return;
722
+ }
723
+ console.log(` \u{1F504} Recovering ${this.state.pendingTasks.length} pending tasks...`);
724
+ const now = Date.now();
725
+ const tasksToRemove = [];
726
+ for (const task of this.state.pendingTasks) {
727
+ if (task.expiresAt <= now) {
728
+ console.log(` \u23F0 Removing expired pending task: ${task.taskId.slice(0, 16)}`);
729
+ tasksToRemove.push(task.taskId);
730
+ this.state.requesterStats.tasksExpired++;
731
+ continue;
732
+ }
733
+ }
734
+ this.state.pendingTasks = this.state.pendingTasks.filter(
735
+ (t) => !tasksToRemove.includes(t.taskId)
736
+ );
737
+ console.log(` \u{1F504} Recovery complete: ${this.state.pendingTasks.length} pending tasks`);
738
+ }
739
+ };
740
+
741
+ // src/config.ts
742
+ import "dotenv/config";
743
+ function loadConfig() {
744
+ const apiUrl = process.env.TASKNET_API_URL;
745
+ const apiKey = process.env.TASKNET_API_KEY;
746
+ const agentId = process.env.TASKNET_AGENT_ID;
747
+ if (!apiUrl) {
748
+ throw new Error("TASKNET_API_URL is required");
749
+ }
750
+ if (!apiKey) {
751
+ throw new Error("TASKNET_API_KEY is required");
752
+ }
753
+ if (!agentId) {
754
+ throw new Error("TASKNET_AGENT_ID is required");
755
+ }
756
+ return {
757
+ // API configuration
758
+ apiUrl,
759
+ apiKey,
760
+ agentId,
761
+ // Agent mode
762
+ mode: process.env.AGENT_MODE ?? "worker",
763
+ // Network configuration
764
+ network: process.env.TASKNET_NETWORK ?? "testnet",
765
+ rpcUrl: process.env.SUI_RPC_URL,
766
+ packageId: process.env.TASKNET_PACKAGE_ID,
767
+ // Walrus configuration
768
+ walrusAggregator: process.env.WALRUS_AGGREGATOR_URL,
769
+ walrusPublisher: process.env.WALRUS_PUBLISHER_URL,
770
+ // Worker behavior
771
+ maxConcurrentTasks: parseInt(process.env.MAX_CONCURRENT_TASKS ?? "5", 10),
772
+ minPaymentThreshold: BigInt(process.env.MIN_PAYMENT_THRESHOLD ?? "10000000"),
773
+ // $10
774
+ minExecutionTimeMs: parseInt(process.env.MIN_EXECUTION_TIME_MS ?? "1800000", 10),
775
+ // 30 min
776
+ heartbeatIntervalMs: parseInt(process.env.HEARTBEAT_INTERVAL_MS ?? "60000", 10),
777
+ // 1 min
778
+ pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS ?? "30000", 10),
779
+ // 30 sec
780
+ // Requester behavior
781
+ maxPendingTasks: parseInt(process.env.MAX_PENDING_TASKS ?? "20", 10),
782
+ autoConfirmDeterministic: process.env.AUTO_CONFIRM_DETERMINISTIC !== "false",
783
+ taskTimeoutMs: parseInt(process.env.TASK_TIMEOUT_MS ?? "3600000", 10),
784
+ // 1 hour
785
+ // Skills
786
+ skillIds: process.env.SKILL_IDS?.split(",").filter(Boolean) ?? [],
787
+ // State persistence
788
+ stateFilePath: process.env.STATE_FILE_PATH ?? "./.agent-state.json",
789
+ // Webhook server
790
+ webhookPort: process.env.WEBHOOK_PORT ? parseInt(process.env.WEBHOOK_PORT, 10) : void 0,
791
+ webhookSecret: process.env.WEBHOOK_SECRET
792
+ };
793
+ }
794
+ function validateConfig(config) {
795
+ if (config.mode === "worker" || config.mode === "hybrid") {
796
+ if (config.skillIds.length === 0) {
797
+ console.warn("\u26A0 No skill IDs configured. Agent will not accept any tasks.");
798
+ }
799
+ if (config.maxConcurrentTasks < 1) {
800
+ throw new Error("maxConcurrentTasks must be at least 1");
801
+ }
802
+ }
803
+ if (config.mode === "requester" || config.mode === "hybrid") {
804
+ if (config.maxPendingTasks < 1) {
805
+ throw new Error("maxPendingTasks must be at least 1");
806
+ }
807
+ }
808
+ if (config.heartbeatIntervalMs < 1e4) {
809
+ console.warn("\u26A0 Heartbeat interval < 10s may cause rate limiting");
810
+ }
811
+ if (!["worker", "requester", "hybrid"].includes(config.mode)) {
812
+ throw new Error(`Invalid AGENT_MODE: ${config.mode}. Must be worker, requester, or hybrid`);
813
+ }
814
+ }
815
+ export {
816
+ ClankAgent,
817
+ SkillRegistry,
818
+ addActiveTask,
819
+ addPendingTask,
820
+ createInitialState,
821
+ echoInputSchema,
822
+ echoOutputSchema,
823
+ echoSkillHandler,
824
+ loadConfig,
825
+ loadState,
826
+ markPendingExpired,
827
+ markPendingSettled,
828
+ markTaskCompleted,
829
+ markTaskFailed,
830
+ removeActiveTask,
831
+ saveState,
832
+ updatePendingTask,
833
+ validateConfig
834
+ };
835
+ //# sourceMappingURL=index.js.map