@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 +15 -0
- package/dist/browser.js +145 -43
- package/dist/index.js +7 -2
- package/dist/types/packages/sdk/src/public.d.ts +2 -0
- package/dist/types/packages/sdk/src/routine-definition-types.d.ts +1 -0
- package/dist/types/shared/app-routine-contract.d.ts +10 -1
- package/dist/types/shared/routine-ai-contract.d.ts +20 -6
- package/dist/types/shared/routine-ai-output-detail.d.ts +11 -0
- package/package.json +1 -1
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
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
|
|
805
|
+
let v;
|
|
731
806
|
try {
|
|
732
|
-
if (
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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(),
|
|
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), (
|
|
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 ??
|
|
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,
|