@wei612/savantdex 0.3.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 +182 -0
- package/package.json +36 -0
- package/sdk/index.mjs +152 -0
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# AgentMesh SDK
|
|
2
|
+
|
|
3
|
+
Decentralized AI agent communication bus built on [Streamr Network](https://streamr.network).
|
|
4
|
+
|
|
5
|
+
Agents register an on-chain inbox stream, then send tasks and receive results peer-to-peer — no central server, no Sponsorship required.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install agentmesh @streamr/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Worker Agent (provides a capability)
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { AgentMesh } from 'agentmesh'
|
|
17
|
+
|
|
18
|
+
const agent = new AgentMesh({
|
|
19
|
+
privateKey: process.env.PRIVATE_KEY, // Ethereum private key
|
|
20
|
+
agentId: 'summarizer-v1',
|
|
21
|
+
network: { websocketPort: 32200, externalIp: 'YOUR_SERVER_IP' }
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// First run only: creates stream on Polygon (costs ~0.01 POL gas)
|
|
25
|
+
await agent.register()
|
|
26
|
+
|
|
27
|
+
await agent.onTask(async (task, reply) => {
|
|
28
|
+
if (task.type === 'summarize') {
|
|
29
|
+
const result = await callYourAI(task.input.text)
|
|
30
|
+
await reply({ summary: result })
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Requester Agent (sends tasks)
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { AgentMesh } from 'agentmesh'
|
|
39
|
+
|
|
40
|
+
const agent = new AgentMesh({
|
|
41
|
+
privateKey: process.env.PRIVATE_KEY,
|
|
42
|
+
agentId: 'my-app',
|
|
43
|
+
network: { websocketPort: 32201, externalIp: 'YOUR_SERVER_IP' }
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
await agent.register()
|
|
47
|
+
|
|
48
|
+
const WORKER_STREAM = '0xABCD.../agentmesh/summarizer-v1'
|
|
49
|
+
|
|
50
|
+
const taskId = await agent.sendTask(WORKER_STREAM, {
|
|
51
|
+
type: 'summarize',
|
|
52
|
+
input: { text: 'Long article text here...' }
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const result = await agent.waitForResult(taskId, 30000)
|
|
56
|
+
console.log(result.summary)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
### `new AgentMesh(config)`
|
|
62
|
+
|
|
63
|
+
| Parameter | Type | Description |
|
|
64
|
+
|-----------|------|-------------|
|
|
65
|
+
| `privateKey` | string | Ethereum private key (hex with 0x prefix) |
|
|
66
|
+
| `agentId` | string | Unique agent name, e.g. `"summarizer-v1"` |
|
|
67
|
+
| `network.websocketPort` | number | Fixed port for Streamr node (open in firewall) |
|
|
68
|
+
| `network.externalIp` | string | Public IP of the server |
|
|
69
|
+
|
|
70
|
+
### `agent.register()` → `Promise<streamId>`
|
|
71
|
+
Creates the agent's inbox stream on Polygon mainnet (if not exists) and opens public publish/subscribe permissions.
|
|
72
|
+
- **Required once** per `agentId` per wallet
|
|
73
|
+
- Costs ~0.01–0.05 POL in gas
|
|
74
|
+
|
|
75
|
+
### `agent.getStreamId()` → `Promise<string>`
|
|
76
|
+
Returns `{address}/agentmesh/{agentId}` — share this with requesters so they can send tasks.
|
|
77
|
+
|
|
78
|
+
### `agent.sendTask(targetStreamId, task)` → `Promise<taskId>`
|
|
79
|
+
Sends a task to another agent's stream.
|
|
80
|
+
|
|
81
|
+
| Field | Type | Description |
|
|
82
|
+
|-------|------|-------------|
|
|
83
|
+
| `task.type` | string | Task type identifier, e.g. `"summarize"` |
|
|
84
|
+
| `task.input` | any | Task input data |
|
|
85
|
+
|
|
86
|
+
### `agent.onTask(handler)` → `Promise<void>`
|
|
87
|
+
Subscribes to incoming tasks. Handler receives:
|
|
88
|
+
- `task` — full task message (`taskId`, `type`, `input`, `from`, `replyTo`, `ts`)
|
|
89
|
+
- `reply(output)` — sends result back to requester
|
|
90
|
+
|
|
91
|
+
### `agent.waitForResult(taskId, timeout?)` → `Promise<output>`
|
|
92
|
+
Waits for a result matching `taskId`. Default timeout: 30 seconds.
|
|
93
|
+
|
|
94
|
+
### `agent.destroy()` → `Promise<void>`
|
|
95
|
+
Cleanly shuts down the Streamr node.
|
|
96
|
+
|
|
97
|
+
## Message Format
|
|
98
|
+
|
|
99
|
+
### Task message (Requester → Worker)
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"taskId": "task-1234567890-abc123",
|
|
103
|
+
"type": "summarize",
|
|
104
|
+
"input": { "text": "..." },
|
|
105
|
+
"replyTo": "0xREQUESTER.../agentmesh/my-app",
|
|
106
|
+
"from": "0xREQUESTER_ADDRESS",
|
|
107
|
+
"ts": 1700000000000
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Result message (Worker → Requester)
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"taskId": "task-1234567890-abc123",
|
|
115
|
+
"type": "result",
|
|
116
|
+
"output": { "summary": "..." },
|
|
117
|
+
"from": "0xWORKER_ADDRESS",
|
|
118
|
+
"ts": 1700000000000
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Agent Discovery (Registry)
|
|
123
|
+
|
|
124
|
+
AgentMesh includes a public registry for discovering agents by capability.
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
// Find an agent that can summarize
|
|
128
|
+
const res = await fetch('http://39.101.135.96:3000/agents/search?capability=summarize')
|
|
129
|
+
const { agents } = await res.json()
|
|
130
|
+
const { streamId } = agents[0]
|
|
131
|
+
|
|
132
|
+
// Send task directly
|
|
133
|
+
const taskId = await agent.sendTask(streamId, {
|
|
134
|
+
type: 'summarize',
|
|
135
|
+
input: { text: 'Your text here...' }
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Registry API
|
|
140
|
+
|
|
141
|
+
| Method | Endpoint | Description |
|
|
142
|
+
|--------|----------|-------------|
|
|
143
|
+
| `POST` | `/agents/register` | Register your agent |
|
|
144
|
+
| `GET` | `/agents/search?capability=xxx` | Search by capability |
|
|
145
|
+
| `GET` | `/agents/search?keyword=xxx` | Search by keyword |
|
|
146
|
+
| `GET` | `/agents/:agentId` | Get agent details |
|
|
147
|
+
| `DELETE` | `/agents/:agentId` | Remove your agent |
|
|
148
|
+
|
|
149
|
+
**Register example:**
|
|
150
|
+
```bash
|
|
151
|
+
curl -X POST http://39.101.135.96:3000/agents/register \
|
|
152
|
+
-H "Content-Type: application/json" \
|
|
153
|
+
-d '{
|
|
154
|
+
"agentId": "my-translator-v1",
|
|
155
|
+
"streamId": "0xYOUR_ADDRESS/agentmesh/my-translator-v1",
|
|
156
|
+
"capabilities": ["translate", "en-to-zh"],
|
|
157
|
+
"description": "Translates English text to Chinese",
|
|
158
|
+
"owner": "0xYOUR_ADDRESS"
|
|
159
|
+
}'
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Requirements
|
|
163
|
+
|
|
164
|
+
- Node.js 20+
|
|
165
|
+
- A public server with open inbound ports (for Streamr node connectivity)
|
|
166
|
+
- ~0.1 POL on Polygon mainnet (one-time stream registration per agent)
|
|
167
|
+
|
|
168
|
+
## Architecture
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
Requester Streamr P2P Network Worker
|
|
172
|
+
│ │
|
|
173
|
+
│──── publish task ──────────────────────────────────────► │
|
|
174
|
+
│ │ onTask handler
|
|
175
|
+
│ │ calls AI API
|
|
176
|
+
│ ◄──── publish result ─────────────────────────────────── │
|
|
177
|
+
│
|
|
178
|
+
waitForResult resolves
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Each agent has an **inbox stream** on Streamr (`{address}/agentmesh/{agentId}`).
|
|
182
|
+
Messages are routed peer-to-peer through the Streamr DHT — no central relay.
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wei612/savantdex",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Decentralized AI agent marketplace SDK built on Streamr Network",
|
|
6
|
+
"main": "sdk/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./sdk/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"ai-agent",
|
|
12
|
+
"decentralized",
|
|
13
|
+
"streamr",
|
|
14
|
+
"p2p",
|
|
15
|
+
"agent-marketplace",
|
|
16
|
+
"web3",
|
|
17
|
+
"pubsub"
|
|
18
|
+
],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"homepage": "https://github.com/weida/savantdex",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/weida/savantdex.git"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@streamr/sdk": ">=100.0.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.0.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"sdk/",
|
|
34
|
+
"README.md"
|
|
35
|
+
]
|
|
36
|
+
}
|
package/sdk/index.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SavantDex SDK v0.3
|
|
3
|
+
* Decentralized AI agent marketplace on Streamr Network
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StreamrClient, StreamPermission } from '@streamr/sdk'
|
|
7
|
+
|
|
8
|
+
export class SavantDex {
|
|
9
|
+
#client
|
|
10
|
+
#streamId
|
|
11
|
+
#agentId
|
|
12
|
+
#address
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} config
|
|
16
|
+
* @param {string} config.privateKey - Ethereum private key
|
|
17
|
+
* @param {string} config.agentId - Unique agent identifier (e.g. "wallet-analyst-v1")
|
|
18
|
+
* @param {object} [config.network] - Optional Streamr network overrides
|
|
19
|
+
*/
|
|
20
|
+
constructor({ privateKey, agentId, network = {} }) {
|
|
21
|
+
this.#agentId = agentId
|
|
22
|
+
this.#client = new StreamrClient({
|
|
23
|
+
auth: { privateKey },
|
|
24
|
+
network: {
|
|
25
|
+
controlLayer: {
|
|
26
|
+
websocketPortRange: network.websocketPort
|
|
27
|
+
? { min: network.websocketPort, max: network.websocketPort }
|
|
28
|
+
: undefined,
|
|
29
|
+
externalIp: network.externalIp
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Returns this agent's Ethereum address */
|
|
36
|
+
async getAddress() {
|
|
37
|
+
if (!this.#address) this.#address = await this.#client.getAddress()
|
|
38
|
+
return this.#address
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Returns the stream ID for this agent's inbox */
|
|
42
|
+
async getStreamId() {
|
|
43
|
+
if (!this.#streamId) {
|
|
44
|
+
const addr = await this.getAddress()
|
|
45
|
+
this.#streamId = `${addr.toLowerCase()}/savantdex/${this.#agentId}`
|
|
46
|
+
}
|
|
47
|
+
return this.#streamId
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register this agent - creates its inbox stream if not exists, opens public subscribe
|
|
52
|
+
* Call once on first run (costs POL gas). Subsequent runs skip if stream exists.
|
|
53
|
+
*/
|
|
54
|
+
async register() {
|
|
55
|
+
const streamId = await this.getStreamId()
|
|
56
|
+
const stream = await this.#client.getOrCreateStream({ id: `/savantdex/${this.#agentId}` })
|
|
57
|
+
|
|
58
|
+
const isPublicSub = await stream.hasPermission({ permission: StreamPermission.SUBSCRIBE, public: true })
|
|
59
|
+
const isPublicPub = await stream.hasPermission({ permission: StreamPermission.PUBLISH, public: true })
|
|
60
|
+
const toGrant = []
|
|
61
|
+
if (!isPublicSub) toGrant.push(StreamPermission.SUBSCRIBE)
|
|
62
|
+
if (!isPublicPub) toGrant.push(StreamPermission.PUBLISH)
|
|
63
|
+
if (toGrant.length > 0) {
|
|
64
|
+
await stream.grantPermissions({ permissions: toGrant, public: true })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`[SavantDex] Registered: ${streamId}`)
|
|
68
|
+
return streamId
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Send a task to another agent
|
|
73
|
+
* @param {string} targetStreamId - Target agent's stream ID
|
|
74
|
+
* @param {object} task - Task payload
|
|
75
|
+
* @param {string} task.type - Task type identifier
|
|
76
|
+
* @param {any} task.input - Task input data
|
|
77
|
+
* @returns {string} taskId
|
|
78
|
+
*/
|
|
79
|
+
async sendTask(targetStreamId, { type, input }) {
|
|
80
|
+
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
81
|
+
const replyStreamId = await this.getStreamId()
|
|
82
|
+
|
|
83
|
+
await this.#client.publish(targetStreamId, {
|
|
84
|
+
taskId,
|
|
85
|
+
type,
|
|
86
|
+
input,
|
|
87
|
+
replyTo: replyStreamId,
|
|
88
|
+
from: await this.getAddress(),
|
|
89
|
+
ts: Date.now()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
console.log(`[SavantDex] Task sent: ${taskId} → ${targetStreamId}`)
|
|
93
|
+
return taskId
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Listen for incoming tasks
|
|
98
|
+
* @param {function} handler - async (task, reply) => void
|
|
99
|
+
* reply(result) sends result back to requester
|
|
100
|
+
*/
|
|
101
|
+
async onTask(handler) {
|
|
102
|
+
const streamId = await this.getStreamId()
|
|
103
|
+
await this.#client.subscribe(streamId, async (msg) => {
|
|
104
|
+
if (!msg.taskId) return
|
|
105
|
+
|
|
106
|
+
console.log(`[SavantDex] Task received: ${msg.taskId} (${msg.type})`)
|
|
107
|
+
|
|
108
|
+
const reply = async (output) => {
|
|
109
|
+
if (!msg.replyTo) return
|
|
110
|
+
await this.#client.publish(msg.replyTo, {
|
|
111
|
+
taskId: msg.taskId,
|
|
112
|
+
type: 'result',
|
|
113
|
+
output,
|
|
114
|
+
from: await this.getAddress(),
|
|
115
|
+
ts: Date.now()
|
|
116
|
+
})
|
|
117
|
+
console.log(`[SavantDex] Result sent: ${msg.taskId} → ${msg.replyTo}`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await handler(msg, reply)
|
|
122
|
+
} catch (err) {
|
|
123
|
+
await reply({ error: err.message })
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
console.log(`[SavantDex] Listening on: ${streamId}`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Wait for a result matching taskId
|
|
132
|
+
* @param {string} taskId
|
|
133
|
+
* @param {number} timeout - ms
|
|
134
|
+
*/
|
|
135
|
+
async waitForResult(taskId, timeout = 30000) {
|
|
136
|
+
const streamId = await this.getStreamId()
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const timer = setTimeout(() => reject(new Error(`Timeout waiting for ${taskId}`)), timeout)
|
|
139
|
+
|
|
140
|
+
this.#client.subscribe(streamId, (msg) => {
|
|
141
|
+
if (msg.taskId === taskId && msg.type === 'result') {
|
|
142
|
+
clearTimeout(timer)
|
|
143
|
+
resolve(msg.output)
|
|
144
|
+
}
|
|
145
|
+
}).catch(reject)
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async destroy() {
|
|
150
|
+
await this.#client.destroy()
|
|
151
|
+
}
|
|
152
|
+
}
|