@panal/sdk 0.10.0 → 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.
@@ -0,0 +1,121 @@
1
+ /**
2
+ * La ficha que un agente sirve en `GET /agent.json`.
3
+ *
4
+ * POR QUÉ ESTE ARCHIVO EXISTE
5
+ *
6
+ * Había dos formatos. El bot colocaba el cobro por llamada en
7
+ * `endpoints.x402Ask`; la plantilla lo ponía en la raíz, como `x402Ask`. Los
8
+ * dos servían la misma información y ningún lector veía las dos, porque cada
9
+ * uno parseaba con un tipo escrito a mano allí donde hacía falta.
10
+ *
11
+ * No fue un descuido de nadie: el tipo `AgentJson` vivía dentro del bot, así
12
+ * que la plantilla —que no depende del bot— no tenía de dónde copiarlo y
13
+ * escribió su propio objeto. Dos implementaciones honestas de una idea que
14
+ * nunca se escribió en un sitio común divergen solas.
15
+ *
16
+ * Aquí está esa idea, en el paquete del que ya dependen la plantilla y el MCP.
17
+ * Quien sirva una ficha, que la sirva con esta forma; quien la lea, que la lea
18
+ * con `leerX402` y `leerMaxBriefChars`, que entienden también la forma vieja.
19
+ *
20
+ * COMPATIBILIDAD
21
+ *
22
+ * Hay agentes desplegados sirviendo el formato antiguo, y no se les puede
23
+ * pedir que se actualicen para seguir siendo contratables. Los lectores
24
+ * aceptan las dos formas y lo seguirán haciendo: quien escribe se moderniza,
25
+ * quien lee perdona. Es la misma regla que ya seguía `agentAddress`.
26
+ */
27
+ import type { Address } from 'viem';
28
+ /** Cobro por llamada (x402), tal y como lo anuncia un agente. */
29
+ export interface FichaX402 {
30
+ method?: 'POST';
31
+ /** Ruta relativa. `url` gana si están las dos. */
32
+ path?: string;
33
+ url?: string;
34
+ scheme?: string;
35
+ /** Token ERC-20 en el que cobra. */
36
+ asset?: Address;
37
+ assetSymbol?: string;
38
+ /** Precio por llamada, en wei del token, como cadena decimal. */
39
+ amount?: string;
40
+ payTo?: Address;
41
+ howTo?: string;
42
+ }
43
+ /** Cómo mandarle el encargo a un agente, y cuánto texto acepta. */
44
+ export interface FichaPostBrief {
45
+ method?: 'POST';
46
+ path?: string;
47
+ signMessage?: string;
48
+ body?: string;
49
+ /**
50
+ * Tope de caracteres del encargo.
51
+ *
52
+ * Publicarlo es lo que evita que un cliente bloquee el pago con un encargo
53
+ * que el agente va a rechazar. Ausente significa NO LO DICE, que no es lo
54
+ * mismo que «no hay tope»: tratarlo como ilimitado es volver a averiguarlo
55
+ * pagando.
56
+ */
57
+ maxBriefChars?: number;
58
+ }
59
+ /** Cómo descargar el resultado ya entregado. */
60
+ export interface FichaGetResult {
61
+ method?: 'GET';
62
+ path?: string;
63
+ signMessage?: string;
64
+ }
65
+ /**
66
+ * La ficha completa. Casi todo es opcional a propósito: los dos motores tienen
67
+ * capacidades distintas —el de la plantilla no lee el registry al servirla, y
68
+ * el del bot sí— y un esquema que obligue a rellenar lo que no se sabe termina
69
+ * rellenándose con mentiras.
70
+ */
71
+ export interface AgentCard {
72
+ /** La dirección on-chain que este dominio declara suya. Es lo que verifica. */
73
+ agent?: Address;
74
+ /** Alias antiguo de `agent`. Se sigue leyendo; no lo escribas en fichas nuevas. */
75
+ agentAddress?: Address;
76
+ protocol?: 'panal';
77
+ network?: string;
78
+ chainId?: number;
79
+ name?: string;
80
+ description?: string;
81
+ skills?: string[];
82
+ price?: {
83
+ amountWei?: string;
84
+ currency?: Address;
85
+ symbol?: string;
86
+ } | null;
87
+ active?: boolean | null;
88
+ contracts?: {
89
+ escrow?: Address;
90
+ registry?: Address;
91
+ token?: Address;
92
+ };
93
+ endpoints?: {
94
+ base?: string | null;
95
+ postBrief?: FichaPostBrief;
96
+ getResult?: FichaGetResult;
97
+ x402Ask?: FichaX402;
98
+ indexer?: string | null;
99
+ };
100
+ /** Alias ANTIGUO de `endpoints.x402Ask`. Se lee; no se escribe. */
101
+ x402Ask?: FichaX402;
102
+ howToHire?: string[];
103
+ }
104
+ /** La dirección que la ficha declara suya, mirando también el alias viejo. */
105
+ export declare function leerDireccion(card: unknown): string | null;
106
+ /**
107
+ * El bloque de cobro por llamada, venga en el sitio nuevo o en el viejo.
108
+ *
109
+ * El sitio canónico gana si están los dos: un agente que sirva ambos está en
110
+ * mitad de una migración, y el nuevo es el que va a seguir manteniendo.
111
+ */
112
+ export declare function leerX402(card: unknown): FichaX402 | null;
113
+ /**
114
+ * El tope de caracteres del encargo, o `null` si la ficha no lo dice.
115
+ *
116
+ * `null` es NO LO SÉ y hay que tratarlo así. La ficha la sirve un desconocido,
117
+ * así que solo cuenta un entero positivo: un 0 o un negativo harían imposible
118
+ * cualquier encargo, y eso lo decide el agente bajando su tope, no mandando
119
+ * basura en un campo.
120
+ */
121
+ export declare function leerMaxBriefChars(card: unknown): number | null;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * La ficha que un agente sirve en `GET /agent.json`.
3
+ *
4
+ * POR QUÉ ESTE ARCHIVO EXISTE
5
+ *
6
+ * Había dos formatos. El bot colocaba el cobro por llamada en
7
+ * `endpoints.x402Ask`; la plantilla lo ponía en la raíz, como `x402Ask`. Los
8
+ * dos servían la misma información y ningún lector veía las dos, porque cada
9
+ * uno parseaba con un tipo escrito a mano allí donde hacía falta.
10
+ *
11
+ * No fue un descuido de nadie: el tipo `AgentJson` vivía dentro del bot, así
12
+ * que la plantilla —que no depende del bot— no tenía de dónde copiarlo y
13
+ * escribió su propio objeto. Dos implementaciones honestas de una idea que
14
+ * nunca se escribió en un sitio común divergen solas.
15
+ *
16
+ * Aquí está esa idea, en el paquete del que ya dependen la plantilla y el MCP.
17
+ * Quien sirva una ficha, que la sirva con esta forma; quien la lea, que la lea
18
+ * con `leerX402` y `leerMaxBriefChars`, que entienden también la forma vieja.
19
+ *
20
+ * COMPATIBILIDAD
21
+ *
22
+ * Hay agentes desplegados sirviendo el formato antiguo, y no se les puede
23
+ * pedir que se actualicen para seguir siendo contratables. Los lectores
24
+ * aceptan las dos formas y lo seguirán haciendo: quien escribe se moderniza,
25
+ * quien lee perdona. Es la misma regla que ya seguía `agentAddress`.
26
+ */
27
+ /** La dirección que la ficha declara suya, mirando también el alias viejo. */
28
+ export function leerDireccion(card) {
29
+ const c = card;
30
+ const dir = typeof c?.agent === 'string' ? c.agent : typeof c?.agentAddress === 'string' ? c.agentAddress : '';
31
+ return dir || null;
32
+ }
33
+ /**
34
+ * El bloque de cobro por llamada, venga en el sitio nuevo o en el viejo.
35
+ *
36
+ * El sitio canónico gana si están los dos: un agente que sirva ambos está en
37
+ * mitad de una migración, y el nuevo es el que va a seguir manteniendo.
38
+ */
39
+ export function leerX402(card) {
40
+ const c = card;
41
+ return c?.endpoints?.x402Ask ?? c?.x402Ask ?? null;
42
+ }
43
+ /**
44
+ * El tope de caracteres del encargo, o `null` si la ficha no lo dice.
45
+ *
46
+ * `null` es NO LO SÉ y hay que tratarlo así. La ficha la sirve un desconocido,
47
+ * así que solo cuenta un entero positivo: un 0 o un negativo harían imposible
48
+ * cualquier encargo, y eso lo decide el agente bajando su tope, no mandando
49
+ * basura en un campo.
50
+ */
51
+ export function leerMaxBriefChars(card) {
52
+ const max = card?.endpoints?.postBrief?.maxBriefChars;
53
+ return typeof max === 'number' && Number.isInteger(max) && max > 0 ? max : null;
54
+ }
package/dist/client.d.ts CHANGED
@@ -65,6 +65,31 @@ export interface HireResult {
65
65
  /** El hash del brief que quedó registrado, para poder probarlo después. */
66
66
  taskHash: Hex;
67
67
  }
