@niscorp/signal 0.1.6 → 0.1.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @niscorp/signal
2
2
 
3
+ ## 0.1.7
4
+
5
+ ### Patch Changes
6
+
7
+ - f4a3f04: `decide()` can ask about images: `images` maps a uuid to an `ImageSource`, and a string in `state` equal to a uuid points at that image.
8
+
9
+ On `typesafe` the images go as the request's `assets` with the `TypeSafe-Danger-Zone-Images: true` header, so Jev answers about them with calibrated probabilities. TypeSafe takes PNG or JPEG data; a `url` image or another `mediaType` fails with `E_VALIDATION_FAILED` before the request. On a chat provider each image follows the state in the emulation message, after a label with its uuid; a model whose registry row says `multimodal: false` is refused before the request. A key that is not a uuid is refused on every provider.
10
+
11
+ A request without `images`, or with `images: {}`, is unchanged: TypeSafe gets the same body and headers, and a chat provider the same single text message.
12
+
13
+ **What to change:** nothing. A caller that posts images to TypeSafe itself can pass them to `decide()` instead.
14
+
3
15
  ## 0.1.6
4
16
 
5
17
  ### Patch Changes
package/DESIGN.md CHANGED
@@ -119,6 +119,10 @@ sides is two changes.
119
119
  to repair. The questions are the schema: the result type and the
120
120
  gate (`decide/gate.ts`) both derive from them, which is the package's
121
121
  one sentence applied to a model that answers instead of writing.
122
+ Images ride beside `state`, keyed by the uuid `state` uses for them —
123
+ TypeSafe's own wire structure — so the adapter translates fields,
124
+ refuses what TypeSafe's wire cannot carry, and never walks or
125
+ rewrites the caller's state.
122
126
  12. **Emulation is honest or absent.** On a chat adapter `decide()`
123
127
  runs the derived schema through `complete()` and returns picks with
124
128
  `calibrated: false` — the discriminant of `DecideResult`, so the
package/DOCS.md CHANGED
@@ -543,6 +543,24 @@ On a chat provider `decide()` still works: the same questions run through struct
543
543
  if (signal.describe().kind === 'decisions') { /* sub-second */ }
544
544
  ```
545
545
 
546
+ ### Images
547
+
548
+ `images` maps a uuid to an `ImageSource`, the same type `complete()` takes in content parts. A string in `state` equal to a uuid points at that image:
549
+
550
+ ```typescript
551
+ const duck = randomUUID();
552
+ const { decisions } = await jev.decide({
553
+ state: { instruction: 'Select all squares containing a duck', tiles: { tile1: duck } },
554
+ images: { [duck]: { type: 'base64', mediaType: 'image/png', data: pngBase64 } },
555
+ questions: { tile1: { type: 'noul', instructions: 'Does the picture in `tiles.tile1` show the requested object?' } },
556
+ });
557
+ ```
558
+
559
+ - **On `typesafe`** the images go as the request's `assets`, with the `TypeSafe-Danger-Zone-Images: true` header. TypeSafe takes PNG or JPEG data: a `url` image or another `mediaType` fails with `E_VALIDATION_FAILED` before the request. TypeSafe's image documentation gives a minimum of 280 pixels a side; signal does not check sizes.
560
+ - **On a chat provider** each image follows the state in the message, after a label with its uuid. A model whose registry row says `multimodal: false` fails with `E_VALIDATION_FAILED` before the request; a model with no row is not checked. Emulation's correction retries send the images again.
561
+
562
+ A key that is not a uuid fails with `E_VALIDATION_FAILED` on every provider. A request with no images, or `images: {}`, is sent exactly as one without the field.
563
+
546
564
  ### Abort
547
565
 
548
566
  ```typescript
package/dist/index.cjs CHANGED
@@ -484,17 +484,46 @@ var jsonOrText = (text) => {
484
484
  }
485
485
  };
486
486
  var providerError = (message, raw) => new SignalError(`Provider error: ${message}`, ErrorCode.PROVIDER_ERROR, { raw });
