@panal/sdk 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +34 -0
- package/dist/client.js +88 -1
- package/dist/files.d.ts +9 -1
- package/dist/files.js +12 -2
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -29,6 +29,20 @@ export interface PanalClientOptions {
|
|
|
29
29
|
account?: Account;
|
|
30
30
|
/** Sobrescribe direcciones concretas (para pruebas o un despliegue propio). */
|
|
31
31
|
addresses?: Partial<PanalAddresses>;
|
|
32
|
+
/**
|
|
33
|
+
* Indexador desde el que buscar agentes. `https://api.panal.lat` por defecto.
|
|
34
|
+
*
|
|
35
|
+
* Buscar leyendo el registro entero deja de funcionar justo cuando más falta
|
|
36
|
+
* hace: `searchAgents` pagina hasta 500 agentes y luego lanza 500 lecturas a
|
|
37
|
+
* la vez, y el RPC público corta a partir de ~50 concurrentes. O sea que
|
|
38
|
+
* cuantos más agentes hay, MENOS puede un agente encontrar a otro.
|
|
39
|
+
*
|
|
40
|
+
* Con el indexador es una petición. Si no responde, se vuelve al registro:
|
|
41
|
+
* peor y con tope, pero nunca sin respuesta.
|
|
42
|
+
*
|
|
43
|
+
* `null` lo desactiva y lee siempre de la cadena.
|
|
44
|
+
*/
|
|
45
|
+
indexerUrl?: string | null;
|
|
32
46
|
}
|
|
33
47
|
export interface HireParams {
|
|
34
48
|
/** Dirección del agente que hará el trabajo. */
|
|
@@ -62,11 +76,21 @@ export declare class PanalClient {
|
|
|
62
76
|
* cliente se creó sin cuenta, o sea en modo solo lectura.
|
|
63
77
|
*/
|
|
64
78
|
readonly walletClient?: WalletClient;
|
|
79
|
+
/** Indexador para buscar agentes, o null si se lee siempre de la cadena. */
|
|
80
|
+
readonly indexerUrl: string | null;
|
|
65
81
|
constructor(options?: PanalClientOptions);
|
|
66
82
|
/** El wallet client, o un error que dice exactamente qué falta. */
|
|
67
83
|
private wallet;
|
|
68
84
|
/** Todos los agentes del registry, activos e inactivos. */
|
|
69
85
|
listAgents(): Promise<Agent[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Los agentes que dice el indexador, o null si no se puede contar con él.
|
|
88
|
+
*
|
|
89
|
+
* Devuelve null —y no una lista vacía— cuando no responde, va atrasado o
|
|
90
|
+
* contesta algo raro: quien llama tiene que poder distinguir «no hay
|
|
91
|
+
* ninguno» de «no lo sé», porque en el segundo caso toca leer la cadena.
|
|
92
|
+
*/
|
|
93
|
+
private buscarEnIndice;
|
|
70
94
|
/** Un agente concreto, con su metadata ya interpretada. */
|
|
71
95
|
getAgent(address: Address): Promise<Agent>;
|
|
72
96
|
/**
|
|
@@ -78,6 +102,8 @@ export declare class PanalClient {
|
|
|
78
102
|
*/
|
|
79
103
|
searchAgents(query?: string, options?: {
|
|
80
104
|
includeInactive?: boolean;
|
|
105
|
+
skill?: string;
|
|
106
|
+
limit?: number;
|
|
81
107
|
}): Promise<Agent[]>;
|
|
82
108
|
/** Una tarea por su id. */
|
|
83
109
|
getTask(taskId: bigint): Promise<Task>;
|
|
@@ -166,6 +192,14 @@ export declare class PanalClient {
|
|
|
166
192
|
quote?: X402Accept;
|
|
167
193
|
allowInsecure?: boolean;
|
|
168
194
|
timeoutMs?: number;
|
|
195
|
+
/**
|
|
196
|
+
* Sobre de la cadena, YA descendido con `descend()`. Va aquí y no se
|
|
197
|
+
* construye dentro porque `askAgent` apunta a un agente concreto: quien
|
|
198
|
+
* elige a quién llamar es quien tiene que gastar el salto. Sin esto, un
|
|
199
|
+
* agente que delega con `askAgent` rompía la cadena — el siguiente no
|
|
200
|
+
* heredaba ni presupuesto ni camino, y el ciclo dejaba de detectarse.
|
|
201
|
+
*/
|
|
202
|
+
envelope?: CallEnvelope;
|
|
169
203
|
}): Promise<AskResult>;
|
|
170
204
|
/**
|
|
171
205
|
* Busca un agente con esa skill, negocia el precio y le paga por la consulta.
|
package/dist/client.js
CHANGED
|
@@ -37,6 +37,8 @@ export class PanalClient {
|
|
|
37
37
|
* cliente se creó sin cuenta, o sea en modo solo lectura.
|
|
38
38
|
*/
|
|
39
39
|
walletClient;
|
|
40
|
+
/** Indexador para buscar agentes, o null si se lee siempre de la cadena. */
|
|
41
|
+
indexerUrl;
|
|
40
42
|
constructor(options = {}) {
|
|
41
43
|
this.network = options.network ?? 'mainnet';
|
|
42
44
|
const chain = chainFor(this.network);
|
|
@@ -45,6 +47,7 @@ export class PanalClient {
|
|
|
45
47
|
throw new Error(`Panal no tiene contratos desplegados en ${this.network}. ` +
|
|
46
48
|
'Usa network: "mainnet", o pasa `addresses` con los tuyos.');
|
|
47
49
|
}
|
|
50
|
+
this.indexerUrl = options.indexerUrl === undefined ? 'https://api.panal.lat' : options.indexerUrl;
|
|
48
51
|
const transport = http(options.rpcUrl ?? chain.rpcUrls.default.http[0]);
|
|
49
52
|
this.publicClient = createPublicClient({ chain, transport });
|
|
50
53
|
this.account = options.account;
|
|
@@ -84,6 +87,80 @@ export class PanalClient {
|
|
|
84
87
|
}
|
|
85
88
|
return Promise.all(addresses.map((address) => this.getAgent(address)));
|
|
86
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Los agentes que dice el indexador, o null si no se puede contar con él.
|
|
92
|
+
*
|
|
93
|
+
* Devuelve null —y no una lista vacía— cuando no responde, va atrasado o
|
|
94
|
+
* contesta algo raro: quien llama tiene que poder distinguir «no hay
|
|
95
|
+
* ninguno» de «no lo sé», porque en el segundo caso toca leer la cadena.
|
|
96
|
+
*/
|
|
97
|
+
async buscarEnIndice(query, options) {
|
|
98
|
+
if (!this.indexerUrl)
|
|
99
|
+
return null;
|
|
100
|
+
try {
|
|
101
|
+
const url = new URL('/index/agents', this.indexerUrl);
|
|
102
|
+
if (query?.trim())
|
|
103
|
+
url.searchParams.set('q', query.trim());
|
|
104
|
+
if (options.skill?.trim())
|
|
105
|
+
url.searchParams.set('skill', options.skill.trim());
|
|
106
|
+
if (options.includeInactive)
|
|
107
|
+
url.searchParams.set('include_inactive', 'true');
|
|
108
|
+
url.searchParams.set('limit', String(Math.min(options.limit ?? 50, 200)));
|
|
109
|
+
const res = await fetchLimited(url.toString(), { timeoutMs: 8000 });
|
|
110
|
+
if (res.status !== 200)
|
|
111
|
+
return null;
|
|
112
|
+
const cuerpo = JSON.parse(res.text);
|
|
113
|
+
if (!Array.isArray(cuerpo.agents))
|
|
114
|
+
return null;
|
|
115
|
+
// `total` solo lo devuelve la respuesta del CATÁLOGO. Sin esta
|
|
116
|
+
// comprobación, un indexador viejo —que no entiende `q` ni `skill` pero
|
|
117
|
+
// responde igual con su lista de siempre— hacía creer que había filtrado:
|
|
118
|
+
// toda búsqueda devolvía todos los agentes, incluida una imposible.
|
|
119
|
+
// Un servidor que no entiende la pregunta y contesta es peor que uno que
|
|
120
|
+
// calla, porque no hay forma de notarlo desde fuera. Aquí sí.
|
|
121
|
+
if (typeof cuerpo.total !== 'number')
|
|
122
|
+
return null;
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const raw of cuerpo.agents) {
|
|
125
|
+
// El indexador es un servicio, o sea que su respuesta se valida como
|
|
126
|
+
// la de cualquier desconocido: una ficha rota se descarta sin llevarse
|
|
127
|
+
// la búsqueda entera por delante.
|
|
128
|
+
const address = typeof raw.address === 'string' ? raw.address : null;
|
|
129
|
+
if (!address || !/^0x[0-9a-fA-F]{40}$/.test(address))
|
|
130
|
+
continue;
|
|
131
|
+
const skills = Array.isArray(raw.skills) ? raw.skills.filter((x) => typeof x === 'string') : [];
|
|
132
|
+
let pricePerTask;
|
|
133
|
+
let registeredAt;
|
|
134
|
+
try {
|
|
135
|
+
pricePerTask = BigInt(String(raw.pricePerTask ?? '0'));
|
|
136
|
+
registeredAt = BigInt(Number(raw.registeredAt ?? 0));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const metadata = {
|
|
142
|
+
name: typeof raw.name === 'string' ? raw.name : '',
|
|
143
|
+
description: typeof raw.description === 'string' ? raw.description : '',
|
|
144
|
+
skills,
|
|
145
|
+
botUrl: typeof raw.botUrl === 'string' ? raw.botUrl : null,
|
|
146
|
+
};
|
|
147
|
+
out.push({
|
|
148
|
+
address: getAddress(address),
|
|
149
|
+
owner: getAddress(typeof raw.owner === 'string' && /^0x[0-9a-fA-F]{40}$/.test(raw.owner) ? raw.owner : address),
|
|
150
|
+
pricePerTask,
|
|
151
|
+
currency: getAddress(typeof raw.currency === 'string' && /^0x[0-9a-fA-F]{40}$/.test(raw.currency) ? raw.currency : NATIVE_CURRENCY),
|
|
152
|
+
active: raw.active !== false,
|
|
153
|
+
registeredAt,
|
|
154
|
+
metadataURI: formatAgentMetadata(metadata),
|
|
155
|
+
metadata,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
87
164
|
/** Un agente concreto, con su metadata ya interpretada. */
|
|
88
165
|
async getAgent(address) {
|
|
89
166
|
const raw = (await this.publicClient.readContract({
|
|
@@ -111,6 +188,13 @@ export class PanalClient {
|
|
|
111
188
|
* paginada sale más barata que montar un índice.
|
|
112
189
|
*/
|
|
113
190
|
async searchAgents(query, options = {}) {
|
|
191
|
+
// Por el indexador primero. Leer el registro entero para buscar deja de
|
|
192
|
+
// funcionar justo cuando más falta hace: son 500 lecturas a la vez contra
|
|
193
|
+
// un RPC que corta a partir de ~50 concurrentes, y con más de 500 agentes
|
|
194
|
+
// ni siquiera los ve. Aquí es una petición.
|
|
195
|
+
const delIndice = await this.buscarEnIndice(query, options);
|
|
196
|
+
if (delIndice !== null)
|
|
197
|
+
return delIndice;
|
|
114
198
|
const all = await this.listAgents();
|
|
115
199
|
const pool = options.includeInactive ? all : all.filter((a) => a.active);
|
|
116
200
|
if (!query?.trim())
|
|
@@ -413,7 +497,10 @@ export class PanalClient {
|
|
|
413
497
|
const tope = remainingBudget(options.envelope ?? null, options.maxSpend);
|
|
414
498
|
if (tope <= 0n)
|
|
415
499
|
throw new X402Error('El presupuesto de la cadena está agotado: no se puede delegar más.');
|
|
416
|
-
|
|
500
|
+
// Se busca por SKILL, no por texto libre: encontrar a alguien porque la
|
|
501
|
+
// palabra aparece en su descripción no sirve para delegar. Si el indexador
|
|
502
|
+
// no está, `searchAgents` cae solo a la cadena con su texto libre.
|
|
503
|
+
const candidates = (await this.searchAgents(skill, { skill }))
|
|
417
504
|
.filter((a) => !excluded.has(a.address.toLowerCase()) && a.metadata.botUrl)
|
|
418
505
|
.slice(0, options.maxCandidates ?? 5);
|
|
419
506
|
if (!candidates.length)
|
package/dist/files.d.ts
CHANGED
|
@@ -108,8 +108,16 @@ export interface DownloadOptions extends UrlGuardOptions {
|
|
|
108
108
|
baseUrl?: string;
|
|
109
109
|
/** Wallet del cliente; el agente solo entrega a quien pagó. */
|
|
110
110
|
address?: string;
|
|
111
|
-
/** Firma de `Panal resultado #<taskId>`, la misma que abre `/result/:id`. */
|
|
111
|
+
/** Firma de `Panal resultado #<taskId> · <expira>`, la misma que abre `/result/:id`. */
|
|
112
112
|
signature?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Segundo en el que caduca esa firma, tal y como se firmó.
|
|
115
|
+
*
|
|
116
|
+
* Va con la firma porque el agente la necesita para reconstruir el mensaje.
|
|
117
|
+
* Mandarla en claro no regala nada: está DENTRO de lo firmado, así que
|
|
118
|
+
* cambiarla invalida la firma.
|
|
119
|
+
*/
|
|
120
|
+
expira?: number;
|
|
113
121
|
maxBytes?: number;
|
|
114
122
|
timeoutMs?: number;
|
|
115
123
|
}
|
package/dist/files.js
CHANGED
|
@@ -204,10 +204,19 @@ export function fileUrl(file, baseUrl) {
|
|
|
204
204
|
*/
|
|
205
205
|
export async function downloadDeliveredFile(file, options = {}) {
|
|
206
206
|
const destino = new URL(fileUrl(file, options.baseUrl));
|
|
207
|
+
// Las credenciales van en CABECERAS, no en la query.
|
|
208
|
+
//
|
|
209
|
+
// Esta firma abre el resultado y todos los archivos de la tarea, o sea que es
|
|
210
|
+
// un pase de acceso. En la query acababa escrita en el log de accesos del
|
|
211
|
+
// proxy y en el historial del navegador — se encontraron 23 en claro en un
|
|
212
|
+
// log de producción. Una cabecera no se registra por defecto.
|
|
213
|
+
const cabeceras = {};
|
|
207
214
|
if (options.address)
|
|
208
|
-
|
|
215
|
+
cabeceras['x-panal-address'] = options.address;
|
|
209
216
|
if (options.signature)
|
|
210
|
-
|
|
217
|
+
cabeceras['x-panal-signature'] = options.signature;
|
|
218
|
+
if (options.expira !== undefined)
|
|
219
|
+
cabeceras['x-panal-expira'] = String(options.expira);
|
|
211
220
|
await assertPublicUrl(destino.toString(), options);
|
|
212
221
|
// El tope se ata al tamaño ANUNCIADO, no al de por defecto: si el manifiesto
|
|
213
222
|
// dice 2 MB, no hay razón para dejar que lleguen 25.
|
|
@@ -216,6 +225,7 @@ export async function downloadDeliveredFile(file, options = {}) {
|
|
|
216
225
|
maxBytes: tope,
|
|
217
226
|
timeoutMs: options.timeoutMs ?? 120_000,
|
|
218
227
|
redirect: 'error',
|
|
228
|
+
headers: cabeceras,
|
|
219
229
|
});
|
|
220
230
|
if (status !== 200) {
|
|
221
231
|
throw new FileVerificationError(`El agente respondió ${status} al pedirle "${file.name}".`, file.name);
|