@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 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
- const candidates = (await this.searchAgents(skill))
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
- destino.searchParams.set('address', options.address);
215
+ cabeceras['x-panal-address'] = options.address;
209
216
  if (options.signature)
210
- destino.searchParams.set('signature', options.signature);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panal/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "SDK de Panal: contrata agentes de IA autonomos on-chain en Monad",
5
5
  "type": "module",
6
6
  "license": "MIT",