68
+ /**
69
+ * ¿Esto que manda el indexador es un nombre de PanalNames?
70
+ *
71
+ * Se valida como todo lo que llega de un servicio: si viene a medias se
72
+ * descarta, porque un `origen` inventado haria que la web avisara de una venta
73
+ * que no existio, o peor, que callara una que si.
74
+ */
75
+ /**
76
+ * La skill pedida y, detrás, versiones cada vez más generales de ella.
77
+ *
78
+ * `searchAgents` exige que TODAS las palabras aparezcan, así que cuantas más
79
+ * lleve la skill, menos gente la cumple. Quien escribe estas cadenas suele ser
80
+ * un modelo, y un modelo pide "Spanish tax law" donde el mercado vende "tax".
81
+ *
82
+ * Se recorta POR LA IZQUIERDA porque en inglés el núcleo del sintagma va al
83
+ * final: "Spanish tax law" → "tax law" → "law" sigue hablando de lo mismo.
84
+ * Recortar por la derecha dejaría "Spanish", que casa con cualquier cosa
85
+ * española y con nada de impuestos: peor que no encontrar a nadie, porque se
86
+ * pagaría al agente equivocado.
87
+ *
88
+ * Nunca baja de una palabra y nunca devuelve duplicados, así que en el caso
89
+ * normal —una o dos palabras, que es lo que el prompt pide— esto es una sola
90
+ * búsqueda y no cambia nada.
91
+ */
92
+ export declare function variantesDeSkill(skill: string): string[];
68
93
  export declare class PanalClient {
69
94
  readonly network: PanalNetwork;
70
95
  readonly addresses: PanalAddresses;
@@ -290,6 +315,7 @@ export declare class PanalClient {
290
315
  depth?: number;
291
316
  }): Promise<AskResult & {
292
317
  agent: Address;
318
+ skill: string;
293
319
  }>;
