@namzu/sdk 5.1.0 → 5.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.
Files changed (158) hide show
  1. package/CHANGELOG.md +361 -0
  2. package/dist/agents/__tests__/run-agent-forwarding.test.d.ts +2 -0
  3. package/dist/agents/__tests__/run-agent-forwarding.test.d.ts.map +1 -0
  4. package/dist/agents/__tests__/run-agent-forwarding.test.js +76 -0
  5. package/dist/agents/__tests__/run-agent-forwarding.test.js.map +1 -0
  6. package/dist/agents/__tests__/run-completed-stop-reason.test.d.ts +2 -0
  7. package/dist/agents/__tests__/run-completed-stop-reason.test.d.ts.map +1 -0
  8. package/dist/agents/__tests__/run-completed-stop-reason.test.js +50 -0
  9. package/dist/agents/__tests__/run-completed-stop-reason.test.js.map +1 -0
  10. package/dist/agents/index.d.ts +1 -0
  11. package/dist/agents/index.d.ts.map +1 -1
  12. package/dist/agents/index.js +4 -0
  13. package/dist/agents/index.js.map +1 -1
  14. package/dist/agents/runAgent.d.ts +34 -0
  15. package/dist/agents/runAgent.d.ts.map +1 -1
  16. package/dist/agents/runAgent.js +5 -3
  17. package/dist/agents/runAgent.js.map +1 -1
  18. package/dist/config/runtime.d.ts +30 -30
  19. package/dist/constants/tools/__tests__/dangerous-patterns.test.d.ts +2 -0
  20. package/dist/constants/tools/__tests__/dangerous-patterns.test.d.ts.map +1 -0
  21. package/dist/constants/tools/__tests__/dangerous-patterns.test.js +54 -0
  22. package/dist/constants/tools/__tests__/dangerous-patterns.test.js.map +1 -0
  23. package/dist/constants/tools/index.d.ts.map +1 -1
  24. package/dist/constants/tools/index.js +28 -1
  25. package/dist/constants/tools/index.js.map +1 -1
  26. package/dist/contracts/a2a.d.ts +44 -44
  27. package/dist/directory/__tests__/derive.test.d.ts +2 -0
  28. package/dist/directory/__tests__/derive.test.d.ts.map +1 -0
  29. package/dist/directory/__tests__/derive.test.js +111 -0
  30. package/dist/directory/__tests__/derive.test.js.map +1 -0
  31. package/dist/directory/__tests__/load-diagnostics.test.d.ts +2 -0
  32. package/dist/directory/__tests__/load-diagnostics.test.d.ts.map +1 -0
  33. package/dist/directory/__tests__/load-diagnostics.test.js +106 -0
  34. package/dist/directory/__tests__/load-diagnostics.test.js.map +1 -0
  35. package/dist/directory/__tests__/load-directory.test.d.ts +2 -0
  36. package/dist/directory/__tests__/load-directory.test.d.ts.map +1 -0
  37. package/dist/directory/__tests__/load-directory.test.js +196 -0
  38. package/dist/directory/__tests__/load-directory.test.js.map +1 -0
  39. package/dist/directory/__tests__/subagents.test.d.ts +2 -0
  40. package/dist/directory/__tests__/subagents.test.d.ts.map +1 -0
  41. package/dist/directory/__tests__/subagents.test.js +117 -0
  42. package/dist/directory/__tests__/subagents.test.js.map +1 -0
  43. package/dist/directory/__tests__/supervisor-budget.test.d.ts +2 -0
  44. package/dist/directory/__tests__/supervisor-budget.test.d.ts.map +1 -0
  45. package/dist/directory/__tests__/supervisor-budget.test.js +97 -0
  46. package/dist/directory/__tests__/supervisor-budget.test.js.map +1 -0
  47. package/dist/directory/derive-supervisor.d.ts +58 -0
  48. package/dist/directory/derive-supervisor.d.ts.map +1 -0
  49. package/dist/directory/derive-supervisor.js +88 -0
  50. package/dist/directory/derive-supervisor.js.map +1 -0
  51. package/dist/directory/derive.d.ts +12 -0
  52. package/dist/directory/derive.d.ts.map +1 -0
  53. package/dist/directory/derive.js +58 -0
  54. package/dist/directory/derive.js.map +1 -0
  55. package/dist/directory/index.d.ts +7 -0
  56. package/dist/directory/index.d.ts.map +1 -0
  57. package/dist/directory/index.js +5 -0
  58. package/dist/directory/index.js.map +1 -0
  59. package/dist/directory/load.d.ts +14 -0
  60. package/dist/directory/load.d.ts.map +1 -0
  61. package/dist/directory/load.js +488 -0
  62. package/dist/directory/load.js.map +1 -0
  63. package/dist/directory/scan.d.ts +36 -0
  64. package/dist/directory/scan.d.ts.map +1 -0
  65. package/dist/directory/scan.js +136 -0
  66. package/dist/directory/scan.js.map +1 -0
  67. package/dist/directory/types.d.ts +215 -0
  68. package/dist/directory/types.d.ts.map +1 -0
  69. package/dist/directory/types.js +8 -0
  70. package/dist/directory/types.js.map +1 -0
  71. package/dist/plugin/__tests__/path-containment.test.d.ts +2 -0
  72. package/dist/plugin/__tests__/path-containment.test.d.ts.map +1 -0
  73. package/dist/plugin/__tests__/path-containment.test.js +91 -0
  74. package/dist/plugin/__tests__/path-containment.test.js.map +1 -0
  75. package/dist/plugin/lifecycle.d.ts.map +1 -1
  76. package/dist/plugin/lifecycle.js +3 -3
  77. package/dist/plugin/lifecycle.js.map +1 -1
  78. package/dist/plugin/loader.d.ts.map +1 -1
  79. package/dist/plugin/loader.js +12 -2
  80. package/dist/plugin/loader.js.map +1 -1
  81. package/dist/provider/__tests__/model-version.test.d.ts +2 -0
  82. package/dist/provider/__tests__/model-version.test.d.ts.map +1 -0
  83. package/dist/provider/__tests__/model-version.test.js +80 -0
  84. package/dist/provider/__tests__/model-version.test.js.map +1 -0
  85. package/dist/provider/__tests__/strict-schema.test.d.ts +2 -0
  86. package/dist/provider/__tests__/strict-schema.test.d.ts.map +1 -0
  87. package/dist/provider/__tests__/strict-schema.test.js +109 -0
  88. package/dist/provider/__tests__/strict-schema.test.js.map +1 -0
  89. package/dist/provider/model-version.d.ts +60 -0
  90. package/dist/provider/model-version.d.ts.map +1 -0
  91. package/dist/provider/model-version.js +64 -0
  92. package/dist/provider/model-version.js.map +1 -0
  93. package/dist/provider/strict-schema.d.ts +49 -0
  94. package/dist/provider/strict-schema.d.ts.map +1 -0
  95. package/dist/provider/strict-schema.js +112 -0
  96. package/dist/provider/strict-schema.js.map +1 -0
  97. package/dist/public-runtime.d.ts +6 -1
  98. package/dist/public-runtime.d.ts.map +1 -1
  99. package/dist/public-runtime.js +21 -1
  100. package/dist/public-runtime.js.map +1 -1
  101. package/dist/public-types.d.ts +2 -0
  102. package/dist/public-types.d.ts.map +1 -1
  103. package/dist/registry/tool/__tests__/strict-registration.test.d.ts +2 -0
  104. package/dist/registry/tool/__tests__/strict-registration.test.d.ts.map +1 -0
  105. package/dist/registry/tool/__tests__/strict-registration.test.js +82 -0
  106. package/dist/registry/tool/__tests__/strict-registration.test.js.map +1 -0
  107. package/dist/registry/tool/execute.d.ts.map +1 -1
  108. package/dist/registry/tool/execute.js +16 -0
  109. package/dist/registry/tool/execute.js.map +1 -1
  110. package/dist/runtime/query/result.d.ts.map +1 -1
  111. package/dist/runtime/query/result.js +5 -0
  112. package/dist/runtime/query/result.js.map +1 -1
  113. package/dist/tools/builtins/__tests__/edit-insert-reachable.test.js +7 -1
  114. package/dist/tools/builtins/__tests__/edit-insert-reachable.test.js.map +1 -1
  115. package/dist/tools/builtins/__tests__/edit.test.js +1 -1
  116. package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
  117. package/dist/tools/builtins/edit.d.ts.map +1 -1
  118. package/dist/tools/builtins/edit.js +20 -2
  119. package/dist/tools/builtins/edit.js.map +1 -1
  120. package/dist/types/plugin/index.d.ts +4 -4
  121. package/dist/types/run/events.d.ts +17 -2
  122. package/dist/types/run/events.d.ts.map +1 -1
  123. package/dist/types/run/events.js.map +1 -1
  124. package/dist/types/verification/index.d.ts +6 -6
  125. package/package.json +1 -1
  126. package/src/agents/__tests__/run-agent-forwarding.test.ts +85 -0
  127. package/src/agents/__tests__/run-completed-stop-reason.test.ts +59 -0
  128. package/src/agents/index.ts +8 -0
  129. package/src/agents/runAgent.ts +30 -4
  130. package/src/constants/tools/__tests__/dangerous-patterns.test.ts +62 -0
  131. package/src/constants/tools/index.ts +28 -1
  132. package/src/directory/__tests__/derive.test.ts +135 -0
  133. package/src/directory/__tests__/load-diagnostics.test.ts +126 -0
  134. package/src/directory/__tests__/load-directory.test.ts +243 -0
  135. package/src/directory/__tests__/subagents.test.ts +155 -0
  136. package/src/directory/__tests__/supervisor-budget.test.ts +117 -0
  137. package/src/directory/derive-supervisor.ts +149 -0
  138. package/src/directory/derive.ts +70 -0
  139. package/src/directory/index.ts +27 -0
  140. package/src/directory/load.ts +556 -0
  141. package/src/directory/scan.ts +163 -0
  142. package/src/directory/types.ts +254 -0
  143. package/src/plugin/__tests__/path-containment.test.ts +107 -0
  144. package/src/plugin/lifecycle.ts +3 -3
  145. package/src/plugin/loader.ts +12 -2
  146. package/src/provider/__tests__/model-version.test.ts +93 -0
  147. package/src/provider/__tests__/strict-schema.test.ts +133 -0
  148. package/src/provider/model-version.ts +98 -0
  149. package/src/provider/strict-schema.ts +125 -0
  150. package/src/public-runtime.ts +31 -0
  151. package/src/public-types.ts +8 -0
  152. package/src/registry/tool/__tests__/strict-registration.test.ts +115 -0
  153. package/src/registry/tool/execute.ts +16 -0
  154. package/src/runtime/query/result.ts +5 -0
  155. package/src/tools/builtins/__tests__/edit-insert-reachable.test.ts +8 -2
  156. package/src/tools/builtins/__tests__/edit.test.ts +1 -1
  157. package/src/tools/builtins/edit.ts +20 -2
  158. package/src/types/run/events.ts +15 -2
