@synsema/wasm 0.6.57 → 0.6.58

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
@@ -238,6 +238,34 @@ in a handler lists only what that request read (v0.6.46: not what the top-level
238
238
  handler mode: `stream` (SSE), `proxy to`, rate limits, `static` mounts, vhosts, TLS —
239
239
  your edge platform does those before calling the handler.
240
240
 
241
+ ## Packages: `project`, `packages` (v0.6.58)
242
+
243
+ There is no `synsema get` here and no module cache: you hand the engine the program's
244
+ `synsema.json` and the files of the packages it pins, and the engine checks every package
245
+ against its hash before the first statement runs.
246
+
247
+ ```js
248
+ const syn = await Synsema.load(wasmBytes, {
249
+ project: synsemaJson, // the text of synsema.json (or the object)
250
+ packages: { "tree-sha256:3cdf…": { "synsema.json": "…", "m.syn": "…" } },
251
+ });
252
+ await syn.ready();
253
+ syn.run('use "tok" as tok\nprint(tok.encode("hola"))\n');
254
+ ```
255
+
256
+ - `packages` is what `synsema get` left in the cache: per hash, every file of the package by its
257
+ path, the package's own `synsema.json` included. Files are text.
258
+ - Both options go to `load`/`loadNode` and to each call. A call's `project` replaces the
259
+ instance's; its `packages` add to the instance's.
260
+ - A package missing from `packages` stops the run with
261
+ `use "tok": module tok 0.1.0 is not in the packages the host passed`, and one whose files changed
262
+ with `… the files of tok (tree-sha256:…) do not match their hash`.
263
+ - The instance keeps what it verified, by hash: a later call that brings (or only names) a
264
+ verified hash does not hash it again, and `@synsema/wasm` does not copy it to the Worker again.
265
+ Each response with a `project` lists those hashes in `packages_held`.
266
+ - `receipt()` and the sealed receipt's header name the packages that ran
267
+ (`modules.packages`, with their version and hash).
268
+
241
269
  ## Also: `test`, `check`, `version`
242
270
 
243
271
  `syn.test(source)` runs the file's `test` blocks; `syn.check(source)` parses and
package/index.d.ts CHANGED
@@ -79,7 +79,28 @@ export interface Host {
79
79
  sleep?: ((secs: number) => void | Promise<void>) | true;
80
80
  }
81
81
 
82
- export interface RunOptions {
82
+ /**
83
+ * v0.6.58 — los paquetes que le pasás al motor, por su hash de árbol: `{"tree-sha256:…": {"<ruta>": "<texto>"}}`,
84
+ * con el `synsema.json` del paquete entre sus archivos (lo que `synsema get` dejó en el caché). El motor verifica
85
+ * cada hash antes de correr; un paquete que no coincide no corre.
86
+ */
87
+ export type Packages = Record<string, Record<string, string>>;
88
+
89
+ /** v0.6.58 — el proyecto de una llamada: el `synsema.json` del programa (texto u objeto) y sus paquetes. */
90
+ export interface ProjectOptions {
91
+ /**
92
+ * El `synsema.json` del programa: fija qué paquete (y qué hash) es cada `use "nombre"`. El de una llamada
93
+ * reemplaza al de `load`/`loadNode`.
94
+ */
95
+ project?: string | Record<string, unknown>;
96
+ /**
97
+ * Los archivos de los paquetes que alcanza el programa. Los de una llamada se suman a los de `load`/`loadNode`.
98
+ * La instancia guarda lo verificado por su hash: lo que ya verificó no lo vuelve a hashear ni a copiar al Worker.
99
+ */
100
+ packages?: Packages;
101
+ }
102
+
103
+ export interface RunOptions extends ProjectOptions {
83
104
  filename?: string;
84
105
  /** Reemplaza al `.env`: `secret("KEY")`/`env("KEY")` resuelven de acá. */
85
106
  env?: Record<string, string>;
@@ -184,6 +205,8 @@ export interface RunResult {
184
205
  * armar, `ok` es false y `errors` lo dice.
185
206
  */
186
207
  receipt?: Record<string, unknown>;
208
+ /** v0.6.58 — los hashes que esta instancia ya verificó (no hace falta volver a mandarlos); en toda respuesta del motor. */
209
+ packages_held?: string[];
187
210
  }
188
211
 
189
212
  export interface TestResult {
@@ -192,11 +215,15 @@ export interface TestResult {
192
215
  failed: number;
193
216
  lines: string[];
194
217
  audit: AuditEntry[];
218
+ /** v0.6.58 — los hashes que esta instancia ya verificó (no hace falta volver a mandarlos); en toda respuesta del motor. */
219
+ packages_held?: string[];
195
220
  }
196
221
 
197
222
  export interface CheckResult {
198
223
  ok: boolean;
199
224
  errors: string[];
225
+ /** v0.6.58 — los hashes que esta instancia ya verificó (no hace falta volver a mandarlos); en toda respuesta del motor. */
226
+ packages_held?: string[];
200
227
  }
201
228
 
202
229
  export interface HandleRequest {
@@ -224,6 +251,8 @@ export interface HandleResult {
224
251
  * del operador: el programa (y el cliente) ven en `errors` un texto fijo, porque cuál sitio cortó lo eligió el
225
252
  * dato privado. No se lo devuelvas al modelo ni al cliente. */
226
253
  operator_detail?: string;
254
+ /** v0.6.58 — los hashes que esta instancia ya verificó (no hace falta volver a mandarlos); en toda respuesta del motor. */
255
+ packages_held?: string[];
227
256
  }
228
257
 
229
258
  export type ModuleSource = WebAssembly.Module | Uint8Array | ArrayBuffer | URL | string | Response;
@@ -238,7 +267,7 @@ export declare function instantiate(
238
267
  hostCall: (kind: string, payload: any) => any,
239
268
  ): Promise<{ exports: WebAssembly.Exports; call(request: object): any }>;
240
269
 
241
- export interface LoadOptions {
270
+ export interface LoadOptions extends ProjectOptions {
242
271
  /** Cuántos Workers usa el modo asíncrono (default 1). Con más, `runAsync` corre en paralelo. */
243
272
  workers?: number;
244
273
  /**
@@ -271,7 +300,7 @@ export declare class Synsema {
271
300
  version(): string;
272
301
  run(source: string, opts?: RunOptions): RunResult;
273
302
  test(source: string, opts?: RunOptions): TestResult;
274
- check(source: string, opts?: { filename?: string }): CheckResult;
303
+ check(source: string, opts?: { filename?: string } & ProjectOptions): CheckResult;
275
304
  handle(source: string, request: HandleRequest, opts?: RunOptions): HandleResult;
276
305
  /**
277
306
  * Modo asíncrono (Worker + Atomics): los hooks del host pueden ser async. Las llamadas esperan en
package/index.mjs CHANGED
@@ -393,6 +393,53 @@ function requestOf(op, source, opts = {}) {
393
393
  return r;
394
394
  }
395
395
 
396
+ /**
397
+ * v0.6.58 (§12, E-12) — el proyecto de una llamada: `project` (el texto de `synsema.json`, o el
398
+ * objeto) y `packages` (`{"tree-sha256:…": {"<ruta>": "<texto>"}}`). Los de la llamada van sobre los
399
+ * de `load`/`loadNode`: el `project` de la llamada reemplaza al de la instancia y sus `packages` se
400
+ * suman a los de la instancia.
401
+ */
402
+ function withProject(request, defaults, opts) {
403
+ const project = opts.project ?? defaults.project;
404
+ if (project == null) {
405
+ if (opts.packages != null) request.packages = opts.packages;
406
+ return request;
407
+ }
408
+ request.project = typeof project === "string" ? project : JSON.stringify(project);
409
+ const packages = { ...(defaults.packages ?? {}), ...(opts.packages ?? {}) };
410
+ if (Object.keys(packages).length > 0) request.packages = packages;
411
+ return request;
412
+ }
413
+
414
+ /**
415
+ * W9: lo que la instancia de wasm ya verificó no se vuelve a mandar (ni a copiar al Worker): el
416
+ * motor lo tiene guardado por su hash, que es el del contenido. `held` es el conjunto de esa
417
+ * instancia; se actualiza con `packages_held` de cada respuesta.
418
+ */
419
+ function withoutHeld(request, held) {
420
+ if (!request.packages || !held || held.size === 0) return request;
421
+ const left = {};
422
+ let dropped = false;
423
+ for (const [h, files] of Object.entries(request.packages)) {
424
+ if (held.has(h)) dropped = true;
425
+ else left[h] = files;
426
+ }
427
+ if (!dropped) return request;
428
+ const r = { ...request };
429
+ if (Object.keys(left).length > 0) r.packages = left;
430
+ else delete r.packages;
431
+ return r;
432
+ }
433
+
434
+ function noteHeld(holder, response) {
435
+ // M6 (ronda 1): una respuesta sin `packages_held` (o un error, en `_dispatch`) no dice qué guarda
436
+ // la instancia, que pudo recrearse: se olvida todo y la próxima llamada manda los paquetes. Ronda 2
437
+ // (N-r2-3): el motor lo pone en TODA respuesta suya (`version`, sin proyecto, un error), así que
438
+ // sólo falta cuando no respondió el motor (un trap, el resultado que arma el Worker).
439
+ holder._held = response && Array.isArray(response.packages_held) ? new Set(response.packages_held) : null;
440
+ return response;
441
+ }
442
+
396
443
  function normalizeRequest(request) {
397
444
  const headers = Array.isArray(request.headers)
398
445
  ? request.headers
@@ -422,6 +469,12 @@ export class Synsema {
422
469
  }
423
470
  this._startMs = opts.startMs;
424
471
  this._bridges = [];
472
+ // v0.6.58 (§12): el proyecto y los paquetes de todas las llamadas de esta instancia.
473
+ this._defaults = { project: opts.project, packages: opts.packages };
474
+ }
475
+
476
+ _req(op, source, opts) {
477
+ return withProject(requestOf(op, source, opts), this._defaults, opts);
425
478
  }
426
479
 
427
480
  /** Carga el .wasm (bytes, Module, URL/path, Response). */
@@ -471,7 +524,7 @@ export class Synsema {
471
524
  const inst = this._instSync();
472
525
  this._host = host;
473
526
  try {
474
- return inst.call(request);
527
+ return noteHeld(inst, inst.call(withoutHeld(request, inst._held)));
475
528
  } catch (e) {
476
529
  // Un trap deja la instancia inservible: se recrea ya (sincrónico, con el Module compilado),
477
530
  // así la próxima llamada anda sin `await syn.ready()` (v0.6.45, D4). Si el host no deja
@@ -508,16 +561,16 @@ export class Synsema {
508
561
  return this._callSync({ op: "version" }).version;
509
562
  }
510
563
  run(source, opts = {}) {
511
- return this._callSync(requestOf("run", source, opts), opts.host);
564
+ return this._callSync(this._req("run", source, opts), opts.host);
512
565
  }
513
566
  test(source, opts = {}) {
514
- return this._callSync(requestOf("test", source, opts), opts.host);
567
+ return this._callSync(this._req("test", source, opts), opts.host);
515
568
  }
516
569
  check(source, opts = {}) {
517
- return this._callSync({ op: "check", source, filename: opts.filename ?? "<embedded>" });
570
+ return this._callSync(withProject({ op: "check", source, filename: opts.filename ?? "<embedded>" }, this._defaults, opts));
518
571
  }
519
572
  handle(source, request, opts = {}) {
520
- return this._callSync({ ...requestOf("handle", source, opts), request: normalizeRequest(request) }, opts.host);
573
+ return this._callSync({ ...this._req("handle", source, opts), request: normalizeRequest(request) }, opts.host);
521
574
  }
522
575
 
523
576
  // ---- modo asíncrono (Worker + Atomics) ----
@@ -591,7 +644,10 @@ export class Synsema {
591
644
  }
592
645
 
593
646
  _dispatch(b, it) {
594
- b.call(it.request, it.host, it.timeoutMs).then(it.resolve, (e) => {
647
+ // W9: cada Worker tiene su instancia de wasm, y con ella lo que ya verificó.
648
+ b.call(withoutHeld(it.request, b._held), it.host, it.timeoutMs).then((r) => it.resolve(noteHeld(b, r)), (e) => {
649
+ // M6 (ronda 1): el Worker pudo recrear su instancia (un trap) o morir: no guarda nada.
650
+ b._held = null;
595
651
  // No se llegó a mandar (el Worker se cerró antes): vuelve al principio de la cola. Lo que sí
596
652
  // corrió (un timeout, un Worker que murió en el medio) NO se reintenta: pudo tener efectos.
597
653
  if (e && e.synsemaWorkerClosed && !this._closed) {
@@ -622,13 +678,13 @@ export class Synsema {
622
678
  });
623
679
  }
624
680
  async runAsync(source, opts = {}) {
625
- return this._callAsync(requestOf("run", source, opts), opts);
681
+ return this._callAsync(this._req("run", source, opts), opts);
626
682
  }
627
683
  async testAsync(source, opts = {}) {
628
- return this._callAsync(requestOf("test", source, opts), opts);
684
+ return this._callAsync(this._req("test", source, opts), opts);
629
685
  }
630
686
  async handleAsync(source, request, opts = {}) {
631
- return this._callAsync({ ...requestOf("handle", source, opts), request: normalizeRequest(request) }, opts);
687
+ return this._callAsync({ ...this._req("handle", source, opts), request: normalizeRequest(request) }, opts);
632
688
  }
633
689
  /**
634
690
  * Termina los Workers del modo asíncrono. Es FINAL (ronda 2, B1): lo que esperaba en la cola falla
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synsema/wasm",
3
- "version": "0.6.57",
3
+ "version": "0.6.58",
4
4
  "description": "Synsema — the programming language for AI agents — embedded as WebAssembly: run .syn programs inside browsers, Node/Bun/Deno and edge runtimes, lending them http/kv/llm from your app",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/synsema.wasm CHANGED
Binary file
package/worker.mjs CHANGED
@@ -84,7 +84,8 @@ onMessage(async (m) => {
84
84
  post({ type: "fatal", message: `the module trapped and could not be instantiated again: ${safeMsg(e2)}` });
85
85
  return;
86
86
  }
87
- if (e && e.synsemaResult) post({ type: "result", response: e.synsemaResult });
87
+ // M6 (ronda 1): la instancia nueva no guarda ningún paquete, diga lo que diga la vieja.
88
+ if (e && e.synsemaResult) post({ type: "result", response: { ...e.synsemaResult, packages_held: [] } });
88
89
  else post({ type: "error", message: `synsema: the module trapped (${safeMsg(e)}); the instance was recreated` });
89
90
  }
90
91
  }