create-panal-agent 0.25.0 → 0.26.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 CHANGED
@@ -235,7 +235,10 @@ arrancar. Con eso, cada `TABLON_MINUTOS`:
235
235
  ocupa un hueco de su cupo, y en el nivel 1 el cupo es de uno.
236
236
 
237
237
  El encargo se lee del buzón del tablón y la entrega se deja ahí antes de anclarla, porque es
238
- de ahí de donde la lee el cliente; por ese camino solo viaja texto. `TABLON_PALABRAS` (el
238
+ de ahí de donde la lee el cliente. **Los archivos van por el mismo camino**: si el encargo trae
239
+ adjuntos, tu agente los baja del buzón del tablón, los comprueba contra el hash del encargo y
240
+ le llegan a `handleTask` en `ctx.adjuntos`, como los de un encargo normal; y si `handleTask`
241
+ devuelve archivos, se suben ahí y su hash entra en la entrega anclada (hasta 5 y 30 MB). `TABLON_PALABRAS` (el
239
242
  anuncio tiene que mencionar alguna) y `TABLON_MINIMO_USD` (lo mínimo que te tiene que dejar
240
243
  en dólares, después del 2,5 %) son opcionales.
241
244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-panal-agent",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Crea un agente de IA para Panal, funcionando y cobrando on-chain, en cinco minutos",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@panal/sdk": "^0.24.0",
13
+ "@panal/sdk": "^0.25.0",
14
14
  "unpdf": "^1.8.1",
15
15
  "dotenv": "^17.0.0",
16
16
  "tsx": "^4.19.0",
@@ -31,6 +31,7 @@ import {
31
31
  assertCanServe,
32
32
  buildQuote,
33
33
  createPanalClient,
34
+ entregaConArchivos,
34
35
  leerEjemplosDeMetadata,
35
36
  leerNiveles,
36
37
  leerNivelesDeMetadata,
@@ -43,11 +44,14 @@ import {
43
44
  nivelPara,
44
45
  parseAttachmentsManifest,
45
46
  parseEnvelope,
47
+ parseFilesManifest,
48
+ problemaConArchivos,
46
49
  permitNonce,
47
50
  readPermitDomain,
48
51
  sanitizeFileName,
49
52
  TaskStatus,
50
53
  verifyAndSettle,
54
+ type ArchivoParaSubir,
51
55
  type AttachedFile,
52
56
  type CallEnvelope,
53
57
  type DeliveredFile,
@@ -863,7 +867,32 @@ export type ResultadoTrabajo = 'entregada' | 'esperando' | 'fallo' | 'en-curso';
863
867
  */
864
868
  interface ComoEntregar {
865
869
  cliente: Address;
866
- entregar: (texto: string) => Promise<{ txHash: Hex }>;
870
+ jobId: bigint;
871
+ /** Deja la entrega en el buzón del tablón —con sus archivos, si hay— y ancla su hash. */
872
+ entregar: (texto: string, archivos: ArchivoParaSubir[]) => Promise<{ txHash: Hex }>;
873
+ /** Los adjuntos del encargo, del buzón del tablón y comprobados contra su hash. */
874
+ bajarAdjuntos: (archivos: AttachedFile[]) => Promise<Uint8Array[]>;
875
+ }
876
+
877
+ /** Los archivos de la salida de `handleTask`, como se suben al tablón. */
878
+ function archivosParaElTablon(files: TaskFile[]): ArchivoParaSubir[] {
879
+ return files.map((f) => ({
880
+ name: sanitizeFileName(f.name),
881
+ bytes: typeof f.data === 'string' ? new TextEncoder().encode(f.data) : new Uint8Array(f.data),
882
+ ...(f.mime ? { mime: f.mime } : {}),
883
+ }));
884
+ }
885
+
886
+ /** Los archivos de una entrega ya guardada, leídos de disco, para volver a subirlos. */
887
+ function archivosGuardados(taskId: bigint, texto: string): ArchivoParaSubir[] {
888
+ return parseFilesManifest(texto).flatMap((f) => {
889
+ try {
890
+ const bytes = new Uint8Array(readFileSync(join(filesDir(taskId), sanitizeFileName(f.name))));
891
+ return [{ name: f.name, bytes, ...(f.mime ? { mime: f.mime } : {}) }];
892
+ } catch {
893
+ return [];
894
+ }
895
+ });
867
896
  }
868
897
 
869
898
  async function work(
@@ -887,7 +916,19 @@ async function work(
887
916
  // que mirar. Sin esto, un agente que se reinicia entre el brief y la
888
917
  // subida se pondría a trabajar sin la foto, entregaría lo que pudiera y
889
918
  // anclaría ese resultado a medias en la cadena.
890
- const { recibidos, faltan } = repasarAdjuntos(taskId, brief);
919
+ let { recibidos, faltan } = repasarAdjuntos(taskId, brief);
920
+ // En el tablón no los sube nadie a este servidor: están en el buzón del
921
+ // tablón. Se bajan, se comprueban contra el hash del encargo y se guardan
922
+ // donde los guarda una subida normal; a partir de ahí es lo mismo.
923
+ if (faltan.length > 0 && delTablon) {
924
+ try {
925
+ const bytes = await delTablon.bajarAdjuntos(faltan);
926
+ faltan.forEach((f, i) => guardarAdjunto(taskId, f.name, bytes[i]!));
927
+ ({ recibidos, faltan } = repasarAdjuntos(taskId, brief));
928
+ } catch (err) {
929
+ console.error(`[tablon] #${taskId} no se pudieron bajar los adjuntos: ${err instanceof Error ? err.message : err}`);
930
+ }
931
+ }
891
932
  if (faltan.length > 0) {
892
933
  console.log(
893
934
  `[panal] #${taskId} en espera de ${faltan.length} adjunto(s): ${faltan.map((f) => f.name).join(', ')}`,
@@ -921,19 +962,32 @@ async function work(
921
962
  // archivos. Los archivos se escriben en disco y su hash se cuela en el
922
963
  // texto: lo que se ancla en la cadena pasa a cubrirlos también.
923
964
  const { text: cuerpo, files } = normalizarSalida(salida);
924
- // Por el buzón del tablón solo viaja texto: los archivos se sirven desde
925
- // este servidor a quien firme como cliente de la tarea, y ahí el cliente
926
- // es PanalTablon. Se dice en vez de entregar un manifiesto que nadie puede abrir.
965
+ // En el tablón los archivos van por SU buzón, que solo los da al cliente
966
+ // que publicó. Se guardan también en disco: si el proceso muere antes de
967
+ // subirlos, retomar los vuelve a subir de ahí.
968
+ let paraElTablon: ArchivoParaSubir[] = [];
927
969
  if (delTablon && files.length) {
928
- console.warn(`[tablon] #${taskId}: ${files.length} archivo(s) no viajan por el tablón; se entrega solo el texto.`);
970
+ saveFiles(taskId, files);
971
+ paraElTablon = archivosParaElTablon(files);
972
+ const problema = problemaConArchivos(paraElTablon);
973
+ if (problema) {
974
+ console.warn(`[tablon] #${taskId}: los archivos no caben en el tablón (${problema}); se entrega solo el texto.`);
975
+ paraElTablon = [];
976
+ }
929
977
  }
930
- const text = files.length && !delTablon ? appendFilesManifest(cuerpo, saveFiles(taskId, files)) : cuerpo;
978
+ const text = delTablon
979
+ ? paraElTablon.length
980
+ ? entregaConArchivos(delTablon.jobId, cuerpo, paraElTablon)
981
+ : cuerpo
982
+ : files.length
983
+ ? appendFilesManifest(cuerpo, saveFiles(taskId, files))
984
+ : cuerpo;
931
985
 
932
986
  // Primero se guarda y luego se entrega: si el orden fuera al revés y el
933
987
  // proceso muriera entre medias, el hash estaría anclado on-chain y el texto
934
988
  // perdido, o sea una entrega imposible de cumplir.
935
989
  saveResult(taskId, text);
936
- const { txHash } = delTablon ? await delTablon.entregar(text) : await panal.deliverResult(taskId, text);
990
+ const { txHash } = delTablon ? await delTablon.entregar(cuerpo, paraElTablon) : await panal.deliverResult(taskId, text);
937
991
  console.log(`[panal] #${taskId} entregada${delTablon ? ' por el tablón' : ''} · tx ${txHash}`);
938
992
  return 'entregada';
939
993
  } catch (err) {
@@ -2016,7 +2070,10 @@ const encargosDelTablon = new EncargosPropios(DATA_DIR);
2016
2070
  const trabajarDelTablon = async (job: EncargoConReglas, brief: string): Promise<boolean> =>
2017
2071
  (await work(job.escrowTaskId, brief, null, {
2018
2072
  cliente: job.cliente,
2019
- entregar: (texto) => panal.deliverBoardJob(job.id, texto),
2073
+ jobId: job.id,
2074
+ // Compone el mismo texto que se guardó (`entregaConArchivos`): su hash es el que se ancla.
2075
+ entregar: (texto, archivos) => panal.deliverBoardJob(job.id, texto, archivos),
2076
+ bajarAdjuntos: (archivos) => panal.downloadBoardJobFiles(job.id, archivos, 'adjunto'),
2020
2077
  })) === 'entregada';
2021
2078
 
2022
2079
  // El vigilante. Va DESPUÉS de escuchar: su primer repaso puede tardar unos
@@ -2061,6 +2118,7 @@ arrancarVigilante({
2061
2118
  propios: encargosDelTablon,
2062
2119
  briefGuardado: loadBrief,
2063
2120
  resultadoGuardado: loadResult,
2121
+ archivosGuardados,
2064
2122
  trabajar: trabajarDelTablon,
2065
2123
  },
2066
2124
  taskId,
@@ -78,7 +78,7 @@ import {
78
78
  formatearDolares,
79
79
  leerAnuncio,
80
80
  } from '@panal/sdk';
81
- import type { EncargoConReglas, PanalClient } from '@panal/sdk';
81
+ import type { ArchivoParaSubir, EncargoConReglas, PanalClient } from '@panal/sdk';
82
82
 
83
83
  export interface OpcionesTablon {
84
84
  /** Cada cuánto se mira el tablón, en minutos. */
@@ -485,6 +485,8 @@ export async function retomarDelTablon(
485
485
  propios: Pick<EncargosPropios, 'anotar'>;
486
486
  briefGuardado: (taskId: bigint) => string | null;
487
487
  resultadoGuardado: (taskId: bigint) => string | null;
488
+ /** Los archivos de una entrega guardada, leídos de disco. */
489
+ archivosGuardados?: (taskId: bigint, texto: string) => ArchivoParaSubir[];
488
490
  trabajar: (job: EncargoConReglas, brief: string) => Promise<boolean>;
489
491
  },
490
492
  taskId: bigint,
@@ -498,6 +500,12 @@ export async function retomarDelTablon(
498
500
 
499
501
  const resultado = deps.resultadoGuardado(taskId);
500
502
  if (resultado !== null) {
503
+ // Si la entrega anuncia archivos, se vuelven a subir de disco antes: el
504
+ // proceso pudo morir entre guardarla y subirlos, y anclar un texto que
505
+ // anuncia archivos que el buzón no tiene dejaría al cliente sin ellos.
506
+ // Subir otra vez lo mismo no ocupa más.
507
+ const archivos = deps.archivosGuardados?.(taskId, resultado) ?? [];
508
+ if (archivos.length > 0) await deps.panal.publishBoardJobFiles(job.id, archivos);
501
509
  const { txHash } = await deps.panal.deliverBoardJob(job.id, resultado);
502
510
  console.log(`[tablon] encargo #${job.id} (tarea #${taskId}) entregado al segundo intento · tx ${txHash}`);
503
511
  return true;