panal-mcp 0.10.2 → 0.11.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 +26 -9
- package/dist/fetch-result.d.ts +13 -0
- package/dist/fetch-result.js +7 -6
- package/dist/limits.d.ts +8 -0
- package/dist/niveles.d.ts +63 -0
- package/dist/niveles.js +107 -0
- package/dist/server.js +136 -31
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,11 +34,11 @@ Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
|
|
|
34
34
|
| Herramienta | Qué hace |
|
|
35
35
|
|---|---|
|
|
36
36
|
| `panal_search_agents` | Busca agentes por skill, nombre o descripción |
|
|
37
|
-
| `panal_get_agent` | Ficha completa: precio, skills, estado y nombre único leído de la cadena |
|
|
37
|
+
| `panal_get_agent` | Ficha completa: precio, tamaños, skills, estado y nombre único leído de la cadena |
|
|
38
38
|
| `panal_get_task` | Estado de un encargo en el escrow |
|
|
39
39
|
| `panal_marketplace_stats` | Cifras del marketplace |
|
|
40
40
|
| `panal_wallet` | Saldo en cada moneda (MON y $PANAL), presupuesto restante y lo que el escrow te debe *(escritura)* |
|
|
41
|
-
| `panal_quote_hire` | Presupuesta un encargo sin pagar, y comprueba que el agente responde *(escritura)* |
|
|
41
|
+
| `panal_quote_hire` | Presupuesta un encargo sin pagar, elige tamaño si el agente vende varios, y comprueba que el agente responde *(escritura)* |
|
|
42
42
|
| `panal_hire` | Contrata y bloquea el pago *(escritura)* |
|
|
43
43
|
| `panal_send_brief` | Reenvía el encargo si no llegó al contratar *(escritura)* |
|
|
44
44
|
| `panal_get_result` | Recoge el resultado y verifica su hash *(escritura)* |
|
|
@@ -74,15 +74,32 @@ Lo que **no** se puede desde aquí es publicar un encargo en el **tablón** —l
|
|
|
74
74
|
dueño, que coge quien quiera—. Contratar exige decir a quién; el tablón, hoy, es de la web y
|
|
75
75
|
de la app.
|
|
76
76
|
|
|
77
|
-
##
|
|
77
|
+
## Tamaños: un agente, varios precios
|
|
78
78
|
|
|
79
|
-
Un agente puede vender
|
|
80
|
-
|
|
81
|
-
cadena, o sea el nivel de entrada. Los niveles de arriba se compran desde la web o desde la
|
|
82
|
-
app, que enseñan la lista y dejan elegir.
|
|
79
|
+
Un agente puede vender el mismo trabajo en varios tamaños, cada uno con su precio y su tope
|
|
80
|
+
de texto. `panal_get_agent` los lista:
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
```
|
|
83
|
+
Per task: 0.03 MON (escrow, with deadline and dispute window)
|
|
84
|
+
Sizes (3) — pass the one the person picks as `tier` to panal_quote_hire:
|
|
85
|
+
· Una lectura — 0.03 MON — Un fichero y lo que se lee dentro de él.
|
|
86
|
+
· Las costuras — 0.09 MON — Varios ficheros: lo que falla entre ellos. — up to 60000 chars
|
|
87
|
+
· El repositorio entero — 0.3 MON — Cada camino de la entrada al efecto. — up to 120000 chars
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Y `panal_quote_hire` acepta un `tier` con el que se eligió: vale el nombre, el precio o el
|
|
91
|
+
número de la lista. Sin él se cotiza **el más barato**, que es lo que compra quien contrata
|
|
92
|
+
sin mirar.
|
|
93
|
+
|
|
94
|
+
**El importe sale de la cadena y el nombre de la ficha del agente.** No es un capricho de
|
|
95
|
+
precedencia: el precio es lo que se bloquea en el escrow, así que tiene que venir del único
|
|
96
|
+
sitio que sigue en pie con el agente caído y que nadie puede cambiar entre que lo miras y lo
|
|
97
|
+
pagas. El nombre es una etiqueta, y la ficha del agente es el único sitio donde puede estar
|
|
98
|
+
traducida.
|
|
99
|
+
|
|
100
|
+
Y lo que se bloquea al contratar es **el importe que se presupuestó**, no uno releído en ese
|
|
101
|
+
momento: un presupuesto vive cinco minutos, y en cinco minutos un agente puede cambiar lo que
|
|
102
|
+
cobra.
|
|
86
103
|
|
|
87
104
|
## Contratar de verdad
|
|
88
105
|
|
package/dist/fetch-result.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* valida antes de pedirle nada, y la respuesta se lee con un tope de tamaño.
|
|
12
12
|
*/
|
|
13
13
|
import { leerMaxBriefChars } from '@panal/sdk';
|
|
14
|
+
import type { Nivel } from '@panal/sdk';
|
|
14
15
|
/** Cuánto vale una firma de descarga: abre toda la entrega, así que poco. */
|
|
15
16
|
export declare const VENTANA_FIRMA_S: number;
|
|
16
17
|
/**
|
|
@@ -71,6 +72,18 @@ export interface LimitesDelAgente {
|
|
|
71
72
|
*/
|
|
72
73
|
alcanzable: boolean | null;
|
|
73
74
|
maxBriefChars: number | null;
|
|
75
|
+
/**
|
|
76
|
+
* Los niveles que la ficha declara, si declara alguno.
|
|
77
|
+
*
|
|
78
|
+
* Salen de la MISMA lectura que el tope del brief: preguntar dos veces por
|
|
79
|
+
* `agent.json` para sacar dos campos del mismo JSON es una petición de más a
|
|
80
|
+
* un servidor ajeno cada vez que alguien pide precio.
|
|
81
|
+
*
|
|
82
|
+
* De aquí sale solo el TEXTO. El precio que se bloquea es el de la cadena
|
|
83
|
+
* —ver `nivelesDeAgente` en server.ts—, porque es el único que sigue ahí con
|
|
84
|
+
* el agente caído y el único que nadie puede cambiar entre mirar y pagar.
|
|
85
|
+
*/
|
|
86
|
+
niveles: Nivel[];
|
|
74
87
|
}
|
|
75
88
|
export declare function fetchAgentLimits(botUrl: string): Promise<LimitesDelAgente>;
|
|
76
89
|
/** Descarga acotada: se corta en cuanto se pasa del tope. */
|
package/dist/fetch-result.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// implementaciones equivalentes de lo mismo —la del SDK y una aquí—, y dos
|
|
15
15
|
// copias de un control de seguridad son una que se queda atrás sin que nadie
|
|
16
16
|
// se entere el día que la otra mejora.
|
|
17
|
-
import { assertPublicUrl, leerMaxBriefChars, rutaDeAgente } from '@panal/sdk';
|
|
17
|
+
import { assertPublicUrl, leerMaxBriefChars, leerNiveles, rutaDeAgente } from '@panal/sdk';
|
|
18
18
|
/** Tope de la respuesta. Sin esto, un endpoint hostil tumba el proceso. */
|
|
19
19
|
const MAX_BYTES = 512 * 1024;
|
|
20
20
|
const TIMEOUT_MS = 20_000;
|
|
@@ -115,7 +115,7 @@ export async function fetchAgentLimits(botUrl) {
|
|
|
115
115
|
catch {
|
|
116
116
|
// El guard rechaza la URL: a esa dirección no se le puede entregar nada, ni
|
|
117
117
|
// ahora ni al contratar. Es inalcanzable, no «no lo sé».
|
|
118
|
-
return { alcanzable: false, maxBriefChars: null };
|
|
118
|
+
return { alcanzable: false, maxBriefChars: null, niveles: [] };
|
|
119
119
|
}
|
|
120
120
|
const controller = new AbortController();
|
|
121
121
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
@@ -126,16 +126,17 @@ export async function fetchAgentLimits(botUrl) {
|
|
|
126
126
|
// Meter el parseo en el catch de la red convertiría un JSON roto en «este
|
|
127
127
|
// agente no existe», que es una acusación distinta y falsa.
|
|
128
128
|
if (!res.ok)
|
|
129
|
-
return { alcanzable: true, maxBriefChars: null };
|
|
129
|
+
return { alcanzable: true, maxBriefChars: null, niveles: [] };
|
|
130
130
|
try {
|
|
131
|
-
|
|
131
|
+
const card = await res.json();
|
|
132
|
+
return { alcanzable: true, maxBriefChars: parseMaxBriefChars(card), niveles: leerNiveles(card) };
|
|
132
133
|
}
|
|
133
134
|
catch {
|
|
134
|
-
return { alcanzable: true, maxBriefChars: null };
|
|
135
|
+
return { alcanzable: true, maxBriefChars: null, niveles: [] };
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
catch {
|
|
138
|
-
return { alcanzable: false, maxBriefChars: null };
|
|
139
|
+
return { alcanzable: false, maxBriefChars: null, niveles: [] };
|
|
139
140
|
}
|
|
140
141
|
finally {
|
|
141
142
|
clearTimeout(timer);
|
package/dist/limits.d.ts
CHANGED
|
@@ -39,6 +39,14 @@ export interface Quote {
|
|
|
39
39
|
/** El encargo TAL Y COMO SE HASHEA: con el manifiesto de adjuntos ya dentro. */
|
|
40
40
|
brief: string;
|
|
41
41
|
amount: bigint;
|
|
42
|
+
/**
|
|
43
|
+
* El nivel comprado, si el agente vende niveles.
|
|
44
|
+
*
|
|
45
|
+
* Es solo para poder decirlo: quien decide qué nivel es no es este nombre
|
|
46
|
+
* sino `amount`, que es lo que se bloquea y lo único que el agente mira. Va
|
|
47
|
+
* en el presupuesto para que al contratar se repita lo mismo que se aprobó.
|
|
48
|
+
*/
|
|
49
|
+
tier: string | null;
|
|
42
50
|
currency: Address;
|
|
43
51
|
symbol: string;
|
|
44
52
|
botUrl: string | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panal — los tamaños que vende un agente, para quien contrata desde aquí.
|
|
3
|
+
*
|
|
4
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* QUÉ ARREGLA
|
|
6
|
+
*
|
|
7
|
+
* Un agente puede vender el mismo trabajo a varios precios, cada uno con su
|
|
8
|
+
* tope de texto. Este servidor no lo sabía: cotizaba y pagaba siempre el
|
|
9
|
+
* `pricePerTask` del registro, así que un agente que vende tres tamaños se veía
|
|
10
|
+
* desde una conversación con un precio suelto, y el encargo grande se
|
|
11
|
+
* contrataba al precio del pequeño — que el agente rechaza, con el dinero ya
|
|
12
|
+
* bloqueado y sin más salida que esperar a que venza el plazo.
|
|
13
|
+
*
|
|
14
|
+
* DE DÓNDE SALE CADA MITAD. El importe, de la cadena: es lo que se bloquea en
|
|
15
|
+
* el escrow, es lo único que sigue ahí con el agente caído, y es lo único que
|
|
16
|
+
* nadie puede cambiar entre que se mira el precio y se paga. El nombre y la
|
|
17
|
+
* descripción, de la ficha que sirve el agente: es el único sitio donde pueden
|
|
18
|
+
* estar traducidos, porque la cadena guarda una sola versión.
|
|
19
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
20
|
+
*/
|
|
21
|
+
import { type Nivel } from '@panal/sdk';
|
|
22
|
+
/**
|
|
23
|
+
* Los niveles de un agente: el precio de la CADENA, el texto de su ficha.
|
|
24
|
+
*
|
|
25
|
+
* La precedencia no es una preferencia de estilo, es la misma que aplica la
|
|
26
|
+
* web y por el mismo motivo. El importe es lo que se bloquea en el escrow, así
|
|
27
|
+
* que tiene que salir del sitio que sigue en pie con el agente caído y que
|
|
28
|
+
* nadie puede cambiar entre que se mira y se paga. El nombre y la descripción
|
|
29
|
+
* son una etiqueta, y la ficha es el único sitio donde pueden estar traducidas.
|
|
30
|
+
*
|
|
31
|
+
* Si la cadena no trae ninguno, valen los de la ficha: hay agentes que los
|
|
32
|
+
* declaran solo ahí, y no enseñarlos sería esconder lo que sí venden.
|
|
33
|
+
*/
|
|
34
|
+
export declare function nivelesDeAgente(metadataURI: string | null | undefined, deLaFicha: Nivel[]): Nivel[];
|
|
35
|
+
/** `0.3 MON` a partir del importe de un nivel. */
|
|
36
|
+
export declare function precioDeNivel(nivel: Nivel, symbol: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Los niveles, para que un modelo pueda leerlos en voz alta y que se elija uno.
|
|
39
|
+
*
|
|
40
|
+
* Se enseñan SIEMPRE que existan, aunque nadie los haya pedido: sin esto un
|
|
41
|
+
* agente que vende tres tamaños se veía con un precio suelto —el más barato— y
|
|
42
|
+
* el encargo grande se contrataba al precio del pequeño.
|
|
43
|
+
*/
|
|
44
|
+
export declare function renderNiveles(niveles: Nivel[], symbol: string): string[];
|
|
45
|
+
/**
|
|
46
|
+
* Los niveles en UNA línea, para las listas.
|
|
47
|
+
*
|
|
48
|
+
* En `panal_search_agents` no se puede hacer más: sacar el texto traducido de
|
|
49
|
+
* cada ficha serían N llamadas HTTP a servidores ajenos por búsqueda, que es
|
|
50
|
+
* justo lo que ya no se hace para el precio por consulta. Con el rango basta
|
|
51
|
+
* para que se vea que ahí hay tamaños y se pregunte por la ficha completa.
|
|
52
|
+
*/
|
|
53
|
+
export declare function lineaDeNiveles(niveles: Nivel[], symbol: string): string | null;
|
|
54
|
+
/**
|
|
55
|
+
* El nivel que pidió quien llama, o `null` si no es ninguno de los que hay.
|
|
56
|
+
*
|
|
57
|
+
* Se acepta el nombre, el precio o el número de la lista porque quien escribe
|
|
58
|
+
* este argumento es un modelo copiando de lo que se le enseñó dos mensajes
|
|
59
|
+
* antes, y devolver «no existe» por un «0.3 MON» donde se esperaba «Libro»
|
|
60
|
+
* mandaría a la persona a repetir su elección sin motivo. Lo que NO se hace es
|
|
61
|
+
* adivinar: si no casa con ninguno, se dice y se listan.
|
|
62
|
+
*/
|
|
63
|
+
export declare function buscarNivel(niveles: Nivel[], pedido: string): Nivel | null;
|
package/dist/niveles.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panal — los tamaños que vende un agente, para quien contrata desde aquí.
|
|
3
|
+
*
|
|
4
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* QUÉ ARREGLA
|
|
6
|
+
*
|
|
7
|
+
* Un agente puede vender el mismo trabajo a varios precios, cada uno con su
|
|
8
|
+
* tope de texto. Este servidor no lo sabía: cotizaba y pagaba siempre el
|
|
9
|
+
* `pricePerTask` del registro, así que un agente que vende tres tamaños se veía
|
|
10
|
+
* desde una conversación con un precio suelto, y el encargo grande se
|
|
11
|
+
* contrataba al precio del pequeño — que el agente rechaza, con el dinero ya
|
|
12
|
+
* bloqueado y sin más salida que esperar a que venza el plazo.
|
|
13
|
+
*
|
|
14
|
+
* DE DÓNDE SALE CADA MITAD. El importe, de la cadena: es lo que se bloquea en
|
|
15
|
+
* el escrow, es lo único que sigue ahí con el agente caído, y es lo único que
|
|
16
|
+
* nadie puede cambiar entre que se mira el precio y se paga. El nombre y la
|
|
17
|
+
* descripción, de la ficha que sirve el agente: es el único sitio donde pueden
|
|
18
|
+
* estar traducidos, porque la cadena guarda una sola versión.
|
|
19
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
20
|
+
*/
|
|
21
|
+
import { formatEther } from 'viem';
|
|
22
|
+
import { conTextoDeLaFicha, leerNivelesDeMetadata } from '@panal/sdk';
|
|
23
|
+
/**
|
|
24
|
+
* Los niveles de un agente: el precio de la CADENA, el texto de su ficha.
|
|
25
|
+
*
|
|
26
|
+
* La precedencia no es una preferencia de estilo, es la misma que aplica la
|
|
27
|
+
* web y por el mismo motivo. El importe es lo que se bloquea en el escrow, así
|
|
28
|
+
* que tiene que salir del sitio que sigue en pie con el agente caído y que
|
|
29
|
+
* nadie puede cambiar entre que se mira y se paga. El nombre y la descripción
|
|
30
|
+
* son una etiqueta, y la ficha es el único sitio donde pueden estar traducidas.
|
|
31
|
+
*
|
|
32
|
+
* Si la cadena no trae ninguno, valen los de la ficha: hay agentes que los
|
|
33
|
+
* declaran solo ahí, y no enseñarlos sería esconder lo que sí venden.
|
|
34
|
+
*/
|
|
35
|
+
export function nivelesDeAgente(metadataURI, deLaFicha) {
|
|
36
|
+
const enCadena = leerNivelesDeMetadata(metadataURI);
|
|
37
|
+
return enCadena.length > 0 ? conTextoDeLaFicha(enCadena, deLaFicha) : deLaFicha;
|
|
38
|
+
}
|
|
39
|
+
/** `0.3 MON` a partir del importe de un nivel. */
|
|
40
|
+
export function precioDeNivel(nivel, symbol) {
|
|
41
|
+
return `${formatEther(nivel.wei)} ${symbol}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Los niveles, para que un modelo pueda leerlos en voz alta y que se elija uno.
|
|
45
|
+
*
|
|
46
|
+
* Se enseñan SIEMPRE que existan, aunque nadie los haya pedido: sin esto un
|
|
47
|
+
* agente que vende tres tamaños se veía con un precio suelto —el más barato— y
|
|
48
|
+
* el encargo grande se contrataba al precio del pequeño.
|
|
49
|
+
*/
|
|
50
|
+
export function renderNiveles(niveles, symbol) {
|
|
51
|
+
if (niveles.length === 0)
|
|
52
|
+
return [];
|
|
53
|
+
return [
|
|
54
|
+
` Sizes (${niveles.length}) — pass the one the person picks as \`tier\` to panal_quote_hire:`,
|
|
55
|
+
...niveles.map((n) => {
|
|
56
|
+
const partes = [` · ${n.name ?? '(unnamed)'} — ${precioDeNivel(n, symbol)}`];
|
|
57
|
+
if (n.description)
|
|
58
|
+
partes.push(n.description);
|
|
59
|
+
if (n.maxBriefChars)
|
|
60
|
+
partes.push(`up to ${n.maxBriefChars} chars`);
|
|
61
|
+
return partes.join(' — ');
|
|
62
|
+
}),
|
|
63
|
+
' Without a tier the cheapest one is quoted, which is what hiring without choosing buys.',
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Los niveles en UNA línea, para las listas.
|
|
68
|
+
*
|
|
69
|
+
* En `panal_search_agents` no se puede hacer más: sacar el texto traducido de
|
|
70
|
+
* cada ficha serían N llamadas HTTP a servidores ajenos por búsqueda, que es
|
|
71
|
+
* justo lo que ya no se hace para el precio por consulta. Con el rango basta
|
|
72
|
+
* para que se vea que ahí hay tamaños y se pregunte por la ficha completa.
|
|
73
|
+
*/
|
|
74
|
+
export function lineaDeNiveles(niveles, symbol) {
|
|
75
|
+
if (niveles.length === 0)
|
|
76
|
+
return null;
|
|
77
|
+
const barato = niveles[0];
|
|
78
|
+
const caro = niveles[niveles.length - 1];
|
|
79
|
+
const rango = niveles.length === 1
|
|
80
|
+
? precioDeNivel(barato, symbol)
|
|
81
|
+
: `${formatEther(barato.wei)} to ${precioDeNivel(caro, symbol)}`;
|
|
82
|
+
return ` Sizes: ${niveles.length} (${rango}) — panal_get_agent lists them`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* El nivel que pidió quien llama, o `null` si no es ninguno de los que hay.
|
|
86
|
+
*
|
|
87
|
+
* Se acepta el nombre, el precio o el número de la lista porque quien escribe
|
|
88
|
+
* este argumento es un modelo copiando de lo que se le enseñó dos mensajes
|
|
89
|
+
* antes, y devolver «no existe» por un «0.3 MON» donde se esperaba «Libro»
|
|
90
|
+
* mandaría a la persona a repetir su elección sin motivo. Lo que NO se hace es
|
|
91
|
+
* adivinar: si no casa con ninguno, se dice y se listan.
|
|
92
|
+
*/
|
|
93
|
+
export function buscarNivel(niveles, pedido) {
|
|
94
|
+
const limpio = pedido.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
95
|
+
if (!limpio)
|
|
96
|
+
return null;
|
|
97
|
+
const porNombre = niveles.find((n) => (n.name ?? '').toLowerCase().replace(/\s+/g, ' ') === limpio);
|
|
98
|
+
if (porNombre)
|
|
99
|
+
return porNombre;
|
|
100
|
+
// "0.3", "0.3 mon": el símbolo sobra, el número es la identidad del nivel.
|
|
101
|
+
const soloNumero = limpio.replace(/[a-z$\s]+$/, '').trim();
|
|
102
|
+
const porPrecio = niveles.find((n) => formatEther(n.wei) === soloNumero);
|
|
103
|
+
if (porPrecio)
|
|
104
|
+
return porPrecio;
|
|
105
|
+
const i = Number(limpio);
|
|
106
|
+
return Number.isInteger(i) && i >= 1 && i <= niveles.length ? niveles[i - 1] : null;
|
|
107
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -44,6 +44,7 @@ import { formatEther, isAddress, keccak256, parseEther, toBytes } from 'viem';
|
|
|
44
44
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
45
45
|
import { NATIVE_CURRENCY, TaskStatus, appendAttachmentsManifest, createPanalClient, downloadDeliveredFile, erc20Abi, matchAttachment, parseAttachmentsManifest, parseFilesManifest, stripFilesManifest, } from '@panal/sdk';
|
|
46
46
|
import { MAX_ADJUNTOS, capacidadesDeAgente, guardarDescarga, leerAdjuntoLocal, raizAdjuntos, raizDescargas, subirAdjunto, } from './adjuntos.js';
|
|
47
|
+
import { buscarNivel, lineaDeNiveles, nivelesDeAgente, precioDeNivel, renderNiveles, } from './niveles.js';
|
|
47
48
|
import { QuoteBook, SpendLedger, limitFor, limitsFromEnv } from './limits.js';
|
|
48
49
|
import { briefSignMessage, expiraEn, fetchAgentLimits, fetchResultText, pushBrief, resultSignMessage, } from './fetch-result.js';
|
|
49
50
|
const SERVER_NAME = 'panal';
|
|
@@ -218,7 +219,7 @@ function trustLine(agent) {
|
|
|
218
219
|
}
|
|
219
220
|
return ` Trust: ${partes.join('; ')}`;
|
|
220
221
|
}
|
|
221
|
-
function renderAgent(agent) {
|
|
222
|
+
function renderAgent(agent, niveles = [], detallado = false) {
|
|
222
223
|
const price = `${formatEther(agent.pricePerTask)} ${symbolOf(agent.currency)}`;
|
|
223
224
|
const skills = agent.metadata.skills.length ? agent.metadata.skills.join(', ') : '(no skills declared)';
|
|
224
225
|
return [
|
|
@@ -229,6 +230,10 @@ function renderAgent(agent) {
|
|
|
229
230
|
// consulta por x402) y llamar "el precio" a uno de ellos fue justo lo que
|
|
230
231
|
// hizo invisible al otro.
|
|
231
232
|
` Per task: ${price} (escrow, with deadline and dispute window)`,
|
|
233
|
+
// Los niveles van DEBAJO del precio de tarea y no en su lugar: ese precio
|
|
234
|
+
// sigue siendo el que se bloquea si nadie elige tamaño, y el que cobran
|
|
235
|
+
// los agentes —casi todos— que no venden ninguno.
|
|
236
|
+
...(detallado ? renderNiveles(niveles, symbolOf(agent.currency)) : [lineaDeNiveles(niveles, symbolOf(agent.currency))]),
|
|
232
237
|
` Skills: ${skills}`,
|
|
233
238
|
agent.metadata.description ? ` Description: ${agent.metadata.description}` : null,
|
|
234
239
|
agent.metadata.botUrl ? ` Endpoint: ${agent.metadata.botUrl}` : null,
|
|
@@ -324,12 +329,16 @@ const READ_TOOLS = [
|
|
|
324
329
|
? `No active agent matches "${str(args.query)}". Try a broader term, or list them all with no query.`
|
|
325
330
|
: 'There are no active agents on the marketplace right now.';
|
|
326
331
|
}
|
|
327
|
-
|
|
332
|
+
// Los de la CADENA y sin pedir ninguna ficha: aquí una lectura por agente
|
|
333
|
+
// serían N peticiones a servidores ajenos por búsqueda.
|
|
334
|
+
return `${found.length} agent(s):\n\n${found
|
|
335
|
+
.map((a) => renderAgent(a, nivelesDeAgente(a.metadataURI, [])))
|
|
336
|
+
.join('\n\n')}`;
|
|
328
337
|
},
|
|
329
338
|
},
|
|
330
339
|
{
|
|
331
340
|
name: 'panal_get_agent',
|
|
332
|
-
description: 'Full profile of a Panal agent, by address.',
|
|
341
|
+
description: 'Full profile of a Panal agent, by address: price, the sizes it sells if it sells several, skills, status and its on-chain name.',
|
|
333
342
|
inputSchema: {
|
|
334
343
|
type: 'object',
|
|
335
344
|
properties: { address: { type: 'string', description: 'The agent 0x… address.' } },
|
|
@@ -345,15 +354,21 @@ const READ_TOOLS = [
|
|
|
345
354
|
// razón de que el precio por consulta fuese invisible desde aquí. En
|
|
346
355
|
// `panal_search_agents` no se hace, porque serían N llamadas HTTP a
|
|
347
356
|
// servidores ajenos por cada búsqueda.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
+
// La ficha se lee a la vez que el precio por consulta: son dos peticiones
|
|
358
|
+
// al MISMO servidor y encadenarlas dobla la espera para nada. De aquí
|
|
359
|
+
// salen los nombres de los niveles —traducidos, si el agente traduce—;
|
|
360
|
+
// los importes siguen viniendo de la cadena.
|
|
361
|
+
const [q, ficha] = await Promise.all([
|
|
362
|
+
panal.quoteAgent(address, 'Price check.').catch(() => null),
|
|
363
|
+
agent.metadata.botUrl
|
|
364
|
+
? fetchAgentLimits(agent.metadata.botUrl).catch(() => null)
|
|
365
|
+
: Promise.resolve(null),
|
|
366
|
+
]);
|
|
367
|
+
const porLlamada = q
|
|
368
|
+
? ` Per question: ${formatEther(BigInt(q.amount))} ${q.assetSymbol ?? symbolOf(q.asset)} (x402, answered on the spot)`
|
|
369
|
+
: ' Per question: not offered (this agent only takes jobs through escrow)';
|
|
370
|
+
const niveles = nivelesDeAgente(agent.metadataURI, ficha?.niveles ?? []);
|
|
371
|
+
return `${renderAgent(agent, niveles, true)}\n${porLlamada}`;
|
|
357
372
|
},
|
|
358
373
|
},
|
|
359
374
|
{
|
|
@@ -417,6 +432,9 @@ const READ_TOOLS = [
|
|
|
417
432
|
agentName: name,
|
|
418
433
|
brief: prompt,
|
|
419
434
|
amount,
|
|
435
|
+
// Una consulta no compra tamaño: los niveles son del escrow, donde hay
|
|
436
|
+
// un importe bloqueado del que deducirlos.
|
|
437
|
+
tier: null,
|
|
420
438
|
currency: quote.asset,
|
|
421
439
|
symbol,
|
|
422
440
|
botUrl: agent.metadata.botUrl,
|
|
@@ -456,7 +474,11 @@ const READ_TOOLS = [
|
|
|
456
474
|
`Escrow: ${panal.addresses.escrow}`,
|
|
457
475
|
`Registry: ${panal.addresses.registry}`,
|
|
458
476
|
'',
|
|
459
|
-
active.length
|
|
477
|
+
active.length
|
|
478
|
+
? `Active now:\n${active
|
|
479
|
+
.map((a) => renderAgent(a, nivelesDeAgente(a.metadataURI, [])))
|
|
480
|
+
.join('\n\n')}`
|
|
481
|
+
: 'No active agents.',
|
|
460
482
|
].join('\n');
|
|
461
483
|
},
|
|
462
484
|
},
|
|
@@ -526,6 +548,13 @@ const WRITE_TOOLS = [
|
|
|
526
548
|
properties: {
|
|
527
549
|
agent: { type: 'string', description: 'The agent 0x… address.' },
|
|
528
550
|
brief: { type: 'string', description: 'The job, written with all the detail the agent will need.' },
|
|
551
|
+
tier: {
|
|
552
|
+
type: 'string',
|
|
553
|
+
description: 'Optional. Which SIZE of the job to buy, for agents that sell several: the tier name as ' +
|
|
554
|
+
'panal_get_agent listed it (its price or its position in the list also work). Each tier is a ' +
|
|
555
|
+
'different price and a different limit on how long the brief can be. Show the person the list ' +
|
|
556
|
+
'and let them pick; without this the cheapest one is quoted.',
|
|
557
|
+
},
|
|
529
558
|
files: {
|
|
530
559
|
type: 'array',
|
|
531
560
|
items: { type: 'string' },
|
|
@@ -584,39 +613,92 @@ const WRITE_TOOLS = [
|
|
|
584
613
|
// Esto —con el manifiesto dentro— es lo que se hashea y lo que hay que
|
|
585
614
|
// volver a entregar palabra por palabra si el envío falla.
|
|
586
615
|
const brief = appendAttachmentsManifest(briefPedido, adjuntos.map((a) => a.file));
|
|
587
|
-
const amount = agent.pricePerTask;
|
|
588
616
|
const symbol = symbolOf(agent.currency);
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
|
|
617
|
+
// ---- La ficha, una sola vez -----------------------------------------
|
|
618
|
+
//
|
|
619
|
+
// De aquí salen tres cosas del mismo JSON: si contesta alguien, cuánto
|
|
620
|
+
// texto acepta y cómo se llaman sus niveles. Se lee ANTES de mirar el
|
|
621
|
+
// presupuesto porque el importe ya no es siempre el precio del registro:
|
|
622
|
+
// con niveles, lo que se bloquea es el del nivel elegido, y comprobar el
|
|
623
|
+
// tope contra un número que todavía no se sabe no comprueba nada.
|
|
624
|
+
let limites = { alcanzable: null, maxBriefChars: null, niveles: [] };
|
|
597
625
|
if (agent.metadata.botUrl) {
|
|
598
|
-
|
|
626
|
+
limites = await fetchAgentLimits(agent.metadata.botUrl);
|
|
599
627
|
// Que el endpoint no conteste descalifica el encargo entero: el brief se
|
|
600
628
|
// entrega DESPUÉS de crear la tarea, así que contratar ahora dejaría el
|
|
601
629
|
// pago bloqueado en una tarea que nadie puede empezar. Aquí no cuesta
|
|
602
630
|
// nada y basta con reintentar cuando el agente vuelva.
|
|
603
|
-
if (alcanzable === false) {
|
|
631
|
+
if (limites.alcanzable === false) {
|
|
604
632
|
return (`${agent.metadata.name || address} publishes ${agent.metadata.botUrl} but nothing answers there, ` +
|
|
605
633
|
`so the job could not be delivered even after paying. Nothing was quoted and nothing was spent. ` +
|
|
606
634
|
`Try again when the agent is back up.`);
|
|
607
635
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
636
|
+
}
|
|
637
|
+
// ---- Qué tamaño se compra -------------------------------------------
|
|
638
|
+
const niveles = nivelesDeAgente(agent.metadataURI, limites.niveles);
|
|
639
|
+
const pedido = str(args.tier);
|
|
640
|
+
let elegido = null;
|
|
641
|
+
if (niveles.length > 0) {
|
|
642
|
+
if (pedido) {
|
|
643
|
+
elegido = buscarNivel(niveles, pedido);
|
|
644
|
+
if (!elegido) {
|
|
645
|
+
return [
|
|
646
|
+
`${agent.metadata.name || address} does not sell a "${pedido}" size. Nothing was quoted.`,
|
|
647
|
+
...renderNiveles(niveles, symbol),
|
|
648
|
+
].join('\n');
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
// El más barato, que es lo que compra quien contrata sin elegir: su
|
|
653
|
+
// precio DEBERÍA ser el del registro, pero es el nivel quien manda —
|
|
654
|
+
// si el agente puso el suyo más caro, pagar el del registro sería
|
|
655
|
+
// pagar por debajo del mínimo que acepta y que rechazara el encargo
|
|
656
|
+
// con el dinero ya bloqueado.
|
|
657
|
+
elegido = niveles[0];
|
|
612
658
|
}
|
|
613
659
|
}
|
|
660
|
+
else if (pedido) {
|
|
661
|
+
// Pedir un nivel a quien no vende ninguno casi siempre es haberse
|
|
662
|
+
// equivocado de agente. Cobrar el precio suelto y callarlo sería
|
|
663
|
+
// contratar una cosa distinta de la que se pidió.
|
|
664
|
+
return (`${agent.metadata.name || address} does not sell sizes: it has one price for any job, ` +
|
|
665
|
+
`${formatEther(agent.pricePerTask)} ${symbol}. Nothing was quoted. Ask again without \`tier\` ` +
|
|
666
|
+
`if that is what you want.`);
|
|
667
|
+
}
|
|
668
|
+
const amount = elegido?.wei ?? agent.pricePerTask;
|
|
669
|
+
const negativa = revisarPresupuesto(agent.currency, amount, symbol);
|
|
670
|
+
if (negativa)
|
|
671
|
+
return negativa;
|
|
672
|
+
// El tope del brief se pregunta ANTES de presupuestar. Si no cabe, el
|
|
673
|
+
// agente lo rechazaría con un 400 al recibirlo — pero eso ocurre ya con
|
|
674
|
+
// el pago bloqueado, porque el encargo se entrega después de crear la
|
|
675
|
+
// tarea. Aquí no cuesta nada y evita dejar dinero atrapado hasta que
|
|
676
|
+
// vence el plazo.
|
|
677
|
+
// El tope del nivel elegido manda sobre el general de la ficha: un agente
|
|
678
|
+
// que vende tamaños declara en cada uno cuánto texto acepta, y comprobar
|
|
679
|
+
// contra el grande dejaría pasar un encargo que el pequeño rechaza.
|
|
680
|
+
const tope = elegido?.maxBriefChars ?? limites.maxBriefChars;
|
|
681
|
+
if (tope !== null && brief.length > tope) {
|
|
682
|
+
// Si hay un tamaño mayor donde SÍ cabe, se dice cuál. La alternativa
|
|
683
|
+
// es mandar a acortar un texto cuando lo que faltaba era pagar el nivel
|
|
684
|
+
// que ese texto necesita, que es justo lo que el agente vende.
|
|
685
|
+
const cabe = niveles.find((n) => (n.maxBriefChars ?? 0) >= brief.length);
|
|
686
|
+
const dondeSiCabe = cabe && cabe !== elegido
|
|
687
|
+
? ` Its "${cabe.name}" size takes ${cabe.maxBriefChars} for ${precioDeNivel(cabe, symbol)}: ` +
|
|
688
|
+
`ask the person if they want that one.`
|
|
689
|
+
: '';
|
|
690
|
+
return (`That job does not fit: ${agent.metadata.name || address}` +
|
|
691
|
+
`${elegido ? ` at the "${elegido.name}" size` : ''} accepts ${tope} characters ` +
|
|
692
|
+
`and this brief is ${brief.length}. Nothing was quoted and nothing was spent. ` +
|
|
693
|
+
`Shorten it by ${brief.length - tope} characters and ask again.${dondeSiCabe}`);
|
|
694
|
+
}
|
|
614
695
|
const quote = quotes.issue({
|
|
615
696
|
kind: 'hire',
|
|
616
697
|
worker: agent.address,
|
|
617
698
|
agentName: agent.metadata.name || agent.address,
|
|
618
699
|
brief,
|
|
619
700
|
amount,
|
|
701
|
+
tier: elegido?.name ?? null,
|
|
620
702
|
currency: agent.currency,
|
|
621
703
|
symbol,
|
|
622
704
|
botUrl: agent.metadata.botUrl,
|
|
@@ -624,7 +706,18 @@ const WRITE_TOOLS = [
|
|
|
624
706
|
});
|
|
625
707
|
return [
|
|
626
708
|
`Quote for ${quote.agentName}:`,
|
|
627
|
-
` Price: ${formatEther(amount)} ${symbol}`,
|
|
709
|
+
` Price: ${formatEther(amount)} ${symbol}${elegido ? ` — size "${elegido.name}"` : ''}`,
|
|
710
|
+
// Los otros tamaños se enseñan SIEMPRE que existan, también cuando ya
|
|
711
|
+
// se eligió uno: quien está leyendo esto es quien paga, y enterarse de
|
|
712
|
+
// que había uno más grande después de contratar no sirve de nada.
|
|
713
|
+
...(niveles.length > 1
|
|
714
|
+
? [
|
|
715
|
+
` Other sizes: ${niveles
|
|
716
|
+
.filter((n) => n !== elegido)
|
|
717
|
+
.map((n) => `${n.name} (${precioDeNivel(n, symbol)})`)
|
|
718
|
+
.join(', ')}`,
|
|
719
|
+
]
|
|
720
|
+
: []),
|
|
628
721
|
` Deadline: ${limits.deadlineHours} h from the moment it is hired`,
|
|
629
722
|
` Job: ${briefPedido.length > 200 ? `${briefPedido.slice(0, 200)}…` : briefPedido}`,
|
|
630
723
|
// Se listan uno a uno, con su ruta real. Lo que se adjunta sale de esta
|
|
@@ -760,7 +853,19 @@ const WRITE_TOOLS = [
|
|
|
760
853
|
}
|
|
761
854
|
const deadline = BigInt(Math.floor(Date.now() / 1000) + limits.deadlineHours * 3600);
|
|
762
855
|
try {
|
|
763
|
-
|
|
856
|
+
// El importe va explícito, y es el del presupuesto. Sin él el SDK
|
|
857
|
+
// vuelve a leer el `pricePerTask` del registro al contratar, que es
|
|
858
|
+
// otro número en cuanto el agente vende niveles —se bloquearía el
|
|
859
|
+
// precio de entrada habiendo aprobado el grande— y que además puede
|
|
860
|
+
// haber cambiado en los cinco minutos que vive un presupuesto: se
|
|
861
|
+
// gastaría de más sin que nadie lo hubiera aprobado y por encima del
|
|
862
|
+
// tope, que se comprobó contra el importe viejo.
|
|
863
|
+
const result = await panal.hire({
|
|
864
|
+
agent: quote.worker,
|
|
865
|
+
brief: quote.brief,
|
|
866
|
+
amount: quote.amount,
|
|
867
|
+
deadline,
|
|
868
|
+
});
|
|
764
869
|
ledger.record(quote.currency, result.amount);
|
|
765
870
|
log(`contratada #${result.taskId} a ${quote.worker} por ${formatEther(result.amount)} ${quote.symbol}`);
|
|
766
871
|
// Contratar sin entregar el encargo deja la tarea a medias: el pago
|
|
@@ -821,7 +926,7 @@ const WRITE_TOOLS = [
|
|
|
821
926
|
return [
|
|
822
927
|
`Hired. Task #${result.taskId} created and payment locked in escrow.`,
|
|
823
928
|
` Agent: ${quote.agentName}`,
|
|
824
|
-
` Amount: ${formatEther(result.amount)} ${quote.symbol}`,
|
|
929
|
+
` Amount: ${formatEther(result.amount)} ${quote.symbol}${quote.tier ? ` — size "${quote.tier}"` : ''}`,
|
|
825
930
|
` tx: ${EXPLORER}/tx/${result.txHash}`,
|
|
826
931
|
'',
|
|
827
932
|
entregaBrief,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panal-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Servidor MCP de Panal: busca y contrata agentes de IA on-chain desde Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"build": "tsc -p tsconfig.json",
|
|
42
42
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
43
|
"start": "tsx src/server.ts",
|
|
44
|
-
"test": "tsx test/mcp.test.ts && tsx test/limites.test.ts && tsx test/monedas.test.ts && tsx test/adjuntos.test.ts",
|
|
44
|
+
"test": "tsx test/mcp.test.ts && tsx test/limites.test.ts && tsx test/monedas.test.ts && tsx test/adjuntos.test.ts && tsx test/niveles.test.ts",
|
|
45
45
|
"test:adjuntos": "tsx test/adjuntos.test.ts",
|
|
46
46
|
"test:limites": "tsx test/limites.test.ts"
|
|
47
47
|
}
|