@jigging/agent-method 0.0.0 → 0.1.0-alpha.3

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 (52) hide show
  1. package/AGENTS.md +98 -0
  2. package/FLOW.contract.json +241 -0
  3. package/FLOW.meta.json +8 -0
  4. package/FLOW.ts +3 -0
  5. package/LICENSE +373 -0
  6. package/README.md +369 -0
  7. package/THIRD_PARTY_NOTICES +9 -0
  8. package/contracts/acp-public-updates.json +75 -0
  9. package/contracts/agent-commands.json +88 -0
  10. package/contracts/agent-replies.json +202 -0
  11. package/contracts/http-request/contract.json +37 -0
  12. package/dist/api.d.ts +11 -0
  13. package/dist/api.js +164 -0
  14. package/dist/conversation.d.ts +68 -0
  15. package/dist/conversation.js +346 -0
  16. package/dist/errors.d.ts +5 -0
  17. package/dist/errors.js +8 -0
  18. package/dist/flow.d.ts +3 -0
  19. package/dist/flow.js +2784 -0
  20. package/dist/index.d.ts +67 -0
  21. package/dist/index.js +220 -0
  22. package/dist/json.d.ts +21 -0
  23. package/dist/json.js +409 -0
  24. package/dist/schema.d.ts +7 -0
  25. package/dist/schema.js +180 -0
  26. package/dist/skills.d.ts +3 -0
  27. package/dist/skills.js +132 -0
  28. package/dist/values.d.ts +11 -0
  29. package/dist/values.js +65 -0
  30. package/justfile +32 -0
  31. package/licenses/flow.LICENSE +202 -0
  32. package/package.json +44 -4
  33. package/settings.schema.json +12 -0
  34. package/skills/answer-check/SKILL.md +5 -0
  35. package/src/api.ts +191 -0
  36. package/src/conversation.ts +387 -0
  37. package/src/errors.ts +11 -0
  38. package/src/flow.ts +66 -0
  39. package/src/index.ts +325 -0
  40. package/src/json.ts +406 -0
  41. package/src/schema.ts +230 -0
  42. package/src/skills.ts +138 -0
  43. package/src/values.ts +77 -0
  44. package/test/api.test.ts +326 -0
  45. package/test/conversation-fixture.ts +70 -0
  46. package/test/conversation.test.ts +305 -0
  47. package/test/json.test.ts +88 -0
  48. package/test/method.test.ts +308 -0
  49. package/test/pack.test.ts +81 -0
  50. package/test/result.test.ts +103 -0
  51. package/test/skills-flow.test.ts +252 -0
  52. package/tsconfig.json +17 -0
