@tanstack-ai-browser/afm 0.0.0-stage → 0.2.2

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/Package.swift +33 -0
  3. package/README.md +75 -2
  4. package/dist/adapter.d.ts +107 -0
  5. package/dist/adapter.d.ts.map +1 -0
  6. package/dist/adapter.js +522 -0
  7. package/dist/electron.d.ts +29 -0
  8. package/dist/electron.d.ts.map +1 -0
  9. package/dist/electron.js +136 -0
  10. package/dist/index.d.ts +17 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +12 -0
  13. package/dist/json-value.d.ts +9 -0
  14. package/dist/json-value.d.ts.map +1 -0
  15. package/dist/json-value.js +19 -0
  16. package/dist/messages.d.ts +12 -0
  17. package/dist/messages.d.ts.map +1 -0
  18. package/dist/messages.js +29 -0
  19. package/dist/model-meta.d.ts +23 -0
  20. package/dist/model-meta.d.ts.map +1 -0
  21. package/dist/model-meta.js +20 -0
  22. package/dist/native.d.ts +175 -0
  23. package/dist/native.d.ts.map +1 -0
  24. package/dist/native.js +21 -0
  25. package/dist/schema.d.ts +17 -0
  26. package/dist/schema.d.ts.map +1 -0
  27. package/dist/schema.js +133 -0
  28. package/dist/validate.d.ts +2 -0
  29. package/dist/validate.d.ts.map +1 -0
  30. package/dist/validate.js +124 -0
  31. package/electron/AfmNode.swift +81 -0
  32. package/electron/afm.c +94 -0
  33. package/electron/afm.node +0 -0
  34. package/electron/afm.node.sha256 +1 -0
  35. package/electron/build.mjs +54 -0
  36. package/electron/preload.cjs +35 -0
  37. package/electron/sources-hash.mjs +40 -0
  38. package/ios/Sources/AfmCore/Bridge.swift +354 -0
  39. package/ios/Sources/AfmCore/Conversation.swift +49 -0
  40. package/ios/Sources/AfmCore/Schema.swift +90 -0
  41. package/ios/Sources/AfmPlugin/AfmPlugin.swift +89 -0
  42. package/package.json +97 -3
  43. package/src/adapter.test.ts +428 -0
  44. package/src/adapter.ts +616 -0
  45. package/src/electron.ts +193 -0
  46. package/src/index.ts +28 -0
  47. package/src/json-value.test.ts +26 -0
  48. package/src/json-value.ts +17 -0
  49. package/src/messages.ts +37 -0
  50. package/src/model-meta.ts +37 -0
  51. package/src/native-build.test.ts +13 -0
  52. package/src/native.ts +128 -0
  53. package/src/schema.test.ts +146 -0
  54. package/src/schema.ts +153 -0
  55. package/src/validate.test.ts +38 -0
  56. package/src/validate.ts +112 -0