487
+ var FORMATS = { "image/png": "png", "image/jpeg": "jpeg" };
488
+ var assetOf = (id, source) => {
489
+ if (source.type === "url") {
490
+ throw new SignalError(`Image ${id}: TypeSafe takes image data, not a URL`, ErrorCode.VALIDATION_FAILED, { image: id });
491
+ }
492
+ const format = FORMATS[source.mediaType];
493
+ if (format === void 0) {
494
+ throw new SignalError(
495
+ `Image ${id}: TypeSafe takes image/png or image/jpeg, not ${source.mediaType}`,
496
+ ErrorCode.VALIDATION_FAILED,
497
+ { image: id }
498
+ );
499
+ }
500
+ return { type: "image", format, data_base64: source.data };
501
+ };
502
+ var assetsOf = (images) => {
503
+ const entries = Object.entries(images ?? {});
504
+ return entries.length === 0 ? void 0 : Object.fromEntries(entries.map(([id, source]) => [id, assetOf(id, source)]));
505
+ };
487
506
  var createSystemOneAdapter = async (config) => {
488
507
  const send = sendOf(config.client);
489
508
  const url = `${config.baseUrl.replace(/\/+$/, "")}/systemone`;
490
509
  const decide = async (request, options) => {
510
+ const assets = assetsOf(request.images);
491
511
  let status;
492
512
  let body;
493
513
  try {
494
514
  const response = await send(url, {
495
515
  method: "POST",
496
- headers: { authorization: `Bearer ${config.apiKey}`, "content-type": "application/json" },
497
- body: JSON.stringify({ model: request.model, state: request.state, questions: request.questions }),
516
+ headers: {
517
+ authorization: `Bearer ${config.apiKey}`,
518
+ "content-type": "application/json",
519
+ ...assets !== void 0 && { "TypeSafe-Danger-Zone-Images": "true" }
520
+ },
521
+ body: JSON.stringify({
522
+ model: request.model,
523
+ state: request.state,
524
+ questions: request.questions,
525
+ ...assets !== void 0 && { assets }
526
+ }),
498
527
  ...options?.signal !== void 0 && { signal: options.signal }
499
528
  });
500
529
  status = response.status;
@@ -550,6 +579,7 @@ var bareSchemaCorrection = (issues) => `Your output was invalid: ${issues}. Repl
550
579
  var decisionEmulationPrompt = (state) => `Answer every field of the OUTPUT SCHEMA about the STATE below. Each field's description is the question and lists everything you may answer with.
551
580
  STATE:
552
581
  ${state}`;
582
+ var decisionImageLabel = (id) => `Image ${id}:`;
553
583
  var decisionQuestionDescription = (question) => {
554
584
  if (question.type === "choice") return `${question.instructions} Options: ${JSON.stringify(question.criteria)}`;
555
585
  if (question.type === "score") {
@@ -1340,6 +1370,35 @@ var assertEffort = (config, effort) => {
1340
1370
  ErrorCode.VALIDATION_FAILED
1341
1371
  );
1342
1372
  };
1373
+ var imageId = zod.z.uuid();
1374
+ var assertImageIds = (images) => {
1375
+ for (const id of Object.keys(images ?? {})) {
1376
+ if (!imageId.safeParse(id).success) {
1377
+ throw new SignalError(`Image key "${id}" is not a uuid`, ErrorCode.VALIDATION_FAILED, { image: id });
1378
+ }
1379
+ }
1380
+ };
1381
+ var assertTakesImages = (config) => {
1382
+ const resolved = modelRowOf(config);
1383
+ const row = resolved?.row;
1384
+ if (resolved === void 0 || row === void 0 || row.capabilities.multimodal) return;
1385
+ throw new SignalError(
1386
+ `${resolved.provider} ${resolved.model} takes no images (measured ${row.verified})`,
1387
+ ErrorCode.VALIDATION_FAILED
1388
+ );
1389
+ };
1390
+ var emulationContent = (state, images) => {
1391
+ const prompt = decisionEmulationPrompt(state);
1392
+ const entries = Object.entries(images ?? {});
1393
+ if (entries.length === 0) return prompt;
1394
+ return [
1395
+ { type: "text", text: prompt },
1396
+ ...entries.flatMap(([id, source]) => [
1397
+ { type: "text", text: decisionImageLabel(id) },
1398
+ { type: "image", source }
1399
+ ])
1400
+ ];
1401
+ };
1343
1402
  var wireIdsOf = (config) => {
1344
1403
  const source = typeof config.provider === "string" ? providerRegistry[config.provider] : config.provider;
1345
1404
  return source === void 0 || source.adapter === "systemone" ? [] : source.wire ?? [];
@@ -1517,7 +1576,12 @@ var createSignalFromConfig = (config) => {
1517
1576
  const stepStream = stepStreamAs("stepStream");
1518
1577
  const decideCalibrated = async (adapter, model, request, started) => {
1519
1578
  const response = await adapter.decide(
1520
- { model, state: request.state, questions: request.questions },
1579
+ {
1580
+ model,
1581
+ state: request.state,
1582
+ questions: request.questions,
1583
+ ...request.images !== void 0 && { images: request.images }
1584
+ },
1521
1585
  request.options?.signal !== void 0 ? { signal: request.options.signal } : void 0
1522
1586
  );
1523
1587
  const decisions = acceptCalibrated(request.questions, response.answers, response.raw);
@@ -1535,10 +1599,11 @@ var createSignalFromConfig = (config) => {
1535
1599
  };
1536
1600
  };
1537
1601
  const decideEmulated = async (model, request, started) => {
1602
+ if (Object.keys(request.images ?? {}).length > 0) assertTakesImages(config);
1538
1603
  const state = typeof request.state === "string" ? request.state : JSON.stringify(request.state);
1539
1604
  const result = await runComplete(
1540
1605
  {
1541
- messages: [{ role: "user", content: decisionEmulationPrompt(state) }],
1606
+ messages: [{ role: "user", content: emulationContent(state, request.images) }],
1542
1607
  schema: uncalibratedSchemaOf(request.questions),
1543
1608
  tools: void 0,
1544
1609
  retries: config.retries ?? 2,
@@ -1637,6 +1702,7 @@ var createSignalFromConfig = (config) => {
1637
1702
  }),
1638
1703
  decide: async (request) => {
1639
1704
  assertQuestions(request.questions);
1705
+ assertImageIds(request.images);
1640
1706
  const started = Date.now();
1641
1707
  const adapter = await getAdapter();
1642
1708
  const model = resolveProvider(config).model;