@oddington/sdk 0.1.35 → 0.1.36

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
@@ -79,6 +79,21 @@ declared `review` handler crosses the Inbox publication seam as `review`; Inbox
79
79
  retains the canonical `invoice-import` routine identity separately, without
80
80
  deploying or executing that handler.
81
81
 
82
+ With `access: { ai: true }`, `os.ai.generateText` returns non-empty `{ text }`.
83
+ `os.ai.generateObject` returns `{ object }` validated against the requested
84
+ `schema`, with its inferred type. An invalid answer throws `RoutineAIError`
85
+ with code `invalid_output`, a clear bounded `detail`, and the original final
86
+ answer as optional `text` for routine-owned recovery. The text is bounded at
87
+ 256 KiB and excluded from the error message and ordinary JSON serialization.
88
+ The routine can inspect it explicitly, apply its own recovery rules, report
89
+ an item failure and continue; it must not cast a rejected answer to the promised
90
+ type. OS does not coerce, drop fields, repair or retry automatically.
91
+
92
+ An AI call failure can be caught and handled without automatically failing the
93
+ Run. Uncaught routine or step exceptions still fail execution, and outstanding
94
+ AI calls drain before completion. The host retains authority, isolation,
95
+ cancellation and byte limits. Local and remote Runs use the same API.
96
+
82
97
  The bounded grammar supports closed objects with required properties, arrays with
83
98
  an explicit `maxItems` from 1 to 64, booleans, strings (at most 4096 code points),
84
99
  finite numbers and safe integers with optional inclusive bounds, and Files.
package/dist/browser.js CHANGED
@@ -221,32 +221,45 @@ function jsonData(value, ancestors = /* @__PURE__ */ new Set()) {
221
221
  ancestors.delete(value);
222
222
  }
223
223
  }