294
320
  /**
295
321
  * Dónde escucha el x402 de un agente.
package/dist/client.js CHANGED
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { createPublicClient, createWalletClient, formatEther, getAddress, http, keccak256, toBytes } from 'viem';
19
19
  import { erc20Abi, escrowAbi, namesAbi, registryAbi } from './abis.js';
20
+ import { leerX402 } from './agent-card.js';
20
21
  import { assertPublicUrl, fetchLimited } from './net.js';
21
22
  import { X402Error, payAndAsk, quoteAsk } from './x402.js';
22
23
  import { descend, newEnvelope, remainingBudget } from './envelope.js';
@@ -33,6 +34,35 @@ const REGISTRY_MAX = 500;
33
34
  * descarta, porque un `origen` inventado haria que la web avisara de una venta
34
35
  * que no existio, o peor, que callara una que si.
35
36
  */
37
+ /**
38
+ * La skill pedida y, detrás, versiones cada vez más generales de ella.
39
+ *
40
+ * `searchAgents` exige que TODAS las palabras aparezcan, así que cuantas más
41
+ * lleve la skill, menos gente la cumple. Quien escribe estas cadenas suele ser
42
+ * un modelo, y un modelo pide "Spanish tax law" donde el mercado vende "tax".
43
+ *
44
+ * Se recorta POR LA IZQUIERDA porque en inglés el núcleo del sintagma va al
45
+ * final: "Spanish tax law" → "tax law" → "law" sigue hablando de lo mismo.
46
+ * Recortar por la derecha dejaría "Spanish", que casa con cualquier cosa
47
+ * española y con nada de impuestos: peor que no encontrar a nadie, porque se
48
+ * pagaría al agente equivocado.
49
+ *
50
+ * Nunca baja de una palabra y nunca devuelve duplicados, así que en el caso
51
+ * normal —una o dos palabras, que es lo que el prompt pide— esto es una sola
52
+ * búsqueda y no cambia nada.
53
+ */
54
+ export function variantesDeSkill(skill) {
55
+ const palabras = skill.trim().split(/\s+/).filter(Boolean);
56
+ if (palabras.length <= 1)
57
+ return [skill.trim()].filter(Boolean);
58
+ const out = [];
59
+ for (let i = 0; i < palabras.length; i++) {
60
+ const v = palabras.slice(i).join(' ');
61
+ if (!out.includes(v))
62
+ out.push(v);
63
+ }
64
+ return out;
65
+ }
36
66
  function esNombre(v) {
37
67
  if (v === null || typeof v !== 'object')
38
68
  return false;
@@ -643,11 +673,31 @@ export class PanalClient {
643
673
  // Se busca por SKILL, no por texto libre: encontrar a alguien porque la
644
674
  // palabra aparece en su descripción no sirve para delegar. Si el indexador
645
675
  // no está, `searchAgents` cae solo a la cadena con su texto libre.
646
- const candidates = (await this.searchAgents(skill, { skill }))
647
- .filter((a) => !excluded.has(a.address.toLowerCase()) && a.metadata.botUrl)
648
- .slice(0, options.maxCandidates ?? 5);
649
- if (!candidates.length)
650
- throw new X402Error(`Ningún agente activo con la skill "${skill}" publica endpoint.`);
676
+ //
677
+ // La búsqueda exige que TODAS las palabras casen, así que una skill de más
678
+ // de dos palabras no encuentra a nadie casi nunca: quien la escribe es un
679
+ // modelo, y un modelo pide "Spanish tax law" donde el mercado vende "tax".
680
+ // Se reintenta quitando palabras POR LA IZQUIERDA porque en inglés el
681
+ // núcleo va al final: "Spanish tax law" → "tax law" → "law". Así se
682
+ // generaliza sin perder de qué se estaba hablando; recortar por la derecha
683
+ // dejaría "Spanish", que casaría con cualquier cosa española.
684
+ let candidates = [];
685
+ let usada = skill;
686
+ for (const intento of variantesDeSkill(skill)) {
687
+ candidates = (await this.searchAgents(intento, { skill: intento }))
688
+ .filter((a) => !excluded.has(a.address.toLowerCase()) && a.metadata.botUrl)
689
+ .slice(0, options.maxCandidates ?? 5);
690
+ if (candidates.length) {
691
+ usada = intento;
692
+ break;
693
+ }
694
+ }
695
+ if (!candidates.length) {
696
+ throw new X402Error(`Ningún agente activo con la skill "${skill}" publica endpoint.` +
697
+ (variantesDeSkill(skill).length > 1
698
+ ? ` Se probó también con ${variantesDeSkill(skill).slice(1).map((v) => `"${v}"`).join(' y ')}.`
699
+ : ''));
700
+ }
651
701
  const quotes = [];
652
702
  const rechazos = [];
653
703
  for (const agent of candidates) {
@@ -683,7 +733,10 @@ export class PanalClient {
683
733
  quote: elegido.accept,
684
734
  envelope: siguiente,
685
735
  });
686
- return { ...result, agent: elegido.agent.address };
736
+ // `skill` es la que de verdad encontró al vendedor, que puede no ser la que
737
+ // pediste: quien llama necesita poder decirlo en su log, o cada búsqueda
738
+ // ensanchada es un cambio de comportamiento invisible.
739
+ return { ...result, agent: elegido.agent.address, skill: usada };
687
740
  }
688
741
  /**
689
742
  * Dónde escucha el x402 de un agente.
@@ -701,8 +754,11 @@ export class PanalClient {
701
754
  const url = await assertPublicUrl(new URL('/agent.json', base).toString(), options);
702
755
  const res = await fetchLimited(url, { timeoutMs: 10_000 });
703
756
  if (res.status === 200) {
704
- const card = JSON.parse(res.text);
705
- const anunciado = card.endpoints?.x402Ask;
757
+ // `leerX402` entiende las dos formas. Antes esto solo miraba
758
+ // `endpoints.x402Ask`, así que con la ficha de un agente de plantilla
759
+ // no encontraba nada y caía a la convención: funcionaba de casualidad,
760
+ // y solo mientras el agente escuchara justo en /x402/ask.
761
+ const anunciado = leerX402(JSON.parse(res.text));
706
762
  if (anunciado?.url)
707
763
  return anunciado.url;
708
764
  if (anunciado?.path)
package/dist/index.d.ts CHANGED
@@ -36,3 +36,5 @@ export type { SettleDeps, SettleResult, X402Payment, X402ServerAccept, X402Serve
36
36
  export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
37
37
  export type { CallEnvelope } from './envelope.js';
38
38
  export type { UrlGuardOptions } from './net.js';
39
+ export { leerDireccion, leerMaxBriefChars, leerX402 } from './agent-card.js';
40
+ export type { AgentCard, FichaGetResult, FichaPostBrief, FichaX402 } from './agent-card.js';
package/dist/index.js CHANGED
@@ -33,3 +33,6 @@ export { FILES_BLOCK, MAX_FILE_BYTES, FileVerificationError, appendFilesManifest
33
33
  export { X402_VERSION, X402_SERVER_SCHEME, buildQuote, enqueueByPayer, parsePaymentHeader, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, } from './x402-server.js';
34
34
  // El sobre que viaja entre agentes: profundidad, presupuesto y detección de ciclos.
35
35
  export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
36
+ // La ficha de GET /agent.json: un solo formato, y lectores que perdonan el
37
+ // antiguo. En agent-card.ts está por qué llegó a haber dos.
38
+ export { leerDireccion, leerMaxBriefChars, leerX402 } from './agent-card.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panal/sdk",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "SDK de Panal: contrata agentes de IA autonomos on-chain en Monad",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "scripts": {
46
46
  "build": "tsc -p tsconfig.json",
47
47
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
- "test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts",
48
+ "test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts && tsx test/skill.test.ts && tsx test/agent-card.test.ts",
49
49
  "test:nombres": "tsx test/nombres.test.ts"
50
50
  }
51
51
  }