@@ -0,0 +1,133 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { getBuiltinTools } from '../../tools/builtins/index.js'
4
+ import { assertStrictSchema, findStrictSchemaViolations } from '../strict-schema.js'
5
+
6
+ /**
7
+ * Strict tool input validates against a SUBSET of JSON Schema, and a keyword
8
+ * outside that subset is not degraded — the vendor rejects the entire request,
9
+ * so one unexpressible field in one tool takes down every tool in the call and
10
+ * the turn dies before producing a token.
11
+ *
12
+ * That shipped. The edit tool declared its integer-or-`"end"` field with
13
+ * `oneOf`, which is outside the subset while the equivalent `anyOf` is inside
14
+ * it, and the driver marked the tool strict without asking whether the schema
15
+ * it was vouching for could be said in that dialect.
16
+ *
17
+ * Measured against the live API:
18
+ *
19
+ * | body | result |
20
+ * |------------------------|----------|
21
+ * | strict: true + oneOf | 400 |
22
+ * | strict: false + oneOf | accepted |
23
+ * | strict: true + anyOf | accepted |
24
+ *
25
+ * The middle row is why nothing caught it. Neither half is wrong alone — the
26
+ * schema is valid JSON Schema, and turning strict on is correct policy — so no
27
+ * test of either one fails. Only the pairing does, and the pairing had no
28
+ * owner. The sweep below is that owner.
29
+ */
30
+
31
+ describe('every tool that asks for strict validation can be expressed strictly', () => {
32
+ // The regression test that matters. Checking only the tool that broke
33
+ // would leave the next one to be found in production, which is how this
34
+ // one was found.
35
+ it.each(
36
+ getBuiltinTools()
37
+ .filter((t) => t.enforceModelInput)
38
+ .map((t) => [t.name, t] as const),
39
+ )('%s', (name, tool) => {
40
+ const violations = findStrictSchemaViolations(tool.modelInputSchema)
41
+ expect(violations, violations.map((v) => `${name}.${v.path}: ${v.remedy}`).join('\n')).toEqual(
42
+ [],
43
+ )
44
+ })
45
+
46
+ it('found at least one tool to check', () => {
47
+ // Guards the sweep itself: a filter that matches nothing passes
48
+ // vacuously, and a rename of `enforceModelInput` would silently turn
49
+ // this whole file into a no-op.
50
+ expect(getBuiltinTools().filter((t) => t.enforceModelInput).length).toBeGreaterThan(0)
51
+ })
52
+ })
53
+
54
+ describe('the violation report names the exact path', () => {
55
+ it('points at the keyword inside a nested property', () => {
56
+ const schema = {
57
+ type: 'object',
58
+ properties: {
59
+ insertLine: { oneOf: [{ type: 'integer' }, { const: 'end' }] },
60
+ },
61
+ }
62
+
63
+ expect(findStrictSchemaViolations(schema)).toEqual([
64
+ {
65
+ path: 'properties.insertLine.oneOf',
66
+ keyword: 'oneOf',
67
+ remedy: 'use `anyOf` — for disjoint branches the two are equivalent',
68
+ },
69
+ ])
70
+ })
71
+
72
+ it('accepts the anyOf spelling of the same union', () => {
73
+ const schema = {
74
+ type: 'object',
75
+ properties: {
76
+ insertLine: { anyOf: [{ type: 'integer' }, { const: 'end' }] },
77
+ },
78
+ }
79
+
80
+ expect(findStrictSchemaViolations(schema)).toEqual([])
81
+ })
82
+
83
+ it('reports numeric and string bounds, which are also outside the subset', () => {
84
+ const schema = {
85
+ type: 'object',
86
+ properties: {
87
+ n: { type: 'integer', minimum: 0 },
88
+ s: { type: 'string', maxLength: 10 },
89
+ },
90
+ }
91
+
92
+ expect(
93
+ findStrictSchemaViolations(schema)
94
+ .map((v) => v.keyword)
95
+ .sort(),
96
+ ).toEqual(['maxLength', 'minimum'])
97
+ })
98
+
99
+ it('admits additionalProperties only as false', () => {
100
+ expect(findStrictSchemaViolations({ additionalProperties: false })).toEqual([])
101
+ expect(findStrictSchemaViolations({ additionalProperties: { type: 'string' } })).toHaveLength(1)
102
+ })
103
+
104
+ it('walks into arrays of subschemas', () => {
105
+ const schema = { anyOf: [{ type: 'string' }, { not: { type: 'null' } }] }
106
+
107
+ expect(findStrictSchemaViolations(schema)[0]?.path).toBe('anyOf[1].not')
108
+ })
109
+
110
+ it('leaves ordinary annotations alone', () => {
111
+ const schema = {
112
+ type: 'object',
113
+ description: 'a tool',
114
+ properties: { a: { type: 'string', description: 'x', enum: ['y'] } },
115
+ required: ['a'],
116
+ additionalProperties: false,
117
+ }
118
+
119
+ expect(findStrictSchemaViolations(schema)).toEqual([])
120
+ })
121
+ })
122
+
123
+ describe('assertStrictSchema refuses rather than letting the request go', () => {
124
+ it('throws naming the tool, the path and the fix', () => {
125
+ expect(() => assertStrictSchema('edit', { properties: { insertLine: { oneOf: [] } } })).toThrow(
126
+ /edit\.properties\.insertLine\.oneOf/,
127
+ )
128
+ })
129
+
130
+ it('says nothing about a schema that is expressible', () => {
131
+ expect(() => assertStrictSchema('edit', { type: 'object' })).not.toThrow()
132
+ })
133
+ })
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Parse a versioned model id, given the vocabulary that names one.
3
+ *
4
+ * Several vendors spell an id the same way — a product segment, a family, a
5
+ * major version, sometimes a minor, and sometimes an 8-digit release date:
6
+ *
7
+ * ```
8
+ * <product>-<family>-<major>[-<minor>][-<YYYYMMDD>]
9
+ * ```
10
+ *
11
+ * The SHAPE is general and lives here; the vocabulary is not, and is supplied
12
+ * by the driver that knows it. That split is deliberate: a driver package
13
+ * exists to speak one service's dialect, and the kernel has no business
14
+ * knowing whose ids these are.
15
+ *
16
+ * It exists at all because three drivers had each written the same matcher and
17
+ * all three had the same defect: the minor-version group was `\d+`, which
18
+ * happily swallowed the date. An id naming no minor therefore parsed as
19
+ * `major.<the date>` and compared as enormously NEWER than one that does, so
20
+ * every capability gate keyed on `minor >= n` inverted for exactly those ids —
21
+ * a model was told it supported features it does not.
22
+ *
23
+ * A real minor version is one to three digits; a date is exactly eight.
24
+ * Bounding the group is what stops it reaching across the separator, and the
25
+ * expression then backtracks into leaving the minor absent so the date suffix
26
+ * can match where it belongs.
27
+ */
28
+
29
+ export interface ModelVersion {
30
+ readonly family: string
31
+ readonly major: number
32
+ /** `0` when the id names no minor — a bare major is `<major>.0`. */
33
+ readonly minor: number
34
+ }
35
+
36
+ /**
37
+ * What a driver must say to have its ids parsed.
38
+ *
39
+ * Every field is a wire value the driver already carries. Passing them in
40
+ * rather than hard-coding them is what keeps this module free of any one
41
+ * service's names.
42
+ */
43
+ export interface ModelIdGrammar {
44
+ /** The product segment an id begins with. */
45
+ readonly product: string
46
+ /** The family segment that follows it. */
47
+ readonly families: readonly string[]
48
+ /** An optional routing segment a gateway may prepend, including its slash. */
49
+ readonly routingPrefix?: string
50
+ }
51
+
52
+ const quoteMeta = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
53
+
54
+ function expressionFor(grammar: ModelIdGrammar): RegExp {
55
+ const routing = grammar.routingPrefix ? `(?:${quoteMeta(grammar.routingPrefix)})?` : ''
56
+ // `.map(quoteMeta)` and not `.map(escape)`: the latter resolves to the
57
+ // global, deprecated `escape()`, which percent-encodes instead of escaping
58
+ // regex metacharacters — and would pass every test here, because no family
59
+ // name contains a character either function changes.
60
+ const families = grammar.families.map((f) => quoteMeta(f)).join('|')
61
+ // `\d{1,3}` on the minor, not `\d+`. That single bound is the whole fix.
62
+ return new RegExp(
63
+ `^${routing}${quoteMeta(grammar.product)}-(${families})-(\\d+)(?:[-_.](\\d{1,3}))?(?:-\\d{8})?$`,
64
+ )
65
+ }
66
+
67
+ /** `undefined` for anything the grammar does not describe — never a guess. */
68
+ export function parseVersionedModelId(
69
+ id: string,
70
+ grammar: ModelIdGrammar,
71
+ ): ModelVersion | undefined {
72
+ const match = id.toLowerCase().match(expressionFor(grammar))
73
+ if (!match) return undefined
74
+ return {
75
+ family: match[1] as string,
76
+ major: Number(match[2]),
77
+ minor: match[3] === undefined ? 0 : Number(match[3]),
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Whether an id names a version at or above `major.minor`.
83
+ *
84
+ * The comparison every caller was writing by hand. An id this cannot parse
85
+ * returns `false`: a capability gate must not open for a name it does not
86
+ * understand, which is the fail-safe reading and the one those callers' own
87
+ * comments already claimed.
88
+ */
89
+ export function modelVersionAtLeast(
90
+ id: string,
91
+ grammar: ModelIdGrammar,
92
+ major: number,
93
+ minor: number,
94
+ ): boolean {
95
+ const version = parseVersionedModelId(id, grammar)
96
+ if (!version) return false
97
+ return version.major > major || (version.major === major && version.minor >= minor)
98
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Which JSON Schema a driver may mark as strictly validated.
3
+ *
4
+ * Strict tool input is not "JSON Schema, enforced" — it is a SUBSET of JSON
5
+ * Schema, and a keyword outside that subset does not degrade. The vendor
6
+ * rejects the entire request, so one unexpressible field in one tool takes down
7
+ * every tool in the call and the turn dies before a single token is produced.
8
+ *
9
+ * This exists because that happened. A tool declared its integer-or-`"end"`
10
+ * field as `oneOf`, which is outside the subset while its synonym `anyOf` is
11
+ * inside it, and the driver marked the tool strict without ever asking whether
12
+ * the schema it was vouching for could be said in that dialect. Measured
13
+ * against the live API: strict + `oneOf` is a 400, strict + `anyOf` is
14
+ * accepted, and non-strict + `oneOf` is accepted.
15
+ *
16
+ * That last row is why nothing caught it. Neither half is wrong on its own —
17
+ * the schema is valid JSON Schema and the strict decision is correct policy —
18
+ * so no test of either one fails. Only the pairing does, and the pairing had no
19
+ * owner until this function.
20
+ *
21
+ * The check is cheap and runs where the pairing is made, which is the only
22
+ * place both facts are in hand.
23
+ */
24
+
25
+ /**
26
+ * Keywords the strict subset does not accept.
27
+ *
28
+ * A deny-list rather than an allow-list, deliberately. An allow-list would have
29
+ * to enumerate every annotation a schema may carry — `description`, `title`,
30
+ * `examples`, vendor extensions — and would refuse a schema for saying
31
+ * something harmless. These are the constructs documented as outside the
32
+ * subset; each one names what to write instead.
33
+ */
34
+ const NO_CONDITIONALS = 'strict mode has no conditional schemas; flatten the object'
35
+ const NO_NUMERIC_BOUNDS = 'numeric bounds are not in the subset; enforce at execution'
36
+ const NO_LENGTH_BOUNDS = 'length bounds are not in the subset; enforce at execution'
37
+
38
+ // A Map rather than an object literal, because one of the keys is `then`: an
39
+ // ordinary object carrying a `then` property is a thenable, and awaiting it
40
+ // anywhere would silently call the string. The lint rule that says so is
41
+ // right, and a Map has no such hazard.
42
+ const UNSUPPORTED: ReadonlyMap<string, string> = new Map([
43
+ ['oneOf', 'use `anyOf` — for disjoint branches the two are equivalent'],
44
+ ['not', 'express the constraint positively, or validate it at execution'],
45
+ ['if', NO_CONDITIONALS],
46
+ ['then', NO_CONDITIONALS],
47
+ ['else', NO_CONDITIONALS],
48
+ ['minimum', NO_NUMERIC_BOUNDS],
49
+ ['maximum', NO_NUMERIC_BOUNDS],
50
+ ['exclusiveMinimum', NO_NUMERIC_BOUNDS],
51
+ ['exclusiveMaximum', NO_NUMERIC_BOUNDS],
52
+ ['multipleOf', NO_NUMERIC_BOUNDS],
53
+ ['minLength', NO_LENGTH_BOUNDS],
54
+ ['maxLength', NO_LENGTH_BOUNDS],
55
+ ['minItems', NO_LENGTH_BOUNDS],
56
+ ['maxItems', NO_LENGTH_BOUNDS],
57
+ ['uniqueItems', 'array constraints are not in the subset; enforce at execution'],
58
+ ['patternProperties', 'name the properties explicitly'],
59
+ ['propertyNames', 'name the properties explicitly'],
60
+ ['dependentSchemas', 'flatten the object and validate at execution'],
61
+ ['dependentRequired', 'flatten the object and validate at execution'],
62
+ ])
63
+
64
+ export interface StrictSchemaViolation {
65
+ /** Dotted path to the offending keyword, e.g. `properties.insertLine.oneOf`. */
66
+ readonly path: string
67
+ readonly keyword: string
68
+ /** What to write instead. */
69
+ readonly remedy: string
70
+ }
71
+
72
+ /**
73
+ * Every place a schema leaves the strict subset, with its exact path.
74
+ *
75
+ * The path is the point. The vendor's own error names the tool and the
76
+ * keyword but not where inside the schema it sits, which on a schema of any
77
+ * size is the difference between a glance and an afternoon.
78
+ */
79
+ export function findStrictSchemaViolations(schema: unknown, path = ''): StrictSchemaViolation[] {
80
+ if (Array.isArray(schema)) {
81
+ return schema.flatMap((item, index) => findStrictSchemaViolations(item, `${path}[${index}]`))
82
+ }
83
+ if (typeof schema !== 'object' || schema === null) return []
84
+
85
+ const found: StrictSchemaViolation[] = []
86
+ for (const [keyword, value] of Object.entries(schema as Record<string, unknown>)) {
87
+ const here = path ? `${path}.${keyword}` : keyword
88
+ const remedy = UNSUPPORTED.get(keyword)
89
+ if (remedy !== undefined) {
90
+ found.push({ path: here, keyword, remedy })
91
+ continue
92
+ }
93
+ // `additionalProperties` is admitted only as `false`; any schema there
94
+ // is an open object, which the subset does not allow.
95
+ if (keyword === 'additionalProperties' && value !== false) {
96
+ found.push({
97
+ path: here,
98
+ keyword,
99
+ remedy: 'strict objects must set `additionalProperties: false`',
100
+ })
101
+ continue
102
+ }
103
+ found.push(...findStrictSchemaViolations(value, here))
104
+ }
105
+ return found
106
+ }
107
+
108
+ /**
109
+ * Refuse a schema the driver is about to vouch for and cannot.
110
+ *
111
+ * Refusing here rather than dropping `strict` quietly: a caller who set
112
+ * `enforceModelInput` asked for the guarantee, and silently not providing it
113
+ * is the failure this repo names `refuse, do not degrade`. The alternative
114
+ * costs a turn and teaches nothing — the vendor's 400 arrives with the tool
115
+ * name and the keyword, but not the path, and not the fix.
116
+ */
117
+ export function assertStrictSchema(toolName: string, schema: unknown): void {
118
+ const violations = findStrictSchemaViolations(schema)
119
+ if (violations.length === 0) return
120
+
121
+ const detail = violations.map((v) => ` ${toolName}.${v.path} — ${v.remedy}`).join('\n')
122
+ throw new Error(
123
+ `Tool "${toolName}" is marked for strict input validation, but its model-facing schema uses ${violations.length} construct(s) the strict subset does not accept. The request would be rejected whole, taking every other tool in it down as well.\n${detail}`,
124
+ )
125
+ }
@@ -70,6 +70,16 @@ export { resolveTaskModel } from './router/task-router.js'
70
70
  // Every driver accepts `thinking`; one that does not implement it must
71
71
  // refuse rather than drop it. Shared so a new driver inherits the rule.
72
72
  export { assertThinkingUnsupported } from './provider/thinking-support.js'
73
+ // One matcher for versioned model ids. Shared because three drivers had each
74
+ // written their own and all three read an 8-digit date suffix as the MINOR
75
+ // version, which inverted every capability decision keyed on it. The shape
76
+ // lives here; the vocabulary comes from the driver that knows it.
77
+ export { modelVersionAtLeast, parseVersionedModelId } from './provider/model-version.js'
78
+ // Strict tool input is a SUBSET of JSON Schema, and a keyword outside it makes
79
+ // the vendor reject the whole request rather than degrade one field.
80
+ export { assertStrictSchema, findStrictSchemaViolations } from './provider/strict-schema.js'
81
+ export type { StrictSchemaViolation } from './provider/strict-schema.js'
82
+ export type { ModelIdGrammar, ModelVersion } from './provider/model-version.js'
73
83
  export { drainQuery, query } from './runtime/query/index.js'
74
84
  // Mid-run guidance. A host holds the channel and the loop drains it at the
75
85
  // tool-result boundary; see the module for why that is the only legal slot.
@@ -125,6 +135,24 @@ export {
125
135
  resolveSkillChain,
126
136
  SkillRegistry,
127
137
  } from './skills/index.js'
138
+
139
+ // ─── the agent directory ─────────────────────────────────────────────────
140
+ //
141
+ // Reading a conventional `agent/` directory — its instructions, tools, skills
142
+ // and delegates — into the same options `runAgent` and `SupervisorAgent`
143
+ // already take. A loader, not a second engine: everything it produces is an
144
+ // ordinary option, so a caller who outgrows the convention passes overrides
145
+ // or stops calling it and keeps everything else.
146
+ //
147
+ // It shipped briefly as its own package. The name was the tell — nothing fit,
148
+ // because a directory reader that needs the kernel to be useful is a function
149
+ // of the kernel, not a product beside it.
150
+ export {
151
+ ALL_SLOTS,
152
+ deriveRunOptions,
153
+ deriveSupervisorOptions,
154
+ loadDirectory,
155
+ } from './directory/index.js'
128
156
  export {
129
157
  AdvisorRegistry,
130
158
  AdvisoryContext,
@@ -145,6 +173,9 @@ export {
145
173
  // The short path: provider + model + prompt. Assembles the identity and
146
174
  // budgets `drainQuery` requires and hands the generated identity back.
147
175
  runAgent,
176
+ DEFAULT_MAX_ITERATIONS,
177
+ DEFAULT_TIMEOUT_MS,
178
+ DEFAULT_TOKEN_BUDGET,
148
179
  SupervisorAgent,
149
180
  } from './agents/index.js'
150
181
 
@@ -18,6 +18,14 @@ export type * from './types/message/index.js'
18
18
  export type * from './types/common/index.js'
19
19
  export type * from './types/bidi/index.js'
20
20
  export type * from './types/tool/index.js'
21
+ // The directory convention: what a loaded `agent/` directory is, and what the
22
+ // loader reports about the files it could not use.
23
+ export type * from './directory/types.js'
24
+ export type {
25
+ DelegatePlan,
26
+ DeriveSupervisorInput,
27
+ SupervisorPlan,
28
+ } from './directory/derive-supervisor.js'
21
29
  export type * from './types/toolset/index.js'
22
30
  export type * from './types/permission/index.js'
23
31
  export type * from './types/run/index.js'
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { z } from 'zod'
3
+
4
+ import type { ToolDefinition } from '../../../types/tool/index.js'
5
+ import { ToolRegistry } from '../execute.js'
6
+
7
+ /**
8
+ * A tool that asks for constrained generation and hands over a schema the
9
+ * constrained dialect cannot express is wrong at the moment it is DECLARED,
10
+ * whichever model it later meets.
11
+ *
12
+ * The first attempt at this check lived in a provider driver. That caught the
13
+ * bug — but per request, in one of the two drivers that mark tools strict, and
14
+ * only once something actually ran. The registry already refused
15
+ * `enforceModelInput` without a `modelInputSchema`, with a comment stating the
16
+ * principle exactly: "Refusing at registration puts the error where the author
17
+ * can fix it rather than at the first request." The rule was written down; the
18
+ * check was in the wrong place.
19
+ *
20
+ * So the pair is here now. One asks whether a model schema EXISTS, the other
21
+ * whether it can carry the guarantee the tool just requested.
22
+ */
23
+
24
+ function tool(overrides: Partial<ToolDefinition>): ToolDefinition {
25
+ return {
26
+ name: 'sample',
27
+ description: 'a tool',
28
+ inputSchema: z.object({}),
29
+ execute: async () => ({ success: true, output: 'ok' }),
30
+ ...overrides,
31
+ } as ToolDefinition
32
+ }
33
+
34
+ describe('a tool cannot register a schema its own guarantee cannot carry', () => {
35
+ it('refuses a keyword outside the strict subset, naming the path', () => {
36
+ const registry = new ToolRegistry()
37
+
38
+ expect(() =>
39
+ registry.register(
40
+ tool({
41
+ name: 'edit',
42
+ enforceModelInput: true,
43
+ modelInputSchema: {
44
+ type: 'object',
45
+ properties: { insertLine: { oneOf: [{ type: 'integer' }, { const: 'end' }] } },
46
+ },
47
+ }),
48
+ ),
49
+ ).toThrow(/edit\.properties\.insertLine\.oneOf/)
50
+ })
51
+
52
+ it('names the remedy, not just the offence', () => {
53
+ const registry = new ToolRegistry()
54
+
55
+ expect(() =>
56
+ registry.register(
57
+ tool({ enforceModelInput: true, modelInputSchema: { properties: { n: { minimum: 0 } } } }),
58
+ ),
59
+ ).toThrow(/enforce at execution/)
60
+ })
61
+
62
+ it('admits the same union spelled the way the subset accepts', () => {
63
+ const registry = new ToolRegistry()
64
+
65
+ expect(() =>
66
+ registry.register(
67
+ tool({
68
+ enforceModelInput: true,
69
+ modelInputSchema: {
70
+ type: 'object',
71
+ properties: { insertLine: { anyOf: [{ type: 'integer' }, { const: 'end' }] } },
72
+ additionalProperties: false,
73
+ },
74
+ }),
75
+ ),
76
+ ).not.toThrow()
77
+ })
78
+
79
+ it('leaves a tool that never asked for the guarantee alone', () => {
80
+ // Without `enforceModelInput` nothing is marked strict, so the schema is
81
+ // sent as ordinary JSON Schema and `oneOf` is perfectly legal there.
82
+ // Refusing it would break working setups for no reason.
83
+ const registry = new ToolRegistry()
84
+
85
+ expect(() =>
86
+ registry.register(
87
+ tool({ modelInputSchema: { properties: { a: { oneOf: [{ type: 'string' }] } } } }),
88
+ ),
89
+ ).not.toThrow()
90
+ })
91
+
92
+ it('still refuses enforcement with no model schema at all', () => {
93
+ // The check this one was added beside. Kept in the same file so a
94
+ // future edit sees both halves of the pair together.
95
+ const registry = new ToolRegistry()
96
+
97
+ expect(() => registry.register(tool({ enforceModelInput: true }))).toThrow(
98
+ /does not define modelInputSchema/,
99
+ )
100
+ })
101
+
102
+ it('refuses through every registration shape', () => {
103
+ // `register` has three overloads and only one of them was exercised
104
+ // above; a check on the wrong one would look like coverage.
105
+ const bad = tool({
106
+ name: 'bad',
107
+ enforceModelInput: true,
108
+ modelInputSchema: { properties: { a: { oneOf: [] } } },
109
+ })
110
+
111
+ expect(() => new ToolRegistry().register(bad)).toThrow(/oneOf/)
112
+ expect(() => new ToolRegistry().register('bad', bad)).toThrow(/oneOf/)
113
+ expect(() => new ToolRegistry().register([bad])).toThrow(/oneOf/)
114
+ })
115
+ })
@@ -1,4 +1,5 @@
1
1
  import { SpanStatusCode, context as otelContext, trace } from '@opentelemetry/api'
2
+ import { assertStrictSchema } from '../../provider/strict-schema.js'
2
3
  import { GENAI, NAMZU, toolSpanName } from '../../telemetry/attributes.js'
3
4
  import { recordToolCall } from '../../telemetry/metrics.js'
4
5
  import { getTracer } from '../../telemetry/runtime-accessors.js'
@@ -177,6 +178,21 @@ export class ToolRegistry extends ManagedRegistry<ToolDefinition> {
177
178
  `Tool "${id}" enables enforceModelInput but does not define modelInputSchema. Constrained input generation requires an explicit provider-safe model schema.`,
178
179
  )
179
180
  }
181
+ // …and the schema has to be one a constrained decoder can actually be
182
+ // given. The check above asks whether a model schema EXISTS; this asks
183
+ // whether it can carry the guarantee the tool just requested.
184
+ //
185
+ // Both belong here for the reason the comment above already states.
186
+ // Strict validation runs against a SUBSET of JSON Schema, and a keyword
187
+ // outside it is not degraded — the request is rejected whole, so one
188
+ // unexpressible field takes down every other tool in the call. The
189
+ // first version of this check lived in a provider driver, which meant
190
+ // it fired per request, in one driver, long after the author had moved
191
+ // on. A tool that asks for a guarantee its own schema cannot carry is
192
+ // wrong at the moment it is declared, whichever model it later meets.
193
+ if (tool.enforceModelInput) {
194
+ assertStrictSchema(id, tool.modelInputSchema)
195
+ }
180
196
  if (tool.tier && this.tierConfig) {
181
197
  const validIds = this.tierConfig.tiers.map((t) => t.id)
182
198
  if (!validIds.includes(tool.tier)) {
@@ -47,6 +47,11 @@ export class ResultAssembler {
47
47
  type: 'run_completed',
48
48
  runId: runMgr.id,
49
49
  result: runMgr.getRun().result ?? '',
50
+ // Read AFTER `markCompleted`, which is where a run that was stopped
51
+ // mid-flight has its reason settled. Carried on the event so a
52
+ // consumer can tell "answered" from "ran out of budget" without
53
+ // holding the `Run`.
54
+ ...(runMgr.getRun().stopReason ? { stopReason: runMgr.getRun().stopReason } : {}),
50
55
  })
51
56
  yield* drainPending()
52
57
 
@@ -70,12 +70,18 @@ describe('the model can emit the idiom the description recommends', () => {
70
70
  })
71
71
 
72
72
  it('admits only "end" as a string, so a synonym cannot be generated', () => {
73
- const insert = (schema().properties as Record<string, { oneOf?: unknown[] }>).insertLine
73
+ const insert = (schema().properties as Record<string, { anyOf?: unknown[] }>).insertLine
74
74
 
75
75
  // The schema is where the synonym problem is solved for a provider that
76
76
  // constrains: `"EOF"` is not emittable because `"end"` is the only
77
77
  // string the union admits.
78
- expect(insert?.oneOf).toEqual([{ type: 'integer', minimum: 0 }, { const: 'end' }])
78
+ //
79
+ // `anyOf`, not `oneOf`: strict tool use validates against a subset of
80
+ // JSON Schema that excludes `oneOf`, and the vendor rejects the whole
81
+ // request rather than one field — so the spelling here is load-bearing,
82
+ // not stylistic. The two are equivalent for disjoint branches.
83
+ // `minimum` is gone for the same reason; the execution schema keeps it.
84
+ expect(insert?.anyOf).toEqual([{ type: 'integer' }, { const: 'end' }])
79
85
  })
80
86
  })
81
87
 
@@ -39,7 +39,7 @@ describe('EditTool', () => {
39
39
  'Exact replacement text. May be empty to delete old_string. Keep under 12000 characters.',
40
40
  },
41
41
  insertLine: {
42
- oneOf: [{ type: 'integer', minimum: 0 }, { const: 'end' }],
42
+ anyOf: [{ type: 'integer' }, { const: 'end' }],
43
43
  description:
44
44
  'Insert instead of replacing. The new_string goes after this 1-indexed line; 0 inserts before the first line; "end" appends. Omit for a find-and-replace.',
45
45
  },
@@ -102,12 +102,30 @@ const modelInputSchema: Record<string, unknown> = {
102
102
  },
103
103
  insertLine: {
104
104
  // The union the execution schema already accepts, stated so a
105
- // constrained decoder can emit it. Declaring it as `oneOf` of an
105
+ // constrained decoder can emit it. Stating it as a union of an
106
106
  // integer and the literal `"end"` also makes the synonym problem
107
107
  // structurally impossible: `"EOF"`, `"append"` and `"last"` are
108
108
  // not emittable, because `"end"` is the only string the schema
109
109
  // admits.
110
- oneOf: [{ type: 'integer', minimum: 0 }, { const: 'end' }],
110
+ //
111
+ // `anyOf`, NOT `oneOf`. Strict tool use validates against a SUBSET
112
+ // of JSON Schema, and `oneOf` is not in it — the vendor rejects the
113
+ // whole request with `tools.N.custom: Schema type 'oneOf' is not
114
+ // supported`, so the tool never mounts and the turn dies before a
115
+ // single token. Measured against the live API: strict + `oneOf` is
116
+ // a 400, strict + `anyOf` is accepted, and non-strict + `oneOf` is
117
+ // accepted — which is why nothing caught it. Both halves were
118
+ // individually fine; only their combination fails, and strict is on
119
+ // for every model at or above the gate.
120
+ //
121
+ // The two branches are disjoint, so `anyOf` and `oneOf` mean the
122
+ // same thing here — nothing is loosened.
123
+ //
124
+ // `minimum` is gone for the same reason: numeric constraints are
125
+ // outside the strict subset too. The bound is not lost — the
126
+ // execution schema still enforces it, which is where a value that
127
+ // crosses a boundary should be checked anyway.
128
+ anyOf: [{ type: 'integer' }, { const: 'end' }],
111
129
  description:
112
130
  'Insert instead of replacing. The new_string goes after this 1-indexed line; 0 inserts before the first line; "end" appends. Omit for a find-and-replace.',
113
131
  },