224
- function validateRoutineSchemaValue(schema2, value) {
225
- switch (schema2.type) {
226
- case "object":
227
- if (!record(value) || (schema2.required ?? []).some((key) => !owns(value, key)) || Object.keys(value).some((key) => !owns(schema2.properties, key))) return invalidValue();
228
- for (let [key, child] of Object.entries(value)) validateRoutineSchemaValue(schema2.properties[key], child);
229
- return;
230
- case "array":
231
- if (!denseArray(value) || value.length > schema2.maxItems) return invalidValue();
232
- for (let child of value) validateRoutineSchemaValue(schema2.items, child);
233
- return;
234
- case "boolean":
235
- return typeof value != "boolean" ? invalidValue() : void 0;
236
- case "string":
237
- return typeof value != "string" || Array.from(value).length > (schema2.maxLength ?? 4096) ? invalidValue() : void 0;
238
- case "integer":
239
- case "number":
240
- return typeof value != "number" || !(schema2.type === "integer" ? Number.isSafeInteger(value) : Number.isFinite(value)) || schema2.minimum !== void 0 && value < schema2.minimum || schema2.maximum !== void 0 && value > schema2.maximum ? invalidValue() : void 0;
241
- case "file":
242
- try {
243
- jsonData(value);
244
- let file2 = parseFileDescriptor(value);
245
- if (schema2.path !== void 0 && file2.path !== schema2.path) return invalidValue();
246
- } catch {
247
- return invalidValue();
224
+ function validateRoutineSchemaValue(schema2, value, rejected) {
225
+ let type = (value2) => value2 === null ? "null" : Array.isArray(value2) ? "array" : ["object", "boolean", "string", "number"].includes(typeof value2) ? typeof value2 : "other";
226
+ function check(schema3, value2, path) {
227
+ let fail4 = (reason, extra = {}) => (rejected?.({ reason, path: path.length <= 256 ? path : "$", expected: schema3.type, received: type(value2), ...extra }), invalidValue());
228
+ switch (schema3.type) {
229
+ case "object":
230
+ if (!record(value2)) return fail4("wrong_type");
231
+ for (let key of schema3.required ?? []) if (!owns(value2, key)) return fail4("missing_field", {
232
+ path: `${path}.${key}`.length <= 256 ? `${path}.${key}` : "$",
233
+ expected: schema3.properties[key].type,
234
+ received: "missing"
235
+ });
236
+ if (Object.keys(value2).some((key) => !owns(schema3.properties, key))) return fail4("unexpected_field");
237
+ for (let [key, child] of Object.entries(value2)) check(schema3.properties[key], child, `${path}.${key}`);
238
+ return;
239
+ case "array":
240
+ if (!denseArray(value2)) return fail4("wrong_type");
241
+ if (value2.length > schema3.maxItems) return fail4("out_of_bounds", { constraint: "maxItems", limit: schema3.maxItems });
242
+ value2.forEach((child, index) => check(schema3.items, child, `${path}[${index}]`));
243
+ return;
244
+ case "boolean":
245
+ return typeof value2 != "boolean" ? fail4("wrong_type") : void 0;
246
+ case "string":
247
+ return typeof value2 != "string" ? fail4("wrong_type") : Array.from(value2).length > (schema3.maxLength ?? 4096) ? fail4("out_of_bounds", { constraint: "maxLength", limit: schema3.maxLength ?? 4096 }) : void 0;
248
+ case "integer":
249
+ case "number":
250
+ return typeof value2 != "number" || !(schema3.type === "integer" ? Number.isSafeInteger(value2) : Number.isFinite(value2)) ? fail4("wrong_type") : schema3.minimum !== void 0 && value2 < schema3.minimum ? fail4("out_of_bounds", { constraint: "minimum", limit: schema3.minimum }) : schema3.maximum !== void 0 && value2 > schema3.maximum ? fail4("out_of_bounds", { constraint: "maximum", limit: schema3.maximum }) : void 0;
251
+ case "file": {
252
+ let file2;
253
+ try {
254
+ jsonData(value2), file2 = parseFileDescriptor(value2);
255
+ } catch {
256
+ return fail4("invalid_file");
257
+ }
258
+ if (schema3.path !== void 0 && file2.path !== schema3.path) return fail4("invalid_file");
248
259
  }
260
+ }
249
261
  }
262
+ check(schema2, value, "$");
250
263
  }
251
264
 
252
265
  var InboxContractError = class extends Error {
@@ -695,20 +708,82 @@ function parseRoutineAccess(value) {
695
708
  }
696
709
  }
697
710
 
698
- var ROUTINE_AI_VALUE_BYTES = 256 * 1024, ROUTINE_AI_EVIDENCE_BYTES = 4 * 1024;
711
+ function parseRoutineAIOutputIssue(value) {
712
+ if (!value || typeof value != "object" || Array.isArray(value)) return;
713
+ let v = value;
714
+ if (["invalid_json", "invalid_response", "empty_text"].includes(String(v.reason)))
715
+ return Object.keys(v).join(",") === "reason" ? { reason: v.reason } : void 0;
716
+ if (!(!["missing_field", "unexpected_field", "wrong_type", "out_of_bounds", "invalid_file"].includes(String(v.reason)) || typeof v.path != "string" || v.path.length > 256 || !/^\$(?:\.[a-z][A-Za-z0-9]*|\[(?:0|[1-9][0-9]*)\])*$/.test(v.path) || !["object", "array", "boolean", "string", "integer", "number", "file"].includes(String(v.expected)) || !["missing", "null", "object", "array", "boolean", "string", "number", "other"].includes(String(v.received)) || Object.keys(v).some((key) => !["reason", "path", "expected", "received", "constraint", "limit"].includes(key)))) {
717
+ if (v.reason === "out_of_bounds") {
718
+ if (!["maxLength", "maxItems", "minimum", "maximum"].includes(String(v.constraint)) || typeof v.limit != "number" || !Number.isFinite(v.limit)) return;
719
+ } else if (Object.hasOwn(v, "constraint") || Object.hasOwn(v, "limit")) return;
720
+ return {
721
+ reason: v.reason,
722
+ path: v.path,
723
+ expected: v.expected,
724
+ received: v.received,
725
+ ...v.reason === "out_of_bounds" ? { constraint: v.constraint, limit: v.limit } : {}
726
+ };
727
+ }
728
+ }
729
+ function routineAIOutputMessage(issue) {
730
+ if (issue.reason === "invalid_json") return "AI replied, but its answer could not be read as JSON.";
731
+ if (issue.reason === "invalid_response") return "AI returned a response OS could not read.";
732
+ if (issue.reason === "empty_text") return "AI returned an empty answer.";
733
+ let field = issue.path === "$" ? "response" : issue.path.slice(issue.path.startsWith("$.") ? 2 : 1), expected = {
734
+ object: "an object",
735
+ array: "a list",
736
+ boolean: "true or false",
737
+ string: "text",
738
+ integer: "a whole number",
739
+ number: "a number",
740
+ file: "a File"
741
+ }[issue.expected], received = {
742
+ missing: "a missing field",
743
+ null: "null",
744
+ object: "an object",
745
+ array: "a list",
746
+ boolean: "true or false",
747
+ string: "text",
748
+ number: "a number",
749
+ other: "an unsupported value"
750
+ }[issue.received];
751
+ if (issue.reason === "missing_field") return `AI response is missing a required field.
752
+ ${field}: expected ${expected}; the field was missing.`;
753
+ if (issue.reason === "unexpected_field") return `AI response includes an unexpected field.
754
+ ${field}: only the fields declared by the routine are allowed.`;
755
+ if (issue.reason === "invalid_file") return `AI response contains an invalid File.
756
+ ${field}: expected a valid File matching the routine's requirements.`;
757
+ if (issue.reason === "out_of_bounds") {
758
+ let constraint = {
759
+ maxLength: `at most ${issue.limit} characters`,
760
+ maxItems: `at most ${issue.limit} items`,
761
+ minimum: `a value of at least ${issue.limit}`,
762
+ maximum: `a value of at most ${issue.limit}`
763
+ }[issue.constraint];
764
+ return `AI response is outside a permitted limit.
765
+ ${field}: expected ${constraint}.`;
766
+ }
767
+ return `AI response has the wrong type.
768
+ ${field}: expected ${expected}; received ${received}.`;
769
+ }
770
+
771
+ var ROUTINE_AI_VALUE_BYTES = 256 * 1024, ROUTINE_AI_RESULT_BYTES = 6 * ROUTINE_AI_VALUE_BYTES + 1024, ROUTINE_AI_EVIDENCE_BYTES = 4 * 1024;
699
772
  var RoutineAIError = class extends Error {
700
- constructor(code) {
701
- super(code);
773
+ constructor(code, options = {}) {
774
+ let detail = code === "invalid_output" ? parseRoutineAIOutputIssue(options.detail) : void 0;
775
+ super(detail ? routineAIOutputMessage(detail) : code);
702
776
  this.code = code;
703
- this.name = "RoutineAIError";
777
+ this.name = "RoutineAIError", detail && (this.detail = Object.freeze(detail)), code === "invalid_output" && typeof options.text == "string" && new TextEncoder().encode(options.text).byteLength <= ROUTINE_AI_VALUE_BYTES && Object.defineProperty(this, "text", { value: options.text });
704
778
  }
705
779
  code;
780
+ detail;
706
781
  };
707
- function capture(value, invalid9, oversized) {
782
+ function capture(value, invalid9, oversized, limit = ROUTINE_AI_VALUE_BYTES) {
708
783
  try {
709
784
  assertRoutineJson(value);
710
785
  let encoded = JSON.stringify(value);
711
- if (new TextEncoder().encode(encoded).byteLength > ROUTINE_AI_VALUE_BYTES) throw new RoutineAIError(oversized);
786
+ if (new TextEncoder().encode(encoded).byteLength > limit) throw new RoutineAIError(oversized);
712
787
  return freezeRoutineMetadata(JSON.parse(encoded));
713
788
  } catch (error) {
714
789
  throw error instanceof RoutineAIError ? error : new RoutineAIError(invalid9);
@@ -727,18 +802,46 @@ function parseRoutineAIRequest(value) {
727
802
  }
728
803
  }
729
804
  function parseRoutineAIResult(request2, value) {
730
- let v = capture(value, "invalid_output", "output_too_large");
805
+ let v;
731
806
  try {
732
- if (request2.kind === "generateText") {
733
- if (assertRoutineFields(v, ["text"]), typeof v.text != "string" || !v.text.trim()) throw new Error();
734
- return freezeRoutineMetadata({ text: v.text });
735
- } else {
736
- if (assertRoutineFields(v, ["object"]), !Object.hasOwn(v, "object")) throw new Error();
737
- return validateRoutineSchemaValue(request2.schema, v.object), freezeRoutineMetadata({ object: v.object });
807
+ if (v = capture(
808
+ value,
809
+ "invalid_output",
810
+ "output_too_large",
811
+ request2.kind === "generateText" ? ROUTINE_AI_RESULT_BYTES : ROUTINE_AI_VALUE_BYTES
812
+ ), assertRoutineFields(v, request2.kind === "generateText" ? ["text"] : ["object"]), request2.kind === "generateText") {
813
+ if (typeof v.text != "string") throw new Error();
814
+ return acceptText(request2, v.text);
738
815
  }
816
+ if (!Object.hasOwn(v, "object")) throw new Error();
817
+ } catch (error) {
818
+ throw error instanceof RoutineAIError ? error : new RoutineAIError("invalid_output", { detail: { reason: "invalid_response" } });
819
+ }
820
+ return validateObject(request2.schema, v.object), freezeRoutineMetadata({ object: v.object });
821
+ }
822
+ function validateObject(schema2, object5, text5) {
823
+ let detail = { reason: "invalid_response" };
824
+ try {
825
+ validateRoutineSchemaValue(schema2, object5, (issue) => {
826
+ detail = issue;
827
+ });
828
+ } catch {
829
+ throw new RoutineAIError("invalid_output", { ...text5 === void 0 ? {} : { text: text5 }, detail });
830
+ }
831
+ }
832
+ function acceptText(request2, text5) {
833
+ if (new TextEncoder().encode(text5).byteLength > ROUTINE_AI_VALUE_BYTES) throw new RoutineAIError("output_too_large");
834
+ if (request2.kind === "generateText") {
835
+ if (!text5.trim()) throw new RoutineAIError("invalid_output", { text: text5, detail: { reason: "empty_text" } });
836
+ return freezeRoutineMetadata({ text: text5 });
837
+ }
838
+ let object5;
839
+ try {
840
+ object5 = JSON.parse(text5);
739
841
  } catch {
740
- throw new RoutineAIError("invalid_output");
842
+ throw new RoutineAIError("invalid_output", { text: text5, detail: { reason: "invalid_json" } });
741
843
  }
844
+ return validateObject(request2.schema, object5, text5), capture({ object: object5 }, "invalid_output", "output_too_large");
742
845
  }
743
846
 
744
847
  var LIMIT = 256 * 1024, invalid3 = () => {
@@ -865,7 +968,7 @@ function captureRoutineInput(definition, input) {
865
968
  return capture2(contract.input, input === void 0 && Object.hasOwn(contract, "inputDefault") ? contract.inputDefault : input);
866
969
  }
867
970
  async function executeRoutineDefinition(definition, input, runtime) {
868
- let captured = captureRoutineInput(definition, input), { contract, steps, run } = definitions.get(definition), active2 = !0, accepting = !0, broken = !1, work = Promise.resolve(), aiFailure, aiWork = /* @__PURE__ */ new Set(), generate = (kind, value) => {
971
+ let captured = captureRoutineInput(definition, input), { contract, steps, run } = definitions.get(definition), active2 = !0, accepting = !0, broken = !1, work = Promise.resolve(), aiWork = /* @__PURE__ */ new Set(), generate = (kind, value) => {
869
972
  let operation2 = (async () => {
870
973
  if (!active2 || broken || contract.access.ai !== !0) throw new RoutineAIError("access_denied");
871
974
  if (!runtime.ai) throw new RoutineAIError("capability_unavailable");
@@ -884,9 +987,7 @@ async function executeRoutineDefinition(definition, input, runtime) {
884
987
  if (!active2 || broken) throw new RoutineAIError("access_denied");
885
988
  return parseRoutineAIResult(request2, result3);
886
989
  })();
887
- return aiWork.add(operation2), operation2.then(() => aiWork.delete(operation2), (error) => {
888
- aiFailure ??= error, broken = !0, aiWork.delete(operation2);
889
- }), operation2;
990
+ return aiWork.add(operation2), operation2.then(() => aiWork.delete(operation2), () => aiWork.delete(operation2)), operation2;
890
991
  }, called = /* @__PURE__ */ new Set(), reached = /* @__PURE__ */ new Set(), guarded = (value, receiver) => typeof value == "function" ? (...args) => {
891
992
  if (!active2 || broken) throw new Error("Run authority ended");
892
993
  return guarded(Reflect.apply(value, receiver, args));
@@ -950,7 +1051,7 @@ async function executeRoutineDefinition(definition, input, runtime) {
950
1051
  for (accepting = !1, await work; aiWork.size; ) await Promise.allSettled([...aiWork]);
951
1052
  active2 = !1;
952
1053
  for (let step of contract.steps) reached.has(step.id) || await runtime.step(step.id, { status: "skipped" });
953
- if (broken) throw failure ?? aiFailure ?? new AppRoutineContractError("invalid_value");
1054
+ if (broken) throw failure ?? new AppRoutineContractError("invalid_value");
954
1055
  if (contract.result !== void 0) return capture2(contract.result, result2);
955
1056
  if (result2 !== void 0) return invalid3();
956
1057
  }
@@ -4016,6 +4117,7 @@ export {
4016
4117
  FileOperationError,
4017
4118
  InboxContractError,
4018
4119
  InboxOperationError,
4120
+ RoutineAIError,
4019
4121
  app,
4020
4122
  asksBlock,
4021
4123
  auth,