@niscorp/signal 0.1.5 → 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,29 @@
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
+
15
+ ## 0.1.6
16
+
17
+ ### Patch Changes
18
+
19
+ - 6776f2d: A reply missing only its closing brace comes back whole, not as a piece of itself.
20
+
21
+ The repair ladder tried `extract` before `close-truncated`. On `{"reasoning":"…","data":{"rows":[…]}` — complete but for the last `}` — `extract` found the `rows` array inside it, and any acceptance schema that admits arrays (Cortex's does) took that array as the answer. Cortex then rejected it as not an envelope, and the model was asked again, up to its output-retry limit. The ladder now closes a cut-off reply before it looks for JSON inside it, so the whole object arrives at `wire.rung: 'close-truncated'`.
22
+
23
+ Only a reply that opens with `{` or `[` and is left unclosed takes a different path. A complete reply, a fenced or prose-wrapped one, and tool-call arguments go through the ladder as before.
24
+
25
+ **What to change:** nothing.
26
+
3
27
  ## 0.1.5
4
28
 
5
29
  ### 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") {
@@ -1176,6 +1206,11 @@ function* ladder(text, strategies) {
1176
1206
  if (trimmed.length === 0) return;
1177
1207
  const direct = tryParse2(trimmed);
1178
1208
  if (direct.ok) yield { value: direct.value, rung: "parse" };
1209
+ const closed = closeTruncated(trimmed);
1210
+ if (closed !== void 0) {
1211
+ const parsed = tryParse2(closed);
1212
+ if (parsed.ok) yield { value: parsed.value, rung: "close-truncated" };
1213
+ }
1179
1214
  const extracted = extractJson(trimmed);
1180
1215
  if (extracted.ok) yield { value: extracted.value, rung: "extract" };
1181
1216
  for (const repaired of repairEscapeDamage(trimmed)) {
@@ -1188,11 +1223,6 @@ function* ladder(text, strategies) {
1188
1223
  if (parsed.ok) yield { value: parsed.value, rung: strategy.id };
1189
1224
  }
1190
1225
  }
1191
- const closed = closeTruncated(trimmed);
1192
- if (closed !== void 0) {
1193
- const parsed = tryParse2(closed);
1194
- if (parsed.ok) yield { value: parsed.value, rung: "close-truncated" };
1195
- }
1196
1226
  }
1197
1227
  var firstAccepted = (candidates, accept) => {
1198
1228
  let nearMiss;
@@ -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;