package/src/schema.ts ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * JSON Schema (what `chat()` gives adapters for tools and structured output)
3
+ * read into the framework's own schema (`NativeSchema`), which the model's
4
+ * output is held to: a tool's arguments always fit its parameters.
5
+ *
6
+ * Properties keep their order, which the model writes them in. Each object is
7
+ * named by its path, so two objects never share a name. A property that may be
8
+ * null is optional. `$ref`s to `$defs` (or `definitions`) become references to
9
+ * named definitions; one to a plain value is written in place.
10
+ */
11
+ import type { NativeGenerationSchema, NativeSchema } from "./native.js"
12
+
13
+ type Json = Record<string, unknown>
14
+
15
+ const isObject = (value: unknown): value is Json => typeof value === "object" && value !== null && !Array.isArray(value)
16
+
17
+ /** Drops undefined fields: the native bridge takes JSON. */
18
+ function defined<T extends object>(value: T): T {
19
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T
20
+ }
21
+
22
+ const isNull = (schema: unknown) =>
23
+ isObject(schema) && (schema.type === "null" || (Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null"))
24
+
25
+ /** Whether a schema allows null: `type: [..., "null"]`, or a null choice in `anyOf`. */
26
+ function nullable(schema: unknown): boolean {
27
+ if (!isObject(schema)) return false
28
+ if (Array.isArray(schema.type) && schema.type.includes("null")) return true
29
+ const variants = schema.anyOf ?? schema.oneOf
30
+ return Array.isArray(variants) && variants.some(isNull)
31
+ }
32
+
33
+ const number = (value: unknown) => (typeof value === "number" && Number.isFinite(value) ? value : undefined)
34
+
35
+ /** A schema's name for the framework: letters, digits and underscores. */
36
+ const typeName = (name: string) => name.replace(/[^A-Za-z0-9_]/g, "_") || "Value"
37
+
38
+ /**
39
+ * The framework's schema for a JSON Schema, named `name` (a tool's name, or
40
+ * the structured answer's).
41
+ */
42
+ export function toNativeSchema(schema: unknown, name: string): NativeGenerationSchema {
43
+ const json = isObject(schema) ? schema : {}
44
+ const defs: Json = { ...(isObject(json.definitions) ? json.definitions : {}), ...(isObject(json.$defs) ? json.$defs : {}) }
45
+ const definitions = new Map<string, NativeSchema>()
46
+
47
+ const convert = (value: unknown, path: string): NativeSchema => {
48
+ if (!isObject(value)) return { kind: "string" }
49
+ if (typeof value.$ref === "string") {
50
+ const target = value.$ref.split("/").at(-1) ?? ""
51
+ const def = defs[target]
52
+ if (!isObject(def)) return { kind: "string" }
53
+ const converted = definitions.get(target) ?? convertDefinition(def, target)
54
+ // Only named schemas (objects, choices) can be referred to.
55
+ return converted.kind === "object" || converted.kind === "anyOf" ? { kind: "reference", name: typeName(target) } : converted
56
+ }
57
+ if ("const" in value && typeof value.const === "string") return { kind: "string", constant: value.const }
58
+ if (Array.isArray(value.enum)) {
59
+ const members: Array<unknown> = value.enum.filter((v) => v !== null)
60
+ if (members.every((v) => typeof v === "string")) {
61
+ return members.length > 0 ? { kind: "string", choices: members } : { kind: "string" }
62
+ }
63
+ // true and false both: any boolean. One alone, the framework can't hold it to.
64
+ if (members.every((v) => typeof v === "boolean")) {
65
+ if (new Set(members).size === 2) return { kind: "boolean" }
66
+ throw new Error(`${path}: an enum of only ${String(members[0])} isn't supported (Apple's framework can't hold a boolean to one value)`)
67
+ }
68
+ // Integers (and strings among them): a choice of exact values, each integer
69
+ // a range of itself, which the framework holds the model to. A decimal
70
+ // can't be: a range of one decimal fails in Apple's generator.
71
+ if (members.some((v) => typeof v === "number" && !Number.isInteger(v))) {
72
+ throw new Error(`${path}: an enum of decimal numbers isn't supported (Apple's framework can't hold a decimal to exact values)`)
73
+ }
74
+ if (members.every((v) => typeof v === "string" || Number.isInteger(v))) {
75
+ const choices: Array<NativeSchema> = members.map((v) =>
76
+ typeof v === "string" ? { kind: "string", constant: v } : { kind: "integer", minimum: v as number, maximum: v as number }
77
+ )
78
+ if (choices.length === 1) return choices[0]
79
+ return defined({ kind: "anyOf" as const, name: typeName(path), description: typeof value.description === "string" ? value.description : undefined, choices })
80
+ }
81
+ throw new Error(`${path}: an enum mixing ${[...new Set(members.map((v) => typeof v))].join(" and ")} values isn't supported by Apple's framework`)
82
+ }
83
+ const variants = value.anyOf ?? value.oneOf
84
+ if (Array.isArray(variants)) {
85
+ const choices = variants.filter((v) => !isNull(v))
86
+ if (choices.length === 1) return convert({ ...choices[0], description: value.description ?? choices[0].description }, path)
87
+ if (choices.length > 1) {
88
+ return defined({
89
+ kind: "anyOf" as const,
90
+ name: typeName(path),
91
+ description: typeof value.description === "string" ? value.description : undefined,
92
+ choices: choices.map((choice, i) => convert(choice, `${path}_${i + 1}`))
93
+ })
94
+ }
95
+ }
96
+ const types = Array.isArray(value.type) ? value.type.filter((t) => t !== "null") : [value.type]
97
+ const type = typeof types[0] === "string" ? types[0] : isObject(value.properties) ? "object" : value.items ? "array" : "string"
98
+ switch (type) {
99
+ case "object": {
100
+ const required = Array.isArray(value.required) ? value.required : []
101
+ const properties = Object.entries(isObject(value.properties) ? value.properties : {}).map(([key, property]) =>
102
+ defined({
103
+ name: key,
104
+ description: isObject(property) && typeof property.description === "string" ? property.description : undefined,
105
+ optional: !required.includes(key) || nullable(property),
106
+ schema: convert(property, `${path}_${key}`)
107
+ })
108
+ )
109
+ return defined({
110
+ kind: "object" as const,
111
+ name: typeName(path),
112
+ description: typeof value.description === "string" ? value.description : undefined,
113
+ properties
114
+ })
115
+ }
116
+ case "array":
117
+ return defined({
118
+ kind: "array" as const,
119
+ items: convert(Array.isArray(value.items) ? value.items[0] : value.items, `${path}_item`),
120
+ minimumElements: number(value.minItems),
121
+ maximumElements: number(value.maxItems)
122
+ })
123
+ case "integer":
124
+ case "number":
125
+ return defined({
126
+ kind: type,
127
+ minimum: number(value.minimum) ?? number(value.exclusiveMinimum),
128
+ maximum: number(value.maximum) ?? number(value.exclusiveMaximum)
129
+ })
130
+ case "boolean":
131
+ return { kind: "boolean" }
132
+ case "null":
133
+ return { kind: "null" }
134
+ default:
135
+ return { kind: "string" }
136
+ }
137
+ }
138
+
139
+ const convertDefinition = (def: Json, target: string): NativeSchema => {
140
+ // Set first, so a definition that refers to itself finds its name.
141
+ definitions.set(target, { kind: "object", name: typeName(target), properties: [] })
142
+ const converted = convert(def, target)
143
+ const named = converted.kind === "object" || converted.kind === "anyOf" ? { ...converted, name: typeName(target) } : converted
144
+ definitions.set(target, named)
145
+ return named
146
+ }
147
+
148
+ const root = convert(json, name)
149
+ return {
150
+ root,
151
+ definitions: [...definitions.values()].filter((d) => d.kind === "object" || d.kind === "anyOf")
152
+ }
153
+ }
@@ -0,0 +1,38 @@
1
+ import assert from "node:assert/strict"
2
+ import { test } from "node:test"
3
+
4
+ import { schemaErrors } from "./validate.js"
5
+
6
+ const city = { type: "object", properties: { city: { type: "string" } }, required: ["city"] }
7
+
8
+ test("a value that fits its schema has no errors", () => {
9
+ assert.deepEqual(schemaErrors(city, { city: "Paris" }), [])
10
+ assert.deepEqual(schemaErrors({ type: "array", items: { type: "integer" }, minItems: 1 }, [1, 2]), [])
11
+ assert.deepEqual(schemaErrors({ type: ["string", "null"] }, null), [])
12
+ })
13
+
14
+ test("missing required fields, wrong types and values outside an enum are errors, with their paths", () => {
15
+ assert.deepEqual(schemaErrors(city, {}), ["/city: missing"])
16
+ assert.deepEqual(schemaErrors(city, { city: 123 }), ["/city: should be string, not number"])
17
+ assert.deepEqual(schemaErrors(city, "Paris"), ["/: should be object, not string"])
18
+ const action = { type: "object", properties: { operation: { type: "string", enum: ["add", "remove"] } }, required: ["operation"] }
19
+ assert.deepEqual(schemaErrors(action, { operation: "burn" }), ['/operation: "burn" is not one of "add", "remove"'])
20
+ assert.deepEqual(schemaErrors({ type: "integer" }, 1.5), ["/: should be integer, not number"])
21
+ })
22
+
23
+ test("nested arrays, $refs, choices, extra properties and bounds are checked", () => {
24
+ const schema = {
25
+ type: "object",
26
+ properties: { actions: { type: "array", items: { $ref: "#/$defs/Action" }, maxItems: 2 } },
27
+ required: ["actions"],
28
+ $defs: { Action: { type: "object", properties: { n: { type: "integer", minimum: 1, maximum: 4 } }, required: ["n"], additionalProperties: false } }
29
+ }
30
+ assert.deepEqual(schemaErrors(schema, { actions: [{ n: 2 }] }), [])
31
+ assert.deepEqual(schemaErrors(schema, { actions: [{ n: 0 }, { n: 2, x: 1 }, { n: 3 }] }), [
32
+ "/actions: more than 2 items",
33
+ "/actions/0/n: less than 1",
34
+ "/actions/1/x: not a property of the schema"
35
+ ])
36
+ assert.deepEqual(schemaErrors({ anyOf: [{ type: "string" }, { type: "integer" }] }, true), ["/: fits 0 of its anyOf choices"])
37
+ assert.deepEqual(schemaErrors({ const: "a" }, "b"), ['/: should be "a", not "b"'])
38
+ })
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Where a value doesn't fit a JSON Schema: each mismatch with its path
3
+ * ("/actions/0/operation: not one of add, remove"). Empty when it fits. It reads
4
+ * what afm converts for Apple's framework and the framework holds answers to:
5
+ * types (with null), objects (properties, required, additionalProperties),
6
+ * arrays (items, minItems, maxItems), enum, const, anyOf, oneOf, allOf, $ref to
7
+ * $defs or definitions, string lengths and patterns, and numeric bounds. Other
8
+ * keywords are left unchecked.
9
+ */
10
+ type Json = Record<string, unknown>
11
+
12
+ const isObject = (value: unknown): value is Json => typeof value === "object" && value !== null && !Array.isArray(value)
13
+
14
+ const typeOf = (value: unknown) => (value === null ? "null" : Array.isArray(value) ? "array" : typeof value)
15
+
16
+ function equal(a: unknown, b: unknown): boolean {
17
+ if (a === b) return true
18
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => equal(v, b[i]))
19
+ if (isObject(a) && isObject(b)) {
20
+ const keys = Object.keys(a)
21
+ return keys.length === Object.keys(b).length && keys.every((k) => Object.hasOwn(b, k) && equal(a[k], b[k]))
22
+ }
23
+ return false
24
+ }
25
+
26
+ function fitsType(type: string, value: unknown) {
27
+ if (type === "integer") return typeof value === "number" && Number.isInteger(value)
28
+ if (type === "number") return typeof value === "number" && Number.isFinite(value)
29
+ return typeOf(value) === type
30
+ }
31
+
32
+ const show = (value: unknown) => JSON.stringify(value)
33
+
34
+ export function schemaErrors(schema: unknown, value: unknown): Array<string> {
35
+ const root = isObject(schema) ? schema : {}
36
+ const errors: Array<string> = []
37
+
38
+ const resolve = (ref: string): unknown => {
39
+ const match = /^#\/(\$defs|definitions)\/(.+)$/.exec(ref)
40
+ const defs = match ? root[match[1]] : undefined
41
+ return match && isObject(defs) ? defs[decodeURIComponent(match[2])] : undefined
42
+ }
43
+
44
+ const check = (s: unknown, v: unknown, path: string, out: Array<string>) => {
45
+ if (s === true || s === undefined) return
46
+ if (s === false) return void out.push(`${path || "/"}: not allowed`)
47
+ if (!isObject(s)) return
48
+ if (typeof s.$ref === "string") {
49
+ const target = resolve(s.$ref)
50
+ if (target === undefined) return void out.push(`${path || "/"}: unknown $ref ${s.$ref}`)
51
+ check(target, v, path, out)
52
+ }
53
+ const at = path || "/"
54
+ if ("const" in s && !equal(s.const, v)) out.push(`${at}: should be ${show(s.const)}, not ${show(v)}`)
55
+ if (Array.isArray(s.enum) && !s.enum.some((e) => equal(e, v))) out.push(`${at}: ${show(v)} is not one of ${s.enum.map(show).join(", ")}`)
56
+ if (s.type !== undefined) {
57
+ const types = (Array.isArray(s.type) ? s.type : [s.type]).filter((t): t is string => typeof t === "string")
58
+ if (types.length > 0 && !types.some((t) => fitsType(t, v))) {
59
+ out.push(`${at}: should be ${types.join(" or ")}, not ${typeOf(v)}`)
60
+ return
61
+ }
62
+ }
63
+ if (Array.isArray(s.allOf)) for (const part of s.allOf) check(part, v, path, out)
64
+ for (const key of ["anyOf", "oneOf"] as const) {
65
+ const choices = s[key]
66
+ if (!Array.isArray(choices)) continue
67
+ const fitting = choices.filter((choice) => {
68
+ const sub: Array<string> = []
69
+ check(choice, v, path, sub)
70
+ return sub.length === 0
71
+ }).length
72
+ if (key === "anyOf" ? fitting === 0 : fitting !== 1) out.push(`${at}: fits ${fitting} of its ${key} choices`)
73
+ }
74
+ if (isObject(v)) {
75
+ const properties = isObject(s.properties) ? s.properties : {}
76
+ if (Array.isArray(s.required)) {
77
+ for (const key of s.required) if (typeof key === "string" && !Object.hasOwn(v, key)) out.push(`${path}/${key}: missing`)
78
+ }
79
+ for (const [key, item] of Object.entries(v)) {
80
+ if (Object.hasOwn(properties, key)) check(properties[key], item, `${path}/${key}`, out)
81
+ else if (s.additionalProperties === false) out.push(`${path}/${key}: not a property of the schema`)
82
+ else if (isObject(s.additionalProperties)) check(s.additionalProperties, item, `${path}/${key}`, out)
83
+ }
84
+ }
85
+ if (Array.isArray(v)) {
86
+ if (typeof s.minItems === "number" && v.length < s.minItems) out.push(`${at}: fewer than ${s.minItems} items`)
87
+ if (typeof s.maxItems === "number" && v.length > s.maxItems) out.push(`${at}: more than ${s.maxItems} items`)
88
+ if (Array.isArray(s.items)) s.items.forEach((item, i) => i < v.length && check(item, v[i], `${path}/${i}`, out))
89
+ else if (s.items !== undefined) v.forEach((item, i) => check(s.items, item, `${path}/${i}`, out))
90
+ }
91
+ if (typeof v === "string") {
92
+ if (typeof s.minLength === "number" && [...v].length < s.minLength) out.push(`${at}: shorter than ${s.minLength}`)
93
+ if (typeof s.maxLength === "number" && [...v].length > s.maxLength) out.push(`${at}: longer than ${s.maxLength}`)
94
+ if (typeof s.pattern === "string") {
95
+ try {
96
+ if (!new RegExp(s.pattern, "u").test(v)) out.push(`${at}: doesn't match ${s.pattern}`)
97
+ } catch {
98
+ // A pattern JavaScript can't read isn't checked.
99
+ }
100
+ }
101
+ }
102
+ if (typeof v === "number") {
103
+ if (typeof s.minimum === "number" && v < s.minimum) out.push(`${at}: less than ${s.minimum}`)
104
+ if (typeof s.maximum === "number" && v > s.maximum) out.push(`${at}: more than ${s.maximum}`)
105
+ if (typeof s.exclusiveMinimum === "number" && v <= s.exclusiveMinimum) out.push(`${at}: not more than ${s.exclusiveMinimum}`)
106
+ if (typeof s.exclusiveMaximum === "number" && v >= s.exclusiveMaximum) out.push(`${at}: not less than ${s.exclusiveMaximum}`)
107
+ }
108
+ }
109
+
110
+ check(root, value, "", errors)
111
+ return errors
112
+ }