package/src/json.ts ADDED
@@ -0,0 +1,406 @@
1
+ export type JsonScalar = null | boolean | number | string
2
+
3
+ export type JsonValue = JsonScalar | readonly JsonValue[] | { readonly [key: string]: JsonValue }
4
+
5
+ export type JsonObject = Readonly<Record<string, JsonValue>>
6
+
7
+ export const JSON_1_LIMITS = Object.freeze({
8
+ bytes: 16_777_216,
9
+ depth: 128,
10
+ nodes: 262_144,
11
+ containerEntries: 65_536,
12
+ stringBytes: 8_388_608,
13
+ memberNameBytes: 1_024,
14
+ numberTokenBytes: 128,
15
+ })
16
+
17
+ const encoder = new TextEncoder()
18
+ const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true })
19
+
20
+ export class Json1Error extends Error {
21
+ constructor(message: string) {
22
+ super(message)
23
+ this.name = 'Json1Error'
24
+ }
25
+ }
26
+
27
+ class Parser {
28
+ private position = 0
29
+ private nodes = 0
30
+
31
+ constructor(private readonly source: string) {}
32
+
33
+ parse(): JsonValue {
34
+ this.skipWhitespace()
35
+ const value = this.parseValue(1)
36
+ this.skipWhitespace()
37
+ if (this.position !== this.source.length) this.fail('unexpected trailing characters')
38
+ return value
39
+ }
40
+
41
+ private parseValue(depth: number): JsonValue {
42
+ if (depth > JSON_1_LIMITS.depth) this.fail('maximum value depth exceeded')
43
+ this.nodes += 1
44
+ if (this.nodes > JSON_1_LIMITS.nodes) this.fail('maximum value nodes exceeded')
45
+
46
+ const char = this.source[this.position]
47
+ if (char === '"') return this.parseString(false)
48
+ if (char === '{') return this.parseObject(depth)
49
+ if (char === '[') return this.parseArray(depth)
50
+ if (char === 't') return this.parseLiteral('true', true)
51
+ if (char === 'f') return this.parseLiteral('false', false)
52
+ if (char === 'n') return this.parseLiteral('null', null)
53
+ if (char === '-' || (char !== undefined && char >= '0' && char <= '9')) {
54
+ return this.parseNumber()
55
+ }
56
+ return this.fail('expected a JSON value')
57
+ }
58
+
59
+ private parseObject(depth: number): JsonObject {
60
+ this.position += 1
61
+ const result: Record<string, JsonValue> = Object.create(null) as Record<string, JsonValue>
62
+ const keys = new Set<string>()
63
+ this.skipWhitespace()
64
+ if (this.consume('}')) return result
65
+
66
+ let members = 0
67
+ while (true) {
68
+ if (this.source[this.position] !== '"') this.fail('expected an object member name')
69
+ const key = this.parseString(true)
70
+ if (keys.has(key)) this.fail(`duplicate object member ${key}`)
71
+ keys.add(key)
72
+ members += 1
73
+ if (members > JSON_1_LIMITS.containerEntries) {
74
+ this.fail('maximum object members exceeded')
75
+ }
76
+ this.skipWhitespace()
77
+ if (!this.consume(':')) this.fail("expected ':' after member name")
78
+ this.skipWhitespace()
79
+ result[key] = this.parseValue(depth + 1)
80
+ this.skipWhitespace()
81
+ if (this.consume('}')) return result
82
+ if (!this.consume(',')) this.fail("expected ',' or '}'")
83
+ this.skipWhitespace()
84
+ }
85
+ }
86
+
87
+ private parseArray(depth: number): readonly JsonValue[] {
88
+ this.position += 1
89
+ const result: JsonValue[] = []
90
+ this.skipWhitespace()
91
+ if (this.consume(']')) return result
92
+ while (true) {
93
+ if (result.length >= JSON_1_LIMITS.containerEntries) {
94
+ this.fail('maximum array items exceeded')
95
+ }
96
+ result.push(this.parseValue(depth + 1))
97
+ this.skipWhitespace()
98
+ if (this.consume(']')) return result
99
+ if (!this.consume(',')) this.fail("expected ',' or ']'")
100
+ this.skipWhitespace()
101
+ }
102
+ }
103
+
104
+ private parseString(memberName: boolean): string {
105
+ this.position += 1
106
+ const parts: string[] = []
107
+ let segmentStart = this.position
108
+ while (this.position < this.source.length) {
109
+ const char = this.source[this.position]
110
+ this.position += 1
111
+ if (char === '"') {
112
+ parts.push(this.source.slice(segmentStart, this.position - 1))
113
+ const value = parts.join('')
114
+ const limit = memberName ? JSON_1_LIMITS.memberNameBytes : JSON_1_LIMITS.stringBytes
115
+ if (encoder.encode(value).byteLength > limit) this.fail('maximum string bytes exceeded')
116
+ return value
117
+ }
118
+ if (char === '\\') {
119
+ parts.push(this.source.slice(segmentStart, this.position - 1))
120
+ parts.push(this.parseEscape())
121
+ segmentStart = this.position
122
+ continue
123
+ }
124
+ if (char === undefined || char.charCodeAt(0) <= 0x1f) {
125
+ this.fail('invalid character in string')
126
+ }
127
+ const code = char.charCodeAt(0)
128
+ if (code >= 0xd800 && code <= 0xdfff) {
129
+ const next = this.source[this.position]
130
+ if (
131
+ code > 0xdbff ||
132
+ next === undefined ||
133
+ next.charCodeAt(0) < 0xdc00 ||
134
+ next.charCodeAt(0) > 0xdfff
135
+ )
136
+ this.fail('lone Unicode surrogate')
137
+ this.position += 1
138
+ }
139
+ }
140
+ return this.fail('unterminated string')
141
+ }
142
+
143
+ private parseEscape(): string {
144
+ const escaped = this.source[this.position]
145
+ this.position += 1
146
+ switch (escaped) {
147
+ case '"':
148
+ case '\\':
149
+ case '/':
150
+ return escaped
151
+ case 'b':
152
+ return '\b'
153
+ case 'f':
154
+ return '\f'
155
+ case 'n':
156
+ return '\n'
157
+ case 'r':
158
+ return '\r'
159
+ case 't':
160
+ return '\t'
161
+ case 'u': {
162
+ const first = this.parseHexCodeUnit()
163
+ if (first >= 0xd800 && first <= 0xdbff) {
164
+ if (this.source.slice(this.position, this.position + 2) !== '\\u') {
165
+ this.fail('high surrogate without low surrogate')
166
+ }
167
+ this.position += 2
168
+ const second = this.parseHexCodeUnit()
169
+ if (second < 0xdc00 || second > 0xdfff) {
170
+ this.fail('high surrogate without low surrogate')
171
+ }
172
+ return String.fromCharCode(first, second)
173
+ }
174
+ if (first >= 0xdc00 && first <= 0xdfff) this.fail('lone low surrogate')
175
+ return String.fromCharCode(first)
176
+ }
177
+ default:
178
+ return this.fail('invalid string escape')
179
+ }
180
+ }
181
+
182
+ private parseHexCodeUnit(): number {
183
+ const token = this.source.slice(this.position, this.position + 4)
184
+ if (!/^[0-9A-Fa-f]{4}$/.test(token)) this.fail('invalid Unicode escape')
185
+ this.position += 4
186
+ return Number.parseInt(token, 16)
187
+ }
188
+
189
+ private parseNumber(): number {
190
+ const start = this.position
191
+ this.consume('-')
192
+ if (this.consume('0')) {
193
+ const next = this.source[this.position]
194
+ if (next !== undefined && next >= '0' && next <= '9') {
195
+ this.fail('leading zero in number')
196
+ }
197
+ } else {
198
+ if (!this.consumeDigit('1', '9')) this.fail('invalid number')
199
+ while (this.consumeDigit('0', '9')) {}
200
+ }
201
+ if (this.consume('.')) {
202
+ if (!this.consumeDigit('0', '9')) this.fail('fraction requires a digit')
203
+ while (this.consumeDigit('0', '9')) {}
204
+ }
205
+ const exponent = this.source[this.position]
206
+ if (exponent === 'e' || exponent === 'E') {
207
+ this.position += 1
208
+ const sign = this.source[this.position]
209
+ if (sign === '+' || sign === '-') this.position += 1
210
+ if (!this.consumeDigit('0', '9')) this.fail('exponent requires a digit')
211
+ while (this.consumeDigit('0', '9')) {}
212
+ }
213
+ const token = this.source.slice(start, this.position)
214
+ if (token.length > JSON_1_LIMITS.numberTokenBytes) {
215
+ this.fail('maximum number token bytes exceeded')
216
+ }
217
+ const value = Number(token)
218
+ if (!Number.isFinite(value)) this.fail('non-finite number')
219
+ if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
220
+ this.fail('integral number outside safe range')
221
+ }
222
+ return value
223
+ }
224
+
225
+ private parseLiteral<T extends JsonValue>(token: string, value: T): T {
226
+ if (this.source.slice(this.position, this.position + token.length) !== token) {
227
+ this.fail(`invalid literal ${token}`)
228
+ }
229
+ this.position += token.length
230
+ return value
231
+ }
232
+
233
+ private consume(expected: string): boolean {
234
+ if (this.source[this.position] !== expected) return false
235
+ this.position += 1
236
+ return true
237
+ }
238
+
239
+ private consumeDigit(minimum: string, maximum: string): boolean {
240
+ const char = this.source[this.position]
241
+ if (char === undefined || char < minimum || char > maximum) return false
242
+ this.position += 1
243
+ return true
244
+ }
245
+
246
+ private skipWhitespace(): void {
247
+ while (true) {
248
+ const char = this.source[this.position]
249
+ if (char !== ' ' && char !== '\n' && char !== '\r' && char !== '\t') return
250
+ this.position += 1
251
+ }
252
+ }
253
+
254
+ private fail(message: string): never {
255
+ throw new Json1Error(`${message} at character ${this.position}`)
256
+ }
257
+ }
258
+
259
+ export function decodeJson1(bytes: Uint8Array): JsonValue {
260
+ if (bytes.byteLength === 0) throw new Json1Error('empty JSON/0 document')
261
+ if (bytes.byteLength > JSON_1_LIMITS.bytes) throw new Json1Error('maximum bytes exceeded')
262
+ if (bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
263
+ throw new Json1Error('UTF-8 BOM is not allowed')
264
+ }
265
+ let text: string
266
+ try {
267
+ text = decoder.decode(bytes)
268
+ } catch {
269
+ throw new Json1Error('invalid UTF-8')
270
+ }
271
+ return new Parser(text).parse()
272
+ }
273
+
274
+ export function validateJson1(value: unknown): asserts value is JsonValue {
275
+ visitJson(value, 1, { nodes: 0, encodedBytes: 0, active: new WeakSet<object>() }, false)
276
+ }
277
+
278
+ interface VisitState {
279
+ nodes: number
280
+ encodedBytes: number
281
+ readonly active: WeakSet<object>
282
+ }
283
+
284
+ function visitJson(value: unknown, depth: number, state: VisitState, memberName: boolean): void {
285
+ if (depth > JSON_1_LIMITS.depth) throw new Json1Error('maximum value depth exceeded')
286
+ state.nodes += 1
287
+ if (state.nodes > JSON_1_LIMITS.nodes) throw new Json1Error('maximum value nodes exceeded')
288
+ if (value === null) {
289
+ chargeEncodedBytes(state, 4)
290
+ return
291
+ }
292
+ if (typeof value === 'boolean') {
293
+ chargeEncodedBytes(state, value ? 4 : 5)
294
+ return
295
+ }
296
+ if (typeof value === 'number') {
297
+ if (!Number.isFinite(value)) throw new Json1Error('non-finite number')
298
+ if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
299
+ throw new Json1Error('integral number outside safe range')
300
+ }
301
+ chargeEncodedBytes(state, (Object.is(value, -0) ? '0' : JSON.stringify(value)).length)
302
+ return
303
+ }
304
+ if (typeof value === 'string') {
305
+ validateString(value, memberName ? JSON_1_LIMITS.memberNameBytes : JSON_1_LIMITS.stringBytes)
306
+ chargeEncodedBytes(state, encodedJsonStringBytes(value))
307
+ return
308
+ }
309
+ if (typeof value !== 'object') throw new Json1Error('value is not JSON/0')
310
+ if (state.active.has(value)) throw new Json1Error('cyclic value')
311
+ state.active.add(value)
312
+ try {
313
+ if (Array.isArray(value)) {
314
+ if (value.length > JSON_1_LIMITS.containerEntries) {
315
+ throw new Json1Error('maximum array items exceeded')
316
+ }
317
+ if (
318
+ Object.getPrototypeOf(value) !== Array.prototype ||
319
+ Reflect.ownKeys(value).length !== value.length + 1
320
+ ) {
321
+ throw new Json1Error('array must contain only ordinary indexed data')
322
+ }
323
+ chargeEncodedBytes(state, 2 + Math.max(0, value.length - 1))
324
+ for (let index = 0; index < value.length; index += 1) {
325
+ const field = Object.getOwnPropertyDescriptor(value, String(index))
326
+ if (field === undefined || !('value' in field) || !field.enumerable) {
327
+ throw new Json1Error('array items must be ordinary data')
328
+ }
329
+ visitJson(field.value, depth + 1, state, false)
330
+ }
331
+ return
332
+ }
333
+ const object = value as Record<string, unknown>
334
+ const prototype = Object.getPrototypeOf(object)
335
+ if (prototype !== Object.prototype && prototype !== null) {
336
+ throw new Json1Error('object must be an ordinary record')
337
+ }
338
+ const keys = Object.keys(object)
339
+ if (Reflect.ownKeys(object).length !== keys.length) {
340
+ throw new Json1Error('object members must be enumerable string data')
341
+ }
342
+ if (keys.length > JSON_1_LIMITS.containerEntries) {
343
+ throw new Json1Error('maximum object members exceeded')
344
+ }
345
+ chargeEncodedBytes(state, 2 + keys.length + Math.max(0, keys.length - 1))
346
+ for (const key of keys) {
347
+ validateString(key, JSON_1_LIMITS.memberNameBytes)
348
+ chargeEncodedBytes(state, encodedJsonStringBytes(key))
349
+ const field = Object.getOwnPropertyDescriptor(object, key)!
350
+ if (!('value' in field)) throw new Json1Error('object members must be ordinary data')
351
+ visitJson(field.value, depth + 1, state, false)
352
+ }
353
+ } finally {
354
+ state.active.delete(value)
355
+ }
356
+ }
357
+
358
+ function chargeEncodedBytes(state: VisitState, bytes: number): void {
359
+ if (bytes > JSON_1_LIMITS.bytes - state.encodedBytes) {
360
+ throw new Json1Error('maximum encoded bytes exceeded')
361
+ }
362
+ state.encodedBytes += bytes
363
+ }
364
+
365
+ function encodedJsonStringBytes(value: string): number {
366
+ return encoder.encode(JSON.stringify(value)).byteLength
367
+ }
368
+
369
+ function validateString(value: string, byteLimit: number): void {
370
+ if (encoder.encode(value).byteLength > byteLimit) {
371
+ throw new Json1Error('maximum string bytes exceeded')
372
+ }
373
+ for (let index = 0; index < value.length; index += 1) {
374
+ const code = value.charCodeAt(index)
375
+ if (code >= 0xd800 && code <= 0xdbff) {
376
+ const next = value.charCodeAt(index + 1)
377
+ if (!(next >= 0xdc00 && next <= 0xdfff)) throw new Json1Error('lone Unicode surrogate')
378
+ index += 1
379
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
380
+ throw new Json1Error('lone Unicode surrogate')
381
+ }
382
+ }
383
+ }
384
+
385
+ /** RFC 8785 canonical bytes for an already validated JSON/0 value. */
386
+ export function canonicalJson(value: JsonValue): Uint8Array {
387
+ validateJson1(value)
388
+ return encoder.encode(canonicalText(value))
389
+ }
390
+
391
+ function canonicalText(value: JsonValue): string {
392
+ if (value === null || typeof value === 'boolean' || typeof value === 'string') {
393
+ return JSON.stringify(value)
394
+ }
395
+ if (typeof value === 'number') return Object.is(value, -0) ? '0' : JSON.stringify(value)
396
+ if (Array.isArray(value)) return `[${value.map(canonicalText).join(',')}]`
397
+ const object = value as JsonObject
398
+ const members = Object.keys(object)
399
+ .sort(compareUtf16)
400
+ .map((key) => `${JSON.stringify(key)}:${canonicalText(object[key]!)}`)
401
+ return `{${members.join(',')}}`
402
+ }
403
+
404
+ function compareUtf16(left: string, right: string): number {
405
+ return left < right ? -1 : left > right ? 1 : 0
406
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,230 @@
1
+ import { AgentMethodError } from './errors.js'
2
+ import { canonicalJson, type JsonObject, type JsonValue, validateJson1 } from './json.js'
3
+ import { exactKeys, ordinaryRecord } from './values.js'
4
+
5
+ const MAX_RESPONSE_SCHEMA_DEPTH = 8
6
+ const MAX_PROPERTIES_PER_OBJECT = 32
7
+ const MAX_RESPONSE_SCHEMA_PROPERTIES = 128
8
+ const MAX_ARRAY_ITEMS = 256
9
+ const MAX_RESPONSE_SCHEMA_ENUM_VALUES = 256
10
+ const MAX_RESPONSE_SCHEMA_SYMBOL_CHARACTERS = 120_000
11
+ const MAX_LARGE_ENUM_CHARACTERS = 15_000
12
+
13
+ interface ResponseSchemaProfileState {
14
+ properties: number
15
+ enumValues: number
16
+ symbolCharacters: number
17
+ }
18
+
19
+ /** Validate Jig's deliberately bounded recursive Agent structured-output profile. */
20
+ export function assertResponseSchema(schema: JsonObject): void {
21
+ requireSchemaJson(schema)
22
+ if (schema.$schema !== 'https://flow.jig.md/schemas/schema-0.json') invalidSchemaProfile()
23
+ const state: ResponseSchemaProfileState = {
24
+ properties: 0,
25
+ enumValues: 0,
26
+ symbolCharacters: 0,
27
+ }
28
+ assertResponseSchemaNode(schema, 1, true, state)
29
+ }
30
+
31
+ function assertResponseSchemaNode(
32
+ value: unknown,
33
+ depth: number,
34
+ root: boolean,
35
+ state: ResponseSchemaProfileState,
36
+ ): void {
37
+ const schema = ordinaryRecord(value)
38
+ if (schema === undefined || depth > MAX_RESPONSE_SCHEMA_DEPTH) invalidSchemaProfile()
39
+
40
+ if (schema.type === 'object') {
41
+ assertClosedResponseObject(schema, depth, root, state)
42
+ return
43
+ }
44
+ if (root || Object.hasOwn(schema, '$schema')) invalidSchemaProfile()
45
+
46
+ if (schema.type === 'array') {
47
+ const baseExpected = Object.hasOwn(schema, 'minItems')
48
+ ? ['items', 'maxItems', 'minItems', 'type']
49
+ : ['items', 'maxItems', 'type']
50
+ if (
51
+ !exactProfileKeys(schema, baseExpected) ||
52
+ !boundedNonnegativeInteger(schema.maxItems, MAX_ARRAY_ITEMS) ||
53
+ (Object.hasOwn(schema, 'minItems') &&
54
+ (!boundedNonnegativeInteger(schema.minItems, MAX_ARRAY_ITEMS) ||
55
+ (schema.minItems as number) > (schema.maxItems as number)))
56
+ ) {
57
+ invalidSchemaProfile()
58
+ }
59
+ assertResponseSchemaNode(schema.items, depth + 1, false, state)
60
+ return
61
+ }
62
+
63
+ if (schema.type === 'integer' || isNullableType(schema.type, 'integer')) {
64
+ if (!exactProfileKeys(schema, ['type'])) invalidSchemaProfile()
65
+ return
66
+ }
67
+ if (schema.type === 'string' || isNullableType(schema.type, 'string')) {
68
+ assertResponseString(schema, isNullableType(schema.type, 'string'), state)
69
+ return
70
+ }
71
+ invalidSchemaProfile()
72
+ }
73
+
74
+ function assertClosedResponseObject(
75
+ schema: Record<string, unknown>,
76
+ depth: number,
77
+ root: boolean,
78
+ state: ResponseSchemaProfileState,
79
+ ): void {
80
+ const expected = root
81
+ ? ['$schema', 'additionalProperties', 'properties', 'required', 'type']
82
+ : ['additionalProperties', 'properties', 'required', 'type']
83
+ const properties = ordinaryRecord(schema.properties)
84
+ if (
85
+ !exactProfileKeys(schema, expected) ||
86
+ schema.additionalProperties !== false ||
87
+ properties === undefined ||
88
+ !Array.isArray(schema.required)
89
+ ) {
90
+ invalidSchemaProfile()
91
+ }
92
+ const names = Object.keys(properties)
93
+ if (
94
+ names.length === 0 ||
95
+ names.length > MAX_PROPERTIES_PER_OBJECT ||
96
+ schema.required.length !== names.length ||
97
+ new Set(schema.required).size !== names.length ||
98
+ schema.required.some((name) => typeof name !== 'string' || !Object.hasOwn(properties, name))
99
+ ) {
100
+ invalidSchemaProfile()
101
+ }
102
+ state.properties += names.length
103
+ if (state.properties > MAX_RESPONSE_SCHEMA_PROPERTIES) invalidSchemaProfile()
104
+ for (const name of names) {
105
+ state.symbolCharacters += unicodeScalarLength(name)
106
+ if (state.symbolCharacters > MAX_RESPONSE_SCHEMA_SYMBOL_CHARACTERS) invalidSchemaProfile()
107
+ assertResponseSchemaNode(properties[name], depth + 1, false, state)
108
+ }
109
+ }
110
+
111
+ function assertResponseString(
112
+ schema: Record<string, unknown>,
113
+ nullable: boolean,
114
+ state: ResponseSchemaProfileState,
115
+ ): void {
116
+ if (!Object.hasOwn(schema, 'enum')) {
117
+ if (!exactProfileKeys(schema, ['type'])) invalidSchemaProfile()
118
+ return
119
+ }
120
+ if (
121
+ !exactProfileKeys(schema, ['enum', 'type']) ||
122
+ !Array.isArray(schema.enum) ||
123
+ schema.enum.length === 0 ||
124
+ schema.enum.length > MAX_RESPONSE_SCHEMA_ENUM_VALUES ||
125
+ schema.enum.some((item) => typeof item !== 'string' && (!nullable || item !== null)) ||
126
+ new Set(schema.enum).size !== schema.enum.length ||
127
+ (nullable &&
128
+ (!schema.enum.includes(null) || !schema.enum.some((item) => typeof item === 'string')))
129
+ ) {
130
+ invalidSchemaProfile()
131
+ }
132
+ state.enumValues += schema.enum.length
133
+ const enumCharacters = schema.enum.reduce(
134
+ (total, item) => total + (typeof item === 'string' ? unicodeScalarLength(item) : 0),
135
+ 0,
136
+ )
137
+ if (schema.enum.length > 250 && enumCharacters > MAX_LARGE_ENUM_CHARACTERS) {
138
+ invalidSchemaProfile()
139
+ }
140
+ state.symbolCharacters += enumCharacters
141
+ if (state.enumValues > MAX_RESPONSE_SCHEMA_ENUM_VALUES) invalidSchemaProfile()
142
+ if (state.symbolCharacters > MAX_RESPONSE_SCHEMA_SYMBOL_CHARACTERS) invalidSchemaProfile()
143
+ }
144
+
145
+ function isNullableType(value: unknown, base: 'integer' | 'string'): boolean {
146
+ return (
147
+ Array.isArray(value) &&
148
+ value.length === 2 &&
149
+ new Set(value).size === 2 &&
150
+ value.includes(base) &&
151
+ value.includes('null')
152
+ )
153
+ }
154
+
155
+ function exactProfileKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
156
+ if (Object.hasOwn(value, 'description') && typeof value.description !== 'string') return false
157
+ return exactKeys(
158
+ value,
159
+ Object.hasOwn(value, 'description') ? [...expected, 'description'] : expected,
160
+ )
161
+ }
162
+
163
+ function boundedNonnegativeInteger(value: unknown, maximum: number): boolean {
164
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= maximum
165
+ }
166
+
167
+ function unicodeScalarLength(value: string): number {
168
+ return [...value].length
169
+ }
170
+
171
+ function invalidSchemaProfile(): never {
172
+ throw new AgentMethodError(
173
+ 'INVALID_INPUT',
174
+ 'Agent responseSchema must use the bounded structured-output profile',
175
+ )
176
+ }
177
+
178
+ function requireSchemaJson(schema: unknown): void {
179
+ try {
180
+ validateJson1(schema)
181
+ if (ordinaryRecord(schema) === undefined) invalidSchemaProfile()
182
+ if (canonicalJson(schema).byteLength > 256 * 1024) {
183
+ throw new AgentMethodError('RESOURCE_EXHAUSTED', 'Agent responseSchema exceeds 256 KiB')
184
+ }
185
+ } catch (error) {
186
+ if (error instanceof AgentMethodError) throw error
187
+ throw new AgentMethodError('INVALID_INPUT', 'Agent responseSchema must be valid JSON/0')
188
+ }
189
+ }
190
+
191
+ /** Remove only the FLOW dialect declaration from a fresh provider-facing copy. */
192
+ export function projectResponseSchema(schema: JsonObject): JsonObject {
193
+ assertResponseSchema(schema)
194
+ const copy = JSON.parse(new TextDecoder().decode(canonicalJson(schema))) as Record<
195
+ string,
196
+ JsonValue
197
+ >
198
+ delete copy.$schema
199
+ return copy
200
+ }
201
+
202
+ /** Check a value against the already asserted closed structured-output profile. */
203
+ export function matchesResponseSchema(schema: JsonObject, value: JsonValue): boolean {
204
+ if (value === null && Array.isArray(schema.type) && schema.type.includes('null')) {
205
+ return !Array.isArray(schema.enum) || schema.enum.includes(null)
206
+ }
207
+ if (schema.type === 'object') {
208
+ const record = ordinaryRecord(value)
209
+ if (record === undefined) return false
210
+ const properties = schema.properties as JsonObject
211
+ return (
212
+ exactKeys(record, Object.keys(properties)) &&
213
+ Object.entries(properties).every(([name, child]) =>
214
+ matchesResponseSchema(child as JsonObject, record[name] as JsonValue),
215
+ )
216
+ )
217
+ }
218
+ if (schema.type === 'array') {
219
+ return (
220
+ Array.isArray(value) &&
221
+ value.length <= (schema.maxItems as number) &&
222
+ value.length >= ((schema.minItems as number | undefined) ?? 0) &&
223
+ value.every((item) => matchesResponseSchema(schema.items as JsonObject, item))
224
+ )
225
+ }
226
+ if (schema.type === 'integer' || isNullableType(schema.type, 'integer')) {
227
+ return typeof value === 'number' && Number.isSafeInteger(value)
228
+ }
229
+ return typeof value === 'string' && (!Array.isArray(schema.enum) || schema.enum.includes(value))
230
+ }