@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/README.md +423 -0
- package/dist/cli.js +990 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +406 -0
- package/dist/index.js +835 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
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
|