broapp 0.1.0 → 0.2.0
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 +13 -4
- package/package.json +10 -2
- package/src/ai/host/adapter.ts +106 -0
- package/src/ai/host/create-ai.ts +200 -0
- package/src/ai/host/fake.ts +223 -0
- package/src/ai/host/from-contract.ts +85 -0
- package/src/ai/host/index.ts +32 -0
- package/src/ai/host/registry.ts +221 -0
- package/src/ai/host/run-types.ts +14 -0
- package/src/ai/host/run.ts +320 -0
- package/src/ai/host/secrets.ts +118 -0
- package/src/ai/host/settings.ts +83 -0
- package/src/ai/host/tool.ts +89 -0
- package/src/ai/react/AiChat.tsx +194 -0
- package/src/ai/react/AiSettings.tsx +222 -0
- package/src/ai/react/ai.css +152 -0
- package/src/ai/react/index.tsx +35 -0
- package/src/ai/react/provider.tsx +97 -0
- package/src/ai/react/use-ai-chat.ts +309 -0
- package/src/ai/react/use-ai-models.ts +70 -0
- package/src/ai/react/use-ai-settings.ts +105 -0
- package/src/ai/shared/contract.ts +138 -0
- package/src/ai/shared/index.ts +16 -0
- package/src/ai/shared/types.check.ts +43 -0
- package/src/ai/shared/types.ts +87 -0
- package/src/host/app.ts +94 -30
- package/src/host/index.ts +5 -0
- package/src/react/hooks.tsx +37 -3
- package/src/react/index.ts +1 -0
- package/src/shared/contract.ts +56 -2
- package/src/shared/errors.ts +10 -0
- package/src/shared/index.ts +8 -2
- package/src/shared/schema.ts +125 -28
package/src/shared/schema.ts
CHANGED
|
@@ -49,6 +49,15 @@ export type Result<T> =
|
|
|
49
49
|
| { readonly ok: true; readonly value: T }
|
|
50
50
|
| { readonly ok: false; readonly issues: readonly Issue[] };
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* A JSON Schema document, as a plain object.
|
|
54
|
+
*
|
|
55
|
+
* Broapp emits a draft 2020-12 subset — enough for a language model provider
|
|
56
|
+
* to describe a tool's arguments, and nothing more. It is deliberately not a
|
|
57
|
+
* typed tree: every consumer so far hands it straight to a provider as JSON.
|
|
58
|
+
*/
|
|
59
|
+
export type JsonSchema = Record<string, unknown>;
|
|
60
|
+
|
|
52
61
|
/** A runtime schema for one JSON value. */
|
|
53
62
|
export interface Schema<T> {
|
|
54
63
|
/** Discriminates a Broapp schema from a foreign one at runtime. */
|
|
@@ -57,6 +66,8 @@ export interface Schema<T> {
|
|
|
57
66
|
check(value: unknown, path?: IssuePath): Result<T>;
|
|
58
67
|
/** Validate, or throw {@link ValidationError}. */
|
|
59
68
|
parse(value: unknown): T;
|
|
69
|
+
/** A JSON Schema (draft 2020-12 subset) describing what `parse` accepts. */
|
|
70
|
+
toJsonSchema(): JsonSchema;
|
|
60
71
|
/** Phantom marker; never present at runtime. */
|
|
61
72
|
readonly _type?: T;
|
|
62
73
|
}
|
|
@@ -64,7 +75,28 @@ export interface Schema<T> {
|
|
|
64
75
|
/** The TypeScript type a schema accepts. */
|
|
65
76
|
export type Infer<S> = S extends Schema<infer T> ? T : never;
|
|
66
77
|
|
|
67
|
-
|
|
78
|
+
/** Collapse an intersection back into one object type, keeping `?` modifiers. */
|
|
79
|
+
type Flatten<T> = { [K in keyof T]: T[K] };
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What an object schema accepts.
|
|
83
|
+
*
|
|
84
|
+
* A field wrapped in `s.optional` may be left out entirely, so its key is
|
|
85
|
+
* optional here rather than merely admitting `undefined`. Without this an
|
|
86
|
+
* `s.optional` field would still have to be spelled out at every construction
|
|
87
|
+
* site, which is the opposite of what the wrapper says.
|
|
88
|
+
*/
|
|
89
|
+
export type InferObject<F> = Flatten<
|
|
90
|
+
{ [K in keyof F as undefined extends Infer<F[K]> ? never : K]: Infer<F[K]> } & {
|
|
91
|
+
[K in keyof F as undefined extends Infer<F[K]> ? K : never]?: Infer<F[K]>;
|
|
92
|
+
}
|
|
93
|
+
>;
|
|
94
|
+
|
|
95
|
+
function schema<T>(
|
|
96
|
+
kind: string,
|
|
97
|
+
check: (value: unknown, path: IssuePath) => Result<T>,
|
|
98
|
+
toJsonSchema: () => JsonSchema,
|
|
99
|
+
): Schema<T> {
|
|
68
100
|
const self: Schema<T> = {
|
|
69
101
|
kind,
|
|
70
102
|
check: (value, path = []) => check(value, path),
|
|
@@ -73,10 +105,25 @@ function schema<T>(kind: string, check: (value: unknown, path: IssuePath) => Res
|
|
|
73
105
|
if (outcome.ok) return outcome.value;
|
|
74
106
|
throw new ValidationError(outcome.issues);
|
|
75
107
|
},
|
|
108
|
+
toJsonSchema,
|
|
76
109
|
};
|
|
77
110
|
return self;
|
|
78
111
|
}
|
|
79
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Assemble a JSON Schema object, dropping every keyword whose option was not
|
|
115
|
+
* given. An explicit `minLength: undefined` disappears from `JSON.stringify`
|
|
116
|
+
* but is still a key at runtime, and a provider that enumerates keywords would
|
|
117
|
+
* see it — so the key is never created in the first place.
|
|
118
|
+
*/
|
|
119
|
+
function keywords(entries: Record<string, unknown>): JsonSchema {
|
|
120
|
+
const out: JsonSchema = {};
|
|
121
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
122
|
+
if (value !== undefined) out[key] = value;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
80
127
|
function fail<T = never>(path: IssuePath, message: string): Result<T> {
|
|
81
128
|
return { ok: false, issues: [{ path, message }] };
|
|
82
129
|
}
|
|
@@ -118,7 +165,15 @@ export const s = {
|
|
|
118
165
|
if (!anchored.test(value)) return fail(path, 'does not match the required format');
|
|
119
166
|
}
|
|
120
167
|
return { ok: true, value };
|
|
121
|
-
}
|
|
168
|
+
},
|
|
169
|
+
() =>
|
|
170
|
+
keywords({
|
|
171
|
+
type: 'string',
|
|
172
|
+
minLength: options.min,
|
|
173
|
+
maxLength: options.max,
|
|
174
|
+
pattern: options.pattern?.source,
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
122
177
|
},
|
|
123
178
|
|
|
124
179
|
number(options: NumberOptions = {}): Schema<number> {
|
|
@@ -134,30 +189,46 @@ export const s = {
|
|
|
134
189
|
return fail(path, `expected <= ${String(options.max)}`);
|
|
135
190
|
}
|
|
136
191
|
return { ok: true, value };
|
|
137
|
-
}
|
|
192
|
+
},
|
|
193
|
+
() =>
|
|
194
|
+
keywords({
|
|
195
|
+
type: options.int === true ? 'integer' : 'number',
|
|
196
|
+
minimum: options.min,
|
|
197
|
+
maximum: options.max,
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
138
200
|
},
|
|
139
201
|
|
|
140
202
|
boolean(): Schema<boolean> {
|
|
141
|
-
return schema(
|
|
142
|
-
|
|
203
|
+
return schema(
|
|
204
|
+
'boolean',
|
|
205
|
+
(value, path) =>
|
|
206
|
+
typeof value === 'boolean' ? { ok: true, value } : fail(path, 'expected a boolean'),
|
|
207
|
+
() => ({ type: 'boolean' }),
|
|
143
208
|
);
|
|
144
209
|
},
|
|
145
210
|
|
|
146
211
|
literal<const T extends string | number | boolean>(expected: T): Schema<T> {
|
|
147
|
-
return schema(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
212
|
+
return schema(
|
|
213
|
+
'literal',
|
|
214
|
+
(value, path) =>
|
|
215
|
+
value === expected
|
|
216
|
+
? { ok: true, value: expected }
|
|
217
|
+
: fail(path, `expected ${JSON.stringify(expected)}`),
|
|
218
|
+
() => ({ const: expected }),
|
|
151
219
|
);
|
|
152
220
|
},
|
|
153
221
|
|
|
154
222
|
/** A closed set of string values. */
|
|
155
223
|
enum<const T extends readonly string[]>(values: T): Schema<T[number]> {
|
|
156
224
|
const allowed = new Set<string>(values);
|
|
157
|
-
return schema(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
225
|
+
return schema(
|
|
226
|
+
'enum',
|
|
227
|
+
(value, path) =>
|
|
228
|
+
typeof value === 'string' && allowed.has(value)
|
|
229
|
+
? { ok: true, value: value as T[number] }
|
|
230
|
+
: fail(path, `expected one of ${values.map((v) => JSON.stringify(v)).join(', ')}`),
|
|
231
|
+
() => ({ type: 'string', enum: [...values] }),
|
|
161
232
|
);
|
|
162
233
|
},
|
|
163
234
|
|
|
@@ -178,7 +249,15 @@ export const s = {
|
|
|
178
249
|
else issues.push(...outcome.issues);
|
|
179
250
|
}
|
|
180
251
|
return issues.length > 0 ? { ok: false, issues } : { ok: true, value: out };
|
|
181
|
-
}
|
|
252
|
+
},
|
|
253
|
+
() =>
|
|
254
|
+
keywords({
|
|
255
|
+
type: 'array',
|
|
256
|
+
items: item.toJsonSchema(),
|
|
257
|
+
minItems: options.min,
|
|
258
|
+
maxItems: options.max,
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
182
261
|
},
|
|
183
262
|
|
|
184
263
|
/**
|
|
@@ -188,9 +267,7 @@ export const s = {
|
|
|
188
267
|
* handler contains only what the schema named, so a property smuggled in by
|
|
189
268
|
* a caller cannot reach application code by accident.
|
|
190
269
|
*/
|
|
191
|
-
object<F extends Record<string, Schema<unknown>>>(
|
|
192
|
-
fields: F,
|
|
193
|
-
): Schema<{ [K in keyof F]: Infer<F[K]> }> {
|
|
270
|
+
object<F extends Record<string, Schema<unknown>>>(fields: F): Schema<InferObject<F>> {
|
|
194
271
|
const entries = Object.entries(fields);
|
|
195
272
|
return schema('object', (value, path) => {
|
|
196
273
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
@@ -208,30 +285,50 @@ export const s = {
|
|
|
208
285
|
}
|
|
209
286
|
return issues.length > 0
|
|
210
287
|
? { ok: false, issues }
|
|
211
|
-
: { ok: true, value: out as
|
|
212
|
-
}
|
|
288
|
+
: { ok: true, value: out as InferObject<F> };
|
|
289
|
+
},
|
|
290
|
+
() => ({
|
|
291
|
+
type: 'object',
|
|
292
|
+
properties: Object.fromEntries(entries.map(([key, field]) => [key, field.toJsonSchema()])),
|
|
293
|
+
// An optional field is one the object may leave out, so optionality is
|
|
294
|
+
// expressed here rather than inside the field's own schema.
|
|
295
|
+
required: entries.filter(([, field]) => field.kind !== 'optional').map(([key]) => key),
|
|
296
|
+
additionalProperties: false,
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
213
299
|
},
|
|
214
300
|
|
|
215
301
|
/** A value that may be absent or `undefined`. */
|
|
216
302
|
optional<T>(inner: Schema<T>): Schema<T | undefined> {
|
|
217
|
-
return schema<T | undefined>(
|
|
218
|
-
|
|
303
|
+
return schema<T | undefined>(
|
|
304
|
+
'optional',
|
|
305
|
+
(value, path) => (value === undefined ? { ok: true, value: undefined } : inner.check(value, path)),
|
|
306
|
+
// JSON Schema has no "optional" keyword; the enclosing object omits the
|
|
307
|
+
// key from `required` instead.
|
|
308
|
+
() => inner.toJsonSchema(),
|
|
219
309
|
);
|
|
220
310
|
},
|
|
221
311
|
|
|
222
312
|
/** A value that may be `null`. */
|
|
223
313
|
nullable<T>(inner: Schema<T>): Schema<T | null> {
|
|
224
|
-
return schema<T | null>(
|
|
225
|
-
|
|
314
|
+
return schema<T | null>(
|
|
315
|
+
'nullable',
|
|
316
|
+
(value, path) => (value === null ? { ok: true, value: null } : inner.check(value, path)),
|
|
317
|
+
() => ({ anyOf: [inner.toJsonSchema(), { type: 'null' }] }),
|
|
226
318
|
);
|
|
227
319
|
},
|
|
228
320
|
|
|
229
321
|
/** Nothing at all. The input type of an operation that takes no argument. */
|
|
230
322
|
void(): Schema<void> {
|
|
231
|
-
return schema(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
323
|
+
return schema(
|
|
324
|
+
'void',
|
|
325
|
+
(value, path) =>
|
|
326
|
+
value === undefined || value === null
|
|
327
|
+
? { ok: true, value: undefined }
|
|
328
|
+
: fail(path, 'expected no value'),
|
|
329
|
+
// A provider that asks for a tool's arguments wants an object, and an
|
|
330
|
+
// operation that takes nothing takes an empty one.
|
|
331
|
+
() => ({ type: 'object', properties: {}, additionalProperties: false }),
|
|
235
332
|
);
|
|
236
333
|
},
|
|
237
334
|
|
|
@@ -243,6 +340,6 @@ export const s = {
|
|
|
243
340
|
* data is untrusted.
|
|
244
341
|
*/
|
|
245
342
|
unknown(): Schema<unknown> {
|
|
246
|
-
return schema('unknown', (value) => ({ ok: true, value }));
|
|
343
|
+
return schema('unknown', (value) => ({ ok: true, value }), () => ({}));
|
|
247
344
|
},
|
|
248
345
|
} as const;
|