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