@namzu/sdk 5.1.0 → 6.0.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/CHANGELOG.md +465 -0
- package/dist/agents/__tests__/run-agent-forwarding.test.d.ts +2 -0
- package/dist/agents/__tests__/run-agent-forwarding.test.d.ts.map +1 -0
- package/dist/agents/__tests__/run-agent-forwarding.test.js +76 -0
- package/dist/agents/__tests__/run-agent-forwarding.test.js.map +1 -0
- package/dist/agents/__tests__/run-completed-stop-reason.test.d.ts +2 -0
- package/dist/agents/__tests__/run-completed-stop-reason.test.d.ts.map +1 -0
- package/dist/agents/__tests__/run-completed-stop-reason.test.js +50 -0
- package/dist/agents/__tests__/run-completed-stop-reason.test.js.map +1 -0
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +4 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/runAgent.d.ts +34 -0
- package/dist/agents/runAgent.d.ts.map +1 -1
- package/dist/agents/runAgent.js +5 -3
- package/dist/agents/runAgent.js.map +1 -1
- package/dist/config/runtime.d.ts +30 -30
- package/dist/constants/tools/__tests__/dangerous-patterns.test.d.ts +2 -0
- package/dist/constants/tools/__tests__/dangerous-patterns.test.d.ts.map +1 -0
- package/dist/constants/tools/__tests__/dangerous-patterns.test.js +54 -0
- package/dist/constants/tools/__tests__/dangerous-patterns.test.js.map +1 -0
- package/dist/constants/tools/index.d.ts.map +1 -1
- package/dist/constants/tools/index.js +28 -1
- package/dist/constants/tools/index.js.map +1 -1
- package/dist/contracts/a2a.d.ts +44 -44
- package/dist/directory/__tests__/derive.test.d.ts +2 -0
- package/dist/directory/__tests__/derive.test.d.ts.map +1 -0
- package/dist/directory/__tests__/derive.test.js +111 -0
- package/dist/directory/__tests__/derive.test.js.map +1 -0
- package/dist/directory/__tests__/load-diagnostics.test.d.ts +2 -0
- package/dist/directory/__tests__/load-diagnostics.test.d.ts.map +1 -0
- package/dist/directory/__tests__/load-diagnostics.test.js +106 -0
- package/dist/directory/__tests__/load-diagnostics.test.js.map +1 -0
- package/dist/directory/__tests__/load-directory.test.d.ts +2 -0
- package/dist/directory/__tests__/load-directory.test.d.ts.map +1 -0
- package/dist/directory/__tests__/load-directory.test.js +196 -0
- package/dist/directory/__tests__/load-directory.test.js.map +1 -0
- package/dist/directory/__tests__/subagents.test.d.ts +2 -0
- package/dist/directory/__tests__/subagents.test.d.ts.map +1 -0
- package/dist/directory/__tests__/subagents.test.js +117 -0
- package/dist/directory/__tests__/subagents.test.js.map +1 -0
- package/dist/directory/__tests__/supervisor-budget.test.d.ts +2 -0
- package/dist/directory/__tests__/supervisor-budget.test.d.ts.map +1 -0
- package/dist/directory/__tests__/supervisor-budget.test.js +97 -0
- package/dist/directory/__tests__/supervisor-budget.test.js.map +1 -0
- package/dist/directory/derive-supervisor.d.ts +58 -0
- package/dist/directory/derive-supervisor.d.ts.map +1 -0
- package/dist/directory/derive-supervisor.js +88 -0
- package/dist/directory/derive-supervisor.js.map +1 -0
- package/dist/directory/derive.d.ts +12 -0
- package/dist/directory/derive.d.ts.map +1 -0
- package/dist/directory/derive.js +58 -0
- package/dist/directory/derive.js.map +1 -0
- package/dist/directory/index.d.ts +7 -0
- package/dist/directory/index.d.ts.map +1 -0
- package/dist/directory/index.js +5 -0
- package/dist/directory/index.js.map +1 -0
- package/dist/directory/load.d.ts +14 -0
- package/dist/directory/load.d.ts.map +1 -0
- package/dist/directory/load.js +488 -0
- package/dist/directory/load.js.map +1 -0
- package/dist/directory/scan.d.ts +36 -0
- package/dist/directory/scan.d.ts.map +1 -0
- package/dist/directory/scan.js +136 -0
- package/dist/directory/scan.js.map +1 -0
- package/dist/directory/types.d.ts +215 -0
- package/dist/directory/types.d.ts.map +1 -0
- package/dist/directory/types.js +8 -0
- package/dist/directory/types.js.map +1 -0
- package/dist/plugin/__tests__/path-containment.test.d.ts +2 -0
- package/dist/plugin/__tests__/path-containment.test.d.ts.map +1 -0
- package/dist/plugin/__tests__/path-containment.test.js +91 -0
- package/dist/plugin/__tests__/path-containment.test.js.map +1 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +3 -3
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +12 -2
- package/dist/plugin/loader.js.map +1 -1
- package/dist/provider/__tests__/model-version.test.d.ts +2 -0
- package/dist/provider/__tests__/model-version.test.d.ts.map +1 -0
- package/dist/provider/__tests__/model-version.test.js +80 -0
- package/dist/provider/__tests__/model-version.test.js.map +1 -0
- package/dist/provider/__tests__/strict-schema.test.d.ts +2 -0
- package/dist/provider/__tests__/strict-schema.test.d.ts.map +1 -0
- package/dist/provider/__tests__/strict-schema.test.js +157 -0
- package/dist/provider/__tests__/strict-schema.test.js.map +1 -0
- package/dist/provider/__tests__/vendor-detail.test.d.ts +2 -0
- package/dist/provider/__tests__/vendor-detail.test.d.ts.map +1 -0
- package/dist/provider/__tests__/vendor-detail.test.js +89 -0
- package/dist/provider/__tests__/vendor-detail.test.js.map +1 -0
- package/dist/provider/errors.d.ts +38 -5
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +107 -5
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/model-version.d.ts +60 -0
- package/dist/provider/model-version.d.ts.map +1 -0
- package/dist/provider/model-version.js +64 -0
- package/dist/provider/model-version.js.map +1 -0
- package/dist/provider/strict-schema.d.ts +49 -0
- package/dist/provider/strict-schema.d.ts.map +1 -0
- package/dist/provider/strict-schema.js +168 -0
- package/dist/provider/strict-schema.js.map +1 -0
- package/dist/public-runtime.d.ts +9 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +27 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/tool/__tests__/dialect.test.d.ts +2 -0
- package/dist/registry/tool/__tests__/dialect.test.d.ts.map +1 -0
- package/dist/registry/tool/__tests__/dialect.test.js +143 -0
- package/dist/registry/tool/__tests__/dialect.test.js.map +1 -0
- package/dist/registry/tool/__tests__/strict-registration.test.d.ts +2 -0
- package/dist/registry/tool/__tests__/strict-registration.test.d.ts.map +1 -0
- package/dist/registry/tool/__tests__/strict-registration.test.js +82 -0
- package/dist/registry/tool/__tests__/strict-registration.test.js.map +1 -0
- package/dist/registry/tool/dialect.d.ts +50 -0
- package/dist/registry/tool/dialect.d.ts.map +1 -0
- package/dist/registry/tool/dialect.js +131 -0
- package/dist/registry/tool/dialect.js.map +1 -0
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +16 -0
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/registry/toolset/catalog.d.ts.map +1 -1
- package/dist/registry/toolset/catalog.js +10 -5
- package/dist/registry/toolset/catalog.js.map +1 -1
- package/dist/runtime/query/__tests__/stream-recovery.test.js +6 -0
- package/dist/runtime/query/__tests__/stream-recovery.test.js.map +1 -1
- package/dist/runtime/query/result.d.ts.map +1 -1
- package/dist/runtime/query/result.js +11 -0
- package/dist/runtime/query/result.js.map +1 -1
- package/dist/tools/builtins/__tests__/edit-insert-reachable.test.js +7 -1
- package/dist/tools/builtins/__tests__/edit-insert-reachable.test.js.map +1 -1
- package/dist/tools/builtins/__tests__/edit.test.js +1 -1
- package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +20 -2
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/types/plugin/index.d.ts +4 -4
- package/dist/types/provider/error.d.ts +20 -4
- package/dist/types/provider/error.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +17 -2
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/verification/index.d.ts +6 -6
- package/package.json +1 -1
- package/src/agents/__tests__/run-agent-forwarding.test.ts +85 -0
- package/src/agents/__tests__/run-completed-stop-reason.test.ts +59 -0
- package/src/agents/index.ts +8 -0
- package/src/agents/runAgent.ts +30 -4
- package/src/constants/tools/__tests__/dangerous-patterns.test.ts +62 -0
- package/src/constants/tools/index.ts +28 -1
- package/src/directory/__tests__/derive.test.ts +135 -0
- package/src/directory/__tests__/load-diagnostics.test.ts +126 -0
- package/src/directory/__tests__/load-directory.test.ts +243 -0
- package/src/directory/__tests__/subagents.test.ts +155 -0
- package/src/directory/__tests__/supervisor-budget.test.ts +117 -0
- package/src/directory/derive-supervisor.ts +149 -0
- package/src/directory/derive.ts +70 -0
- package/src/directory/index.ts +27 -0
- package/src/directory/load.ts +556 -0
- package/src/directory/scan.ts +163 -0
- package/src/directory/types.ts +254 -0
- package/src/plugin/__tests__/path-containment.test.ts +107 -0
- package/src/plugin/lifecycle.ts +3 -3
- package/src/plugin/loader.ts +12 -2
- package/src/provider/__tests__/model-version.test.ts +93 -0
- package/src/provider/__tests__/strict-schema.test.ts +189 -0
- package/src/provider/__tests__/vendor-detail.test.ts +107 -0
- package/src/provider/errors.ts +106 -5
- package/src/provider/model-version.ts +98 -0
- package/src/provider/strict-schema.ts +182 -0
- package/src/public-runtime.ts +38 -0
- package/src/public-types.ts +8 -0
- package/src/registry/tool/__tests__/dialect.test.ts +197 -0
- package/src/registry/tool/__tests__/strict-registration.test.ts +115 -0
- package/src/registry/tool/dialect.ts +136 -0
- package/src/registry/tool/execute.ts +16 -0
- package/src/registry/toolset/catalog.ts +10 -5
- package/src/runtime/query/__tests__/stream-recovery.test.ts +6 -0
- package/src/runtime/query/result.ts +11 -0
- package/src/tools/builtins/__tests__/edit-insert-reachable.test.ts +8 -2
- package/src/tools/builtins/__tests__/edit.test.ts +1 -1
- package/src/tools/builtins/edit.ts +20 -2
- package/src/types/provider/error.ts +20 -4
- package/src/types/run/events.ts +15 -2
|
@@ -0,0 +1,182 @@
|
|
|
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. Each entry names what to write instead.
|
|
32
|
+
*
|
|
33
|
+
* MEASURED against the live API, not read off a page. The first version of this
|
|
34
|
+
* list was derived from documentation and was wrong in both directions: it
|
|
35
|
+
* refused `minLength`/`maxLength`, which the wire accepts, and it permitted
|
|
36
|
+
* `prefixItems`, which the wire rejects. A deny-list nobody probed is a guess
|
|
37
|
+
* with a confident tone, and this one would have refused working tools while
|
|
38
|
+
* still letting a broken one through.
|
|
39
|
+
*
|
|
40
|
+
* The probe lives in the live contract test; run it against a new model before
|
|
41
|
+
* trusting this list on that model.
|
|
42
|
+
*/
|
|
43
|
+
const NO_CONDITIONALS = 'strict mode has no conditional schemas; flatten the object'
|
|
44
|
+
const NO_NUMERIC_BOUNDS = 'numeric bounds are not in the subset; enforce at execution'
|
|
45
|
+
const NO_ARRAY_BOUNDS = 'array bounds are not in the subset; enforce at execution'
|
|
46
|
+
|
|
47
|
+
// A Map rather than an object literal, because one of the keys is `then`: an
|
|
48
|
+
// ordinary object carrying a `then` property is a thenable, and awaiting it
|
|
49
|
+
// anywhere would silently call the string. The lint rule that says so is
|
|
50
|
+
// right, and a Map has no such hazard.
|
|
51
|
+
const UNSUPPORTED: ReadonlyMap<string, string> = new Map([
|
|
52
|
+
['oneOf', 'use `anyOf` — for disjoint branches the two are equivalent'],
|
|
53
|
+
['not', 'express the constraint positively, or validate it at execution'],
|
|
54
|
+
['if', NO_CONDITIONALS],
|
|
55
|
+
['then', NO_CONDITIONALS],
|
|
56
|
+
['else', NO_CONDITIONALS],
|
|
57
|
+
['minimum', NO_NUMERIC_BOUNDS],
|
|
58
|
+
['maximum', NO_NUMERIC_BOUNDS],
|
|
59
|
+
['exclusiveMinimum', NO_NUMERIC_BOUNDS],
|
|
60
|
+
['exclusiveMaximum', NO_NUMERIC_BOUNDS],
|
|
61
|
+
['multipleOf', NO_NUMERIC_BOUNDS],
|
|
62
|
+
// `minLength`/`maxLength` are NOT here. The first version of this list put
|
|
63
|
+
// them here on documentation alone, and measurement says the wire accepts
|
|
64
|
+
// both — so the list was refusing tools that would have worked. A deny-list
|
|
65
|
+
// derived from prose and never probed is a guess with a confident tone.
|
|
66
|
+
// `minItems` is NOT a flat denial — see MIN_ITEMS_ALLOWED below. The wire
|
|
67
|
+
// accepts 0 and 1 and refuses everything above, naming the value in the
|
|
68
|
+
// error, so a blanket entry here would refuse `.nonempty()` on a schema the
|
|
69
|
+
// wire would have taken.
|
|
70
|
+
['maxItems', NO_ARRAY_BOUNDS],
|
|
71
|
+
['uniqueItems', NO_ARRAY_BOUNDS],
|
|
72
|
+
// The other direction of the same mistake: this was missing, and it is the
|
|
73
|
+
// one that interacts with the dialect conversion. A tuple becomes
|
|
74
|
+
// `prefixItems` for the 2020-12 wire — and strict rejects `prefixItems`
|
|
75
|
+
// outright, so a tool that is BOTH strict AND tuple-shaped cannot be
|
|
76
|
+
// expressed at all. Better to say that at registration than to convert a
|
|
77
|
+
// schema into a different rejection.
|
|
78
|
+
['prefixItems', 'strict arrays take one `items` schema; a tuple cannot be expressed'],
|
|
79
|
+
['patternProperties', 'name the properties explicitly'],
|
|
80
|
+
['propertyNames', 'name the properties explicitly'],
|
|
81
|
+
['dependentSchemas', 'flatten the object and validate at execution'],
|
|
82
|
+
['dependentRequired', 'flatten the object and validate at execution'],
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The only `minItems` values the strict subset admits.
|
|
87
|
+
*
|
|
88
|
+
* Measured: `minItems: 0` and `minItems: 1` are accepted, `minItems: 2` comes
|
|
89
|
+
* back *"For 'array' type, 'minItems' values other than 0 or 1 are not
|
|
90
|
+
* supported"*. So the constraint is on the VALUE, not the keyword, and that is
|
|
91
|
+
* the whole difference between refusing a required-non-empty array — the
|
|
92
|
+
* ordinary spelling of `z.array(...).nonempty()` — and letting it through.
|
|
93
|
+
*/
|
|
94
|
+
const MIN_ITEMS_ALLOWED = new Set([0, 1])
|
|
95
|
+
|
|
96
|
+
export interface StrictSchemaViolation {
|
|
97
|
+
/** Dotted path to the offending keyword, e.g. `properties.insertLine.oneOf`. */
|
|
98
|
+
readonly path: string
|
|
99
|
+
readonly keyword: string
|
|
100
|
+
/** What to write instead. */
|
|
101
|
+
readonly remedy: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Every place a schema leaves the strict subset, with its exact path.
|
|
106
|
+
*
|
|
107
|
+
* The path is the point. The vendor's own error names the tool and the
|
|
108
|
+
* keyword but not where inside the schema it sits, which on a schema of any
|
|
109
|
+
* size is the difference between a glance and an afternoon.
|
|
110
|
+
*/
|
|
111
|
+
export function findStrictSchemaViolations(schema: unknown, path = ''): StrictSchemaViolation[] {
|
|
112
|
+
if (Array.isArray(schema)) {
|
|
113
|
+
return schema.flatMap((item, index) => findStrictSchemaViolations(item, `${path}[${index}]`))
|
|
114
|
+
}
|
|
115
|
+
if (typeof schema !== 'object' || schema === null) return []
|
|
116
|
+
|
|
117
|
+
const found: StrictSchemaViolation[] = []
|
|
118
|
+
for (const [keyword, value] of Object.entries(schema as Record<string, unknown>)) {
|
|
119
|
+
const here = path ? `${path}.${keyword}` : keyword
|
|
120
|
+
const remedy = UNSUPPORTED.get(keyword)
|
|
121
|
+
if (remedy !== undefined) {
|
|
122
|
+
found.push({ path: here, keyword, remedy })
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
// `additionalProperties` is admitted only as `false`; any schema there
|
|
126
|
+
// is an open object, which the subset does not allow.
|
|
127
|
+
if (keyword === 'additionalProperties' && value !== false) {
|
|
128
|
+
found.push({
|
|
129
|
+
path: here,
|
|
130
|
+
keyword,
|
|
131
|
+
remedy: 'strict objects must set `additionalProperties: false`',
|
|
132
|
+
})
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
// A tuple in the OTHER spelling. This check runs at registration, on the
|
|
136
|
+
// schema as rendered — which is draft-07, where a tuple is `items: [a,
|
|
137
|
+
// b]` — while the wire sees the 2020-12 `prefixItems` the driver
|
|
138
|
+
// converts it to. So a `prefixItems` entry alone never fires on the
|
|
139
|
+
// path that produces tuples, and the entry added to catch this case was
|
|
140
|
+
// dead for exactly the case it was added for.
|
|
141
|
+
//
|
|
142
|
+
// Both spellings mean the same thing and strict admits neither, so this
|
|
143
|
+
// names the tuple rather than the dialect it happens to be written in.
|
|
144
|
+
if (keyword === 'items' && Array.isArray(value)) {
|
|
145
|
+
found.push({
|
|
146
|
+
path: here,
|
|
147
|
+
keyword,
|
|
148
|
+
remedy: 'strict arrays take one `items` schema; a tuple cannot be expressed',
|
|
149
|
+
})
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
if (keyword === 'minItems' && typeof value === 'number' && !MIN_ITEMS_ALLOWED.has(value)) {
|
|
153
|
+
found.push({
|
|
154
|
+
path: here,
|
|
155
|
+
keyword,
|
|
156
|
+
remedy: 'strict accepts `minItems` of 0 or 1 only; enforce a larger bound at execution',
|
|
157
|
+
})
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
found.push(...findStrictSchemaViolations(value, here))
|
|
161
|
+
}
|
|
162
|
+
return found
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Refuse a schema the driver is about to vouch for and cannot.
|
|
167
|
+
*
|
|
168
|
+
* Refusing here rather than dropping `strict` quietly: a caller who set
|
|
169
|
+
* `enforceModelInput` asked for the guarantee, and silently not providing it
|
|
170
|
+
* is the failure this repo names `refuse, do not degrade`. The alternative
|
|
171
|
+
* costs a turn and teaches nothing — the vendor's 400 arrives with the tool
|
|
172
|
+
* name and the keyword, but not the path, and not the fix.
|
|
173
|
+
*/
|
|
174
|
+
export function assertStrictSchema(toolName: string, schema: unknown): void {
|
|
175
|
+
const violations = findStrictSchemaViolations(schema)
|
|
176
|
+
if (violations.length === 0) return
|
|
177
|
+
|
|
178
|
+
const detail = violations.map((v) => ` ${toolName}.${v.path} — ${v.remedy}`).join('\n')
|
|
179
|
+
throw new Error(
|
|
180
|
+
`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}`,
|
|
181
|
+
)
|
|
182
|
+
}
|
package/src/public-runtime.ts
CHANGED
|
@@ -70,6 +70,23 @@ 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
|
+
// A tool has one schema; what changes per provider is the DIALECT the wire
|
|
82
|
+
// parses, which is the wire's property. Rendered once, converted at the driver.
|
|
83
|
+
export { findDraft07Only, toSchemaDialect } from './registry/tool/dialect.js'
|
|
84
|
+
export type { JsonSchemaDialect } from './registry/tool/dialect.js'
|
|
85
|
+
// The renderer itself, so a driver or a contract test can ask what a tool will
|
|
86
|
+
// actually put on the wire without reaching into the registry.
|
|
87
|
+
export { renderToolSchema } from './registry/tool/schema.js'
|
|
88
|
+
export type { StrictSchemaViolation } from './provider/strict-schema.js'
|
|
89
|
+
export type { ModelIdGrammar, ModelVersion } from './provider/model-version.js'
|
|
73
90
|
export { drainQuery, query } from './runtime/query/index.js'
|
|
74
91
|
// Mid-run guidance. A host holds the channel and the loop drains it at the
|
|
75
92
|
// tool-result boundary; see the module for why that is the only legal slot.
|
|
@@ -125,6 +142,24 @@ export {
|
|
|
125
142
|
resolveSkillChain,
|
|
126
143
|
SkillRegistry,
|
|
127
144
|
} from './skills/index.js'
|
|
145
|
+
|
|
146
|
+
// ─── the agent directory ─────────────────────────────────────────────────
|
|
147
|
+
//
|
|
148
|
+
// Reading a conventional `agent/` directory — its instructions, tools, skills
|
|
149
|
+
// and delegates — into the same options `runAgent` and `SupervisorAgent`
|
|
150
|
+
// already take. A loader, not a second engine: everything it produces is an
|
|
151
|
+
// ordinary option, so a caller who outgrows the convention passes overrides
|
|
152
|
+
// or stops calling it and keeps everything else.
|
|
153
|
+
//
|
|
154
|
+
// It shipped briefly as its own package. The name was the tell — nothing fit,
|
|
155
|
+
// because a directory reader that needs the kernel to be useful is a function
|
|
156
|
+
// of the kernel, not a product beside it.
|
|
157
|
+
export {
|
|
158
|
+
ALL_SLOTS,
|
|
159
|
+
deriveRunOptions,
|
|
160
|
+
deriveSupervisorOptions,
|
|
161
|
+
loadDirectory,
|
|
162
|
+
} from './directory/index.js'
|
|
128
163
|
export {
|
|
129
164
|
AdvisorRegistry,
|
|
130
165
|
AdvisoryContext,
|
|
@@ -145,6 +180,9 @@ export {
|
|
|
145
180
|
// The short path: provider + model + prompt. Assembles the identity and
|
|
146
181
|
// budgets `drainQuery` requires and hands the generated identity back.
|
|
147
182
|
runAgent,
|
|
183
|
+
DEFAULT_MAX_ITERATIONS,
|
|
184
|
+
DEFAULT_TIMEOUT_MS,
|
|
185
|
+
DEFAULT_TOKEN_BUDGET,
|
|
148
186
|
SupervisorAgent,
|
|
149
187
|
} from './agents/index.js'
|
|
150
188
|
|
package/src/public-types.ts
CHANGED
|
@@ -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,197 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { findDraft07Only, toSchemaDialect } from '../dialect.js'
|
|
5
|
+
import { renderToolSchema } from '../schema.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The mechanism, tested in the kernel that owns it.
|
|
9
|
+
*
|
|
10
|
+
* The drivers each have their own test proving the conversion reaches their
|
|
11
|
+
* wire. This one is about the conversion itself: what it rewrites, what it
|
|
12
|
+
* deliberately leaves alone, and the two properties the prompt cache depends
|
|
13
|
+
* on — a stable reference and a frozen result.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
describe('saying a schema in the dialect a wire parses', () => {
|
|
17
|
+
it('moves a tuple from `items` to `prefixItems`', () => {
|
|
18
|
+
const draft07 = {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
range: {
|
|
22
|
+
type: 'array',
|
|
23
|
+
items: [{ type: 'integer' }, { type: 'integer' }],
|
|
24
|
+
minItems: 2,
|
|
25
|
+
maxItems: 2,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
expect(toSchemaDialect(draft07, '2020-12')).toEqual({
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
range: {
|
|
34
|
+
type: 'array',
|
|
35
|
+
prefixItems: [{ type: 'integer' }, { type: 'integer' }],
|
|
36
|
+
minItems: 2,
|
|
37
|
+
maxItems: 2,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('leaves a homogeneous array alone, where `items` means the same thing', () => {
|
|
44
|
+
// The distinction the whole conversion turns on: `items` is only a
|
|
45
|
+
// tuple when it holds an ARRAY of schemas. One schema means "every
|
|
46
|
+
// element", which both dialects spell identically.
|
|
47
|
+
const schema = { type: 'array', items: { type: 'string' } }
|
|
48
|
+
|
|
49
|
+
expect(toSchemaDialect(schema, '2020-12')).toEqual(schema)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('turns `additionalItems` into the 2020-12 `items`', () => {
|
|
53
|
+
// `additionalItems` only ever qualified an array-form `items` — it says
|
|
54
|
+
// what the elements AFTER the tuple look like. 2020-12 gave that job to
|
|
55
|
+
// `items` once `prefixItems` holds the positional schemas.
|
|
56
|
+
const converted = toSchemaDialect(
|
|
57
|
+
{
|
|
58
|
+
type: 'array',
|
|
59
|
+
items: [{ type: 'integer' }],
|
|
60
|
+
additionalItems: { type: 'string' },
|
|
61
|
+
},
|
|
62
|
+
'2020-12',
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
expect(converted).toEqual({
|
|
66
|
+
type: 'array',
|
|
67
|
+
prefixItems: [{ type: 'integer' }],
|
|
68
|
+
items: { type: 'string' },
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('drops `additionalItems: false`, which both dialects already imply', () => {
|
|
73
|
+
// Not a lossy shortcut: once `prefixItems` is set, a closed tuple is the
|
|
74
|
+
// default in 2020-12, so emitting `items: false` would add a byte to
|
|
75
|
+
// every request to say what was already true.
|
|
76
|
+
expect(
|
|
77
|
+
toSchemaDialect(
|
|
78
|
+
{ type: 'array', items: [{ type: 'integer' }], additionalItems: false },
|
|
79
|
+
'2020-12',
|
|
80
|
+
),
|
|
81
|
+
).toEqual({ type: 'array', prefixItems: [{ type: 'integer' }] })
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('ignores `additionalItems` with no tuple to qualify', () => {
|
|
85
|
+
// Meaningless in draft-07 too, so carrying it forward would be inventing
|
|
86
|
+
// a constraint the author did not write.
|
|
87
|
+
expect(
|
|
88
|
+
toSchemaDialect(
|
|
89
|
+
{ type: 'array', items: { type: 'string' }, additionalItems: { type: 'integer' } },
|
|
90
|
+
'2020-12',
|
|
91
|
+
),
|
|
92
|
+
).toEqual({ type: 'array', items: { type: 'string' } })
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('converts a tuple nested inside another tuple', () => {
|
|
96
|
+
const converted = toSchemaDialect(
|
|
97
|
+
{ type: 'array', items: [{ type: 'array', items: [{ type: 'integer' }] }] },
|
|
98
|
+
'2020-12',
|
|
99
|
+
) as Record<string, Record<string, unknown>[]>
|
|
100
|
+
|
|
101
|
+
expect(converted.prefixItems?.[0]).toEqual({
|
|
102
|
+
type: 'array',
|
|
103
|
+
prefixItems: [{ type: 'integer' }],
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('hands back the very same object for draft-07', () => {
|
|
108
|
+
// Not an equal object — the SAME one. The tools block sits at position 0
|
|
109
|
+
// of the prompt-cache prefix, so a driver that speaks draft-07 must not
|
|
110
|
+
// pay an allocation or risk a differently-ordered copy per request.
|
|
111
|
+
const schema = { type: 'object' }
|
|
112
|
+
|
|
113
|
+
expect(toSchemaDialect(schema, 'draft-07')).toBe(schema)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('returns the same converted object every time it is asked', () => {
|
|
117
|
+
// Same reason. Conversion runs once per schema per dialect; a fresh
|
|
118
|
+
// object each iteration would invalidate the cache for the whole run
|
|
119
|
+
// even though the bytes were equal.
|
|
120
|
+
const schema = { type: 'array', items: [{ type: 'integer' }] }
|
|
121
|
+
|
|
122
|
+
expect(toSchemaDialect(schema, '2020-12')).toBe(toSchemaDialect(schema, '2020-12'))
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('freezes what it hands out, all the way down', () => {
|
|
126
|
+
// A caller that mutates a cached schema would poison every later render,
|
|
127
|
+
// and the symptom would be a silently invalidated prompt cache rather
|
|
128
|
+
// than an error. Freezing turns that into a throw at the mutation site.
|
|
129
|
+
const converted = toSchemaDialect(
|
|
130
|
+
{ type: 'object', properties: { a: { type: 'array', items: [{ type: 'integer' }] } } },
|
|
131
|
+
'2020-12',
|
|
132
|
+
) as { properties: { a: { prefixItems: unknown[] } } }
|
|
133
|
+
|
|
134
|
+
expect(Object.isFrozen(converted)).toBe(true)
|
|
135
|
+
expect(Object.isFrozen(converted.properties.a)).toBe(true)
|
|
136
|
+
expect(Object.isFrozen(converted.properties.a.prefixItems)).toBe(true)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('finding what a 2020-12 wire will refuse', () => {
|
|
141
|
+
it('names the path to an array-form `items`', () => {
|
|
142
|
+
expect(
|
|
143
|
+
findDraft07Only({
|
|
144
|
+
type: 'object',
|
|
145
|
+
properties: { range: { type: 'array', items: [{ type: 'integer' }] } },
|
|
146
|
+
}),
|
|
147
|
+
).toEqual(['properties.range.items'])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('names `additionalItems` too', () => {
|
|
151
|
+
expect(findDraft07Only({ additionalItems: false })).toEqual(['additionalItems'])
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('walks into arrays, indexing the branch', () => {
|
|
155
|
+
expect(
|
|
156
|
+
findDraft07Only({
|
|
157
|
+
anyOf: [{ type: 'string' }, { type: 'array', items: [{ type: 'integer' }] }],
|
|
158
|
+
}),
|
|
159
|
+
).toEqual(['anyOf[1].items'])
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('does not descend into a tuple it has already reported', () => {
|
|
163
|
+
// Reporting the tuple and then each of its positional schemas would
|
|
164
|
+
// turn one fixable finding into a list nobody reads.
|
|
165
|
+
expect(
|
|
166
|
+
findDraft07Only({ type: 'array', items: [{ type: 'integer' }, { type: 'string' }] }),
|
|
167
|
+
).toEqual(['items'])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('says nothing about a schema that is already 2020-12', () => {
|
|
171
|
+
expect(
|
|
172
|
+
findDraft07Only({
|
|
173
|
+
type: 'array',
|
|
174
|
+
prefixItems: [{ type: 'integer' }],
|
|
175
|
+
items: { type: 'string' },
|
|
176
|
+
}),
|
|
177
|
+
).toEqual([])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('tolerates the leaves', () => {
|
|
181
|
+
expect(findDraft07Only(null)).toEqual([])
|
|
182
|
+
expect(findDraft07Only('a string')).toEqual([])
|
|
183
|
+
expect(findDraft07Only(42)).toEqual([])
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('the round trip a real tool takes', () => {
|
|
188
|
+
it('renders a Zod tuple as draft-07 and converts it clean', () => {
|
|
189
|
+
// The actual defect, end to end: this is what `read.readRange` is.
|
|
190
|
+
const rendered = renderToolSchema(
|
|
191
|
+
z.object({ readRange: z.tuple([z.number(), z.number()]).optional() }),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
expect(findDraft07Only(rendered)).not.toEqual([])
|
|
195
|
+
expect(findDraft07Only(toSchemaDialect(rendered, '2020-12'))).toEqual([])
|
|
196
|
+
})
|
|
197
|
+
})
|
|
@@ -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
|
+
})
|